feat: 优化手机端显示效果

This commit is contained in:
Cassianvale
2025-03-11 13:05:42 +08:00
parent c6165c2587
commit a4a93973d2
12 changed files with 1169 additions and 77 deletions

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

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

View File

@@ -1,6 +1,6 @@
<template> <template>
<div v-if="showAnnouncement" class="announcement-container"> <div v-if="showAnnouncement" class="announcement-container" :class="{ 'login-page-announcement': isLoginPage }">
<n-card class="announcement-card"> <n-card class="announcement-card mobile-card" :class="{ 'login-card-style': isLoginPage }">
<template #header> <template #header>
<div class="announcement-header"> <div class="announcement-header">
<n-icon size="18" :component="InformationCircleIcon" class="info-icon" /> <n-icon size="18" :component="InformationCircleIcon" class="info-icon" />
@@ -10,7 +10,7 @@
<div class="announcement-content" v-html="processedContent"></div> <div class="announcement-content" v-html="processedContent"></div>
<div class="announcement-timer">{{ remainingTimeText }}</div> <div class="announcement-timer">{{ remainingTimeText }}</div>
<template #action> <template #action>
<n-button quaternary circle size="small" @click="closeAnnouncement"> <n-button quaternary circle size="small" @click="closeAnnouncement" class="mobile-touch-target">
<template #icon> <template #icon>
<n-icon :component="CloseIcon" /> <n-icon :component="CloseIcon" />
</template> </template>
@@ -25,12 +25,20 @@ import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import { NCard, NIcon, NButton } from 'naive-ui'; import { NCard, NIcon, NButton } from 'naive-ui';
import { InformationCircleOutline as InformationCircleIcon } from '@vicons/ionicons5'; import { InformationCircleOutline as InformationCircleIcon } from '@vicons/ionicons5';
import { Close as CloseIcon } from '@vicons/ionicons5'; import { Close as CloseIcon } from '@vicons/ionicons5';
import { useRoute } from 'vue-router';
const props = defineProps<{ const props = defineProps<{
content: string; content: string;
autoCloseTime?: number; autoCloseTime?: number;
}>(); }>();
const emit = defineEmits<{
(e: 'close'): void;
}>();
const route = useRoute();
const isLoginPage = computed(() => route.path === '/login');
const showAnnouncement = ref(true); const showAnnouncement = ref(true);
const remainingTime = ref(props.autoCloseTime || 5); const remainingTime = ref(props.autoCloseTime || 5);
const timer = ref<number | null>(null); const timer = ref<number | null>(null);
@@ -54,6 +62,7 @@ function closeAnnouncement() {
window.clearInterval(timer.value); window.clearInterval(timer.value);
timer.value = null; timer.value = null;
} }
emit('close');
} }
function updateTimer() { function updateTimer() {
@@ -87,6 +96,11 @@ onBeforeUnmount(() => {
.announcement-card { .announcement-card {
border-left: 4px solid var(--n-primary-color); 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 { .announcement-header {
@@ -125,4 +139,69 @@ onBeforeUnmount(() => {
transform: translateY(0); 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> </style>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="api-config-section"> <div class="api-config-section">
<n-button <n-button
class="toggle-button" class="toggle-button mobile-touch-target"
size="small" size="small"
@click="toggleConfig" @click="toggleConfig"
:quaternary="true" :quaternary="true"
@@ -14,7 +14,7 @@
</n-button> </n-button>
<n-collapse-transition :show="expanded"> <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"> <n-alert title="OpenAI API配置" type="info" v-if="isApiInfoVisible" class="api-info-alert">
<template #icon> <template #icon>
<n-icon :component="InformationCircleIcon" /> <n-icon :component="InformationCircleIcon" />
@@ -29,8 +29,8 @@
</div> </div>
</n-alert> </n-alert>
<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="14"> <n-grid-item :span="24" :md-span="14" :lg-span="14">
<n-form-item label="API URL" path="apiUrl"> <n-form-item label="API URL" path="apiUrl">
<n-input <n-input
v-model:value="apiConfig.apiUrl" v-model:value="apiConfig.apiUrl"
@@ -54,7 +54,7 @@
</n-form-item> </n-form-item>
</n-grid-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-form-item label="API Key" path="apiKey">
<n-input <n-input
v-model:value="apiConfig.apiKey" v-model:value="apiConfig.apiKey"
@@ -71,7 +71,7 @@
</n-form-item> </n-form-item>
</n-grid-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-form-item label="模型" path="apiModel">
<n-input <n-input
v-model:value="apiConfig.apiModel" v-model:value="apiConfig.apiModel"
@@ -118,7 +118,7 @@
</n-form-item> </n-form-item>
</n-grid-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-form-item label="超时时间(秒)" path="apiTimeout">
<n-input-number <n-input-number
v-model:value="apiTimeout" v-model:value="apiTimeout"
@@ -612,11 +612,113 @@ onMounted(() => {
.api-actions { .api-actions {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
width: 100%;
} }
.api-buttons { .api-buttons {
width: 100%; width: 100%;
justify-content: space-between; 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;
} }
} }

View File

@@ -1,5 +1,13 @@
<template> <template>
<div class="login-container"> <div class="login-container">
<!-- 公告横幅 -->
<AnnouncementBanner
v-if="announcement && showAnnouncementBanner"
:content="announcement"
:auto-close-time="5"
@close="handleAnnouncementClose"
/>
<div class="login-background"> <div class="login-background">
<div class="login-shape shape1"></div> <div class="login-shape shape1"></div>
<div class="login-shape shape2"></div> <div class="login-shape shape2"></div>
@@ -80,22 +88,22 @@ import {
NIcon, NIcon,
NText, NText,
useMessage, useMessage,
useNotification
} from 'naive-ui'; } from 'naive-ui';
import type { FormInst, FormRules } from 'naive-ui'; import type { FormInst, FormRules } from 'naive-ui';
import { import {
BarChartOutline as BarChartIcon, BarChartOutline as BarChartIcon,
LockClosedOutline as LockClosedIcon, LockClosedOutline as LockClosedIcon,
NotificationsOutline as NotificationsIcon
} from '@vicons/ionicons5'; } from '@vicons/ionicons5';
import { apiService } from '@/services/api'; import { apiService } from '@/services/api';
import type { LoginRequest } from '@/types'; import type { LoginRequest } from '@/types';
import AnnouncementBanner from '@/components/AnnouncementBanner.vue';
const message = useMessage(); const message = useMessage();
const notification = useNotification();
const router = useRouter(); const router = useRouter();
const formRef = ref<FormInst | null>(null); const formRef = ref<FormInst | null>(null);
const loading = ref(false); const loading = ref(false);
const announcement = ref('');
const showAnnouncementBanner = ref(true);
const formValue = reactive({ const formValue = reactive({
password: '' password: ''
@@ -114,16 +122,14 @@ const rules: FormRules = {
const showAnnouncement = (content: string) => { const showAnnouncement = (content: string) => {
if (!content) return; if (!content) return;
notification.info({ // 使用AnnouncementBanner组件显示公告
title: '系统公告', announcement.value = content;
content: () => h('div', { style: 'display: flex; align-items: center;' }, [ showAnnouncementBanner.value = true;
h(NIcon, { component: NotificationsIcon, style: 'margin-right: 8px; font-size: 18px;' }), };
h('span', null, content)
]), // 处理公告关闭事件
duration: 10000, const handleAnnouncementClose = () => {
keepAliveOnHover: true, showAnnouncementBanner.value = false;
closable: true
});
}; };
// 页面加载时检查是否已登录并获取系统公告 // 页面加载时检查是否已登录并获取系统公告
@@ -265,6 +271,11 @@ html, body {
overflow: hidden; overflow: hidden;
} }
/* 确保公告在登录页面上方显示 */
:deep(.announcement-container) {
z-index: 100;
}
.login-background { .login-background {
position: absolute; position: absolute;
width: 100%; width: 100%;

View File

@@ -1,65 +1,95 @@
<template> <template>
<n-card class="market-time-card"> <n-card class="market-time-card mobile-card mobile-shadow">
<n-grid :x-gap="16" :y-gap="16" :cols="gridCols"> <n-grid :x-gap="16" :y-gap="16" :cols="gridCols" responsive="screen">
<!-- 当前时间 --> <!-- 当前时间 -->
<n-grid-item> <n-grid-item :span="24" :md-span="6">
<div class="time-block"> <div class="time-block current-time-block">
<p class="time-label">当前时间</p> <p class="time-label">当前时间</p>
<p class="current-time">{{ marketInfo.currentTime }}</p> <p class="current-time">{{ marketInfo.currentTime }}</p>
</div> </div>
</n-grid-item> </n-grid-item>
<!-- A股状态 --> <!-- A股状态 -->
<n-grid-item> <n-grid-item :span="24" :md-span="6">
<div class="time-block"> <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> <p class="time-label">A股市场</p>
<div class="market-status" :class="marketInfo.cnMarket.isOpen ? 'status-open' : 'status-closed'"> <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> <template #icon><n-icon size="18"><pulse-icon /></n-icon></template>
交易中 交易中
</n-tag> </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> <template #icon><n-icon size="18"><time-icon /></n-icon></template>
已休市 已休市
</n-tag> </n-tag>
</div> </div>
<p class="time-counter">{{ marketInfo.cnMarket.nextTime }}</p> <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> </div>
</n-grid-item> </n-grid-item>
<!-- 港股状态 --> <!-- 港股状态 -->
<n-grid-item> <n-grid-item :span="24" :md-span="6">
<div class="time-block"> <div class="time-block market-block" :class="{'market-open-block': marketInfo.hkMarket.isOpen, 'market-closed-block': !marketInfo.hkMarket.isOpen}">
<p class="time-label">港股市场</p> <p class="time-label">港股市场</p>
<div class="market-status" :class="marketInfo.hkMarket.isOpen ? 'status-open' : 'status-closed'"> <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> <template #icon><n-icon size="18"><pulse-icon /></n-icon></template>
交易中 交易中
</n-tag> </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> <template #icon><n-icon size="18"><time-icon /></n-icon></template>
已休市 已休市
</n-tag> </n-tag>
</div> </div>
<p class="time-counter">{{ marketInfo.hkMarket.nextTime }}</p> <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> </div>
</n-grid-item> </n-grid-item>
<!-- 美股状态 --> <!-- 美股状态 -->
<n-grid-item> <n-grid-item :span="24" :md-span="6">
<div class="time-block"> <div class="time-block market-block" :class="{'market-open-block': marketInfo.usMarket.isOpen, 'market-closed-block': !marketInfo.usMarket.isOpen}">
<p class="time-label">美股市场</p> <p class="time-label">美股市场</p>
<div class="market-status" :class="marketInfo.usMarket.isOpen ? 'status-open' : 'status-closed'"> <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> <template #icon><n-icon size="18"><pulse-icon /></n-icon></template>
交易中 交易中
</n-tag> </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> <template #icon><n-icon size="18"><time-icon /></n-icon></template>
已休市 已休市
</n-tag> </n-tag>
</div> </div>
<p class="time-counter">{{ marketInfo.usMarket.nextTime }}</p> <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> </div>
</n-grid-item> </n-grid-item>
</n-grid> </n-grid>
@@ -71,10 +101,10 @@ import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import { NCard, NGrid, NGridItem, NTag, NIcon } from 'naive-ui'; import { NCard, NGrid, NGridItem, NTag, NIcon } from 'naive-ui';
import { import {
PulseOutline as PulseIcon, PulseOutline as PulseIcon,
TimeOutline as TimeIcon TimeOutline as TimeIcon,
} from '@vicons/ionicons5'; } from '@vicons/ionicons5';
import { updateMarketTimeInfo } from '@/utils'; import { updateMarketTimeInfo } from '@/utils';
import type { MarketTimeInfo } from '@/types'; import type { MarketTimeInfo, MarketStatus } from '@/types';
const props = defineProps({ const props = defineProps({
isMobile: { isMobile: {
@@ -90,14 +120,121 @@ const marketInfo = ref<MarketTimeInfo>({
usMarket: { isOpen: false, nextTime: '' } usMarket: { isOpen: false, nextTime: '' }
}); });
// 根据屏幕尺寸自动调整布局
const gridCols = computed(() => { const gridCols = computed(() => {
return props.isMobile ? 1 : 4; return 24;
}); });
let intervalId: number | null = null; let intervalId: number | null = null;
function updateMarketTime() { 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(() => { onMounted(() => {
@@ -116,7 +253,10 @@ onBeforeUnmount(() => {
<style scoped> <style scoped>
.market-time-card { .market-time-card {
margin-bottom: 1.5rem; 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 { .time-block {
@@ -124,7 +264,32 @@ onBeforeUnmount(() => {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
text-align: 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 { .time-label {
@@ -145,13 +310,18 @@ onBeforeUnmount(() => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: 32px; min-height: 36px;
width: 100%;
} }
.market-status :deep(.n-tag) { .status-tag {
padding: 0 12px; padding: 0 16px !important;
height: 32px; height: 36px !important;
font-size: 1rem; font-size: 1rem;
display: flex;
align-items: center;
justify-content: center;
min-width: 100px;
} }
.market-status :deep(.n-tag__icon) { .market-status :deep(.n-tag__icon) {
@@ -159,13 +329,14 @@ onBeforeUnmount(() => {
} }
.status-open :deep(.n-tag) { .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); border: 1px solid var(--n-success-color);
animation: pulse 2s infinite; animation: pulse 2s infinite;
} }
.status-closed :deep(.n-tag) { .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 { .time-counter {
@@ -174,15 +345,232 @@ onBeforeUnmount(() => {
margin-top: 0.5rem; 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% { 0% {
box-shadow: 0 0 0 0 rgba(var(--success-color), 0.4); background-position: 200% 0;
}
70% {
box-shadow: 0 0 0 6px rgba(var(--success-color), 0);
} }
100% { 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> </style>

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

View File

@@ -1,10 +1,18 @@
<template> <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 class="main-layout">
<n-layout-content class="main-content"> <n-layout-content class="main-content mobile-content-container">
<!-- 市场时间显示 --> <!-- 市场时间显示 -->
<MarketTimeDisplay /> <MarketTimeDisplay :is-mobile="isMobile" />
<!-- API配置面板 --> <!-- API配置面板 -->
<ApiConfigPanel <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"> <div class="config-section">
<n-form-item label="选择市场类型"> <n-form-item label="选择市场类型">
<n-select <n-select
@@ -63,7 +71,7 @@
</n-grid-item> </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-section">
<div class="results-header"> <div class="results-header">
<n-space align="center" justify="space-between"> <n-space align="center" justify="space-between">
@@ -135,13 +143,16 @@
</n-grid-item> </n-grid-item>
</n-grid> </n-grid>
</n-card> </n-card>
<!-- 底部安全区域 -->
<SafeAreaBottom />
</n-layout-content> </n-layout-content>
</n-layout> </n-layout>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, h } from 'vue'; import { ref, onMounted, h, computed, onBeforeUnmount } from 'vue';
import { import {
NLayout, NLayout,
NLayoutContent, NLayoutContent,
@@ -173,6 +184,8 @@ import MarketTimeDisplay from './MarketTimeDisplay.vue';
import ApiConfigPanel from './ApiConfigPanel.vue'; import ApiConfigPanel from './ApiConfigPanel.vue';
import StockSearch from './StockSearch.vue'; import StockSearch from './StockSearch.vue';
import StockCard from './StockCard.vue'; import StockCard from './StockCard.vue';
import SafeAreaBottom from './SafeAreaBottom.vue';
import AnnouncementBanner from './AnnouncementBanner.vue';
import { apiService } from '@/services/api'; import { apiService } from '@/services/api';
import type { StockInfo, ApiConfig, StreamInitMessage, StreamAnalysisUpdate } from '@/types'; import type { StockInfo, ApiConfig, StreamInitMessage, StreamAnalysisUpdate } from '@/types';
@@ -189,6 +202,7 @@ const defaultApiUrl = ref('');
const defaultApiModel = ref(''); const defaultApiModel = ref('');
const defaultApiTimeout = ref('60'); const defaultApiTimeout = ref('60');
const announcement = ref(''); const announcement = ref('');
const showAnnouncementBanner = ref(true);
// 股票分析配置 // 股票分析配置
const marketType = ref('A'); const marketType = ref('A');
@@ -206,20 +220,24 @@ const apiConfig = ref<ApiConfig>({
saveApiConfig: false saveApiConfig: false
}); });
// 移动端检测
const isMobile = computed(() => {
return window.innerWidth <= 768;
});
// 监听窗口大小变化
function handleResize() {
// 窗口大小变化时isMobile计算属性会自动更新
// 这里可以添加其他需要在窗口大小变化时执行的逻辑
}
// 显示系统公告 // 显示系统公告
const showAnnouncement = (content: string) => { const showAnnouncement = (content: string) => {
if (!content) return; if (!content) return;
notification.info({ // 使用AnnouncementBanner组件显示公告
title: '系统公告', announcement.value = content;
content: () => h('div', { style: 'display: flex; align-items: center;' }, [ showAnnouncementBanner.value = true;
h(NIcon, { component: NotificationsIcon, style: 'margin-right: 8px; font-size: 18px;' }),
h('span', null, content)
]),
duration: 0, // 设置为0表示不会自动关闭
keepAliveOnHover: true,
closable: true
});
}; };
// 市场选项 // 市场选项
@@ -911,6 +929,9 @@ function getChineseVolumeStatus(status: string): string {
// 页面加载时获取默认配置和公告 // 页面加载时获取默认配置和公告
onMounted(async () => { onMounted(async () => {
try { try {
// 添加窗口大小变化监听
window.addEventListener('resize', handleResize);
// 从API获取配置信息 // 从API获取配置信息
const config = await apiService.getConfig(); const config = await apiService.getConfig();
@@ -938,6 +959,16 @@ onMounted(async () => {
console.error('获取默认配置时出错:', error); console.error('获取默认配置时出错:', error);
} }
}); });
// 组件销毁前移除事件监听
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize);
});
// 处理公告关闭事件
function handleAnnouncementClose() {
showAnnouncementBanner.value = false;
}
</script> </script>
<style scoped> <style scoped>
@@ -969,7 +1000,7 @@ onMounted(async () => {
} }
.analysis-container { .analysis-container {
margin-bottom: 2rem; margin-bottom: 1rem;
} }
.config-section { .config-section {
@@ -998,4 +1029,95 @@ onMounted(async () => {
text-overflow: ellipsis; text-overflow: ellipsis;
word-break: break-word; 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> </style>

View File

@@ -1,5 +1,5 @@
<template> <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="card-header">
<div class="header-main"> <div class="header-main">
<div class="header-left"> <div class="header-left">
@@ -1010,4 +1010,133 @@ const getStatusText = computed(() => {
border-radius: 4px; border-radius: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 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> </style>

View File

@@ -219,4 +219,73 @@ onBeforeUnmount(() => {
.result-market-value { .result-market-value {
margin-top: 0.25rem; 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> </style>

View File

@@ -1,6 +1,7 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import './style.css' import './style.css'
import './assets/css/global.css' import './assets/css/global.css'
import './assets/styles/mobile.css'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'

View File

@@ -49,6 +49,7 @@ export interface SearchResult {
export interface MarketStatus { export interface MarketStatus {
isOpen: boolean; isOpen: boolean;
nextTime: string; nextTime: string;
progressPercentage?: number;
} }
export interface MarketTimeInfo { export interface MarketTimeInfo {