feat: 优化手机端显示效果
This commit is contained in:
3
frontend/public/index.html
Normal file
3
frontend/public/index.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
</head>
|
||||
163
frontend/src/assets/styles/mobile.css
Normal file
163
frontend/src/assets/styles/mobile.css
Normal file
@@ -0,0 +1,163 @@
|
||||
/* 移动端通用样式优化 */
|
||||
|
||||
/* 增大触摸目标区域 */
|
||||
.mobile-touch-target {
|
||||
min-height: 44px; /* 推荐的最小触摸目标尺寸 */
|
||||
min-width: 44px;
|
||||
}
|
||||
|
||||
/* 优化触摸反馈效果 */
|
||||
.mobile-touch-feedback {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.mobile-touch-feedback:active {
|
||||
transform: scale(0.96);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 移动端表单元素优化 */
|
||||
.mobile-input {
|
||||
font-size: 16px !important; /* 防止iOS自动缩放 */
|
||||
line-height: 1.2;
|
||||
padding: 12px !important;
|
||||
}
|
||||
|
||||
.mobile-select {
|
||||
height: 44px !important;
|
||||
}
|
||||
|
||||
/* 响应式容器 */
|
||||
.mobile-container {
|
||||
width: 100%;
|
||||
padding: 0 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 自适应字体大小 */
|
||||
@media (max-width: 480px) {
|
||||
.mobile-adaptive-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.mobile-adaptive-heading {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端表格优化 */
|
||||
.mobile-table-container {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* 底部操作区固定 */
|
||||
.mobile-action-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 8px 16px;
|
||||
background-color: var(--n-color);
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mobile-action-bar-spacer {
|
||||
height: 60px; /* 预留底部空间 */
|
||||
}
|
||||
|
||||
/* 移动端友好的卡片样式 */
|
||||
.mobile-card {
|
||||
border-radius: 12px !important;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 12px 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 {
|
||||
border-width: 1px !important;
|
||||
border-style: solid;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 确保右侧边框在移动端正确显示 */
|
||||
.mobile-right-border {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mobile-right-border::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background-color: var(--n-border-color, rgba(0, 0, 0, 0.1));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 底部背景延伸 */
|
||||
.mobile-bottom-extend {
|
||||
position: relative;
|
||||
padding-bottom: env(safe-area-inset-bottom, 0);
|
||||
margin-bottom: -1px; /* 防止底部出现缝隙 */
|
||||
}
|
||||
|
||||
/* 全宽容器 */
|
||||
.mobile-full-width {
|
||||
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-shadow {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08) !important;
|
||||
}
|
||||
|
||||
/* 移动端内容容器 - 确保内容不会太靠近边缘 */
|
||||
.mobile-content-container {
|
||||
padding: 0.75rem !important;
|
||||
width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.mobile-content-container {
|
||||
padding: 0.5rem !important;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div v-if="showAnnouncement" class="announcement-container">
|
||||
<n-card class="announcement-card">
|
||||
<div v-if="showAnnouncement" class="announcement-container" :class="{ 'login-page-announcement': isLoginPage }">
|
||||
<n-card class="announcement-card mobile-card" :class="{ 'login-card-style': isLoginPage }">
|
||||
<template #header>
|
||||
<div class="announcement-header">
|
||||
<n-icon size="18" :component="InformationCircleIcon" class="info-icon" />
|
||||
@@ -10,7 +10,7 @@
|
||||
<div class="announcement-content" v-html="processedContent"></div>
|
||||
<div class="announcement-timer">{{ remainingTimeText }}</div>
|
||||
<template #action>
|
||||
<n-button quaternary circle size="small" @click="closeAnnouncement">
|
||||
<n-button quaternary circle size="small" @click="closeAnnouncement" class="mobile-touch-target">
|
||||
<template #icon>
|
||||
<n-icon :component="CloseIcon" />
|
||||
</template>
|
||||
@@ -25,12 +25,20 @@ 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';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const props = defineProps<{
|
||||
content: string;
|
||||
autoCloseTime?: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void;
|
||||
}>();
|
||||
|
||||
const route = useRoute();
|
||||
const isLoginPage = computed(() => route.path === '/login');
|
||||
|
||||
const showAnnouncement = ref(true);
|
||||
const remainingTime = ref(props.autoCloseTime || 5);
|
||||
const timer = ref<number | null>(null);
|
||||
@@ -54,6 +62,7 @@ function closeAnnouncement() {
|
||||
window.clearInterval(timer.value);
|
||||
timer.value = null;
|
||||
}
|
||||
emit('close');
|
||||
}
|
||||
|
||||
function updateTimer() {
|
||||
@@ -87,6 +96,11 @@ onBeforeUnmount(() => {
|
||||
|
||||
.announcement-card {
|
||||
border-left: 4px solid var(--n-primary-color);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
backdrop-filter: blur(10px);
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.announcement-header {
|
||||
@@ -125,4 +139,69 @@ onBeforeUnmount(() => {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 登录页面适配 */
|
||||
.login-page-announcement {
|
||||
z-index: 1000;
|
||||
top: 1.5rem;
|
||||
right: 1.5rem;
|
||||
}
|
||||
|
||||
.login-card-style {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(10px);
|
||||
border-left: 4px solid #2080f0;
|
||||
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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="api-config-section">
|
||||
<n-button
|
||||
class="toggle-button"
|
||||
class="toggle-button mobile-touch-target"
|
||||
size="small"
|
||||
@click="toggleConfig"
|
||||
:quaternary="true"
|
||||
@@ -14,7 +14,7 @@
|
||||
</n-button>
|
||||
|
||||
<n-collapse-transition :show="expanded">
|
||||
<n-card class="api-config-card" :bordered="false">
|
||||
<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">
|
||||
<template #icon>
|
||||
<n-icon :component="InformationCircleIcon" />
|
||||
@@ -29,8 +29,8 @@
|
||||
</div>
|
||||
</n-alert>
|
||||
|
||||
<n-grid :cols="24" :x-gap="16" :y-gap="16">
|
||||
<n-grid-item :span="24" :lg-span="14">
|
||||
<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-input
|
||||
v-model:value="apiConfig.apiUrl"
|
||||
@@ -54,7 +54,7 @@
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
|
||||
<n-grid-item :span="24" :lg-span="10">
|
||||
<n-grid-item :span="24" :md-span="10" :lg-span="10">
|
||||
<n-form-item label="API Key" path="apiKey">
|
||||
<n-input
|
||||
v-model:value="apiConfig.apiKey"
|
||||
@@ -71,7 +71,7 @@
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
|
||||
<n-grid-item :span="12" :lg-span="12">
|
||||
<n-grid-item :span="12" :md-span="12" :lg-span="12">
|
||||
<n-form-item label="模型" path="apiModel">
|
||||
<n-input
|
||||
v-model:value="apiConfig.apiModel"
|
||||
@@ -118,7 +118,7 @@
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
|
||||
<n-grid-item :span="12" :lg-span="12">
|
||||
<n-grid-item :span="12" :md-span="12" :lg-span="12">
|
||||
<n-form-item label="超时时间(秒)" path="apiTimeout">
|
||||
<n-input-number
|
||||
v-model:value="apiTimeout"
|
||||
@@ -612,11 +612,113 @@ onMounted(() => {
|
||||
.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: 0.75rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.api-config-card {
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.625rem !important;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<!-- 公告横幅 -->
|
||||
<AnnouncementBanner
|
||||
v-if="announcement && showAnnouncementBanner"
|
||||
:content="announcement"
|
||||
:auto-close-time="5"
|
||||
@close="handleAnnouncementClose"
|
||||
/>
|
||||
|
||||
<div class="login-background">
|
||||
<div class="login-shape shape1"></div>
|
||||
<div class="login-shape shape2"></div>
|
||||
@@ -80,22 +88,22 @@ import {
|
||||
NIcon,
|
||||
NText,
|
||||
useMessage,
|
||||
useNotification
|
||||
} from 'naive-ui';
|
||||
import type { FormInst, FormRules } from 'naive-ui';
|
||||
import {
|
||||
BarChartOutline as BarChartIcon,
|
||||
LockClosedOutline as LockClosedIcon,
|
||||
NotificationsOutline as NotificationsIcon
|
||||
} from '@vicons/ionicons5';
|
||||
import { apiService } from '@/services/api';
|
||||
import type { LoginRequest } from '@/types';
|
||||
import AnnouncementBanner from '@/components/AnnouncementBanner.vue';
|
||||
|
||||
const message = useMessage();
|
||||
const notification = useNotification();
|
||||
const router = useRouter();
|
||||
const formRef = ref<FormInst | null>(null);
|
||||
const loading = ref(false);
|
||||
const announcement = ref('');
|
||||
const showAnnouncementBanner = ref(true);
|
||||
|
||||
const formValue = reactive({
|
||||
password: ''
|
||||
@@ -114,16 +122,14 @@ const rules: FormRules = {
|
||||
const showAnnouncement = (content: string) => {
|
||||
if (!content) return;
|
||||
|
||||
notification.info({
|
||||
title: '系统公告',
|
||||
content: () => h('div', { style: 'display: flex; align-items: center;' }, [
|
||||
h(NIcon, { component: NotificationsIcon, style: 'margin-right: 8px; font-size: 18px;' }),
|
||||
h('span', null, content)
|
||||
]),
|
||||
duration: 10000,
|
||||
keepAliveOnHover: true,
|
||||
closable: true
|
||||
});
|
||||
// 使用AnnouncementBanner组件显示公告
|
||||
announcement.value = content;
|
||||
showAnnouncementBanner.value = true;
|
||||
};
|
||||
|
||||
// 处理公告关闭事件
|
||||
const handleAnnouncementClose = () => {
|
||||
showAnnouncementBanner.value = false;
|
||||
};
|
||||
|
||||
// 页面加载时检查是否已登录并获取系统公告
|
||||
@@ -265,6 +271,11 @@ html, body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 确保公告在登录页面上方显示 */
|
||||
:deep(.announcement-container) {
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.login-background {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
|
||||
@@ -1,65 +1,95 @@
|
||||
<template>
|
||||
<n-card class="market-time-card">
|
||||
<n-grid :x-gap="16" :y-gap="16" :cols="gridCols">
|
||||
<n-card class="market-time-card mobile-card mobile-shadow">
|
||||
<n-grid :x-gap="16" :y-gap="16" :cols="gridCols" responsive="screen">
|
||||
<!-- 当前时间 -->
|
||||
<n-grid-item>
|
||||
<div class="time-block">
|
||||
<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>
|
||||
</div>
|
||||
</n-grid-item>
|
||||
|
||||
<!-- A股状态 -->
|
||||
<n-grid-item>
|
||||
<div class="time-block">
|
||||
<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>
|
||||
<div class="market-status" :class="marketInfo.cnMarket.isOpen ? 'status-open' : 'status-closed'">
|
||||
<n-tag v-if="marketInfo.cnMarket.isOpen" type="success" size="medium" round>
|
||||
<n-tag v-if="marketInfo.cnMarket.isOpen" type="success" size="medium" round class="status-tag mobile-touch-target">
|
||||
<template #icon><n-icon size="18"><pulse-icon /></n-icon></template>
|
||||
交易中
|
||||
</n-tag>
|
||||
<n-tag v-else type="default" size="medium" round>
|
||||
<n-tag v-else type="default" size="medium" round class="status-tag mobile-touch-target">
|
||||
<template #icon><n-icon size="18"><time-icon /></n-icon></template>
|
||||
已休市
|
||||
</n-tag>
|
||||
</div>
|
||||
<p class="time-counter">{{ marketInfo.cnMarket.nextTime }}</p>
|
||||
<div class="market-progress-container">
|
||||
<div class="market-progress-bar"
|
||||
:class="marketInfo.cnMarket.isOpen ? 'progress-open' : 'progress-closed'"
|
||||
:style="{ width: marketInfo.cnMarket.progressPercentage + '%' }">
|
||||
</div>
|
||||
<div class="progress-markers" :class="{'reverse-markers': !marketInfo.cnMarket.isOpen}">
|
||||
<div class="progress-marker" :class="marketInfo.cnMarket.isOpen ? 'start' : 'end'">开盘</div>
|
||||
<div class="progress-marker" :class="marketInfo.cnMarket.isOpen ? 'end' : 'start'">收盘</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-grid-item>
|
||||
|
||||
<!-- 港股状态 -->
|
||||
<n-grid-item>
|
||||
<div class="time-block">
|
||||
<n-grid-item :span="24" :md-span="6">
|
||||
<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'">
|
||||
<n-tag v-if="marketInfo.hkMarket.isOpen" type="success" size="medium" round>
|
||||
<n-tag v-if="marketInfo.hkMarket.isOpen" type="success" size="medium" round class="status-tag mobile-touch-target">
|
||||
<template #icon><n-icon size="18"><pulse-icon /></n-icon></template>
|
||||
交易中
|
||||
</n-tag>
|
||||
<n-tag v-else type="default" size="medium" round>
|
||||
<n-tag v-else type="default" size="medium" round class="status-tag mobile-touch-target">
|
||||
<template #icon><n-icon size="18"><time-icon /></n-icon></template>
|
||||
已休市
|
||||
</n-tag>
|
||||
</div>
|
||||
<p class="time-counter">{{ marketInfo.hkMarket.nextTime }}</p>
|
||||
<div class="market-progress-container">
|
||||
<div class="market-progress-bar"
|
||||
:class="marketInfo.hkMarket.isOpen ? 'progress-open' : 'progress-closed'"
|
||||
:style="{ width: marketInfo.hkMarket.progressPercentage + '%' }">
|
||||
</div>
|
||||
<div class="progress-markers" :class="{'reverse-markers': !marketInfo.hkMarket.isOpen}">
|
||||
<div class="progress-marker" :class="marketInfo.hkMarket.isOpen ? 'start' : 'end'">开盘</div>
|
||||
<div class="progress-marker" :class="marketInfo.hkMarket.isOpen ? 'end' : 'start'">收盘</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-grid-item>
|
||||
|
||||
<!-- 美股状态 -->
|
||||
<n-grid-item>
|
||||
<div class="time-block">
|
||||
<n-grid-item :span="24" :md-span="6">
|
||||
<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'">
|
||||
<n-tag v-if="marketInfo.usMarket.isOpen" type="success" size="medium" round>
|
||||
<n-tag v-if="marketInfo.usMarket.isOpen" type="success" size="medium" round class="status-tag mobile-touch-target">
|
||||
<template #icon><n-icon size="18"><pulse-icon /></n-icon></template>
|
||||
交易中
|
||||
</n-tag>
|
||||
<n-tag v-else type="default" size="medium" round>
|
||||
<n-tag v-else type="default" size="medium" round class="status-tag mobile-touch-target">
|
||||
<template #icon><n-icon size="18"><time-icon /></n-icon></template>
|
||||
已休市
|
||||
</n-tag>
|
||||
</div>
|
||||
<p class="time-counter">{{ marketInfo.usMarket.nextTime }}</p>
|
||||
<div class="market-progress-container">
|
||||
<div class="market-progress-bar"
|
||||
:class="marketInfo.usMarket.isOpen ? 'progress-open' : 'progress-closed'"
|
||||
:style="{ width: marketInfo.usMarket.progressPercentage + '%' }">
|
||||
</div>
|
||||
<div class="progress-markers" :class="{'reverse-markers': !marketInfo.usMarket.isOpen}">
|
||||
<div class="progress-marker" :class="marketInfo.usMarket.isOpen ? 'start' : 'end'">开盘</div>
|
||||
<div class="progress-marker" :class="marketInfo.usMarket.isOpen ? 'end' : 'start'">收盘</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
@@ -71,10 +101,10 @@ import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { NCard, NGrid, NGridItem, NTag, NIcon } from 'naive-ui';
|
||||
import {
|
||||
PulseOutline as PulseIcon,
|
||||
TimeOutline as TimeIcon
|
||||
TimeOutline as TimeIcon,
|
||||
} from '@vicons/ionicons5';
|
||||
import { updateMarketTimeInfo } from '@/utils';
|
||||
import type { MarketTimeInfo } from '@/types';
|
||||
import type { MarketTimeInfo, MarketStatus } from '@/types';
|
||||
|
||||
const props = defineProps({
|
||||
isMobile: {
|
||||
@@ -90,14 +120,121 @@ const marketInfo = ref<MarketTimeInfo>({
|
||||
usMarket: { isOpen: false, nextTime: '' }
|
||||
});
|
||||
|
||||
// 根据屏幕尺寸自动调整布局
|
||||
const gridCols = computed(() => {
|
||||
return props.isMobile ? 1 : 4;
|
||||
return 24;
|
||||
});
|
||||
|
||||
let intervalId: number | null = null;
|
||||
|
||||
function updateMarketTime() {
|
||||
marketInfo.value = updateMarketTimeInfo();
|
||||
const baseInfo = updateMarketTimeInfo();
|
||||
|
||||
// 计算各市场的进度百分比
|
||||
marketInfo.value = {
|
||||
currentTime: baseInfo.currentTime,
|
||||
cnMarket: {
|
||||
...baseInfo.cnMarket,
|
||||
progressPercentage: calculateProgressPercentage(baseInfo.cnMarket)
|
||||
},
|
||||
hkMarket: {
|
||||
...baseInfo.hkMarket,
|
||||
progressPercentage: calculateProgressPercentage(baseInfo.hkMarket)
|
||||
},
|
||||
usMarket: {
|
||||
...baseInfo.usMarket,
|
||||
progressPercentage: calculateProgressPercentage(baseInfo.usMarket)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 计算进度百分比的函数
|
||||
function calculateProgressPercentage(market: MarketStatus): number {
|
||||
// 从nextTime中提取时间信息来计算进度
|
||||
const timeText = market.nextTime;
|
||||
|
||||
// 如果没有时间文本,返回默认值50%
|
||||
if (!timeText) return 50;
|
||||
|
||||
try {
|
||||
// 特殊情况处理
|
||||
if (timeText.includes("已休市") || timeText.includes("已闭市")) {
|
||||
return market.isOpen ? 100 : 0; // 休市状态:开市时为100%,休市时为0%
|
||||
}
|
||||
|
||||
if (timeText.includes("即将开市") || timeText.includes("即将开盘")) {
|
||||
return market.isOpen ? 5 : 95; // 即将开市:开市时为5%,休市时为95%
|
||||
}
|
||||
|
||||
// 提取小时和分钟,支持多种格式
|
||||
let hours = 0;
|
||||
let minutes = 0;
|
||||
|
||||
// 匹配"XX小时XX分钟"格式
|
||||
const hourMinuteMatch = timeText.match(/(\d+)\s*小时\s*(\d+)\s*分钟/);
|
||||
if (hourMinuteMatch) {
|
||||
hours = parseInt(hourMinuteMatch[1]);
|
||||
minutes = parseInt(hourMinuteMatch[2]);
|
||||
} else {
|
||||
// 单独匹配小时和分钟
|
||||
const hourMatch = timeText.match(/(\d+)\s*小时/);
|
||||
const minuteMatch = timeText.match(/(\d+)\s*分钟/);
|
||||
|
||||
hours = hourMatch ? parseInt(hourMatch[1]) : 0;
|
||||
minutes = minuteMatch ? parseInt(minuteMatch[1]) : 0;
|
||||
}
|
||||
|
||||
// 总分钟数
|
||||
const totalMinutes = hours * 60 + minutes;
|
||||
|
||||
// 根据市场类型设置不同的交易时长
|
||||
let tradingMinutes = 240; // 默认交易时长4小时
|
||||
let nonTradingMinutes = 1200; // 默认非交易时长20小时
|
||||
|
||||
// 根据市场调整时长
|
||||
if (timeText.includes("A股") || timeText.includes("沪深") ||
|
||||
(!timeText.includes("港股") && !timeText.includes("美股"))) {
|
||||
tradingMinutes = 240; // A股交易4小时
|
||||
nonTradingMinutes = 1200; // 非交易20小时
|
||||
} else if (timeText.includes("港股")) {
|
||||
tradingMinutes = 390; // 港股交易6.5小时
|
||||
nonTradingMinutes = 1050; // 非交易17.5小时
|
||||
} else if (timeText.includes("美股")) {
|
||||
tradingMinutes = 390; // 美股交易6.5小时
|
||||
nonTradingMinutes = 1050; // 非交易17.5小时
|
||||
}
|
||||
|
||||
// 根据市场状态计算进度
|
||||
if (market.isOpen) {
|
||||
// 市场开市状态 - 从开盘到收盘方向
|
||||
if (timeText.includes("距离收市") || timeText.includes("距离闭市") ||
|
||||
timeText.includes("距离休市") || timeText.includes("距离收盘")) {
|
||||
// 计算已经交易的时间比例
|
||||
const tradedMinutes = tradingMinutes - totalMinutes;
|
||||
const percentage = (tradedMinutes / tradingMinutes) * 100;
|
||||
return Math.max(0, Math.min(100, percentage));
|
||||
} else {
|
||||
// 处理交易开始阶段但没有明确提示的情况
|
||||
return 5; // 开盘初期设为5%
|
||||
}
|
||||
} else {
|
||||
// 市场休市状态 - 从收盘到开盘方向
|
||||
if (timeText.includes("距离开市") || timeText.includes("距离开盘")) {
|
||||
// 计算接近开盘的时间比例
|
||||
const closedMinutes = nonTradingMinutes - totalMinutes;
|
||||
const percentage = (closedMinutes / nonTradingMinutes) * 100;
|
||||
// 反转比例:0% 表示刚刚休市,100% 表示即将开盘
|
||||
return Math.max(0, Math.min(100, 100 - percentage));
|
||||
} else {
|
||||
// 处理休市开始阶段但没有明确提示的情况
|
||||
return 5; // 刚休市设为5%
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("计算市场进度时出错:", error);
|
||||
// 出错时返回默认值
|
||||
return market.isOpen ? 50 : 5;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -116,7 +253,10 @@ onBeforeUnmount(() => {
|
||||
<style scoped>
|
||||
.market-time-card {
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
background: linear-gradient(to bottom, rgba(250, 250, 252, 0.8), rgba(245, 245, 250, 0.5));
|
||||
}
|
||||
|
||||
.time-block {
|
||||
@@ -124,7 +264,32 @@ onBeforeUnmount(() => {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.625rem;
|
||||
transition: all 0.3s ease;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.current-time-block {
|
||||
background-color: rgba(32, 128, 240, 0.05);
|
||||
border: 1px solid rgba(32, 128, 240, 0.1);
|
||||
}
|
||||
|
||||
.market-block {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.market-open-block {
|
||||
background-color: rgba(24, 160, 88, 0.05);
|
||||
border-color: rgba(24, 160, 88, 0.1);
|
||||
}
|
||||
|
||||
.market-closed-block {
|
||||
background-color: rgba(128, 128, 128, 0.05);
|
||||
border-color: rgba(128, 128, 128, 0.1);
|
||||
}
|
||||
|
||||
.time-label {
|
||||
@@ -145,13 +310,18 @@ onBeforeUnmount(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 32px;
|
||||
min-height: 36px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.market-status :deep(.n-tag) {
|
||||
padding: 0 12px;
|
||||
height: 32px;
|
||||
.status-tag {
|
||||
padding: 0 16px !important;
|
||||
height: 36px !important;
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.market-status :deep(.n-tag__icon) {
|
||||
@@ -159,13 +329,14 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.status-open :deep(.n-tag) {
|
||||
background-color: rgba(var(--success-color), 0.15);
|
||||
background-color: rgba(24, 160, 88, 0.15);
|
||||
border: 1px solid var(--n-success-color);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status-closed :deep(.n-tag) {
|
||||
background-color: rgba(var(--n-text-color-3), 0.1);
|
||||
background-color: rgba(128, 128, 128, 0.1);
|
||||
border: 1px solid rgba(128, 128, 128, 0.3);
|
||||
}
|
||||
|
||||
.time-counter {
|
||||
@@ -174,15 +345,232 @@ onBeforeUnmount(() => {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
/* 进度条样式 */
|
||||
.market-progress-container {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background-color: rgba(200, 200, 200, 0.3);
|
||||
border-radius: 3px;
|
||||
margin-top: 0.75rem;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(200, 200, 200, 0.4);
|
||||
}
|
||||
|
||||
.market-progress-bar {
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
transition: width 0.5s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.market-progress-bar::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(90deg,
|
||||
rgba(255, 255, 255, 0.15) 0%,
|
||||
rgba(255, 255, 255, 0.4) 50%,
|
||||
rgba(255, 255, 255, 0.15) 100%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
.progress-open {
|
||||
background-color: rgba(24, 160, 88, 0.9);
|
||||
box-shadow: 0 0 8px rgba(24, 160, 88, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
||||
border: 1px solid rgba(24, 160, 88, 1);
|
||||
}
|
||||
|
||||
.progress-closed {
|
||||
background-color: rgba(100, 100, 100, 0.8);
|
||||
box-shadow: 0 0 5px rgba(100, 100, 100, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(80, 80, 80, 1);
|
||||
}
|
||||
|
||||
/* 进度条标记 */
|
||||
.progress-markers {
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.75rem;
|
||||
color: var(--n-text-color-2);
|
||||
padding: 0 2px;
|
||||
font-weight: 500;
|
||||
text-shadow: 0 1px 1px rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
/* 反向标记(休市状态) */
|
||||
.reverse-markers {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.progress-marker {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(var(--success-color), 0.4);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 6px rgba(var(--success-color), 0);
|
||||
background-position: 200% 0;
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(var(--success-color), 0);
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(24, 160, 88, 0.4);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 6px rgba(24, 160, 88, 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(24, 160, 88, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
@media (max-width: 768px) {
|
||||
.market-time-card {
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.time-block {
|
||||
padding: 0.625rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.current-time {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.time-label {
|
||||
font-size: 0.9375rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
min-width: 120px;
|
||||
height: 40px !important;
|
||||
}
|
||||
|
||||
/* 增强视觉层次 */
|
||||
.market-open-block::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background-color: var(--n-success-color);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.market-closed-block::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background-color: rgba(128, 128, 128, 0.5);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.market-progress-container {
|
||||
height: 5px;
|
||||
margin-top: 0.5rem;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.progress-markers {
|
||||
top: -20px;
|
||||
font-size: 0.6875rem;
|
||||
}
|
||||
|
||||
.progress-marker.start::before,
|
||||
.progress-marker.end::before {
|
||||
top: -10px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
/* 增强移动端进度条可见性 */
|
||||
.progress-open {
|
||||
background-color: rgba(24, 160, 88, 1);
|
||||
box-shadow: 0 0 6px rgba(24, 160, 88, 0.6);
|
||||
}
|
||||
|
||||
.progress-closed {
|
||||
background-color: rgba(90, 90, 90, 0.9);
|
||||
box-shadow: 0 0 4px rgba(90, 90, 90, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
/* 小屏幕手机适配 */
|
||||
@media (max-width: 480px) {
|
||||
.market-time-card {
|
||||
padding: 0.375rem;
|
||||
}
|
||||
|
||||
.time-block {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.current-time {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.time-label {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.time-counter {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
min-width: 100px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* 确保边框在小屏幕上清晰可见 */
|
||||
.time-block {
|
||||
border-width: 1px !important;
|
||||
}
|
||||
|
||||
.market-progress-container {
|
||||
height: 4px;
|
||||
margin-top: 0.375rem;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.progress-markers {
|
||||
top: -20px;
|
||||
font-size: 0.625rem;
|
||||
}
|
||||
|
||||
.progress-marker.start::before,
|
||||
.progress-marker.end::before {
|
||||
top: -8px;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
/* 进一步增强小屏幕进度条可见性 */
|
||||
.market-progress-container {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.progress-open, .progress-closed {
|
||||
border-width: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
24
frontend/src/components/SafeAreaBottom.vue
Normal file
24
frontend/src/components/SafeAreaBottom.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="safe-area-bottom"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 底部安全区域组件,用于解决底部背景显示问题
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.safe-area-bottom {
|
||||
width: 100%;
|
||||
height: env(safe-area-inset-bottom, 0);
|
||||
min-height: 10px; /* 确保在不支持env()的浏览器上也有一定的空间 */
|
||||
background-color: inherit;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.safe-area-bottom {
|
||||
min-height: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,10 +1,18 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div class="app-container mobile-bottom-extend">
|
||||
<!-- 公告横幅 -->
|
||||
<AnnouncementBanner
|
||||
v-if="announcement && showAnnouncementBanner"
|
||||
:content="announcement"
|
||||
:auto-close-time="5"
|
||||
@close="handleAnnouncementClose"
|
||||
/>
|
||||
|
||||
<n-layout class="main-layout">
|
||||
<n-layout-content class="main-content">
|
||||
<n-layout-content class="main-content mobile-content-container">
|
||||
|
||||
<!-- 市场时间显示 -->
|
||||
<MarketTimeDisplay />
|
||||
<MarketTimeDisplay :is-mobile="isMobile" />
|
||||
|
||||
<!-- API配置面板 -->
|
||||
<ApiConfigPanel
|
||||
@@ -15,11 +23,11 @@
|
||||
/>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<n-card class="analysis-container">
|
||||
<n-card class="analysis-container mobile-card mobile-card-spacing mobile-shadow">
|
||||
|
||||
<n-grid :cols="24" :x-gap="16" :y-gap="16">
|
||||
<n-grid :cols="24" :x-gap="16" :y-gap="16" responsive="screen">
|
||||
<!-- 左侧配置区域 -->
|
||||
<n-grid-item :span="24" :lg-span="8">
|
||||
<n-grid-item :span="24" :sm-span="24" :md-span="10" :lg-span="8">
|
||||
<div class="config-section">
|
||||
<n-form-item label="选择市场类型">
|
||||
<n-select
|
||||
@@ -63,7 +71,7 @@
|
||||
</n-grid-item>
|
||||
|
||||
<!-- 右侧结果区域 -->
|
||||
<n-grid-item :span="24" :lg-span="16">
|
||||
<n-grid-item :span="24" :sm-span="24" :md-span="14" :lg-span="16">
|
||||
<div class="results-section">
|
||||
<div class="results-header">
|
||||
<n-space align="center" justify="space-between">
|
||||
@@ -135,13 +143,16 @@
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</n-card>
|
||||
|
||||
<!-- 底部安全区域 -->
|
||||
<SafeAreaBottom />
|
||||
</n-layout-content>
|
||||
</n-layout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, h } from 'vue';
|
||||
import { ref, onMounted, h, computed, onBeforeUnmount } from 'vue';
|
||||
import {
|
||||
NLayout,
|
||||
NLayoutContent,
|
||||
@@ -173,6 +184,8 @@ import MarketTimeDisplay from './MarketTimeDisplay.vue';
|
||||
import ApiConfigPanel from './ApiConfigPanel.vue';
|
||||
import StockSearch from './StockSearch.vue';
|
||||
import StockCard from './StockCard.vue';
|
||||
import SafeAreaBottom from './SafeAreaBottom.vue';
|
||||
import AnnouncementBanner from './AnnouncementBanner.vue';
|
||||
|
||||
import { apiService } from '@/services/api';
|
||||
import type { StockInfo, ApiConfig, StreamInitMessage, StreamAnalysisUpdate } from '@/types';
|
||||
@@ -189,6 +202,7 @@ const defaultApiUrl = ref('');
|
||||
const defaultApiModel = ref('');
|
||||
const defaultApiTimeout = ref('60');
|
||||
const announcement = ref('');
|
||||
const showAnnouncementBanner = ref(true);
|
||||
|
||||
// 股票分析配置
|
||||
const marketType = ref('A');
|
||||
@@ -206,20 +220,24 @@ const apiConfig = ref<ApiConfig>({
|
||||
saveApiConfig: false
|
||||
});
|
||||
|
||||
// 移动端检测
|
||||
const isMobile = computed(() => {
|
||||
return window.innerWidth <= 768;
|
||||
});
|
||||
|
||||
// 监听窗口大小变化
|
||||
function handleResize() {
|
||||
// 窗口大小变化时,isMobile计算属性会自动更新
|
||||
// 这里可以添加其他需要在窗口大小变化时执行的逻辑
|
||||
}
|
||||
|
||||
// 显示系统公告
|
||||
const showAnnouncement = (content: string) => {
|
||||
if (!content) return;
|
||||
|
||||
notification.info({
|
||||
title: '系统公告',
|
||||
content: () => h('div', { style: 'display: flex; align-items: center;' }, [
|
||||
h(NIcon, { component: NotificationsIcon, style: 'margin-right: 8px; font-size: 18px;' }),
|
||||
h('span', null, content)
|
||||
]),
|
||||
duration: 0, // 设置为0表示不会自动关闭
|
||||
keepAliveOnHover: true,
|
||||
closable: true
|
||||
});
|
||||
// 使用AnnouncementBanner组件显示公告
|
||||
announcement.value = content;
|
||||
showAnnouncementBanner.value = true;
|
||||
};
|
||||
|
||||
// 市场选项
|
||||
@@ -911,6 +929,9 @@ function getChineseVolumeStatus(status: string): string {
|
||||
// 页面加载时获取默认配置和公告
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// 添加窗口大小变化监听
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// 从API获取配置信息
|
||||
const config = await apiService.getConfig();
|
||||
|
||||
@@ -938,6 +959,16 @@ onMounted(async () => {
|
||||
console.error('获取默认配置时出错:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// 组件销毁前移除事件监听
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
// 处理公告关闭事件
|
||||
function handleAnnouncementClose() {
|
||||
showAnnouncementBanner.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -969,7 +1000,7 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.analysis-container {
|
||||
margin-bottom: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.config-section {
|
||||
@@ -998,4 +1029,95 @@ onMounted(async () => {
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* 移动端适配的媒体查询 */
|
||||
@media (max-width: 768px) {
|
||||
.main-content {
|
||||
padding: 0.5rem;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.action-buttons .n-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.analysis-container {
|
||||
margin-bottom: 0.75rem;
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.config-section, .results-section {
|
||||
padding: 0.25rem;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 确保表格内容在移动端可滚动 */
|
||||
.n-data-table-wrapper {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
width: 100%;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
/* 改善表单项在移动端的间距 */
|
||||
: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;
|
||||
}
|
||||
}
|
||||
|
||||
/* 小屏幕手机适配 */
|
||||
@media (max-width: 480px) {
|
||||
.main-content {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.results-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
:deep(.n-space) {
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
:deep(.n-space .n-button) {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.analysis-container {
|
||||
border-radius: 0.625rem;
|
||||
}
|
||||
|
||||
/* 确保下拉菜单在小屏幕上正确显示 */
|
||||
:deep(.n-dropdown-menu) {
|
||||
max-width: 90vw;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<n-card class="stock-card" :bordered="false" :class="{ 'is-analyzing': isAnalyzing }">
|
||||
<n-card class="stock-card mobile-card mobile-shadow" :bordered="false" :class="{ 'is-analyzing': isAnalyzing }">
|
||||
<div class="card-header">
|
||||
<div class="header-main">
|
||||
<div class="header-left">
|
||||
@@ -1010,4 +1010,133 @@ const getStatusText = computed(() => {
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 移动端适配样式 */
|
||||
@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 {
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.header-main {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
margin-top: 0.5rem;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.stock-price-info {
|
||||
flex-direction: row;
|
||||
margin-top: 0.5rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.stock-summary {
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.indicators-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
padding: 0.5rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.actions-bar {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
width: 100%;
|
||||
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;
|
||||
}
|
||||
|
||||
.analysis-result :deep(pre) {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.analysis-result :deep(table) {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
width: 100%;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.indicator-item {
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.625rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 小屏幕手机适配 */
|
||||
@media (max-width: 480px) {
|
||||
.stock-card {
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: 0.625rem !important;
|
||||
}
|
||||
|
||||
.stock-info {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.stock-name {
|
||||
margin-left: 0;
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.indicators-grid {
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
}
|
||||
|
||||
.indicator-item {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.analysis-result {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
margin: 0.375rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 0.625rem;
|
||||
}
|
||||
|
||||
/* 确保边框在小屏幕上清晰可见 */
|
||||
.stock-card, .indicator-item, .analysis-result {
|
||||
border: 1px solid rgba(0, 0, 0, 0.08) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -219,4 +219,73 @@ onBeforeUnmount(() => {
|
||||
.result-market-value {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
@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) {
|
||||
.result-symbol-name, .result-meta {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.result-name, .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>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import './assets/css/global.css'
|
||||
import './assets/styles/mobile.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface SearchResult {
|
||||
export interface MarketStatus {
|
||||
isOpen: boolean;
|
||||
nextTime: string;
|
||||
progressPercentage?: number;
|
||||
}
|
||||
|
||||
export interface MarketTimeInfo {
|
||||
|
||||
Reference in New Issue
Block a user