diff --git a/backend/apps/pisadmin/dashboard/views.py b/backend/apps/pisadmin/dashboard/views.py
index 6f95d5e..d148498 100644
--- a/backend/apps/pisadmin/dashboard/views.py
+++ b/backend/apps/pisadmin/dashboard/views.py
@@ -26,13 +26,16 @@ class BuyerTaskSerializer(serializers.Serializer):
inquiry_no = serializers.CharField()
status = serializers.CharField()
created_at = serializers.DateTimeField()
+ # 须声明,否则校验时会被丢弃,SerializerMethodField 无法读到库里的采购方式
+ buying_method = serializers.IntegerField(required=False, allow_null=True)
method = serializers.SerializerMethodField()
quote_deadline = serializers.DateTimeField(allow_null=True, required=False)
bid_start_time = serializers.DateTimeField(allow_null=True, required=False)
bid_end_time = serializers.DateTimeField(allow_null=True, required=False)
def get_method(self, obj):
- if obj.get('buying_method') == 2:
+ bm = obj.get('buying_method')
+ if bm == 2:
return '招标'
return '询价'
@@ -54,13 +57,15 @@ class SupplierQuoteSerializer(serializers.Serializer):
unit = serializers.CharField()
quantity = serializers.CharField(allow_blank=True, required=False)
status = serializers.IntegerField()
+ buying_method = serializers.IntegerField(required=False, allow_null=True)
method = serializers.SerializerMethodField()
quote_deadline = serializers.DateTimeField(allow_null=True, required=False)
bid_start_time = serializers.DateTimeField(allow_null=True, required=False)
bid_end_time = serializers.DateTimeField(allow_null=True, required=False)
def get_method(self, obj):
- if obj.get('buying_method') == 2:
+ bm = obj.get('buying_method')
+ if bm == 2:
return '招标'
return '询价'
@@ -139,10 +144,10 @@ class DashboardView(views.APIView):
}
def get_buyer_tasks(self, user):
- """采购方待办任务"""
+ """采购方待办任务(仅询价状态为发布/报价中/报价结束/比议价中的单,见 Inquiry.BUYER_DASHBOARD_TASK_STATUSES)。"""
inquiries = Inquiry.objects.filter(
create_user=user.username,
- status__in=[1, 2, 3]
+ status__in=Inquiry.BUYER_DASHBOARD_TASK_STATUSES,
).order_by('-create_time')[:10]
return [
{
@@ -164,12 +169,14 @@ class DashboardView(views.APIView):
# 超级管理员看到所有供应商汇总数据
if user.is_superuser:
total_quotes = QuotationMaster.objects.count()
- pending_quotes = QuotationMaster.objects.filter(status__in=[1, 2]).count()
+ pending_quotes = QuotationMaster.objects.filter(
+ status__in=QuotationMaster.DASHBOARD_PENDING_STATUSES
+ ).count()
won_quotes = QuotationMaster.objects.filter(is_awarded=1).count()
else:
total_quotes = QuotationMaster.objects.filter(supplier_code=user.username).count()
pending_quotes = QuotationMaster.objects.filter(
- supplier_code=user.username, status__in=[1, 2]
+ supplier_code=user.username, status__in=QuotationMaster.DASHBOARD_PENDING_STATUSES
).count()
won_quotes = QuotationMaster.objects.filter(
supplier_code=user.username, is_awarded=1
@@ -200,24 +207,53 @@ class DashboardView(views.APIView):
'overdue_quotes': overdue_quotes,
}
+ def _supplier_quote_remaining_sort_key(self, q, inquiry, now):
+ """用于待报价清单排序:剩余时间由少到多(越早越靠前);无截止时间排最后。"""
+ bm = (inquiry.buying_method if inquiry is not None else None) or q.buying_method
+ if bm == 2:
+ start, end = q.bid_start_time, q.bid_end_time
+ if not start:
+ return None
+ if now < start:
+ return (start - now).total_seconds()
+ if end and now < end:
+ return (end - now).total_seconds()
+ return float('inf')
+ if q.quote_deadline:
+ return (q.quote_deadline - now).total_seconds()
+ return None
+
def get_supplier_pending_quotes(self, user):
- """供应商待报价清单"""
- # 超级管理员看到所有待报价,供应商只看自己的
- if user.is_superuser:
- quotes = QuotationMaster.objects.filter(status__in=[1, 2]).order_by('-creattime')[:10]
- else:
- quotes = QuotationMaster.objects.filter(
- supplier_code=user.username,
- status__in=[1, 2]
- ).order_by('-creattime')[:10]
+ """供应商待报价清单:仅未报价(待报价)/报价中;按剩余时间由少到多取前 10 条。"""
+ base = QuotationMaster.objects.filter(status__in=QuotationMaster.DASHBOARD_PENDING_STATUSES)
+ if not user.is_superuser:
+ base = base.filter(supplier_code=user.username)
+ quotes = list(base)
+ if not quotes:
+ return []
+
+ now = timezone.now()
+ inquiry_nos = {q.inquiry_no for q in quotes}
+ inquiries = {i.inquiry_no: i for i in Inquiry.objects.filter(inquiry_no__in=inquiry_nos)}
+
+ def sort_key(q):
+ inq = inquiries.get(q.inquiry_no)
+ k = self._supplier_quote_remaining_sort_key(q, inq, now)
+ if k is None:
+ return float('inf')
+ return k
+
+ quotes.sort(key=sort_key)
+ quotes = quotes[:10]
+
+ from apps.pisadmin.miscprocurement.models import InquiryRfqItem
+
result = []
for q in quotes:
- inquiry = Inquiry.objects.filter(inquiry_no=q.inquiry_no).first()
- # 获取询价单中的数量
+ inquiry = inquiries.get(q.inquiry_no)
quantity = ''
unit = ''
if inquiry:
- from apps.pisadmin.miscprocurement.models import InquiryRfqItem
item = InquiryRfqItem.objects.filter(inquiry_no=q.inquiry_no).first()
if item:
quantity = item.qty
@@ -229,7 +265,7 @@ class DashboardView(views.APIView):
'quantity': quantity,
'unit': unit,
'status': q.status,
- 'buying_method': q.buying_method,
+ 'buying_method': (inquiry.buying_method if inquiry is not None else q.buying_method),
'quote_deadline': q.quote_deadline,
'bid_start_time': q.bid_start_time,
'bid_end_time': q.bid_end_time,
@@ -473,4 +509,5 @@ class DashboardView(views.APIView):
serializer = DashboardResponseSerializer(data=response_data)
serializer.is_valid(raise_exception=True)
- return SuccessResponse(data=serializer.validated_data, msg="获取成功")
+ # 须用 .data:validated_data 不含 SerializerMethodField(如 tasks[].method)
+ return SuccessResponse(data=serializer.data, msg="获取成功")
diff --git a/backend/apps/pisadmin/miscprocurement/models.py b/backend/apps/pisadmin/miscprocurement/models.py
index 47264bc..2c1a01c 100644
--- a/backend/apps/pisadmin/miscprocurement/models.py
+++ b/backend/apps/pisadmin/miscprocurement/models.py
@@ -195,6 +195,8 @@ class Inquiry(CoreModel):
(9, "落标(结束)"),
(0, "作废"),
)
+ # 采购端仪表盘「我的待办任务」:发布、报价中、报价结束(业务上常称「报价完成」)、比议价中
+ BUYER_DASHBOARD_TASK_STATUSES = (3, 4, 5, 6)
PAYMENT_METHOD_CHOICES = (
(1, "月结30天"),
(2, "月结60天"),
diff --git a/backend/apps/pissupplier/models.py b/backend/apps/pissupplier/models.py
index 0795918..8b3ab02 100644
--- a/backend/apps/pissupplier/models.py
+++ b/backend/apps/pissupplier/models.py
@@ -19,6 +19,8 @@ class QuotationMaster(models.Model):
(3, "已报价"),
(4, "已过期"),
)
+ # 供应商仪表盘「待报价清单」仅包含:待报价(业务上常称「未报价」)、报价中
+ DASHBOARD_PENDING_STATUSES = (1, 2)
AWARD_STATUS_CHOICES = (
(0, "未中标"),
diff --git a/web/src/stores/modules/dashboard.ts b/web/src/stores/modules/dashboard.ts
index 8a597ed..bf0592c 100644
--- a/web/src/stores/modules/dashboard.ts
+++ b/web/src/stores/modules/dashboard.ts
@@ -15,6 +15,10 @@ interface BuyerDashboard {
inquiry_no: string;
status: string;
created_at: string;
+ method?: string;
+ quote_deadline?: string | null;
+ bid_start_time?: string | null;
+ bid_end_time?: string | null;
}>;
messages: Array<{
id: number;
@@ -23,7 +27,7 @@ interface BuyerDashboard {
is_read: boolean;
created_at: string;
}>;
- trend: Array<{ month: string; count: number }>;
+ trend: Array<{ month?: string; day?: number; count: number }>;
}
interface SupplierDashboard {
@@ -37,10 +41,13 @@ interface SupplierDashboard {
id: number;
inquiry_no: string;
item_name: string;
- quantity: number;
+ quantity: string | number;
unit: string;
- deadline: string;
status: number;
+ method?: string;
+ quote_deadline?: string | null;
+ bid_start_time?: string | null;
+ bid_end_time?: string | null;
}>;
messages: Array<{
id: number;
@@ -49,7 +56,7 @@ interface SupplierDashboard {
is_read: boolean;
created_at: string;
}>;
- trend: Array<{ month: string; quotes: number; won: number }>;
+ trend: Array<{ month?: string; day?: number; quotes?: number; won?: number }>;
}
export const useDashboardStore = defineStore('dashboard', {
diff --git a/web/src/views/pisadmin/dashboard/BuyerDashboard.vue b/web/src/views/pisadmin/dashboard/BuyerDashboard.vue
index f742a8c..10f44a6 100644
--- a/web/src/views/pisadmin/dashboard/BuyerDashboard.vue
+++ b/web/src/views/pisadmin/dashboard/BuyerDashboard.vue
@@ -44,7 +44,7 @@
| 询价单号 |
采购方式 |
- 物料名称 |
+ 询价单名称 |
当前状态 |
截止时间 / 剩余时间 |
操作 |
@@ -61,10 +61,23 @@
{{ task.status }}
-
- {{ getDeadlineText(task) }}
- {{ getRemainingTimeText(task) }}
-
+
+
+
+ | {{ getDeadlinePrimaryLabel(task) }} |
+
+ {{ getDeadlinePrimaryValue(task) }}
+ |
+
+
+ |
+
+ 剩余:
+ {{ getDeadlineRemainingValue(task) }}
+ |
+
+
+
|
|
- | 暂无待办任务 |
+ 暂无待办任务 |
@@ -157,6 +170,25 @@ interface NewsItem {
title: string;
}
+/** 与 dashboard API buyer.tasks 一致 */
+interface DashboardBuyerTask {
+ id: number;
+ title: string;
+ inquiry_no: string;
+ status: string;
+ created_at: string;
+ method?: string;
+ quote_deadline?: string | null;
+ bid_start_time?: string | null;
+ bid_end_time?: string | null;
+}
+
+interface DashboardTrendPoint {
+ month?: string;
+ day?: number;
+ count: number;
+}
+
const store = useDashboardStore();
const { buyer } = storeToRefs(store);
const userInfo = useUserInfo();
@@ -191,13 +223,8 @@ const kpi = computed(
quote_timely_rate: 0,
}
);
-const tasks = computed(() => {
- const t = buyer.value?.tasks || [];
- console.log('[BuyerDashboard] buyer.value:', buyer.value);
- console.log('[BuyerDashboard] tasks computed:', t);
- return t;
-});
-const trend = computed(() => buyer.value?.trend || []);
+const tasks = computed((): DashboardBuyerTask[] => (buyer.value?.tasks || []) as DashboardBuyerTask[]);
+const trend = computed((): DashboardTrendPoint[] => (buyer.value?.trend || []) as DashboardTrendPoint[]);
// 获取消息列表
const getMsg = (): void => {
@@ -235,54 +262,119 @@ function getStatusClass(status: string) {
return 'status-badge status-gray';
}
-function getMethodClass(method: string) {
+function getMethodClass(method: string | undefined) {
if (method === '招标') return 'method-badge method-tender';
return 'method-badge method-inquiry';
}
-function getMethodText(method: string) {
+function getMethodText(method: string | undefined) {
return method || '询价';
}
-function getDeadlineClass(task: any): string {
- // Debug: log full task object to find actual field names
- console.log('[BuyerDashboard] getDeadlineClass full task:', JSON.stringify(task));
- const deadline = task.method === '招标' ? task.bid_end_time : task.quote_deadline;
- console.log('[BuyerDashboard] getDeadlineClass task:', task.inquiry_no, 'method:', task.method, 'deadline:', deadline);
- if (!deadline) return '';
- const now = new Date();
- const deadlineDate = new Date(deadline);
- const diffHours = (deadlineDate.getTime() - now.getTime()) / (1000 * 60 * 60);
- console.log('[BuyerDashboard] diffHours:', diffHours);
- if (diffHours < 0) return 'deadline-expired';
- if (diffHours < 24) return 'deadline-urgent'; // <24h red
- if (diffHours < 48) return 'deadline-warning'; // 24-48h yellow
- return '';
+function isTenderTask(task: any): boolean {
+ return task?.method === '招标';
}
-function getDeadlineText(task: any): string {
- const deadline = task.method === '招标' ? task.bid_end_time : task.quote_deadline;
- if (!deadline) return '-';
- const d = new Date(deadline);
- return `${d.getMonth() + 1}/${d.getDate()} ${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
+function formatDateTimeYMDHM(iso: string | Date | null | undefined): string {
+ if (!iso) return '-';
+ const d = new Date(iso);
+ if (Number.isNaN(d.getTime())) return '-';
+ const y = d.getFullYear();
+ const m = String(d.getMonth() + 1).padStart(2, '0');
+ const day = String(d.getDate()).padStart(2, '0');
+ const h = String(d.getHours()).padStart(2, '0');
+ const min = String(d.getMinutes()).padStart(2, '0');
+ return `${y}-${m}-${day} ${h}:${min}`;
}
-function getRemainingTimeText(task: any): string {
- const deadline = task.method === '招标' ? task.bid_end_time : task.quote_deadline;
- console.log('[BuyerDashboard] getRemainingTimeText task:', task.inquiry_no, 'deadline:', deadline);
- if (!deadline) return '';
- const now = new Date();
- const deadlineDate = new Date(deadline);
- const diffMs = deadlineDate.getTime() - now.getTime();
- console.log('[BuyerDashboard] diffMs:', diffMs);
- if (diffMs <= 0) return '已到期';
- const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
- const days = Math.floor(diffHours / 24);
- const hours = diffHours % 24;
- if (days > 0) {
- return `剩余 ${days}天${hours}小时`;
+function formatTimeHM(iso: string | Date): string {
+ const d = new Date(iso);
+ if (Number.isNaN(d.getTime())) return '-';
+ const h = String(d.getHours()).padStart(2, '0');
+ const min = String(d.getMinutes()).padStart(2, '0');
+ return `${h}:${min}`;
+}
+
+/** 投标时间:同一天为 yyyy-mm-dd hh:mm 至 hh:mm;跨天则两端均为完整日期时间 */
+function formatTenderBidTimeRange(
+ start: string | Date | null | undefined,
+ end: string | Date | null | undefined
+): string {
+ if (!start && !end) return '-';
+ if (!start) return formatDateTimeYMDHM(end);
+ if (!end) return formatDateTimeYMDHM(start);
+ const s = new Date(start);
+ const e = new Date(end);
+ if (Number.isNaN(s.getTime()) || Number.isNaN(e.getTime())) return '-';
+ const sameDay =
+ s.getFullYear() === e.getFullYear() &&
+ s.getMonth() === e.getMonth() &&
+ s.getDate() === e.getDate();
+ if (sameDay) {
+ const y = s.getFullYear();
+ const m = String(s.getMonth() + 1).padStart(2, '0');
+ const day = String(s.getDate()).padStart(2, '0');
+ return `${y}-${m}-${day} ${formatTimeHM(s)} 至 ${formatTimeHM(e)}`;
}
- return `剩余 ${hours}小时`;
+ return `${formatDateTimeYMDHM(s)} 至 ${formatDateTimeYMDHM(e)}`;
+}
+
+function formatDurationCn(diffMs: number): string {
+ if (diffMs <= 0) return '已到期';
+ const totalHours = Math.floor(diffMs / (1000 * 60 * 60));
+ const days = Math.floor(totalHours / 24);
+ const hours = totalHours % 24;
+ if (days > 0) return `${days}天${hours}小时`;
+ return `${hours}小时`;
+}
+
+/** 询价:距报价截止的剩余小时(用于着色);招标不用 */
+function getInquiryHoursToQuoteDeadline(task: any): number | null {
+ const q = task?.quote_deadline;
+ if (!q) return null;
+ return (new Date(q).getTime() - Date.now()) / (1000 * 60 * 60);
+}
+
+function getDeadlinePrimaryLabel(task: any): string {
+ return isTenderTask(task) ? '投标时间:' : '报价截止时间:';
+}
+
+function getDeadlinePrimaryValue(task: any): string {
+ if (isTenderTask(task)) {
+ return formatTenderBidTimeRange(task?.bid_start_time, task?.bid_end_time);
+ }
+ return formatDateTimeYMDHM(task?.quote_deadline);
+}
+
+function getDeadlineRemainingValue(task: any): string {
+ if (isTenderTask(task)) {
+ const start = task?.bid_start_time;
+ const end = task?.bid_end_time;
+ if (!start) return '-';
+ const now = Date.now();
+ const startMs = new Date(start).getTime();
+ const endMs = end ? new Date(end).getTime() : null;
+ if (now < startMs) return formatDurationCn(startMs - now);
+ if (endMs != null && now < endMs) return '投标进行中';
+ if (endMs != null && now >= endMs) return '已结束';
+ return '已开始';
+ }
+ const q = task?.quote_deadline;
+ if (!q) return '-';
+ return formatDurationCn(new Date(q).getTime() - Date.now());
+}
+
+/** 第一行时间 + 第二行「剩余」数值共用样式类 */
+function getDeadlineValueClass(task: any): string {
+ if (isTenderTask(task)) {
+ return 'deadline-tender-accent';
+ }
+ const h = getInquiryHoursToQuoteDeadline(task);
+ if (h == null || task?.quote_deadline == null) return '';
+ if (h < 0) return 'deadline-expired';
+ if (h < 24) return 'deadline-urgent';
+ if (h < 48) return 'deadline-warning';
+ return 'deadline-accent-normal';
}
function getActionText(status: string) {
@@ -384,8 +476,6 @@ function initChart() {
}
onMounted(() => {
- console.log('[BuyerDashboard] onMounted - buyer store:', buyer.value);
- console.log('[BuyerDashboard] onMounted - tasks:', tasks.value);
initChart();
getMsg();
window.addEventListener('resize', () => chartRef.value && echarts.getInstanceByDom(chartRef.value)?.resize());
@@ -455,12 +545,26 @@ watch(trend, () => {
transform: translateY(-2px);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
+
+ &.blue .kpi-icon {
+ color: var(--primary-color);
+ }
+ &.green .kpi-icon {
+ color: var(--success);
+ }
+ &.orange .kpi-icon {
+ color: var(--warning);
+ }
}
.kpi-icon {
font-size: 28px;
margin-bottom: 12px;
- opacity: 0.8;
+ opacity: 0.9;
+
+ i {
+ color: inherit;
+ }
}
.kpi-title {
@@ -630,19 +734,40 @@ tr:last-child td {
border: 1px solid #e9d5ff;
}
-/* 截止时间样式 */
-.deadline-cell {
- display: flex;
- flex-direction: column;
- gap: 2px;
+/* 截止时间 / 剩余时间(与采购端待办表格对齐) */
+.deadline-inner {
+ border-collapse: collapse;
+ width: 100%;
+ table-layout: auto;
}
-.deadline-time {
+.deadline-td-label {
+ vertical-align: top;
+ white-space: nowrap;
font-size: 13px;
color: var(--text-main);
+ padding: 0 8px 4px 0;
+ line-height: 1.45;
}
-.deadline-remaining {
+.deadline-td-value {
+ vertical-align: top;
+ font-size: 13px;
+ line-height: 1.45;
+ padding: 0 0 4px 0;
+}
+.deadline-td-gap {
+ padding: 0;
+ width: 0;
+}
+.deadline-td-remaining {
+ vertical-align: top;
font-size: 12px;
+ line-height: 1.45;
+ padding: 0;
+}
+.deadline-remaining-label {
color: var(--text-secondary);
+ font-weight: 400;
+ margin-right: 2px;
}
.deadline-urgent {
color: var(--danger) !important;
@@ -654,8 +779,19 @@ tr:last-child td {
}
.deadline-expired {
color: #9ca3af !important;
+ font-weight: 500;
text-decoration: line-through;
}
+/* 询价:距截止 >48h 时的强调色(参考稿橙红) */
+.deadline-accent-normal {
+ color: #ea580c !important;
+ font-weight: 600;
+}
+/* 招标:两行强调色统一 */
+.deadline-tender-accent {
+ color: #ea580c !important;
+ font-weight: 600;
+}
.action-btn {
padding: 6px 16px;
diff --git a/web/src/views/pisadmin/dashboard/SupplierDashboard.vue b/web/src/views/pisadmin/dashboard/SupplierDashboard.vue
index 5b3cb67..c89a681 100644
--- a/web/src/views/pisadmin/dashboard/SupplierDashboard.vue
+++ b/web/src/views/pisadmin/dashboard/SupplierDashboard.vue
@@ -8,7 +8,7 @@
-
+
报价单总数
{{ kpi.total_quotes.toLocaleString() }}
@@ -18,15 +18,15 @@
待报价
{{ kpi.pending_quotes }}
-
等待报价
+
等待报价
-
+
已中标
{{ kpi.won_quotes }}
中标成功
-
+
中标率
{{ kpi.conversion_rate }}%
@@ -53,21 +53,24 @@
{{ quote.inquiry_no }}
{{ getQuoteStatusText(quote.status) }}
{{ getMethodText(quote.method) }}
-
-
- 投标时间
- {{
- quote.bid_start_time ? formatDateRange(quote.bid_start_time, quote.bid_end_time) : '-'
- }}
- {{ getRemainingTimeText(quote) }}
-
-
- 报价截止时间
- {{
- quote.quote_deadline ? formatDate(quote.quote_deadline) : '-'
- }}
- {{ getRemainingTimeText(quote) }}
-
+
+
+
+
+ | {{ getDeadlinePrimaryLabel(quote) }} |
+
+ {{ getDeadlinePrimaryValue(quote) }}
+ |
+
+
+ |
+
+ 剩余:
+ {{ getDeadlineRemainingValue(quote) }}
+ |
+
+
+
@@ -196,12 +199,7 @@ const kpi = computed(
conversion_rate: 0,
}
);
-const pendingQuotes = computed(() => {
- const q = supplier.value?.pending_quotes || [];
- console.log('[SupplierDashboard] supplier.value:', supplier.value);
- console.log('[SupplierDashboard] pendingQuotes computed:', q);
- return q;
-});
+const pendingQuotes = computed(() => supplier.value?.pending_quotes || []);
const trend = computed(() => supplier.value?.trend || []);
// 获取消息列表
@@ -246,65 +244,122 @@ function getQuoteStatusClass(status: number) {
}
function getQuoteStatusText(status: number) {
- if (status === 1) return '待报价';
+ if (status === 1) return '未报价';
if (status === 2) return '报价中';
if (status === 3) return '已报价';
- return '待报价';
+ return '未报价';
}
-function getMethodClass(method: string) {
+function getMethodClass(method: string | undefined) {
if (method === '招标') return 'method-tender';
return 'method-inquiry';
}
-function getMethodText(method: string) {
+function getMethodText(method: string | undefined) {
return method || '询价';
}
-function getDeadlineClass(quote: any): string {
- const deadline = quote.method === '招标' ? quote.bid_end_time : quote.quote_deadline;
- console.log('[SupplierDashboard] getDeadlineClass quote:', quote.inquiry_no, 'method:', quote.method, 'deadline:', deadline);
- if (!deadline) return '';
- const now = new Date();
- const deadlineDate = new Date(deadline);
- const diffHours = (deadlineDate.getTime() - now.getTime()) / (1000 * 60 * 60);
- console.log('[SupplierDashboard] diffHours:', diffHours);
- if (diffHours < 0) return 'deadline-expired';
- if (diffHours < 24) return 'deadline-urgent';
- if (diffHours < 48) return 'deadline-warning';
- return '';
+function isTenderQuote(quote: any): boolean {
+ return quote?.method === '招标';
}
-function getRemainingTimeText(quote: any): string {
- const deadline = quote.method === '招标' ? quote.bid_end_time : quote.quote_deadline;
- console.log('[SupplierDashboard] getRemainingTimeText quote:', quote.inquiry_no, 'deadline:', deadline);
- if (!deadline) return '';
- const now = new Date();
- const deadlineDate = new Date(deadline);
- const diffMs = deadlineDate.getTime() - now.getTime();
- console.log('[SupplierDashboard] diffMs:', diffMs);
- if (diffMs <= 0) return '已到期';
- const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
- const days = Math.floor(diffHours / 24);
- const hours = diffHours % 24;
- if (days > 0) {
- return `剩余 ${days}天${hours}小时`;
- }
- return `剩余 ${hours}小时`;
+function formatDateTimeYMDHM(iso: string | Date | null | undefined): string {
+ if (!iso) return '-';
+ const d = new Date(iso);
+ if (Number.isNaN(d.getTime())) return '-';
+ const y = d.getFullYear();
+ const m = String(d.getMonth() + 1).padStart(2, '0');
+ const day = String(d.getDate()).padStart(2, '0');
+ const h = String(d.getHours()).padStart(2, '0');
+ const min = String(d.getMinutes()).padStart(2, '0');
+ return `${y}-${m}-${day} ${h}:${min}`;
}
-function formatDate(date: string) {
- if (!date) return '';
- const d = new Date(date);
- return `${d.getMonth() + 1}/${d.getDate()} ${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
+function formatTimeHM(iso: string | Date): string {
+ const d = new Date(iso);
+ if (Number.isNaN(d.getTime())) return '-';
+ const h = String(d.getHours()).padStart(2, '0');
+ const min = String(d.getMinutes()).padStart(2, '0');
+ return `${h}:${min}`;
}
-function formatDateRange(start: string, end: string) {
- if (!start || !end) return '-';
+function formatTenderBidTimeRange(
+ start: string | Date | null | undefined,
+ end: string | Date | null | undefined
+): string {
+ if (!start && !end) return '-';
+ if (!start) return formatDateTimeYMDHM(end);
+ if (!end) return formatDateTimeYMDHM(start);
const s = new Date(start);
const e = new Date(end);
- const formatShort = (d: Date) => `${d.getMonth() + 1}/${d.getDate()}`;
- return `${formatShort(s)}-${formatShort(e)}`;
+ if (Number.isNaN(s.getTime()) || Number.isNaN(e.getTime())) return '-';
+ const sameDay =
+ s.getFullYear() === e.getFullYear() &&
+ s.getMonth() === e.getMonth() &&
+ s.getDate() === e.getDate();
+ if (sameDay) {
+ const y = s.getFullYear();
+ const m = String(s.getMonth() + 1).padStart(2, '0');
+ const day = String(s.getDate()).padStart(2, '0');
+ return `${y}-${m}-${day} ${formatTimeHM(s)} 至 ${formatTimeHM(e)}`;
+ }
+ return `${formatDateTimeYMDHM(s)} 至 ${formatDateTimeYMDHM(e)}`;
+}
+
+function formatDurationCn(diffMs: number): string {
+ if (diffMs <= 0) return '已到期';
+ const totalHours = Math.floor(diffMs / (1000 * 60 * 60));
+ const days = Math.floor(totalHours / 24);
+ const hours = totalHours % 24;
+ if (days > 0) return `${days}天${hours}小时`;
+ return `${hours}小时`;
+}
+
+function getInquiryHoursToQuoteDeadline(quote: any): number | null {
+ const q = quote?.quote_deadline;
+ if (!q) return null;
+ return (new Date(q).getTime() - Date.now()) / (1000 * 60 * 60);
+}
+
+function getDeadlinePrimaryLabel(quote: any): string {
+ return isTenderQuote(quote) ? '投标时间:' : '报价截止时间:';
+}
+
+function getDeadlinePrimaryValue(quote: any): string {
+ if (isTenderQuote(quote)) {
+ return formatTenderBidTimeRange(quote?.bid_start_time, quote?.bid_end_time);
+ }
+ return formatDateTimeYMDHM(quote?.quote_deadline);
+}
+
+function getDeadlineRemainingValue(quote: any): string {
+ if (isTenderQuote(quote)) {
+ const start = quote?.bid_start_time;
+ const end = quote?.bid_end_time;
+ if (!start) return '-';
+ const now = Date.now();
+ const startMs = new Date(start).getTime();
+ const endMs = end ? new Date(end).getTime() : null;
+ if (now < startMs) return formatDurationCn(startMs - now);
+ if (endMs != null && now < endMs) return '投标进行中';
+ if (endMs != null && now >= endMs) return '已结束';
+ return '已开始';
+ }
+ const q = quote?.quote_deadline;
+ if (!q) return '-';
+ return formatDurationCn(new Date(q).getTime() - Date.now());
+}
+
+function getDeadlineValueClass(quote: any): string {
+ if (isTenderQuote(quote)) {
+ return 'deadline-tender-accent';
+ }
+ const h = getInquiryHoursToQuoteDeadline(quote);
+ if (h == null || quote?.quote_deadline == null) return '';
+ if (h < 0) return 'deadline-expired';
+ if (h < 24) return 'deadline-urgent';
+ if (h < 48) return 'deadline-warning';
+ return 'deadline-accent-normal';
}
function initChart() {
@@ -395,8 +450,6 @@ function initChart() {
}
onMounted(() => {
- console.log('[SupplierDashboard] onMounted - supplier store:', supplier.value);
- console.log('[SupplierDashboard] onMounted - pendingQuotes:', pendingQuotes.value);
initChart();
getMsg();
window.addEventListener('resize', () => chartRef.value && echarts.getInstanceByDom(chartRef.value)?.resize());
@@ -434,7 +487,7 @@ watch(trend, () => {
--danger: #ef4444;
--warning: #f59e0b;
--success: #10b981;
- --purple: #8b5cf6;
+ --gold: #d4af37;
--border-color: #e5e7eb;
font-family: 'Inter', 'PingFang SC', 'Microsoft YaHei', sans-serif;
background: var(--bg-body);
@@ -467,12 +520,43 @@ watch(trend, () => {
transform: translateY(-2px);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
+
+ &.green .kpi-icon {
+ color: var(--success);
+ }
+ &.orange .kpi-icon {
+ color: var(--warning);
+ }
+ &.gold .kpi-icon {
+ color: var(--gold);
+ }
+ &.blue .kpi-icon {
+ color: var(--primary-color);
+ }
+
+ /* 底部说明与卡片主题一致(避免语义类与主题色错位) */
+ &.green .kpi-trend.trend-up {
+ color: var(--success);
+ }
+ &.orange .kpi-trend {
+ color: var(--warning);
+ }
+ &.gold .kpi-trend {
+ color: var(--gold);
+ }
+ &.blue .kpi-trend.trend-flat {
+ color: var(--primary-color);
+ }
}
.kpi-icon {
font-size: 28px;
margin-bottom: 12px;
- opacity: 0.8;
+ opacity: 0.9;
+
+ i {
+ color: inherit;
+ }
}
.kpi-title {
@@ -701,7 +785,49 @@ tr:last-child td {
border: 1px solid #e9d5ff;
}
-/* 截止时间样式 */
+/* 截止时间 / 剩余时间(与采购端仪表盘一致) */
+.quote-deadline-cell {
+ margin-left: auto;
+ min-width: 220px;
+ max-width: 340px;
+ padding: 6px 10px;
+ background: #f9fafb;
+ border-radius: 6px;
+}
+.deadline-inner {
+ border-collapse: collapse;
+ width: 100%;
+ table-layout: auto;
+}
+.deadline-td-label {
+ vertical-align: top;
+ white-space: nowrap;
+ font-size: 13px;
+ color: var(--text-main);
+ padding: 0 8px 4px 0;
+ line-height: 1.45;
+}
+.deadline-td-value {
+ vertical-align: top;
+ font-size: 13px;
+ line-height: 1.45;
+ padding: 0 0 4px 0;
+}
+.deadline-td-gap {
+ padding: 0;
+ width: 0;
+}
+.deadline-td-remaining {
+ vertical-align: top;
+ font-size: 12px;
+ line-height: 1.45;
+ padding: 0;
+}
+.deadline-remaining-label {
+ color: var(--text-secondary);
+ font-weight: 400;
+ margin-right: 2px;
+}
.deadline-urgent {
color: var(--danger) !important;
font-weight: 700;
@@ -712,42 +838,16 @@ tr:last-child td {
}
.deadline-expired {
color: #9ca3af !important;
+ font-weight: 500;
text-decoration: line-through;
}
-
-.quote-countdown {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-left: auto;
- padding: 6px 12px;
- background: #f9fafb;
- border-radius: 6px;
+.deadline-accent-normal {
+ color: #ea580c !important;
+ font-weight: 600;
}
-
-.countdown-label {
- font-size: 12px;
- color: var(--text-secondary);
-}
-
-.countdown-time {
- font-size: 13px;
- color: var(--text-main);
- font-weight: 500;
-
- &.is-urgent {
- color: var(--danger);
- }
-}
-
-.countdown-remaining {
- font-size: 12px;
- color: var(--text-secondary);
-
- &.is-urgent {
- color: var(--danger);
- font-weight: 500;
- }
+.deadline-tender-accent {
+ color: #ea580c !important;
+ font-weight: 600;
}
.quote-info-row {