Merge branch 'main' of git.yf93.cn:yang/vue
This commit is contained in:
+2
-1
@@ -41,13 +41,14 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
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'
|
import { fetchTradeCalendar } from '@/composables/useTradeCalendar'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const isSidebarCollapsed = ref(false)
|
const isSidebarCollapsed = ref(false)
|
||||||
const iconMap = {
|
const iconMap = {
|
||||||
|
Calendar,
|
||||||
Download,
|
Download,
|
||||||
Histogram,
|
Histogram,
|
||||||
TrendCharts
|
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) {
|
export function getLimitBoardBreakNextPerformance(params) {
|
||||||
return request({
|
return request({
|
||||||
url: '/limit-board/break-next-performance',
|
url: '/limit-board/break-next-performance',
|
||||||
|
|||||||
+17
-11
@@ -3,19 +3,13 @@ import { createRouter, createWebHistory } from 'vue-router'
|
|||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
redirect: '/stock/analyze'
|
redirect: '/stock/second-calendar'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/stock/analyze',
|
path: '/stock/second-calendar',
|
||||||
name: 'StockAnalyze',
|
name: 'StockSecondBoardCalendar',
|
||||||
component: () => import('@/views/stock/Analyze.vue'),
|
component: () => import('@/views/stock/SecondBoardCalendar.vue'),
|
||||||
meta: { title: '涨停回落分析', icon: 'TrendCharts' }
|
meta: { title: '二板日历', icon: 'Calendar' }
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/stock/limit-board',
|
|
||||||
name: 'StockLimitBoard',
|
|
||||||
component: () => import('@/views/stock/LimitBoard.vue'),
|
|
||||||
meta: { title: '涨停连板概览', icon: 'Histogram' }
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/stock/second-board',
|
path: '/stock/second-board',
|
||||||
@@ -29,6 +23,18 @@ const routes = [
|
|||||||
component: () => import('@/views/stock/BreakNextPerformance.vue'),
|
component: () => import('@/views/stock/BreakNextPerformance.vue'),
|
||||||
meta: { title: '炸板次日表现', icon: 'TrendCharts' }
|
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',
|
path: '/stock/download',
|
||||||
redirect: '/stock/second-board'
|
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