优化
This commit is contained in:
1 parent
d9a50e23dd
commit
4f1acaa451
25 files changed
+1265
-335
No files matched your search
@@ -3,7 +3,7 @@ import request from "@/utils/request";
|
||||
/**
|
||||
* CRM 业务管线接口:线索 → 商机 → 项目,回访贯穿全流程。
|
||||
*
|
||||
* 关联类型 related_type:1=线索 2=商机 3=项目
|
||||
* 关联类型 related_type:1=线索 2=商机 3=项目 4=投标 5=招标 6=合同
|
||||
*/
|
||||
|
||||
/* --------------------------------- 线索 --------------------------------- */
|
||||
|
||||
@@ -110,8 +110,8 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(["update:visible", "refresh", "edit"]);
|
||||
|
||||
// 合同操作日志与项目共用关联类型 3
|
||||
const RELATED_TYPE = 3;
|
||||
// 合同操作日志独立关联类型 6(与项目 3 解耦,避免 ID 撞车时日志串数据)
|
||||
const RELATED_TYPE = 6;
|
||||
// 电子合同附件专属关联类型(与线索 1 / 商机 2 / 项目 3 区分)
|
||||
const E_CONTRACT_TYPE = 4;
|
||||
// 发票管理附件专属关联类型
|
||||
|
||||
@@ -60,6 +60,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 状态 Tab 栏:按业务流程顺序展示 -->
|
||||
<div class="status-tabs">
|
||||
<el-tabs v-model="activeStatusTab" @tab-change="handleStatusTabChange">
|
||||
<el-tab-pane label="全部" name="" />
|
||||
<el-tab-pane label="草稿" name="1" />
|
||||
<el-tab-pane label="已完成" name="2" />
|
||||
<el-tab-pane label="履约中" name="4" />
|
||||
<el-tab-pane label="异常" name="5" />
|
||||
<el-tab-pane label="已作废" name="3" />
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar">
|
||||
<el-form :inline="true" :model="filters" @submit.prevent>
|
||||
<el-form-item>
|
||||
@@ -88,11 +100,6 @@
|
||||
<el-option label="无头合同" value="headless" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="filters.status" clearable placeholder="全部" style="width: 110px">
|
||||
<el-option v-for="i in CONTRACT_STATUS_OPTIONS" :key="i.value" :label="i.label" :value="i.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
|
||||
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
|
||||
@@ -245,6 +252,15 @@ const filters = reactive({
|
||||
});
|
||||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 });
|
||||
|
||||
/** 状态 Tab 绑定值,空字符串表示"全部" */
|
||||
const activeStatusTab = ref("");
|
||||
|
||||
/** 切换状态 Tab:同步到 filters.status 并刷新列表 */
|
||||
function handleStatusTabChange(name) {
|
||||
filters.status = name;
|
||||
handleSearch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计周期:all(全部)/ month / quarter / year / custom(自定义选区间)。
|
||||
* 默认「本年」——列表与统计都按签订日期落在本年内归类,打开即看当年合同。
|
||||
@@ -401,6 +417,7 @@ function resetFilters() {
|
||||
filters.contract_category = "";
|
||||
filters.project_type = "";
|
||||
filters.status = "";
|
||||
activeStatusTab.value = "";
|
||||
handleSearch();
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 状态 Tab 栏 -->
|
||||
<div class="status-tabs">
|
||||
<el-tabs v-model="activeStatusTab" @tab-change="handleStatusTabChange">
|
||||
<el-tab-pane label="全部" name="" />
|
||||
<el-tab-pane
|
||||
v-for="i in PROJECT_STATUS_OPTIONS"
|
||||
:key="i.value"
|
||||
:label="i.label"
|
||||
:name="i.value"
|
||||
/>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar">
|
||||
<el-form :inline="true" :model="filters" @submit.prevent>
|
||||
<el-form-item>
|
||||
@@ -23,11 +36,6 @@
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="项目状态">
|
||||
<el-select v-model="filters.status" clearable placeholder="全部" style="width: 130px">
|
||||
<el-option v-for="i in PROJECT_STATUS_OPTIONS" :key="i.value" :label="i.label" :value="i.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- 年度筛选:按项目开始时间(关联合同汇总)所在年份过滤,选项由后端按现有项目汇总 -->
|
||||
<el-form-item label="年度">
|
||||
<el-select v-model="filters.year" clearable placeholder="全部" style="width: 120px">
|
||||
@@ -179,6 +187,15 @@ const pagination = reactive({ page: 1, pageSize: 20, total: 0 });
|
||||
/** 年度筛选项:由后端按现有项目的开始时间汇总返回(倒序),避免前端写死年份范围 */
|
||||
const yearOptions = ref([]);
|
||||
|
||||
/** 状态 Tab 绑定值,空字符串表示"全部" */
|
||||
const activeStatusTab = ref("");
|
||||
|
||||
/** 切换状态 Tab:同步到 filters.status 并刷新列表 */
|
||||
function handleStatusTabChange(name) {
|
||||
filters.status = name;
|
||||
handleSearch();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchList();
|
||||
});
|
||||
@@ -221,6 +238,7 @@ function resetFilters() {
|
||||
filters.keyword = "";
|
||||
filters.status = "";
|
||||
filters.year = "";
|
||||
activeStatusTab.value = "";
|
||||
handleSearch();
|
||||
}
|
||||
|
||||
|
||||
@@ -240,7 +240,7 @@ function tagType(type) {
|
||||
padding-left: 10px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
color: var(--el-text-color-primary);
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
@@ -251,7 +251,7 @@ function tagType(type) {
|
||||
width: 3px;
|
||||
height: 14px;
|
||||
border-radius: 2px;
|
||||
background: #2563eb;
|
||||
background: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,7 +300,7 @@ function tagType(type) {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #fff;
|
||||
border: 2px solid var(--el-bg-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -327,7 +327,7 @@ function tagType(type) {
|
||||
flex: 1;
|
||||
width: 0;
|
||||
margin-top: 6px;
|
||||
border-left: 1px dashed #cbd5e1;
|
||||
border-left: 1px dashed var(--el-border-color);
|
||||
}
|
||||
|
||||
.flow-main {
|
||||
@@ -350,28 +350,28 @@ function tagType(type) {
|
||||
.flow-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.flow-sub {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.flow-time {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.flow-comment {
|
||||
margin-top: 8px;
|
||||
padding: 8px 10px;
|
||||
background: #f1f5f9;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
color: var(--el-text-color-regular);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ onMounted(loadList);
|
||||
|
||||
<style lang="less" scoped>
|
||||
.approval-page {
|
||||
background: #f5f7fa;
|
||||
background: var(--el-bg-color-page);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
@@ -182,13 +182,13 @@ onMounted(loadList);
|
||||
|
||||
h2 {
|
||||
margin: 0 0 8px;
|
||||
color: #303133;
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #909399;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
@@ -202,10 +202,10 @@ onMounted(loadList);
|
||||
|
||||
.filter-card,
|
||||
.table-card {
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
background: var(--el-bg-color);
|
||||
border-radius: var(--el-border-radius-base);
|
||||
padding: 18px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.filter-card {
|
||||
|
||||
@@ -181,7 +181,7 @@ onMounted(loadList);
|
||||
|
||||
<style lang="less" scoped>
|
||||
.approval-page {
|
||||
background: #f5f7fa;
|
||||
background: var(--el-bg-color-page);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
@@ -193,23 +193,23 @@ onMounted(loadList);
|
||||
|
||||
h2 {
|
||||
margin: 0 0 8px;
|
||||
color: #303133;
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #909399;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-card,
|
||||
.table-card {
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
background: var(--el-bg-color);
|
||||
border-radius: var(--el-border-radius-base);
|
||||
padding: 18px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.filter-card {
|
||||
|
||||
@@ -60,7 +60,7 @@ onMounted(loadFlows);
|
||||
|
||||
<style lang="less" scoped>
|
||||
.approval-settings-page {
|
||||
background: #f5f7fa;
|
||||
background: var(--el-bg-color-page);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
@@ -69,23 +69,23 @@ onMounted(loadFlows);
|
||||
|
||||
h2 {
|
||||
margin: 0 0 8px;
|
||||
color: #303133;
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #909399;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.reference-card {
|
||||
margin-top: 16px;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
background: var(--el-bg-color);
|
||||
border-radius: var(--el-border-radius-base);
|
||||
padding: 18px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
@@ -94,7 +94,7 @@ onMounted(loadFlows);
|
||||
padding-left: 10px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
color: var(--el-text-color-primary);
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
@@ -105,13 +105,13 @@ onMounted(loadFlows);
|
||||
width: 3px;
|
||||
height: 14px;
|
||||
border-radius: 2px;
|
||||
background: #2563eb;
|
||||
background: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin: 0 0 12px;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
@@ -121,8 +121,138 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧 30%:通知公告 + 工作日程提醒 -->
|
||||
<!-- 右侧 30%:今日概览 + 通知公告 + 工作日程提醒 -->
|
||||
<div class="col-right">
|
||||
<!-- 今日概览卡片(顶部,与左侧 6 张指标卡区域等高) -->
|
||||
<div class="today-card">
|
||||
<div class="tc-greet">
|
||||
<div class="tc-hello">Hi,{{ displayName }}</div>
|
||||
<div class="tc-sub">
|
||||
{{ timeGreeting }},今天是 {{ todayWeekday }} {{ todayDay }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tc-tiles">
|
||||
<div class="tc-tile">
|
||||
<div class="tc-tile-head">
|
||||
<span class="tc-tile-label">天气</span>
|
||||
<el-icon
|
||||
class="tc-refresh"
|
||||
:class="{ spinning: weatherLoading }"
|
||||
@click="refreshWeather"
|
||||
>
|
||||
<Refresh />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="tc-weather" v-if="weather.available">
|
||||
<el-icon :size="26" class="tc-weather-icon">
|
||||
<component :is="weather.icon" />
|
||||
</el-icon>
|
||||
<div class="tc-weather-text">
|
||||
<div class="tc-weather-temp">{{ weather.temp }}°</div>
|
||||
<div class="tc-weather-desc">
|
||||
{{ weather.text }}<span v-if="weather.city"> · {{ weather.city }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tc-weather tc-weather-empty" v-else>
|
||||
<el-icon :size="22"><Cloudy /></el-icon>
|
||||
<span>暂未获取</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tc-tile">
|
||||
<div class="tc-tile-head">
|
||||
<span class="tc-tile-label">今日排班</span>
|
||||
</div>
|
||||
<div class="tc-shift" :class="todaySchedule.status">
|
||||
<span class="tc-shift-dot" />
|
||||
<span class="tc-shift-text">{{ todaySchedule.label }}</span>
|
||||
</div>
|
||||
<div class="tc-shift-meta">
|
||||
<span v-if="todaySchedule.shiftName">{{ todaySchedule.shiftName }}</span>
|
||||
<span v-if="todaySchedule.time">{{ todaySchedule.time }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tc-schedule">
|
||||
<div class="tc-schedule-head">
|
||||
<span class="tc-section-title">工作日程</span>
|
||||
<el-button link type="primary" @click="goSchedule">查看全部</el-button>
|
||||
</div>
|
||||
<el-tabs v-model="scheduleRange" class="schedule-tabs">
|
||||
<el-tab-pane
|
||||
v-for="tab in scheduleRanges"
|
||||
:key="tab.value"
|
||||
:name="tab.value"
|
||||
>
|
||||
<template #label>
|
||||
<span class="tab-label">{{ tab.label }}</span>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<div v-loading="scheduleLoading" class="schedule-list">
|
||||
<div
|
||||
v-for="item in filteredScheduleReminders"
|
||||
:key="item.id"
|
||||
class="schedule-item"
|
||||
:class="{ done: item.status === 1 }"
|
||||
:style="{ '--item-color': item.color }"
|
||||
@click="openScheduleDetail(item)"
|
||||
>
|
||||
<span
|
||||
class="item-status-dot"
|
||||
:class="{ done: item.status === 1 }"
|
||||
/>
|
||||
<div class="schedule-item-main">
|
||||
<div
|
||||
class="schedule-item-title"
|
||||
:class="{ done: item.status === 1 }"
|
||||
>
|
||||
<i v-if="hasImages(item)" class="fa-solid fa-image schedule-image-icon"></i>
|
||||
{{ item.title }}
|
||||
<el-tag
|
||||
v-if="item.priority > 0"
|
||||
:type="item.priority === 2 ? 'danger' : 'warning'"
|
||||
size="small"
|
||||
effect="light"
|
||||
>
|
||||
{{ item.priority === 2 ? "紧急" : "重要" }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-if="item.source_id > 0 && item.status === 0"
|
||||
type="warning"
|
||||
size="small"
|
||||
effect="plain"
|
||||
>
|
||||
延期第 {{ item.carry_count + 1 }} 天
|
||||
</el-tag>
|
||||
</div>
|
||||
<div
|
||||
class="schedule-item-time"
|
||||
:class="{ done: item.status === 1 }"
|
||||
>
|
||||
<span :class="{ overdue: isOverdueSchedule(item) }">{{
|
||||
scheduleDateTag(item)
|
||||
}}</span>
|
||||
<span>{{ scheduleTimeText(item) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty
|
||||
v-if="!filteredScheduleReminders.length && !scheduleLoading"
|
||||
:description="
|
||||
scheduleRange === 'today'
|
||||
? '今日暂无日程安排'
|
||||
: '该时间范围内暂无日程'
|
||||
"
|
||||
:image-size="60"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-card notice-card">
|
||||
<div class="chart-title notice-title">
|
||||
<span>通知公告</span>
|
||||
@@ -156,84 +286,6 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-card schedule-card">
|
||||
<div class="chart-title schedule-title">
|
||||
<span>工作日程提醒</span>
|
||||
<el-button link type="primary" @click="goSchedule"
|
||||
>查看全部</el-button
|
||||
>
|
||||
</div>
|
||||
<el-tabs v-model="scheduleRange" class="schedule-tabs">
|
||||
<el-tab-pane
|
||||
v-for="tab in scheduleRanges"
|
||||
:key="tab.value"
|
||||
:name="tab.value"
|
||||
>
|
||||
<template #label>
|
||||
<span class="tab-label">{{ tab.label }}</span>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<div v-loading="scheduleLoading" class="schedule-list">
|
||||
<div
|
||||
v-for="item in filteredScheduleReminders"
|
||||
:key="item.id"
|
||||
class="schedule-item"
|
||||
:class="{ done: item.status === 1 }"
|
||||
:style="{ '--item-color': item.color }"
|
||||
@click="openScheduleDetail(item)"
|
||||
>
|
||||
<span
|
||||
class="item-status-dot"
|
||||
:class="{ done: item.status === 1 }"
|
||||
/>
|
||||
<div class="schedule-item-main">
|
||||
<div
|
||||
class="schedule-item-title"
|
||||
:class="{ done: item.status === 1 }"
|
||||
>
|
||||
<i v-if="hasImages(item)" class="fa-solid fa-image schedule-image-icon"></i>
|
||||
{{ item.title }}
|
||||
<el-tag
|
||||
v-if="item.priority > 0"
|
||||
:type="item.priority === 2 ? 'danger' : 'warning'"
|
||||
size="small"
|
||||
effect="light"
|
||||
>
|
||||
{{ item.priority === 2 ? "紧急" : "重要" }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-if="item.source_id > 0 && item.status === 0"
|
||||
type="warning"
|
||||
size="small"
|
||||
effect="plain"
|
||||
>
|
||||
延期第 {{ item.carry_count + 1 }} 天
|
||||
</el-tag>
|
||||
</div>
|
||||
<div
|
||||
class="schedule-item-time"
|
||||
:class="{ done: item.status === 1 }"
|
||||
>
|
||||
<span :class="{ overdue: isOverdueSchedule(item) }">{{
|
||||
scheduleDateTag(item)
|
||||
}}</span>
|
||||
<span>{{ scheduleTimeText(item) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty
|
||||
v-if="!filteredScheduleReminders.length && !scheduleLoading"
|
||||
:description="
|
||||
scheduleRange === 'today'
|
||||
? '今日暂无日程安排'
|
||||
: '该时间范围内暂无日程'
|
||||
"
|
||||
:image-size="60"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -290,17 +342,35 @@ import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue"
|
||||
import { useRouter } from "vue-router";
|
||||
import { ElMessage } from "element-plus";
|
||||
import * as echarts from "echarts";
|
||||
import { Refresh } from "@element-plus/icons-vue";
|
||||
import {
|
||||
Refresh,
|
||||
Sunny,
|
||||
Cloudy,
|
||||
PartlyCloudy,
|
||||
Moon,
|
||||
OfficeBuilding,
|
||||
Lightning,
|
||||
Pouring,
|
||||
Drizzling
|
||||
} from "@element-plus/icons-vue";
|
||||
import request from "@/utils/request";
|
||||
import { getReimbursementDashboard } from "@/api/reimburse";
|
||||
import {
|
||||
getNoticeDetail,
|
||||
getNoticePortal
|
||||
} from "@/api/oaNotice";
|
||||
import { finishSchedule, getScheduleList, getScheduleStats } from "@/api/oaSchedule";
|
||||
import { getMyScheduleBoard } from "@/api/oaScheduling";
|
||||
import ScheduleDetail from "../schedule/components/detail.vue";
|
||||
import SchedulePostpone from "../schedule/components/postponeDialog.vue";
|
||||
// 时间显示统一为:YYYY-MM-DD HH:mm:ss
|
||||
import { formatDateTime } from "@/utils/datetime";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const displayName = computed(
|
||||
() => authStore.user?.name || authStore.user?.account || "朋友"
|
||||
);
|
||||
|
||||
const emptyStat = () => ({ total: 0, paid: 0, unpaid: 0, count: 0 });
|
||||
const dashboard = reactive({
|
||||
@@ -318,6 +388,102 @@ const statCards = computed(() => [
|
||||
{ key: "year", label: "本年报销", data: dashboard.year },
|
||||
]);
|
||||
|
||||
// ---------- 今日概览 ----------
|
||||
const todayDate = new Date();
|
||||
const WEEKDAYS = ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"];
|
||||
const todayWeekday = WEEKDAYS[todayDate.getDay()];
|
||||
const todayDay = `${todayDate.getMonth() + 1}月${todayDate.getDate()}日`;
|
||||
const timeGreeting = (() => {
|
||||
const h = new Date().getHours();
|
||||
if (h < 6) return "凌晨好";
|
||||
if (h < 9) return "早上好";
|
||||
if (h < 12) return "上午好";
|
||||
if (h < 14) return "中午好";
|
||||
if (h < 18) return "下午好";
|
||||
return "晚上好";
|
||||
})();
|
||||
const pad2 = (n) => String(n).padStart(2, "0");
|
||||
const todayStr = `${todayDate.getFullYear()}-${pad2(todayDate.getMonth() + 1)}-${pad2(todayDate.getDate())}`;
|
||||
|
||||
const todayScheduleEntry = ref(null);
|
||||
const todaySchedule = computed(() => {
|
||||
const entry = todayScheduleEntry.value;
|
||||
const isRest = !entry || !entry.shift_id || Number(entry.shift_id) === 0;
|
||||
const workStart = entry?.work_start || "";
|
||||
const workEnd = entry?.work_end || "";
|
||||
const time = workStart && workEnd ? `${workStart} - ${workEnd}` : "";
|
||||
return {
|
||||
status: isRest ? "rest" : "work",
|
||||
label: isRest ? "今日休息" : "今日上班",
|
||||
icon: isRest ? Moon : OfficeBuilding,
|
||||
time,
|
||||
shiftName: entry?.shift_name || "",
|
||||
};
|
||||
});
|
||||
|
||||
const weather = ref({
|
||||
available: false,
|
||||
temp: "",
|
||||
text: "",
|
||||
city: "",
|
||||
icon: PartlyCloudy,
|
||||
});
|
||||
// 天气图标:按中国天气网的天气现象文字(晴/多云/阴/小雨…)匹配
|
||||
const weatherIconOf = (desc) => {
|
||||
const s = String(desc || "");
|
||||
if (s.includes("雷")) return Lightning;
|
||||
if (s.includes("暴雨") || s.includes("大雨")) return Pouring;
|
||||
if (s.includes("雨")) return Drizzling;
|
||||
if (s.includes("雪")) return Cloudy;
|
||||
if (s.includes("阴")) return Cloudy;
|
||||
if (s.includes("云")) return PartlyCloudy;
|
||||
if (s.includes("晴")) return Sunny;
|
||||
return PartlyCloudy;
|
||||
};
|
||||
|
||||
const loadTodaySchedule = async () => {
|
||||
try {
|
||||
const res = await getMyScheduleBoard({ start_date: todayStr, end_date: todayStr });
|
||||
const list = res?.data?.list || [];
|
||||
todayScheduleEntry.value = list.length ? list[0] : null;
|
||||
} catch (e) {
|
||||
console.warn("加载今日排班失败:", e?.message, e);
|
||||
todayScheduleEntry.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const WEATHER_CITY = "连云港";
|
||||
const weatherLoading = ref(false);
|
||||
const loadWeather = async () => {
|
||||
if (weatherLoading.value) return;
|
||||
weatherLoading.value = true;
|
||||
try {
|
||||
const res = await request.get("/backend/weather/now", {
|
||||
params: { city: WEATHER_CITY },
|
||||
});
|
||||
const d = res?.data || {};
|
||||
if (!d.temp && !d.text) throw new Error(res?.msg || "天气数据为空");
|
||||
weather.value = {
|
||||
available: true,
|
||||
temp: d.temp || "",
|
||||
text: d.text || "",
|
||||
city: d.city || WEATHER_CITY,
|
||||
icon: weatherIconOf(d.text),
|
||||
};
|
||||
} catch (e) {
|
||||
console.warn("加载天气失败:", e?.message);
|
||||
weather.value = { ...weather.value, available: false };
|
||||
} finally {
|
||||
weatherLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 手动刷新天气(点击卡片右上角刷新图标)
|
||||
const refreshWeather = async () => {
|
||||
await loadWeather();
|
||||
if (weather.value.available) ElMessage.success("天气已更新");
|
||||
};
|
||||
|
||||
// 趋势图
|
||||
const trendChartRef = ref(null);
|
||||
let trendChart = null;
|
||||
@@ -423,7 +589,6 @@ const scheduleRanges = [
|
||||
];
|
||||
const scheduleRange = ref("today");
|
||||
|
||||
const pad2 = (n) => String(n).padStart(2, "0");
|
||||
const fmtDate = (d) =>
|
||||
`${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
||||
|
||||
@@ -660,6 +825,11 @@ const loadScheduleStats = async () => {
|
||||
};
|
||||
|
||||
const loadDashboard = async () => {
|
||||
// 今日概览与经营看板并行拉取
|
||||
await Promise.allSettled([
|
||||
loadTodaySchedule(),
|
||||
loadWeather(),
|
||||
]);
|
||||
try {
|
||||
const data = responseData(await getReimbursementDashboard());
|
||||
dashboard.week = data.week || emptyStat();
|
||||
@@ -730,11 +900,8 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.el-col {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
/* 行内换行/窄屏堆叠时的行间距;与下一张卡的间距由 .col-left 的 gap 统一控制 */
|
||||
row-gap: 16px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
@@ -782,7 +949,6 @@ onBeforeUnmount(() => {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.todo-stat-card {
|
||||
@@ -828,12 +994,12 @@ onBeforeUnmount(() => {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.col-left > .chart-card {
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
/* 统一卡片间距:两栏内部全部用 16px 的 flex gap,卡片自身不再写 margin */
|
||||
.col-left,
|
||||
.col-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
@@ -869,13 +1035,182 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.type-card {
|
||||
margin-bottom: 0;
|
||||
/* 今日概览卡片:与左侧 6 张指标卡区域等高 */
|
||||
.today-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 18px;
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.04);
|
||||
min-height: 250px;
|
||||
|
||||
/* 打招呼 */
|
||||
.tc-greet {
|
||||
.tc-hello {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.tc-sub {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
/* 天气 / 排班 两块信息 */
|
||||
.tc-tiles {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tc-tile {
|
||||
min-width: 0;
|
||||
padding: 10px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: 8px;
|
||||
|
||||
.tc-tile-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
min-height: 16px;
|
||||
}
|
||||
|
||||
.tc-tile-label {
|
||||
font-size: 11px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
.tc-refresh {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&.spinning {
|
||||
animation: tc-spin 1s linear infinite;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tc-weather {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
|
||||
.tc-weather-icon {
|
||||
flex-shrink: 0;
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.tc-weather-text {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tc-weather-temp {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 1.1;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.tc-weather-desc {
|
||||
margin-top: 2px;
|
||||
font-size: 11px;
|
||||
color: var(--el-text-color-secondary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&.tc-weather-empty {
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
}
|
||||
|
||||
.tc-shift {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.tc-shift-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
background: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
.tc-shift-text {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
&.work {
|
||||
.tc-shift-dot {
|
||||
background: var(--el-color-success);
|
||||
}
|
||||
.tc-shift-text {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tc-shift-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
/* 工作日程提醒(与今日概览合并展示) */
|
||||
.tc-schedule {
|
||||
margin-top: 2px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px dashed var(--el-border-color-lighter);
|
||||
|
||||
.tc-schedule-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.tc-section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tc-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.notice-card {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.notice-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -957,43 +1292,9 @@ onBeforeUnmount(() => {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.schedule-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/*min-height: 340px; */
|
||||
height: 600px;
|
||||
|
||||
.schedule-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.schedule-tabs {
|
||||
margin-bottom: 20px;
|
||||
|
||||
:deep(.el-tabs__header) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav-wrap::after) {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.schedule-list {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.schedule-tabs {
|
||||
:deep(.el-tabs__header) {
|
||||
margin: 0 0 10px;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
/* 列表由下方 .schedule-list 承载,tabs 只作切换栏,隐藏空的面板容器 */
|
||||
@@ -1025,16 +1326,12 @@ onBeforeUnmount(() => {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.schedule-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-height: 120px;
|
||||
max-height: 520px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -1229,10 +1526,6 @@ onBeforeUnmount(() => {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.col-right {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -1261,6 +1554,12 @@ html.dark & {
|
||||
background: var(--el-bg-color-page);
|
||||
}
|
||||
|
||||
.today-card {
|
||||
background: var(--el-bg-color);
|
||||
border-color: var(--el-border-color);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
h2 {
|
||||
color: var(--el-text-color-primary);
|
||||
|
||||
@@ -436,13 +436,20 @@ const dateRangeText = (row) => {
|
||||
return `${startDate || '?'} ~ ${endDate || '至今'}`;
|
||||
};
|
||||
|
||||
/** 合同状态:以结束日期判断履行中 / 已到期 */
|
||||
/**
|
||||
* 合同档案状态:按起止日期自动推导三态。
|
||||
* - 过期:结束日期已过
|
||||
* - 废弃:开始日期尚未到来(签订后未生效)
|
||||
* - 正常:履行中 / 无固定期限 / 仅一端有日期等其余场景
|
||||
*/
|
||||
const contractStatus = (row) => {
|
||||
const startDate = formatDateOnly(row.start_date);
|
||||
const endDate = formatDateOnly(row.end_date);
|
||||
if (!endDate) return { text: '长期', type: 'success' };
|
||||
return endDate >= new Date().toISOString().slice(0, 10)
|
||||
? { text: '履行中', type: 'success' }
|
||||
: { text: '已到期', type: 'warning' };
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
|
||||
if (endDate && endDate < today) return { text: '过期', type: 'warning' };
|
||||
if (startDate && startDate > today) return { text: '废弃', type: 'info' };
|
||||
return { text: '正常', type: 'success' };
|
||||
};
|
||||
|
||||
const isImage = (url) => /\.(png|jpe?g|gif|webp|bmp)(\?.*)?$/i.test(String(url || ''));
|
||||
|
||||
@@ -161,7 +161,7 @@ const uploadAccept = computed(() => (
|
||||
));
|
||||
const uploadTip = computed(() => (
|
||||
meta.value.type === 3
|
||||
? '支持图片、PDF、Word、Excel,大小不超过 20MB'
|
||||
? '支持图片、PDF、Word、Excel,大小不超过 30MB'
|
||||
: '支持图片 / PDF,大小不超过 10MB'
|
||||
));
|
||||
|
||||
@@ -218,7 +218,7 @@ const beforeUpload = (file) => {
|
||||
? ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']
|
||||
: ['application/pdf'];
|
||||
const allowed = file.type.startsWith('image/') || allowedTypes.includes(file.type);
|
||||
const limitMB = isContract ? 20 : 10;
|
||||
const limitMB = isContract ? 30 : 10;
|
||||
const withinLimit = file.size / 1024 / 1024 < limitMB;
|
||||
if (!allowed) ElMessage.error(isContract ? '仅支持图片、PDF、Word 或 Excel 格式' : '仅支持图片或 PDF 格式');
|
||||
if (!withinLimit) ElMessage.error(`文件大小不能超过 ${limitMB}MB`);
|
||||
|
||||
@@ -5,11 +5,16 @@
|
||||
<span v-if="overdueDays > 0" class="postpone-overdue"
|
||||
>该日程已逾期 {{ overdueDays }} 天。</span
|
||||
>
|
||||
延期后「{{ schedule.title }}」在 {{ schedule.schedule_date }}
|
||||
当天标记为已完成,并在目标日期自动生成一条待办继续跟进。
|
||||
「{{ schedule.title }}」在 {{ schedule.schedule_date }} 当天会标记为已完成。延期到会仅在目标日期创建一条待办;延期至会在次日至目标日期的每一天分别创建一条待办。
|
||||
</p>
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="延期到">
|
||||
<el-form-item label="延期方式">
|
||||
<el-radio-group v-model="postponeMode">
|
||||
<el-radio-button label="to">延期到(点对点)</el-radio-button>
|
||||
<el-radio-button label="through">延期至(逐日创建)</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="postponeMode === 'to' ? '延期到' : '延期至'">
|
||||
<el-date-picker
|
||||
v-model="targetDate"
|
||||
type="date"
|
||||
@@ -118,6 +123,7 @@ const overdueDays = computed(() => {
|
||||
});
|
||||
|
||||
const targetDate = ref("");
|
||||
const postponeMode = ref("to");
|
||||
const saving = ref(false);
|
||||
|
||||
// 打开时给出默认日期:逾期任务默认补到今天,未到期任务默认顺延到下一天
|
||||
@@ -148,10 +154,11 @@ async function submit() {
|
||||
saving.value = true;
|
||||
try {
|
||||
const res = await carrySchedule(props.schedule.id, {
|
||||
target_date: targetDate.value
|
||||
target_date: targetDate.value,
|
||||
mode: postponeMode.value
|
||||
});
|
||||
if (res?.code === 200) {
|
||||
ElMessage.success(`已延期到 ${targetDate.value}`);
|
||||
ElMessage.success(postponeMode.value === "to" ? `已延期到 ${targetDate.value}` : `已延期至 ${targetDate.value}`);
|
||||
visible.value = false;
|
||||
emit("saved");
|
||||
} else {
|
||||
|
||||
@@ -113,17 +113,7 @@
|
||||
<span v-else-if="cell.tag === 'work'" class="holiday-tag is-work"
|
||||
>班</span
|
||||
>
|
||||
<span v-if="cell.isToday" class="today-tag">今</span>
|
||||
<span v-else-if="cell.day === 1" class="month-tag"
|
||||
>{{ cell.month }}月</span
|
||||
>
|
||||
<span
|
||||
v-if="cell.myEntry"
|
||||
class="my-shift-chip"
|
||||
:style="{ background: cell.myEntry.color }"
|
||||
:title="`${cell.myEntry.shift_name} ${cell.myEntry.work_start}-${cell.myEntry.work_end}`"
|
||||
>{{ cell.myEntry.short_name || cell.myEntry.shift_name }}</span
|
||||
>
|
||||
<span v-if="cell.day === 1" class="month-tag">{{ cell.month }}月</span>
|
||||
</div>
|
||||
<div v-if="cell.holidayName" class="cell-holiday" :title="cell.holidayName">
|
||||
{{ cell.holidayName }}
|
||||
@@ -158,6 +148,8 @@
|
||||
还有 {{ cell.items.length - 3 }} 项
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="cell.myTag === 'work'" class="cell-sched-badge is-work">班</div>
|
||||
<div v-else-if="cell.myTag === 'rest'" class="cell-sched-badge is-rest">休</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1372,6 +1364,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.calendar-cell {
|
||||
position: relative;
|
||||
border-right: 1px solid #f2f4f8;
|
||||
border-bottom: 1px solid #f2f4f8;
|
||||
padding: 6px;
|
||||
@@ -1393,12 +1386,6 @@ onMounted(() => {
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.calendar-cell.selected {
|
||||
background: #ecf5ff;
|
||||
box-shadow: inset 0 0 0 2px #409eff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.cell-date {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1411,18 +1398,6 @@ onMounted(() => {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.calendar-cell.today .day-num {
|
||||
background: #409eff;
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.today-tag,
|
||||
.month-tag {
|
||||
font-size: 11px;
|
||||
color: #409eff;
|
||||
@@ -1468,73 +1443,90 @@ onMounted(() => {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 我的排班背景:上班浅绿 / 休息浅橙。排班是实际作息,覆盖节假日底色,
|
||||
但保留休/班角标与节日名;today / selected 在后面定义,优先级仍最高 */
|
||||
/* 我的排班背景:上班绿 / 休息橙。排班是实际作息,覆盖节假日底色,
|
||||
但保留休/班角标与节日名;today / selected 在后面定义,优先级仍最高。
|
||||
仅靠提高底色饱和度区分,不加边框/竖条 */
|
||||
.calendar-cell.is-my-work {
|
||||
background: #f0f9eb;
|
||||
background: #e2f6d2;
|
||||
}
|
||||
|
||||
.calendar-cell.is-my-work:hover {
|
||||
background: #e6f5db;
|
||||
background: #d3f0bd;
|
||||
}
|
||||
|
||||
/* 休息日:斜纹底 + 右上角「休」角标。
|
||||
斜纹比纯色更像「非工作日」,角标保证文字含义仍在,且不压住日程条目 */
|
||||
.calendar-cell.is-my-rest {
|
||||
background: #fdf6ec;
|
||||
background: repeating-linear-gradient(
|
||||
135deg,
|
||||
#fdf3e6 0,
|
||||
#fdf3e6 7px,
|
||||
#fbe6cd 7px,
|
||||
#fbe6cd 14px
|
||||
);
|
||||
}
|
||||
|
||||
.calendar-cell.is-my-rest:hover {
|
||||
background: #faecd8;
|
||||
background: repeating-linear-gradient(
|
||||
135deg,
|
||||
#fbeadb 0,
|
||||
#fbeadb 7px,
|
||||
#f8dcbb 7px,
|
||||
#f8dcbb 14px
|
||||
);
|
||||
}
|
||||
|
||||
/* 格内班次角标:用班次定义色做实底小色块 */
|
||||
.my-shift-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
/* 排班角标:上班「班」绿 / 休息「休」橙,统一放右上角,与日期行互不干扰 */
|
||||
.cell-sched-badge {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 0 4px;
|
||||
border-radius: 3px;
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
max-width: 56px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
|
||||
&.is-work {
|
||||
background: #67c23a;
|
||||
box-shadow: 0 1px 2px rgba(103, 194, 58, 0.35);
|
||||
}
|
||||
|
||||
&.is-rest {
|
||||
background: #e6a23c;
|
||||
box-shadow: 0 1px 2px rgba(230, 162, 60, 0.35);
|
||||
}
|
||||
}
|
||||
|
||||
/* 今天:整格实蓝底,最醒目。放在 is-off / is-work 之后,
|
||||
保证今天即使逢节假日也保留蓝色标识。
|
||||
实底较深,格内文字统一转白(或白底反白)以保证对比度。 */
|
||||
/* 选中态优先级高于排班底色:蓝色斜条纹 + 主色描边 */
|
||||
.calendar-cell.selected {
|
||||
background: repeating-linear-gradient(
|
||||
135deg,
|
||||
#ecf5ff 0,
|
||||
#ecf5ff 6px,
|
||||
#d9ecff 6px,
|
||||
#d9ecff 12px
|
||||
);
|
||||
box-shadow: inset 0 0 0 2px #409eff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 今天:不再用实底蓝,改为绿色描边框,保留排班底色(上班绿 / 休息斜纹)不变。
|
||||
放在 is-off / is-work / is-my-* 之后,保证框始终可见。 */
|
||||
.calendar-cell.today {
|
||||
background: #409eff;
|
||||
box-shadow: inset 0 0 0 2px #67c23a;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.calendar-cell.today:hover {
|
||||
background: #66b1ff;
|
||||
}
|
||||
|
||||
/* 日期数字改为白底蓝字,避免在蓝底上"蓝圆叠蓝底"糊成一片 */
|
||||
/* 今天的日期数字用绿色加粗强调,替代原来的「今」字标签 */
|
||||
.calendar-cell.today .day-num {
|
||||
background: #fff;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.calendar-cell.today .cell-holiday,
|
||||
.calendar-cell.today .today-tag {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 休/班角标改为白底,保持原有红/灰字色可辨识 */
|
||||
.calendar-cell.today .holiday-tag.is-off {
|
||||
background: #fff;
|
||||
color: #f56c6c;
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
.calendar-cell.today .holiday-tag.is-work {
|
||||
background: #fff;
|
||||
color: #909399;
|
||||
border-color: #fff;
|
||||
color: #529b2e;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 格内节日名称 */
|
||||
@@ -1845,11 +1837,16 @@ html.dark & {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
&.selected {
|
||||
background: var(--el-color-primary-light-9);
|
||||
background: repeating-linear-gradient(
|
||||
135deg,
|
||||
rgba(64, 158, 255, 0.14) 0,
|
||||
rgba(64, 158, 255, 0.14) 6px,
|
||||
rgba(64, 158, 255, 0.3) 6px,
|
||||
rgba(64, 158, 255, 0.3) 12px
|
||||
);
|
||||
box-shadow: inset 0 0 0 2px var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
.today-tag,
|
||||
.month-tag {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
@@ -1888,34 +1885,12 @@ html.dark & {
|
||||
background: rgba(230, 162, 60, 0.16);
|
||||
}
|
||||
}
|
||||
/* 今天:整格主色实底,与浅色主题保持一致 */
|
||||
/* 今天:与浅色主题一致,只用绿色描边框,不覆盖排班底色 */
|
||||
.calendar-cell.today {
|
||||
background: var(--el-color-primary);
|
||||
|
||||
&:hover {
|
||||
background: var(--el-color-primary-light-3);
|
||||
}
|
||||
box-shadow: inset 0 0 0 2px #67c23a;
|
||||
|
||||
.day-num {
|
||||
background: #fff;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.cell-holiday,
|
||||
.today-tag {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.holiday-tag.is-off {
|
||||
background: #fff;
|
||||
color: #f56c6c;
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
.holiday-tag.is-work {
|
||||
background: #fff;
|
||||
color: #909399;
|
||||
border-color: #fff;
|
||||
color: #95d475;
|
||||
}
|
||||
}
|
||||
.cell-holiday {
|
||||
|
||||
@@ -31,16 +31,69 @@
|
||||
<span class="period-range">{{ rangeText }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 主指标:本期购销、库存与应收 -->
|
||||
<div class="stats-grid">
|
||||
<div v-for="item in primaryStats" :key="item.label" class="stat-card" :class="item.tone">
|
||||
<div class="stat-icon">
|
||||
<el-icon :size="24"><component :is="item.icon" /></el-icon>
|
||||
<!-- 主指标:本期购销、库存与应收 + 右侧今日概览 -->
|
||||
<div class="top-row">
|
||||
<div class="top-row-left">
|
||||
<div class="stats-grid">
|
||||
<div v-for="item in primaryStats" :key="item.label" class="stat-card" :class="item.tone">
|
||||
<div class="stat-icon">
|
||||
<el-icon :size="24"><component :is="item.icon" /></el-icon>
|
||||
</div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-label">{{ item.label }}</div>
|
||||
<div class="stat-value">{{ item.value }}</div>
|
||||
<div class="stat-hint">{{ item.hint }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-label">{{ item.label }}</div>
|
||||
<div class="stat-value">{{ item.value }}</div>
|
||||
<div class="stat-hint">{{ item.hint }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 今日概览卡片:星期几、是否上班、天气 -->
|
||||
<div class="today-card">
|
||||
<div class="today-header">
|
||||
<div class="today-date">
|
||||
<div class="today-weekday">{{ todayWeekday }}</div>
|
||||
<div class="today-day">{{ todayDay }}</div>
|
||||
</div>
|
||||
<div class="today-schedule" :class="todaySchedule.status">
|
||||
<el-icon :size="18">
|
||||
<component :is="todaySchedule.icon" />
|
||||
</el-icon>
|
||||
<div class="schedule-info">
|
||||
<span class="schedule-label">{{ todaySchedule.label }}</span>
|
||||
<span class="schedule-time" v-if="todaySchedule.time">{{ todaySchedule.time }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="today-weather" v-if="weather.available">
|
||||
<div class="weather-main">
|
||||
<el-icon :size="36"><component :is="weather.icon" /></el-icon>
|
||||
<div class="weather-temp">{{ weather.temp }}°</div>
|
||||
</div>
|
||||
<div class="weather-detail">
|
||||
<span>{{ weather.text }}</span>
|
||||
<span v-if="weather.city" class="weather-city">{{ weather.city }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="today-weather weather-fallback" v-else>
|
||||
<el-icon :size="32"><Cloudy /></el-icon>
|
||||
<div class="weather-detail">
|
||||
<span>天气暂未获取</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="today-extra">
|
||||
<div class="extra-item">
|
||||
<span class="extra-label">是否上班</span>
|
||||
<span class="extra-value" :class="todaySchedule.status === 'work' ? 'text-success' : 'text-muted'">
|
||||
{{ todaySchedule.status === 'work' ? '上班' : '休息' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="extra-item" v-if="todaySchedule.shiftName">
|
||||
<span class="extra-label">班次</span>
|
||||
<span class="extra-value">{{ todaySchedule.shiftName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -144,9 +197,16 @@ import {
|
||||
TrendCharts,
|
||||
Coin,
|
||||
Refresh,
|
||||
Sunny,
|
||||
Cloudy,
|
||||
PartlyCloudy,
|
||||
Moon,
|
||||
Sunset,
|
||||
OfficeBuilding,
|
||||
} from '@element-plus/icons-vue';
|
||||
import { getErpDashboard } from '@/api/erpReport';
|
||||
import { getDocumentList } from '@/api/erpDocument';
|
||||
import { getMyScheduleBoard } from '@/api/oaScheduling';
|
||||
import {
|
||||
docTypeLabel,
|
||||
docStatusText,
|
||||
@@ -207,6 +267,89 @@ const netChart = shallowRef<echarts.ECharts | null>(null);
|
||||
const displayName = computed(() => authStore.user.name || authStore.user.account || '管理员');
|
||||
const tenantName = computed(() => authStore.user.tenant_name || '');
|
||||
|
||||
/** 今日日期信息 */
|
||||
const today = new Date();
|
||||
const WEEKDAYS = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
|
||||
const todayWeekday = WEEKDAYS[today.getDay()];
|
||||
const todayDay = `${today.getMonth() + 1}月${today.getDate()}日`;
|
||||
const todayStr = today.toISOString().slice(0, 10);
|
||||
|
||||
/** 今日排班(来自后端 /backend/oa/scheduling/my,仅今日) */
|
||||
const todayScheduleEntry = ref<any>(null);
|
||||
const SCHEDULE_STATUS_HOLIDAYS = [0]; // shift_id=0 或无记录 => 休息
|
||||
const todaySchedule = computed(() => {
|
||||
const entry = todayScheduleEntry.value;
|
||||
const isRest = !entry || !entry.shift_id || Number(entry.shift_id) === 0;
|
||||
const workStart = entry?.work_start || '';
|
||||
const workEnd = entry?.work_end || '';
|
||||
const time = workStart && workEnd ? `${workStart} - ${workEnd}` : '';
|
||||
return {
|
||||
status: isRest ? 'rest' : 'work',
|
||||
label: isRest ? '今日休息' : '今日上班',
|
||||
icon: isRest ? Moon : OfficeBuilding,
|
||||
time,
|
||||
shiftName: entry?.shift_name || '',
|
||||
};
|
||||
});
|
||||
|
||||
/** 天气:用 wttr.in 免费接口(无需 key),失败时优雅降级 */
|
||||
type WeatherInfo = {
|
||||
available: boolean;
|
||||
temp: string;
|
||||
text: string;
|
||||
city: string;
|
||||
icon: any;
|
||||
};
|
||||
const weather = ref<WeatherInfo>({
|
||||
available: false,
|
||||
temp: '',
|
||||
text: '',
|
||||
city: '',
|
||||
icon: PartlyCloudy,
|
||||
});
|
||||
const WEATHER_ICON_MAP: Record<string, any> = {
|
||||
Sunny, Clear: Sunny, Sun: Sunny,
|
||||
Cloudy, Overcast: Cloudy,
|
||||
'Partly cloudy': PartlyCloudy,
|
||||
Rain: Cloudy, Snow: Cloudy, Stormy: Cloudy,
|
||||
};
|
||||
const weatherIconOf = (desc: string) => {
|
||||
const key = String(desc || '').trim();
|
||||
return WEATHER_ICON_MAP[key] || PartlyCloudy;
|
||||
};
|
||||
|
||||
const loadTodaySchedule = async () => {
|
||||
try {
|
||||
const res = await getMyScheduleBoard({ start_date: todayStr, end_date: todayStr });
|
||||
const list = res?.data?.list || [];
|
||||
todayScheduleEntry.value = list.length ? list[0] : null;
|
||||
} catch {
|
||||
todayScheduleEntry.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const loadWeather = async () => {
|
||||
try {
|
||||
const res = await fetch('https://wttr.in/?format=j1', { mode: 'cors' });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const data = await res.json();
|
||||
const cur = data?.current_condition?.[0] || {};
|
||||
const area = data?.nearest_area?.[0] || {};
|
||||
const areaName =
|
||||
area?.areaName?.[0]?.value || area?.region?.[0]?.value || area?.country?.[0]?.value || '';
|
||||
const desc = cur.lang_zh?.[0]?.value || cur.weatherDesc?.[0]?.value || '';
|
||||
weather.value = {
|
||||
available: true,
|
||||
temp: cur.temp_C ?? '',
|
||||
text: desc,
|
||||
city: areaName,
|
||||
icon: weatherIconOf(desc),
|
||||
};
|
||||
} catch {
|
||||
weather.value = { ...weather.value, available: false };
|
||||
}
|
||||
};
|
||||
|
||||
const currentDate = computed(() => {
|
||||
const date = new Date();
|
||||
const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
|
||||
@@ -454,7 +597,14 @@ const loadAll = async () => {
|
||||
loading.value = true;
|
||||
noErp.value = false;
|
||||
try {
|
||||
const status = await loadDashboard();
|
||||
// 今日概览(排班 + 天气)和经营看板并行拉取
|
||||
const [dashboardStatus, scheduleRes, weatherPromise] = await Promise.allSettled([
|
||||
loadDashboard(),
|
||||
loadTodaySchedule(),
|
||||
loadWeather(),
|
||||
]);
|
||||
|
||||
const status = dashboardStatus.status === 'fulfilled' ? dashboardStatus.value : 'fail';
|
||||
if (status === 'denied') return;
|
||||
|
||||
const [approvingRes, draftRes, latestRes] = await Promise.allSettled([
|
||||
@@ -578,6 +728,24 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
// 主指标卡 + 右侧今日概览双列
|
||||
.top-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 320px;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
align-items: stretch;
|
||||
|
||||
.top-row-left {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 主指标卡
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
@@ -666,6 +834,163 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
// 今日概览卡片:与左侧 stats-grid 等高
|
||||
.today-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #f0f6ff 0%, #fef6f0 100%);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 12px;
|
||||
min-height: 0;
|
||||
|
||||
.today-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
|
||||
.today-date {
|
||||
.today-weekday {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.3;
|
||||
}
|
||||
.today-day {
|
||||
margin-top: 2px;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: var(--el-text-color-primary);
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
|
||||
.today-schedule {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.work {
|
||||
background: rgba(57, 115, 255, 0.1);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
&.rest {
|
||||
background: rgba(134, 144, 156, 0.12);
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.schedule-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 1.3;
|
||||
|
||||
.schedule-time {
|
||||
font-size: 11px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.today-weather {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 14px;
|
||||
background: rgba(255, 255, 255, 0.65);
|
||||
border-radius: 10px;
|
||||
|
||||
.weather-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #f59e0b;
|
||||
|
||||
.weather-temp {
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.weather-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 2px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
text-align: right;
|
||||
|
||||
.weather-city {
|
||||
font-size: 11px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
&.weather-fallback {
|
||||
justify-content: center;
|
||||
color: var(--el-text-color-placeholder);
|
||||
.weather-main {
|
||||
color: var(--el-text-color-placeholder);
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.weather-temp {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.today-extra {
|
||||
margin-top: auto;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
|
||||
.extra-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
|
||||
.extra-label {
|
||||
font-size: 11px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
.extra-value {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
|
||||
&.text-success {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
&.text-muted {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html.dark {
|
||||
.today-card {
|
||||
background: linear-gradient(135deg, #1e2a44 0%, #2a1e18 100%);
|
||||
border-color: #3a3a3a;
|
||||
|
||||
.today-weather {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 次级指标
|
||||
.mini-grid {
|
||||
display: grid;
|
||||
@@ -839,6 +1164,29 @@ onUnmounted(() => {
|
||||
|
||||
// 响应式
|
||||
@media (max-width: 1200px) {
|
||||
.top-row {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
.today-card {
|
||||
flex-direction: row;
|
||||
gap: 18px;
|
||||
|
||||
.today-header {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.today-weather {
|
||||
flex-shrink: 0;
|
||||
min-width: 180px;
|
||||
}
|
||||
.today-extra {
|
||||
margin-top: 0;
|
||||
grid-template-columns: 1fr;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.charts-section,
|
||||
.lists-section {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
Reference in new issue
Block a user