Merge branch 'main' of git.yf93.cn:yang/vue
This commit is contained in:
+2
-1
@@ -41,13 +41,14 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ArrowLeftBold, ArrowRightBold, Download, Histogram, TrendCharts } from '@element-plus/icons-vue'
|
||||
import { ArrowLeftBold, ArrowRightBold, Calendar, Download, Histogram, TrendCharts } from '@element-plus/icons-vue'
|
||||
import { fetchTradeCalendar } from '@/composables/useTradeCalendar'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const isSidebarCollapsed = ref(false)
|
||||
const iconMap = {
|
||||
Calendar,
|
||||
Download,
|
||||
Histogram,
|
||||
TrendCharts
|
||||
|
||||
@@ -40,6 +40,14 @@ export function getSecondBoardProfit(params) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getSecondBoardCalendar(params) {
|
||||
return request({
|
||||
url: '/limit-board/second-cal',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getLimitBoardBreakNextPerformance(params) {
|
||||
return request({
|
||||
url: '/limit-board/break-next-performance',
|
||||
|
||||
+17
-11
@@ -3,19 +3,13 @@ import { createRouter, createWebHistory } from 'vue-router'
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/stock/analyze'
|
||||
redirect: '/stock/second-calendar'
|
||||
},
|
||||
{
|
||||
path: '/stock/analyze',
|
||||
name: 'StockAnalyze',
|
||||
component: () => import('@/views/stock/Analyze.vue'),
|
||||
meta: { title: '涨停回落分析', icon: 'TrendCharts' }
|
||||
},
|
||||
{
|
||||
path: '/stock/limit-board',
|
||||
name: 'StockLimitBoard',
|
||||
component: () => import('@/views/stock/LimitBoard.vue'),
|
||||
meta: { title: '涨停连板概览', icon: 'Histogram' }
|
||||
path: '/stock/second-calendar',
|
||||
name: 'StockSecondBoardCalendar',
|
||||
component: () => import('@/views/stock/SecondBoardCalendar.vue'),
|
||||
meta: { title: '二板日历', icon: 'Calendar' }
|
||||
},
|
||||
{
|
||||
path: '/stock/second-board',
|
||||
@@ -29,6 +23,18 @@ const routes = [
|
||||
component: () => import('@/views/stock/BreakNextPerformance.vue'),
|
||||
meta: { title: '炸板次日表现', icon: 'TrendCharts' }
|
||||
},
|
||||
{
|
||||
path: '/stock/analyze',
|
||||
name: 'StockAnalyze',
|
||||
component: () => import('@/views/stock/Analyze.vue'),
|
||||
meta: { title: '涨停回落分析', icon: 'TrendCharts' }
|
||||
},
|
||||
{
|
||||
path: '/stock/limit-board',
|
||||
name: 'StockLimitBoard',
|
||||
component: () => import('@/views/stock/LimitBoard.vue'),
|
||||
meta: { title: '涨停连板概览', icon: 'Histogram' }
|
||||
},
|
||||
{
|
||||
path: '/stock/download',
|
||||
redirect: '/stock/second-board'
|
||||
|
||||
@@ -0,0 +1,773 @@
|
||||
<template>
|
||||
<div
|
||||
v-loading="loading"
|
||||
class="second-calendar-page"
|
||||
element-loading-text="正在加载二板日历..."
|
||||
element-loading-background="rgba(245, 248, 252, 0.72)"
|
||||
>
|
||||
<section class="month-filter">
|
||||
<el-date-picker
|
||||
v-model="selectedMonth"
|
||||
type="month"
|
||||
value-format="YYYY-MM"
|
||||
format="YYYY年MM月"
|
||||
:clearable="false"
|
||||
:editable="false"
|
||||
:disabled-date="disableMonths"
|
||||
placeholder="选择月份"
|
||||
class="month-picker"
|
||||
@change="handleMonthChange"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="calendar-stage">
|
||||
<el-tooltip content="上一月" placement="right">
|
||||
<button
|
||||
type="button"
|
||||
class="calendar-nav prev"
|
||||
:disabled="loading"
|
||||
aria-label="上一月"
|
||||
@click="handleMonthStep(-1)"
|
||||
>
|
||||
<el-icon><ArrowLeftIcon /></el-icon>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
|
||||
<div class="calendar-panel">
|
||||
<div class="calendar-head">
|
||||
<div class="month-title">{{ monthTitle }}</div>
|
||||
<div class="month-range">{{ monthRangeLabel }}</div>
|
||||
</div>
|
||||
|
||||
<div class="weekday-row">
|
||||
<div v-for="weekday in weekdays" :key="weekday" class="weekday-cell">
|
||||
{{ weekday }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-grid">
|
||||
<article
|
||||
v-for="day in monthCells"
|
||||
:key="day.date"
|
||||
:class="[
|
||||
'day-card',
|
||||
{
|
||||
muted: !day.isCurrentMonth,
|
||||
today: day.isToday,
|
||||
closed: day.isCurrentMonth && !day.isOpen,
|
||||
empty: day.isCurrentMonth && day.isOpen && !day.stat,
|
||||
detailable: day.stat,
|
||||
},
|
||||
]"
|
||||
:tabindex="day.stat ? 0 : undefined"
|
||||
:role="day.stat ? 'button' : undefined"
|
||||
@click="openDayDetail(day)"
|
||||
@keydown.enter="openDayDetail(day)"
|
||||
>
|
||||
<header class="day-card-head">
|
||||
<span class="day-number">{{ day.dayNumber }}</span>
|
||||
<span v-if="day.isCurrentMonth && !day.isOpen" class="day-badge">
|
||||
休
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div v-if="day.stat" class="day-metrics">
|
||||
<div class="metric-row profit">
|
||||
<strong>{{ formatRate(day.stat.next_intraday_profit_rate) }}</strong>
|
||||
</div>
|
||||
<div class="metric-row break">
|
||||
<span>炸板</span>
|
||||
<strong>{{ formatRatio(day.stat.break_next_profit_count, day.stat.break_count) }}</strong>
|
||||
</div>
|
||||
<div class="metric-row limit">
|
||||
<span>涨停板</span>
|
||||
<strong>{{ formatRatio(day.stat.limit_next_profit_count, day.stat.limit_count) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="day.isCurrentMonth" class="day-placeholder">
|
||||
{{ day.isOpen ? "暂无数据" : "非交易日" }}
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-tooltip content="下一月" placement="left">
|
||||
<button
|
||||
type="button"
|
||||
class="calendar-nav next"
|
||||
:disabled="loading || !canGoNextMonth"
|
||||
aria-label="下一月"
|
||||
@click="handleMonthStep(1)"
|
||||
>
|
||||
<el-icon><ArrowRightIcon /></el-icon>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
</section>
|
||||
|
||||
<el-dialog
|
||||
v-model="detailVisible"
|
||||
class="second-calendar-detail-dialog"
|
||||
:title="detailDialogTitle"
|
||||
width="82%"
|
||||
align-center
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="detail-summary">
|
||||
<div class="detail-summary-item">
|
||||
<span>盈利率</span>
|
||||
<strong>{{ formatRate(selectedDayStat?.next_intraday_profit_rate) }}</strong>
|
||||
</div>
|
||||
<div class="detail-summary-item">
|
||||
<span>涨停板</span>
|
||||
<strong>{{
|
||||
formatRatio(
|
||||
selectedDayStat?.limit_next_profit_count,
|
||||
selectedDayStat?.limit_count,
|
||||
)
|
||||
}}</strong>
|
||||
</div>
|
||||
<div class="detail-summary-item">
|
||||
<span>炸板</span>
|
||||
<strong>{{
|
||||
formatRatio(
|
||||
selectedDayStat?.break_next_profit_count,
|
||||
selectedDayStat?.break_count,
|
||||
)
|
||||
}}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeDetailTab" class="detail-tabs">
|
||||
<el-tab-pane :label="`涨停板(${limitStocks.length})`" name="limit">
|
||||
<el-table :data="limitStocks" border stripe max-height="520">
|
||||
<el-table-column prop="name" label="股票简称" min-width="120" />
|
||||
<el-table-column prop="ts_code" label="股票代码" min-width="120" />
|
||||
<el-table-column prop="industry" label="行业" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="T日最高" width="100" align="right">
|
||||
<template #default="{ row }">{{ formatPrice(row.t_high) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="T日收盘" width="100" align="right">
|
||||
<template #default="{ row }">{{ formatPrice(row.t_close) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="T+1最高" width="100" align="right">
|
||||
<template #default="{ row }">{{ formatPrice(row.next_high) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="T+1盈利" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatProfitFlag(row.next_intraday_profitable) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最高收益" width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ formatSignedRate(row.next_high_profit_pct) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="`炸板(${breakStocks.length})`" name="break">
|
||||
<el-table :data="breakStocks" border stripe max-height="520">
|
||||
<el-table-column prop="name" label="股票简称" min-width="120" />
|
||||
<el-table-column prop="ts_code" label="股票代码" min-width="120" />
|
||||
<el-table-column prop="industry" label="行业" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="T日最高" width="100" align="right">
|
||||
<template #default="{ row }">{{ formatPrice(row.t_high) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="T日收盘" width="100" align="right">
|
||||
<template #default="{ row }">{{ formatPrice(row.t_close) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="T+1最高" width="100" align="right">
|
||||
<template #default="{ row }">{{ formatPrice(row.next_high) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="T+1盈利" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatProfitFlag(row.next_intraday_profitable) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最高收益" width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ formatSignedRate(row.next_high_profit_pct) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
ArrowLeft as ArrowLeftIcon,
|
||||
ArrowRight as ArrowRightIcon,
|
||||
} from "@element-plus/icons-vue";
|
||||
import { fetchTradeCalendar } from "@/composables/useTradeCalendar";
|
||||
import { getSecondBoardCalendar } from "@/api/stock";
|
||||
|
||||
defineOptions({
|
||||
name: "SecondBoardCalendar",
|
||||
});
|
||||
|
||||
const weekdays = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
|
||||
|
||||
const selectedMonth = ref(dayjs().format("YYYY-MM"));
|
||||
const loading = ref(false);
|
||||
const openDatesSet = ref(new Set());
|
||||
const maxTradeDate = ref("");
|
||||
const calendarStats = ref([]);
|
||||
const detailVisible = ref(false);
|
||||
const selectedDay = ref(null);
|
||||
const activeDetailTab = ref("limit");
|
||||
|
||||
const monthStart = computed(() =>
|
||||
selectedMonth.value
|
||||
? dayjs(`${selectedMonth.value}-01`)
|
||||
: dayjs().startOf("month"),
|
||||
);
|
||||
const monthEnd = computed(() => monthStart.value.endOf("month"));
|
||||
const monthTitle = computed(() => monthStart.value.format("YYYY年MM月"));
|
||||
const monthRangeLabel = computed(
|
||||
() => `${monthStart.value.format("YYYY-MM-DD")} 至 ${monthEnd.value.format("YYYY-MM-DD")}`,
|
||||
);
|
||||
const canGoNextMonth = computed(() => {
|
||||
if (!maxTradeDate.value) return true;
|
||||
|
||||
const nextMonth = monthStart.value.add(1, "month").format("YYYY-MM");
|
||||
const latestMonth = dayjs(maxTradeDate.value).format("YYYY-MM");
|
||||
return nextMonth <= latestMonth;
|
||||
});
|
||||
const selectedDayStat = computed(() => selectedDay.value?.stat || null);
|
||||
const detailDialogTitle = computed(() => {
|
||||
const date = selectedDay.value?.date || selectedDayStat.value?.trade_date || "";
|
||||
return date ? `${date} 二板详情` : "二板详情";
|
||||
});
|
||||
const detailStocks = computed(() =>
|
||||
Array.isArray(selectedDayStat.value?.stocks) ? selectedDayStat.value.stocks : [],
|
||||
);
|
||||
const limitStocks = computed(() =>
|
||||
detailStocks.value.filter((stock) => stock?.status === "limit"),
|
||||
);
|
||||
const breakStocks = computed(() =>
|
||||
detailStocks.value.filter((stock) => stock?.status === "break"),
|
||||
);
|
||||
const statMap = computed(() => {
|
||||
const map = new Map();
|
||||
|
||||
calendarStats.value.forEach((item) => {
|
||||
if (item?.trade_date) map.set(item.trade_date, item);
|
||||
});
|
||||
|
||||
return map;
|
||||
});
|
||||
const monthCells = computed(() => {
|
||||
const firstDay = monthStart.value.startOf("month");
|
||||
const offset = (firstDay.day() + 6) % 7;
|
||||
const gridStart = firstDay.subtract(offset, "day");
|
||||
const cellCount = Math.ceil((offset + monthEnd.value.date()) / 7) * 7;
|
||||
const today = dayjs().format("YYYY-MM-DD");
|
||||
|
||||
return Array.from({ length: cellCount }, (_, index) => {
|
||||
const current = gridStart.add(index, "day");
|
||||
const date = current.format("YYYY-MM-DD");
|
||||
const isCurrentMonth = current.format("YYYY-MM") === selectedMonth.value;
|
||||
const isOpen = openDatesSet.value.size
|
||||
? openDatesSet.value.has(date)
|
||||
: current.day() !== 0 && current.day() !== 6;
|
||||
|
||||
return {
|
||||
date,
|
||||
dayNumber: current.date(),
|
||||
isCurrentMonth,
|
||||
isToday: date === today,
|
||||
isOpen,
|
||||
stat: statMap.value.get(date),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await initializePage();
|
||||
});
|
||||
|
||||
const initializePage = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const calendar = await fetchTradeCalendar();
|
||||
const { open_dates: openDates = [], max_date: maxDate = "" } =
|
||||
calendar || {};
|
||||
|
||||
openDatesSet.value = new Set(openDates);
|
||||
maxTradeDate.value = maxDate;
|
||||
selectedMonth.value = maxDate ? dayjs(maxDate).format("YYYY-MM") : dayjs().format("YYYY-MM");
|
||||
} catch (err) {
|
||||
console.error("初始化二板日历失败", err);
|
||||
} finally {
|
||||
await fetchMonthData(false);
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const disableMonths = (time) => {
|
||||
if (!maxTradeDate.value) return false;
|
||||
|
||||
const currentMonth = dayjs(time).format("YYYY-MM");
|
||||
const latestMonth = dayjs(maxTradeDate.value).format("YYYY-MM");
|
||||
return currentMonth > latestMonth;
|
||||
};
|
||||
|
||||
const handleMonthChange = async () => {
|
||||
await fetchMonthData();
|
||||
};
|
||||
|
||||
const handleMonthStep = async (step) => {
|
||||
if (step > 0 && !canGoNextMonth.value) return;
|
||||
|
||||
selectedMonth.value = monthStart.value.add(step, "month").format("YYYY-MM");
|
||||
await fetchMonthData();
|
||||
};
|
||||
|
||||
const openDayDetail = (day) => {
|
||||
if (!day?.stat) return;
|
||||
|
||||
selectedDay.value = day;
|
||||
activeDetailTab.value = limitStocks.value.length ? "limit" : "break";
|
||||
detailVisible.value = true;
|
||||
};
|
||||
|
||||
const fetchMonthData = async (withLoading = true) => {
|
||||
if (!selectedMonth.value) return;
|
||||
|
||||
if (withLoading) loading.value = true;
|
||||
try {
|
||||
const res = await getSecondBoardCalendar({
|
||||
tradeStartDate: monthStart.value.format("YYYY-MM-DD"),
|
||||
tradeEndDate: monthEnd.value.format("YYYY-MM-DD"),
|
||||
});
|
||||
|
||||
calendarStats.value = Array.isArray(res)
|
||||
? res
|
||||
: Array.isArray(res?.list)
|
||||
? res.list
|
||||
: [];
|
||||
} catch (err) {
|
||||
console.error("加载二板日历失败", err);
|
||||
calendarStats.value = [];
|
||||
} finally {
|
||||
if (withLoading) loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const formatRate = (value) => {
|
||||
if (value === null || value === undefined || value === "") return "--";
|
||||
|
||||
const number = Number(value);
|
||||
if (!Number.isFinite(number)) return "--";
|
||||
return `${number.toFixed(2)}%`;
|
||||
};
|
||||
|
||||
const formatCount = (value) => {
|
||||
const number = Number(value ?? 0);
|
||||
return Number.isFinite(number) ? number : 0;
|
||||
};
|
||||
|
||||
const formatRatio = (numerator, denominator) =>
|
||||
`${formatCount(numerator)}/${formatCount(denominator)}`;
|
||||
|
||||
const formatPrice = (value) => {
|
||||
if (value === null || value === undefined || value === "") return "--";
|
||||
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? number.toFixed(2) : "--";
|
||||
};
|
||||
|
||||
const formatSignedRate = (value) => {
|
||||
if (value === null || value === undefined || value === "") return "--";
|
||||
|
||||
const number = Number(value);
|
||||
if (!Number.isFinite(number)) return "--";
|
||||
return `${number > 0 ? "+" : ""}${number.toFixed(2)}%`;
|
||||
};
|
||||
|
||||
const formatProfitFlag = (value) => {
|
||||
if (value === true) return "是";
|
||||
if (value === false) return "否";
|
||||
return "--";
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.second-calendar-page {
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
background: #f4f7f9;
|
||||
}
|
||||
|
||||
.month-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.month-picker {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.calendar-stage {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: 72px minmax(0, 1fr) 72px;
|
||||
align-items: stretch;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.calendar-panel {
|
||||
min-width: 0;
|
||||
padding: 22px 24px 26px;
|
||||
box-sizing: border-box;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e5edf5;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 22px rgba(31, 42, 55, 0.04);
|
||||
}
|
||||
|
||||
.calendar-nav {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
border: 1px solid #d8e3f0;
|
||||
border-radius: 8px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-color: #d8e3f0;
|
||||
color: #315f96;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 8px 18px rgba(31, 42, 55, 0.06);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
background 0.2s ease,
|
||||
color 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.calendar-nav .el-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.calendar-nav:hover:not(:disabled) {
|
||||
border-color: #9abbe1;
|
||||
background: #eef6ff;
|
||||
color: #1f5fa8;
|
||||
}
|
||||
|
||||
.calendar-nav:disabled {
|
||||
cursor: not-allowed;
|
||||
color: #a8b2c1;
|
||||
background: #f7f9fc;
|
||||
box-shadow: none;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.calendar-head {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.month-title {
|
||||
font-size: 22px;
|
||||
line-height: 30px;
|
||||
font-weight: 700;
|
||||
color: #1f2a37;
|
||||
}
|
||||
|
||||
.month-range {
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
color: #7c8aa5;
|
||||
}
|
||||
|
||||
.weekday-row,
|
||||
.calendar-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.weekday-row {
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.weekday-cell {
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
font-weight: 700;
|
||||
color: #5f6f86;
|
||||
}
|
||||
|
||||
.calendar-grid {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.day-card {
|
||||
min-height: 138px;
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #e4ebf3;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
box-shadow 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
}
|
||||
|
||||
.day-card:hover {
|
||||
border-color: #b9cbe2;
|
||||
box-shadow: 0 8px 18px rgba(31, 42, 55, 0.07);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.day-card.detailable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.day-card.detailable:focus-visible {
|
||||
outline: 2px solid #4f8fd9;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.day-card.muted {
|
||||
background: #f7f9fc;
|
||||
color: #a8b2c1;
|
||||
}
|
||||
|
||||
.day-card.closed {
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.day-card.empty {
|
||||
background: #fbfcfe;
|
||||
}
|
||||
|
||||
.day-card.today {
|
||||
border-color: #4f8fd9;
|
||||
box-shadow: inset 0 0 0 1px rgba(79, 143, 217, 0.28);
|
||||
}
|
||||
|
||||
.day-card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
min-height: 24px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.day-number {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 700;
|
||||
color: #27364a;
|
||||
}
|
||||
|
||||
.muted .day-number,
|
||||
.closed .day-number {
|
||||
color: #9aa7b8;
|
||||
}
|
||||
|
||||
.day-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 999px;
|
||||
background: #eef2f7;
|
||||
color: #7c8aa5;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.day-metrics {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.metric-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
min-height: 24px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
color: #5f6f86;
|
||||
}
|
||||
|
||||
.metric-row strong {
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
font-weight: 800;
|
||||
color: #26364b;
|
||||
}
|
||||
|
||||
.metric-row.profit strong {
|
||||
color: #1f7a55;
|
||||
}
|
||||
|
||||
.metric-row.break strong {
|
||||
color: #c2414b;
|
||||
}
|
||||
|
||||
.metric-row.limit strong {
|
||||
color: #2f6fc0;
|
||||
}
|
||||
|
||||
.day-placeholder {
|
||||
min-height: 70px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
color: #a0acbb;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.detail-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.detail-summary-item {
|
||||
min-width: 0;
|
||||
padding: 12px 14px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #e5edf5;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.detail-summary-item span {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
color: #6d7d93;
|
||||
}
|
||||
|
||||
.detail-summary-item strong {
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
color: #223047;
|
||||
}
|
||||
|
||||
.detail-tabs {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
:deep(.second-calendar-detail-dialog .el-dialog__body) {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.second-calendar-page {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.calendar-panel {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.calendar-stage {
|
||||
grid-template-columns: 58px minmax(0, 1fr) 58px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.day-card {
|
||||
min-height: 128px;
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.second-calendar-page {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.calendar-head {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.calendar-stage {
|
||||
grid-template-columns: 42px minmax(0, 1fr) 42px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.calendar-panel {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.calendar-nav {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.calendar-nav .el-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.weekday-row,
|
||||
.calendar-grid {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.weekday-cell {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.day-card {
|
||||
min-height: 112px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.day-number {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.metric-row {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.metric-row strong {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.day-placeholder {
|
||||
min-height: 58px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.detail-summary {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user