diff --git a/backend/apps/pisadmin/basicinfo/admin.py b/backend/apps/pisadmin/basicinfo/admin.py index 64f2e0b..7fcd190 100644 --- a/backend/apps/pisadmin/basicinfo/admin.py +++ b/backend/apps/pisadmin/basicinfo/admin.py @@ -25,7 +25,7 @@ class SupplierAdmin(admin.ModelAdmin): @admin.register(SupplierUser) class SupplierUserAdmin(admin.ModelAdmin): - list_display = ['supplier_id', 'supplier_name', 'user_email', 'user_name', 'company_code', 'status'] + list_display = ['supplier_id', 'supplier_name', 'user_email', 'user_name', 'status'] search_fields = ['supplier_id', 'supplier_name', 'user_email', 'user_name'] diff --git a/backend/apps/pisadmin/basicinfo/models.py b/backend/apps/pisadmin/basicinfo/models.py index 4d81d9e..d2ac2a8 100644 --- a/backend/apps/pisadmin/basicinfo/models.py +++ b/backend/apps/pisadmin/basicinfo/models.py @@ -74,9 +74,14 @@ class Supplier(CoreModel): class SupplierUser(CoreModel): - """供应商用户信息""" - company_code = models.CharField(max_length=10, verbose_name="交易厂区", help_text="交易厂区(公司)", null=True, blank=True) - supplier_id = models.CharField(max_length=20, unique=True, verbose_name="供应商唯一ID", help_text="供应商唯一ID") + """供应商用户信息(独立主数据;通过 supplier_id 与供应商主档逻辑关联,非数据库外键)""" + + supplier_id = models.CharField( + max_length=20, + db_index=True, + verbose_name="供应商唯一ID", + help_text="与供应商信息表 supplier_id 同值,可多条(多联系人)", + ) supplier_name = models.CharField(max_length=100, verbose_name="供应商全称", help_text="供应商全称") supplier_role = models.IntegerField(verbose_name="供应商角色", help_text="供应商角色(1:报价)") user_email = models.CharField(max_length=100, verbose_name="联络人邮箱", help_text="联络人邮箱") @@ -89,9 +94,6 @@ class SupplierUser(CoreModel): verbose_name = "供应商用户信息" verbose_name_plural = verbose_name ordering = ("-create_datetime", "id") - indexes = [ - models.Index(fields=["company_code"]), - ] def __str__(self): return self.supplier_name diff --git a/backend/apps/pisadmin/basicinfo/views/email_template.py b/backend/apps/pisadmin/basicinfo/views/email_template.py index b370daf..3b429b9 100644 --- a/backend/apps/pisadmin/basicinfo/views/email_template.py +++ b/backend/apps/pisadmin/basicinfo/views/email_template.py @@ -2,14 +2,16 @@ """邮件正文/主题模板:按业务场景 key 渲染,供询价发布、报价结束、询价截止提醒等流程调用。 扩展方式: -1. 在下方注册 ``EMAIL_TEMPLATE_FILES``(主题模板路径 + HTML 正文路径), - 再实现 ``build_context_<场景>(...)``,通过 ``render_email(TEMPLATE_xxx, ctx)`` 调用。 -2. 若需完全自定义渲染逻辑(非一对 .txt/.html),将 ``TEMPLATE_xxx`` 登记到 ``_CUSTOM_RENDERERS``。 +1. 推荐:单文件 ``emails/_template.html``,首行 ```` 为主题, + 余下为 HTML 正文;在 ``_CUSTOM_RENDERERS`` 中注册渲染函数(通常调用 ``_split_combined_subject_html``)。 +2. 或:在 ``EMAIL_TEMPLATE_FILES`` 登记 (subject.txt, body.html) 路径对,由 ``render_template_pair`` 渲染。 +3. 运行时注册:``register_email_template(..., renderer=...)`` 或 ``register_email_template(..., sub, body)``。 """ from __future__ import annotations import os +import re from typing import Any, Callable, Dict, Optional, Tuple from django.conf import settings @@ -19,6 +21,8 @@ from django.utils import timezone # 杂采询价单发布通知(新询价邀请) TEMPLATE_RFS_PUBLISH = "RFS_publish" +# 招标方式发布通知(单文件 HTML:首行注释解析主题 + 正文) +TEMPLATE_BIDS_PUBLISH = "Bids_publish" # 全部供应商已报价 → 询价单进入「报价结束」— 通知采购负责人 TEMPLATE_QUOTE_ENDED = "Quote_ended" # 报价截止后无待报价/报价中单 → 询价单收口为「报价结束」(典型:sync_expired)— 询价截止提醒 @@ -129,6 +133,56 @@ def build_context_rfs_publish( } +def build_context_bids_publish( + inquiry: Any, + supplier_group: Dict[str, Any], + *, + purchaser_company_name: str = "", +) -> Dict[str, Any]: + """ + 招标邀请邮件(``bids_publish_template.html``):在 ``build_context_rfs_publish`` 基础上增加 + ``bid_time_range``、``material_short``、``portal_url``、``purchaser_name`` 等变量。 + """ + ctx = build_context_rfs_publish(inquiry, supplier_group, purchaser_company_name=purchaser_company_name) + + bs = getattr(inquiry, "bid_start_time", None) + be = getattr(inquiry, "bid_end_time", None) + if bs and be: + bsl = _for_local_display(bs) + bel = _for_local_display(be) + if bsl and bel and bsl.date() == bel.date(): + bid_time_range = f"{bsl.strftime('%Y-%m-%d %H:%M')}~{bel.strftime('%H:%M')}" + elif bsl and bel: + bid_time_range = f"{bsl.strftime('%Y-%m-%d %H:%M')} ~ {bel.strftime('%Y-%m-%d %H:%M')}" + else: + bid_time_range = "—" + elif be: + bel = _for_local_display(be) + bid_time_range = bel.strftime("%Y-%m-%d %H:%M") if bel else "请登录系统查看" + elif bs: + bsl = _for_local_display(bs) + bid_time_range = (bsl.strftime("%Y-%m-%d %H:%M") + " 起") if bsl else "请登录系统查看" + else: + bid_time_range = str(ctx.get("deadline_time") or "请登录系统查看") + + mat_plain = str(ctx.get("material_info") or "") + material_short = mat_plain[:120] + ("…" if len(mat_plain) > 120 else "") + + purchaser_name = _quote_ended_purchaser_name(inquiry) + portal_url = str(ctx.get("system_link") or "").strip() + + ctx.update( + { + "bid_time_range": bid_time_range, + "material_short": material_short, + "portal_url": portal_url, + "purchaser_name": purchaser_name, + "purchaser_phone": str(ctx.get("contact_phone") or "").strip(), + } + ) + return ctx + + def _quote_ended_purchaser_name(inquiry: Any) -> str: """采购负责人展示名:优先系统用户姓名,否则采购负责人字段原文。""" buyer = (getattr(inquiry, "buyer", None) or "").strip() @@ -357,14 +411,10 @@ def build_context_quote_deadline_expired( # --------------------------------------------------------------------------- -# 模板文件注册:template_key -> (subject 相对 templates/, body 相对 templates/) -# 新增一类邮件时:在此增加一行,并放置对应 templates/emails/*.txt / *.html +# 可选:template_key -> (subject 相对 templates/, body 相对 templates/),由 render_template_pair 渲染。 +# 内置场景均已改为单文件 *_template.html,见 _CUSTOM_RENDERERS。 # --------------------------------------------------------------------------- -EMAIL_TEMPLATE_FILES: Dict[str, Tuple[str, str]] = { - TEMPLATE_QUOTE_ENDED: ("emails/quote_ended_subject.txt", "emails/quote_ended_body.html"), - TEMPLATE_QUOTE_TIMEOUT: ("emails/quote_timeout_subject.txt", "emails/quote_timeout_body.html"), - TEMPLATE_QUOTE_DEADLINE_EXPIRED: ("emails/quote_deadline_expired_subject.txt", "emails/quote_deadline_expired_body.html"), -} +EMAIL_TEMPLATE_FILES: Dict[str, Tuple[str, str]] = {} def render_template_pair( @@ -382,7 +432,7 @@ def render_template_pair( def _render_rfs_publish_body(ctx: Dict[str, Any]) -> Tuple[str, str]: - """RFS_publish:主题需 material_short,在渲染前注入。""" + """RFS_publish:单文件 ``rfs_publish_template.html``;主题需 material_short,在渲染前注入。""" mat_plain = str(ctx.get("material_info") or "") material_short = mat_plain[:120] + ("…" if len(mat_plain) > 120 else "") @@ -390,15 +440,52 @@ def _render_rfs_publish_body(ctx: Dict[str, Any]) -> Tuple[str, str]: **ctx, "material_short": material_short, } - return render_template_pair( - "emails/rfs_publish_subject.txt", - "emails/rfs_publish_body.html", - render_ctx, - ) + html = render_to_string("emails/rfs_publish_template.html", render_ctx) + return _split_combined_subject_html(html) + + +def _render_quote_ended_combined(ctx: Dict[str, Any]) -> Tuple[str, str]: + """Quote_ended:单文件 ``quote_ended_template.html``。""" + html = render_to_string("emails/quote_ended_template.html", ctx) + return _split_combined_subject_html(html) + + +def _render_quote_timeout_combined(ctx: Dict[str, Any]) -> Tuple[str, str]: + """Quote_timeout:单文件 ``quote_timeout_template.html``。""" + html = render_to_string("emails/quote_timeout_template.html", ctx) + return _split_combined_subject_html(html) + + +# 首行必须为:(主题内勿含连续两个减号 ``--``,以免破坏 HTML 注释) +_COMBINED_EMAIL_SUBJECT_RE = re.compile( + r"^\s*\s*", + re.IGNORECASE | re.DOTALL, +) + + +def _split_combined_subject_html(html: str) -> Tuple[str, str]: + """从合并模板中解析 (subject, body_html)。""" + m = _COMBINED_EMAIL_SUBJECT_RE.match(html) + if not m: + raise ValueError( + "合并邮件模板必须以 开头(首行),参见 emails/*_template.html" + ) + subject = m.group(1).replace("\r", " ").replace("\n", " ").strip() + body = html[m.end() :].lstrip() + return subject, body + + +def _render_bids_publish_combined(ctx: Dict[str, Any]) -> Tuple[str, str]: + """Bids_publish:单文件 HTML,首行注释为主题。""" + html = render_to_string("emails/bids_publish_template.html", ctx) + return _split_combined_subject_html(html) _CUSTOM_RENDERERS: Dict[str, Callable[[Dict[str, Any]], Tuple[str, str]]] = { TEMPLATE_RFS_PUBLISH: _render_rfs_publish_body, + TEMPLATE_BIDS_PUBLISH: _render_bids_publish_combined, + TEMPLATE_QUOTE_ENDED: _render_quote_ended_combined, + TEMPLATE_QUOTE_TIMEOUT: _render_quote_timeout_combined, } @@ -407,7 +494,7 @@ def render_email(template_key: str, context: Dict[str, Any]) -> Tuple[str, str]: 按模板类型渲染邮件。 解析顺序: - 1. ``_CUSTOM_RENDERERS`` 中注册的完全自定义渲染器; + 1. ``_CUSTOM_RENDERERS``(单文件 ``*_template.html`` 等); 2. ``EMAIL_TEMPLATE_FILES`` 中的 (subject, body) 路径对; 否则抛出 ``ValueError``。 diff --git a/backend/apps/pisadmin/basicinfo/views/email_utils.py b/backend/apps/pisadmin/basicinfo/views/email_utils.py index 353ed44..a5dd0e3 100644 --- a/backend/apps/pisadmin/basicinfo/views/email_utils.py +++ b/backend/apps/pisadmin/basicinfo/views/email_utils.py @@ -4,13 +4,15 @@ import tempfile import urllib.parse import urllib.request from datetime import timedelta -from typing import Tuple, Dict, Any, List +from typing import Any, Dict, List, Tuple from rest_framework import serializers from rest_framework.decorators import action +from rest_framework.response import Response from django.conf import settings from django.core.mail import EmailMessage, get_connection from django.utils import timezone +from django.utils.dateparse import parse_datetime from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.viewset import CustomModelViewSet from django.contrib.auth import get_user_model @@ -85,6 +87,7 @@ def resolve_inquiry_purchaser_emails(inquiry) -> list: def send_quote_ended_notice_to_purchaser(inquiry, *, last_quotation_no: str = "") -> bool: """ 询价单进入「报价结束」时通知采购负责人(HTML 邮件 + EmailNotice 记录)。 + 正文与主题由 ``emails/quote_ended_template.html`` 首行 ```` 解析。 无有效收件人时跳过发送,返回 False。 """ from apps.pisadmin.basicinfo.views.email_template import ( @@ -155,6 +158,7 @@ def send_quote_timeout_notice_to_purchaser(inquiry) -> bool: """ 询价截止提醒(HTML + EmailNotice):由 ``notify_purchasers_quote_timeout_for_inquiries`` 在 供应商端 ``POST .../quotation_master/sync_expired/`` 将超期未报价单置为已过期之后按需调用。 + 正文与主题由 ``emails/quote_timeout_template.html`` 首行 ```` 解析。 无有效收件人时跳过发送,返回 False。 """ from apps.pisadmin.basicinfo.views.email_template import ( @@ -229,6 +233,76 @@ def notify_purchasers_quote_timeout_for_inquiries(inquiry_nos: List[str]) -> int return sent +def send_bids_publish_notice_to_supplier( + inquiry, + supplier_group: Dict[str, Any], + *, + purchaser_company_name: str = "", +) -> Tuple[bool, EmailNotice]: + """ + 招标邀请邮件(单文件 ``bids_publish_template.html``):向单个供应商分组发送 HTML 邮件并写入 EmailNotice。 + + 无有效收件人时仍创建 notice 并标记失败,返回 (False, notice);成功发送返回 (True, notice)。 + """ + from apps.pisadmin.basicinfo.views.email_template import ( + TEMPLATE_BIDS_PUBLISH, + build_context_bids_publish, + render_email, + ) + + email = (supplier_group.get("contact_email") or "").strip() + to_list = [email] if email else [] + + ctx = build_context_bids_publish( + inquiry, + supplier_group, + purchaser_company_name=purchaser_company_name or "", + ) + subject, body = render_email(TEMPLATE_BIDS_PUBLISH, ctx) + + notice = EmailNotice.objects.create( + subject=subject, + body=body, + to_emails=to_list, + cc_emails=[], + bcc_emails=[], + attachments=[], + biz_type="inquiry_bids_publish", + biz_id=getattr(inquiry, "inquiry_no", None) or "", + status="pending", + payload={ + "template_key": TEMPLATE_BIDS_PUBLISH, + "is_html": True, + "inquiry_no": ctx.get("rfq_number"), + "inquiry_title": ctx.get("inquiry_title"), + "bid_time_range": ctx.get("bid_time_range"), + "supplier_name": (supplier_group.get("supplier_name") or "").strip(), + }, + ) + + if not to_list: + notice.status = "failed" + notice.last_error = "缺少供应商邮箱" + notice.save(update_fields=["status", "last_error", "update_datetime"]) + return False, notice + + notice.status = "sending" + notice.save(update_fields=["status", "update_datetime"]) + + success, detail = send_email_notice(notice) + notice.response = detail or {} + if success: + notice.status = "success" + notice.sent_at = timezone.now() + notice.last_error = None + else: + notice.status = "failed" + notice.last_error = detail.get("error") if isinstance(detail, dict) else str(detail) + + notice.save(update_fields=["status", "sent_at", "response", "last_error", "update_datetime"]) + return success, notice + + def send_email_notice(notice) -> Tuple[bool, Dict[str, Any]]: """Send an email based on EmailNotice instance. diff --git a/backend/apps/pisadmin/basicinfo/views/supplier.py b/backend/apps/pisadmin/basicinfo/views/supplier.py index 7737feb..a45625d 100644 --- a/backend/apps/pisadmin/basicinfo/views/supplier.py +++ b/backend/apps/pisadmin/basicinfo/views/supplier.py @@ -1,15 +1,13 @@ # -*- coding: utf-8 -*- from rest_framework import serializers -from rest_framework.decorators import action from apps.pisadmin.basicinfo.models import Supplier, SupplierUser -from dvadmin.utils.json_response import DetailResponse, ErrorResponse, SuccessResponse from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.viewset import CustomModelViewSet class SupplierUserSerializer(CustomModelSerializer): - """供应商用户信息(子表 pis_supplier_users)""" + """供应商用户信息""" class Meta: model = SupplierUser @@ -26,25 +24,13 @@ class SupplierUserCreateUpdateSerializer(CustomModelSerializer): class SupplierSerializer(CustomModelSerializer): - """供应商信息-序列化器(含关联 supplier_user)""" - - supplier_user = serializers.SerializerMethodField(read_only=True) + """供应商信息""" class Meta: model = Supplier - fields = [f.name for f in Supplier._meta.concrete_fields] + ["supplier_user"] + fields = [f.name for f in Supplier._meta.concrete_fields] read_only_fields = ["id"] - def get_supplier_user(self, obj): - cache = self.context.get("supplier_user_by_sid") - if cache is not None: - su = cache.get(obj.supplier_id) - else: - su = SupplierUser.objects.filter(supplier_id=obj.supplier_id).first() - if su is None: - return None - return SupplierUserSerializer(su, context=self.context).data - class SupplierCreateUpdateSerializer(CustomModelSerializer): """供应商信息 创建/更新""" @@ -73,71 +59,14 @@ class SupplierViewSet(CustomModelViewSet): search_fields = ["supplier_name", "supplier_short_name", "supplier_id", "company_code"] ordering = ["-create_datetime"] - def get_serializer_context(self): - ctx = super().get_serializer_context() - m = getattr(self, "_supplier_user_map", None) - if m is not None: - ctx["supplier_user_by_sid"] = m - return ctx - - def list(self, request, *args, **kwargs): - queryset = self.filter_queryset(self.get_queryset()) - page = self.paginate_queryset(queryset) - if page is not None: - rows = page - else: - rows = list(queryset) - sids = [row.supplier_id for row in rows] - self._supplier_user_map = { - u.supplier_id: u for u in SupplierUser.objects.filter(supplier_id__in=sids) - } - try: - serializer = self.get_serializer(rows, many=True, request=request) - if page is not None: - return self.get_paginated_response(serializer.data) - return SuccessResponse(data=serializer.data, msg="获取成功") - finally: - self._supplier_user_map = None - - @action(methods=["get", "put", "patch"], detail=True, url_path="supplier_user") - def supplier_user_nested(self, request, pk=None): - """按供应商主键维护关联的供应商用户信息(与 Supplier.supplier_id 对应,唯一)。""" - supplier = self.get_object() - sid = (supplier.supplier_id or "").strip() - if not sid: - return ErrorResponse(msg="供应商缺少 supplier_id") - - if request.method == "GET": - su = SupplierUser.objects.filter(supplier_id=sid).first() - if su is None: - return DetailResponse(data=None, msg="暂无供应商用户信息") - return DetailResponse(data=SupplierUserSerializer(su, context={"request": request}).data) - - su = SupplierUser.objects.filter(supplier_id=sid).first() - ser = SupplierUserCreateUpdateSerializer( - instance=su, - data=request.data, - partial=request.method == "PATCH", - context={"request": request}, - ) - ser.is_valid(raise_exception=True) - name = ser.validated_data.get("supplier_name") - if name is None or str(name).strip() == "": - name = supplier.supplier_name - obj = ser.save(supplier_id=sid, supplier_name=name) - return DetailResponse( - data=SupplierUserSerializer(obj, context={"request": request}).data, - msg="保存成功", - ) - class SupplierUserViewSet(CustomModelViewSet): - """供应商用户信息接口(子表;与 Supplier.supplier_id 关联)""" + """供应商用户信息(独立表;按 supplier_id 筛选可得到某供应商下全部联系人)""" queryset = SupplierUser.objects.all() serializer_class = SupplierUserSerializer create_serializer_class = SupplierUserCreateUpdateSerializer update_serializer_class = SupplierUserCreateUpdateSerializer - filter_fields = ["company_code", "supplier_id", "supplier_role", "status", "user_email"] + filter_fields = ["supplier_id", "supplier_role", "status", "user_email"] search_fields = ["supplier_id", "supplier_name", "user_email", "user_name", "user_phone"] ordering = ["-create_datetime", "id"] diff --git a/backend/apps/pisadmin/miscprocurement/models.py b/backend/apps/pisadmin/miscprocurement/models.py index cf3b556..8fd763c 100644 --- a/backend/apps/pisadmin/miscprocurement/models.py +++ b/backend/apps/pisadmin/miscprocurement/models.py @@ -921,67 +921,5 @@ class RFQOperationLogs(CoreModel): except Exception: logger.exception("写入询价操作日志失败") - @classmethod - def bulk_append_quote_deadline_expired( - cls, - quotation_rows: list, - inquiry_purchase_type: dict, - *, - operation_user: Optional[str] = None, - ) -> int: - """ - 供应商端批量将超期报价单置为「已过期」时,按报价单一笔一条写入操作类型「报价截止」(5)。 - - ``quotation_rows``:``values()`` 得到的列表,每项须含 ``quotation_no``、``inquiry_no``、``status``(置过期前,1/2)、``supplier_name``。 - ``inquiry_purchase_type``:``inquiry_no`` -> ``purchase_type``(杂采/策采)。 - """ - from apps.pissupplier.models import QuotationMaster - - if not quotation_rows: - return 0 - now = timezone.now() - op_user = (operation_user or "").strip()[:20] if operation_user else None - qm_status_label = dict(QuotationMaster.STATUS_CHOICES) - cur_label = (qm_status_label.get(4) or "已过期")[:100] - - batch = [] - for row in quotation_rows: - inq_no = str(row.get("inquiry_no") or "").strip()[:20] - qn = str(row.get("quotation_no") or "").strip()[:20] or "-" - supplier_name = (str(row.get("supplier_name") or "").strip() or "—")[:50] - desc = f"供应商({supplier_name})逾期未报价"[:200] - try: - old_st = int(row.get("status")) - except (TypeError, ValueError): - old_st = None - pt = inquiry_purchase_type.get(inq_no) - if pt is None: - continue - per_label = (qm_status_label.get(old_st) or "")[:50] if old_st is not None else None - batch.append( - cls( - operation_type="5", - operation_user=op_user, - operation_time=now, - operation_desc=desc, - inquiry_no=inq_no, - quotation_no=qn, - per_status=per_label or None, - cur_status=cur_label, - is_show_user=1, - purchase_type=int(pt), - create_datetime=now, - update_datetime=now, - ) - ) - if not batch: - return 0 - try: - cls.objects.bulk_create(batch, batch_size=300) - except Exception: - logger.exception("批量写入报价截止询价操作日志失败") - return 0 - return len(batch) - def __str__(self) -> str: return f"{self.inquiry_no}-{self.quotation_no}" diff --git a/backend/apps/pisadmin/miscprocurement/views.py b/backend/apps/pisadmin/miscprocurement/views.py index 71d5584..7fa320f 100644 --- a/backend/apps/pisadmin/miscprocurement/views.py +++ b/backend/apps/pisadmin/miscprocurement/views.py @@ -1,5 +1,6 @@ import logging from decimal import Decimal +from datetime import datetime from typing import Optional from django.db import transaction @@ -16,7 +17,7 @@ from apps.pisadmin.basicinfo.views.email_template import ( build_context_rfs_publish, render_email, ) -from apps.pisadmin.basicinfo.views.email_utils import send_email_notice +from apps.pisadmin.basicinfo.views.email_utils import send_bids_publish_notice_to_supplier, send_email_notice from apps.pissupplier.models import ( QuotationMaster, @@ -30,6 +31,70 @@ from apps.pissupplier.models import ( logger = logging.getLogger(__name__) +def _fmt_dt_for_inquiry_quotation(dt: Optional[datetime]) -> Optional[str]: + if not dt: + return None + if timezone.is_naive(dt): + dt = timezone.make_aware(dt, timezone.get_current_timezone()) + return dt.strftime("%Y-%m-%d %H:%M:%S") + + +def _quotation_master_status_label(status) -> str: + """与 ``QuotationMaster.STATUS_CHOICES`` 一致的可读文案。""" + labels = {1: "待报价", 2: "报价中", 3: "已报价", 4: "已过期"} + try: + st = int(status) if status is not None else 1 + except (TypeError, ValueError): + return "—" + return labels.get(st, str(st)) + + +def build_invited_supplier_quotation_rows(inquiry) -> list: + """询价单发布后各受邀供应商报价单当前状态(与 ``QuotationMaster`` 一致,供操作日志嵌套表使用)。""" + inquiry_no = (inquiry.inquiry_no or "").strip() + if not inquiry_no: + return [] + + name_map: dict[str, str] = {} + for inv in InquirySupplier.objects.filter(inquiry_no=inquiry_no).only("supplier_code", "supplier_name"): + code = (inv.supplier_code or "").strip() + if not code: + continue + n = (inv.supplier_name or "").strip() + prev = name_map.get(code, "") + if len(n) > len(prev): + name_map[code] = n + + rows_out: list[dict] = [] + qms = QuotationMaster.objects.filter(inquiry_no=inquiry_no).order_by("supplier_code", "autoid") + for qm in qms: + code = (qm.supplier_code or "").strip() + full_name = (name_map.get(code) or (qm.supplier_name or "").strip() or code or "—").strip() + + st = int(qm.status or 1) + if st == 3 and qm.quotetime: + op_time = _fmt_dt_for_inquiry_quotation(qm.quotetime) + else: + op_time = _fmt_dt_for_inquiry_quotation(qm.creattime) if qm.creattime else None + + if st == 3: + op_user = (qm.quoteuser or "").strip() or "—" + else: + op_user = "—" + + rows_out.append( + { + "operation_time": op_time or "—", + "supplier_full_name": full_name, + "operator": op_user, + "quotation_status": _quotation_master_status_label(qm.status), + "operation_desc": "", + "quotation_no": (qm.quotation_no or "").strip(), + } + ) + return rows_out + + from .models import ( MiscProcurementMaterialInfo, MiscProcurementStationInfo, @@ -150,12 +215,13 @@ def _rfq_qty_decimal(inquiry: Inquiry, pid: str) -> Optional[Decimal]: def _sync_misc_low_price_records(inquiry: Inquiry, part_id: str) -> None: """ 制程最低价落库(与比价展开明细一致): + - 前端比价展开不依赖成本结构模板:材料按材质分组仅展示重量/单价/材料费用,加工按工站分组仅展示加工费;落库口径仍按下列规则。 - 询价单下**每个上阶物料料号**单独一套主/次表数据(多料号互不合并)。 - **材料**:次表两行分别记录最低重量、最低单价;souce_no 为取得该最小值对应的报价单单号,单价若来自「杂采材料信息」 则存交易厂区(factory);主表材料行 souce_no 聚合为 W:…;U:…(重量来源与单价来源可能不同)。 - **加工**:仅主表一行(无次表、不按工站);每报价单合计加工费后取最小,全空/全 0 仍写入 MinPrice=0。 - 注意:材料行必须用 quotation_no__in=报价单号列表 过滤,勿用 QuerySet(QuotationMaster) 作 __in,否则 ORM 按主键匹配会查不到材料行。 - - **其它**:包装费、运输费分列取 min(优先 sup_quotation_other;无则用上阶物料 total_other_expense 回退)。 + - **其它**:包装费、运输费分列取 min(优先 sup_quotation_other;无则用上阶物料 total_other_expense 回退)。采购端比价主表「其它成本」行制程最低价列与「加工成本」相同,可链向合计最低的报价单详情。 - **利润率**:cost_type=6,各报价单该料号利润率取 min。杂采不落库管销研(cost_type=5)。 """ inquiry_no = (inquiry.inquiry_no or "").strip() @@ -363,6 +429,8 @@ class MiscMaterialViewSet(CustomModelViewSet): update_serializer_class = MiscMaterialCreateUpdateSerializer search_fields = ["materialtype", "factory"] ordering = ["-create_datetime"] + # 与询价单 `Inquiry.company_code` 一致,须精确匹配 `factory`,避免 sz_avc 与 sz_avcx 串数据 + filter_fields = ("factory", "status", "materialtype") class MiscStationViewSet(CustomModelViewSet): @@ -372,6 +440,7 @@ class MiscStationViewSet(CustomModelViewSet): update_serializer_class = MiscStationCreateUpdateSerializer search_fields = ["stationname", "stationcode", "company_code"] ordering = ["-create_datetime"] + filter_fields = ("company_code", "status", "stationcode", "stationname") class MiscPartViewSet(CustomModelViewSet): @@ -783,6 +852,7 @@ class InquiryViewSet(CustomModelViewSet): 按询价单供应商子表 `InquirySupplier` 汇总:同一 `supplier_code`(空代码时用行级占位)合并多料号 `part_id`, 供 `_create_supplier_quotations` 为每个供应商生成一份 `QuotationMaster`。 返回条目的 supplier_code / supplier_name / 联系人字段均来自子表行数据(截断至报价主表字段长度)。 + 采购端保存时,联系人/邮箱/电话由前端在「供应商用户信息」表中按 supplier_id 选择后写入子表,此处不再与主档做 ORM 关联校验。 """ supplier_map = {} for row in inquiry.suppliers.all(): @@ -1100,6 +1170,8 @@ class InquiryViewSet(CustomModelViewSet): return ErrorResponse(msg='只有“确认”状态的询价单才能发布') current_user = self._get_request_username() or None current_time = timezone.now() + supplier_groups = self._group_inquiry_suppliers(instance) + supplier_count = len(supplier_groups) try: with transaction.atomic(): self._create_supplier_quotations(instance) # 发布询价单时按供应商关联表,逐个创建对应的报价单 @@ -1112,7 +1184,7 @@ class InquiryViewSet(CustomModelViewSet): release_user=current_user, release_time=current_time, operation_type=3, - operation_desc="询价单发布", + operation_desc=f"询价单发布,共{supplier_count}个受邀供应商", ) except serializers.ValidationError as exc: detail = getattr(exc, "detail", None) @@ -1226,6 +1298,13 @@ class InquiryViewSet(CustomModelViewSet): data = RFQOperationLogsSerializer(qs, many=True).data return DetailResponse(data=data, msg="success") + @action(methods=["get"], detail=True, url_path="invited_supplier_quotations") + def invited_supplier_quotations(self, request, pk=None): + """受邀供应商报价单一览(与发布生成的 ``QuotationMaster`` 一致,供操作日志嵌套表使用)。""" + instance = self.get_object() + data = build_invited_supplier_quotation_rows(instance) + return DetailResponse(data=data, msg="success") + @action(methods=["put"], detail=True, url_path="save_negotiation_records") def save_negotiation_records(self, request, pk=None): """按报价单写入杂采议价记录:议价结果 + 该报价单议价前含税/不含税总价快照(来自上阶物料明细)。""" @@ -1320,11 +1399,20 @@ class InquiryViewSet(CustomModelViewSet): return purchaser_company_name = self._resolve_purchaser_company_name(inquiry) + is_bidding = int(getattr(inquiry, "buying_method", 1) or 1) == 2 for vendor in supplier_groups: if not isinstance(vendor, dict): continue + if is_bidding: + send_bids_publish_notice_to_supplier( + inquiry, + vendor, + purchaser_company_name=purchaser_company_name, + ) + continue + supplier_name = (vendor.get("supplier_name") or "").strip() email = (vendor.get("contact_email") or "").strip() to_list = [email] if email else [] diff --git a/backend/apps/pissupplier/serializers.py b/backend/apps/pissupplier/serializers.py index e40bd39..6895777 100644 --- a/backend/apps/pissupplier/serializers.py +++ b/backend/apps/pissupplier/serializers.py @@ -266,6 +266,8 @@ class NestedQuotationProcessSerializer(QuotationProcessSerializer): class QuotationOtherSerializer(serializers.ModelSerializer): + """报价单其它成本子表:按料号维度存包装费、运输费;采购端比价展开「其它成本」明细与此字段一致。""" + class Meta: model = QuotationOther fields = [ @@ -447,6 +449,7 @@ class QuotationMasterSerializer(BusinessAuditSerializer): return cache[inquiry_no] def get_inquiry_company_code(self, obj): + """与 `Inquiry.company_code` 一致;供应商端拉取杂采材质/工站列表时须按此精确过滤厂区。""" code, _ = self._inquiry_plant_tuple(getattr(obj, "inquiry_no", None)) return code diff --git a/backend/apps/pissupplier/views.py b/backend/apps/pissupplier/views.py index d4c8114..ec45a6a 100644 --- a/backend/apps/pissupplier/views.py +++ b/backend/apps/pissupplier/views.py @@ -43,8 +43,8 @@ def _supplier_bidding_window_error(instance: QuotationMaster): be = getattr(instance, "bid_end_time", None) if bs is None or be is None: return ErrorResponse(msg="招标项目未设置投标开始或截止时间,无法报价或提交") - if now < bs: - return ErrorResponse(msg="投标尚未开始") + # if now < bs: + # return ErrorResponse(msg="投标尚未开始") if now > be: return ErrorResponse(msg="已超过投标截止时间") return None @@ -53,7 +53,7 @@ def _supplier_bidding_window_error(instance: QuotationMaster): class QuotationMasterViewSet(CustomModelViewSet): """杂采报价单主表管理接口 - 详情 GET 与采购端比价弹窗「供应商报价预览」共用:返回 `QuotationMasterSerializer` 及嵌套材料/加工/其它/利润/上阶物料等。 + 详情 GET 与采购端比价弹窗「供应商报价预览」共用:返回 `QuotationMasterSerializer` 及嵌套材料/加工/其它(包装费、运输费)/利润/上阶物料等。 """ queryset = QuotationMaster.objects.prefetch_related( @@ -114,12 +114,6 @@ class QuotationMasterViewSet(CustomModelViewSet): username = getattr(getattr(request, "user", None), "username", None) with transaction.atomic(): updated = qs.update(status=4) - if quotation_rows: - RFQOperationLogs.bulk_append_quote_deadline_expired( - quotation_rows, - inquiry_purchase_type, - operation_user=username, - ) inquiries_closed = Inquiry.sync_to_quote_closed_when_no_open_quotations( inquiry_nos, actor_username=username, diff --git a/backend/templates/emails/bids_publish_template.html b/backend/templates/emails/bids_publish_template.html new file mode 100644 index 0000000..cae4724 --- /dev/null +++ b/backend/templates/emails/bids_publish_template.html @@ -0,0 +1,41 @@ + + + + + +

尊敬的 {{ vendor_name }} 合作伙伴:

+

您好!

+

我司({{ purchaser_company_name }})现正式发布新的采购询价单,诚邀贵司参与投标,我们非常期待与您的合作。

+ +

📋 询价单概要

+ + +

💡 如何参与报价?

+

请点击下方按钮登录系统查看详情并填写报价:

+ {% if portal_url %} +

+ 立即前往投标 +

+

若按钮无法点击,请复制以下链接到浏览器打开:

+

{{ portal_url }}

+ {% else %} +

系统未配置供应商门户地址(PIS_SUPPLIER_PORTAL_URL),请联系管理员获取登录方式。

+ {% endif %} + +

⚠️ 注意事项

+ + +

祝商祺!

+

{{ purchaser_company_name }} 采购部

+

{{ current_date }}

+

此邮件由系统自动发送,请勿直接回复。

+ + diff --git a/backend/templates/emails/quote_ended_subject.txt b/backend/templates/emails/quote_ended_subject.txt deleted file mode 100644 index eaac796..0000000 --- a/backend/templates/emails/quote_ended_subject.txt +++ /dev/null @@ -1 +0,0 @@ -{% autoescape off %}【报价完成通知】询价单{{ inquiry_no }}-{{ material_or_project_short }}已全部报价,请进行比价{% endautoescape %} diff --git a/backend/templates/emails/quote_ended_body.html b/backend/templates/emails/quote_ended_template.html similarity index 92% rename from backend/templates/emails/quote_ended_body.html rename to backend/templates/emails/quote_ended_template.html index 5d2d7df..6183771 100644 --- a/backend/templates/emails/quote_ended_body.html +++ b/backend/templates/emails/quote_ended_template.html @@ -1,3 +1,4 @@ + diff --git a/backend/templates/emails/quote_timeout_subject.txt b/backend/templates/emails/quote_timeout_subject.txt deleted file mode 100644 index b322be3..0000000 --- a/backend/templates/emails/quote_timeout_subject.txt +++ /dev/null @@ -1 +0,0 @@ -{% autoescape off %}【询价截止提醒】询价单{{ inquiry_no }}-{{ material_or_project_short }}已截止,请进行比价{% endautoescape %} diff --git a/backend/templates/emails/quote_timeout_body.html b/backend/templates/emails/quote_timeout_template.html similarity index 92% rename from backend/templates/emails/quote_timeout_body.html rename to backend/templates/emails/quote_timeout_template.html index c94a4c2..08ef59b 100644 --- a/backend/templates/emails/quote_timeout_body.html +++ b/backend/templates/emails/quote_timeout_template.html @@ -1,3 +1,4 @@ + diff --git a/backend/templates/emails/rfs_publish_subject.txt b/backend/templates/emails/rfs_publish_subject.txt deleted file mode 100644 index 2a2d429..0000000 --- a/backend/templates/emails/rfs_publish_subject.txt +++ /dev/null @@ -1 +0,0 @@ -【新询价邀请】{{ rfq_number|default:"—" }} - {{ material_short }} - 截止:{{ deadline_date_subject|default:"—" }} diff --git a/backend/templates/emails/rfs_publish_body.html b/backend/templates/emails/rfs_publish_template.html similarity index 93% rename from backend/templates/emails/rfs_publish_body.html rename to backend/templates/emails/rfs_publish_template.html index 37d3f8c..5d6fe1f 100644 --- a/backend/templates/emails/rfs_publish_body.html +++ b/backend/templates/emails/rfs_publish_template.html @@ -1,3 +1,4 @@ + diff --git a/docs/superpowers/specs/2026-04-03-git-collaboration-design.md b/docs/superpowers/specs/2026-04-03-git-collaboration-design.md index 3e3683c..d13086f 100644 --- a/docs/superpowers/specs/2026-04-03-git-collaboration-design.md +++ b/docs/superpowers/specs/2026-04-03-git-collaboration-design.md @@ -1,7 +1,8 @@ # Git 协作规范设计 > 创建日期:2026-04-03 -> 状态:已批准 +> 状态:评审中 +> 作者:Lewis Yan > 关联:Git 协作标准化 --- @@ -119,12 +120,12 @@ develop ──────────────────────── **安装方式:** ```bash cd backend -pip install pre-commit commitlint +pip install pre-commit ruff pre-commit install ``` **检查内容:** -- Python:`flake8`、`black` +- Python:`ruff`(替代 flake8 + black + isort,速度快 10-100x) - 通用:禁止 debug 代码、敏感信息检查 **.pre-commit-config.yaml 示例:** @@ -138,11 +139,11 @@ repos: - id: check-yaml - id: check-added-large-files - - repo: https://github.com/pycqa/flake8 - rev: 7.0.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.9.0 hooks: - - id: flake8 - args: [--max-line-length=120] + - id: ruff + - id: ruff-format ``` ### 前端(Node.js)— husky @@ -204,7 +205,7 @@ npx husky add .husky/pre-commit "npx lint-staged" Push → 自动触发 Jenkins Build ├── Backend │ ├── 单元测试 (pytest) - │ ├── 代码检查 (flake8) + │ ├── 代码检查 (ruff) │ └── 构建 │ ├── Frontend @@ -256,7 +257,7 @@ Push → 自动触发 Jenkins Build | Git Hooks(后端) | pre-commit | backend/ | | Git Hooks(前端) | husky + lint-staged | web/ | | CI/CD | Jenkins | 全项目 | -| 代码风格(后端) | flake8, black | backend/ | +| 代码风格(后端) | ruff(lint + format) | backend/ | | 代码风格(前端) | eslint, prettier | web/ | --- diff --git a/web/package-lock.json b/web/package-lock.json index 2286d0e..6f3f865 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -80,6 +80,8 @@ "baseline-browser-mapping": "^2.9.19", "eslint": "^8.57.1", "eslint-plugin-vue": "^9.27.0", + "husky": "^9.1.7", + "lint-staged": "^16.4.0", "prettier": "^2.8.1", "sass": "^1.56.2", "typescript": "^4.9.4", @@ -1482,7 +1484,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1499,7 +1500,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1516,7 +1516,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1533,7 +1532,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1550,7 +1548,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1567,7 +1564,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1584,7 +1580,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1601,7 +1596,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1618,7 +1612,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1635,7 +1628,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1652,7 +1644,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1669,7 +1660,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1686,7 +1676,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1703,7 +1692,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1720,7 +1708,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1737,7 +1724,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1754,7 +1740,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1771,7 +1756,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1788,7 +1772,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1805,7 +1788,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1822,7 +1804,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1839,7 +1820,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1856,7 +1836,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2440,7 +2419,6 @@ "version": "2.5.6", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -2480,7 +2458,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2501,7 +2478,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2522,7 +2498,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2543,7 +2518,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2564,7 +2538,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2585,7 +2558,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2606,7 +2578,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2627,7 +2598,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2648,7 +2618,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2669,7 +2638,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2690,7 +2658,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2711,7 +2678,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2732,7 +2698,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2779,7 +2744,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2793,7 +2757,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2807,7 +2770,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2821,7 +2783,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2835,7 +2796,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2849,7 +2809,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2863,7 +2822,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2877,7 +2835,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2891,7 +2848,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2905,7 +2861,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2919,7 +2874,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2933,7 +2887,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2947,7 +2900,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2961,7 +2913,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2975,7 +2926,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2989,7 +2939,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3003,7 +2952,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3017,7 +2965,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3031,7 +2978,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3045,7 +2991,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3059,7 +3004,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3073,7 +3017,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3087,7 +3030,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3101,7 +3043,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3115,7 +3056,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3879,7 +3819,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, "node_modules/@types/event-emitter": { @@ -4746,6 +4685,22 @@ "integrity": "sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ==", "license": "MIT" }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmmirror.com/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -5261,7 +5216,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "readdirp": "^4.0.1" @@ -5278,6 +5233,56 @@ "resolved": "https://registry.npmjs.org/claygl/-/claygl-1.3.0.tgz", "integrity": "sha512-+gGtJjT6SSHD2l2yC3MCubW/sCV40tZuSs5opdtn79vFSGUgp/lH139RNEQ6Jy078/L0aV8odCw8RSrUcMfLaQ==" }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "8.2.0", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-8.2.0.tgz", + "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/clipboard": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", @@ -5331,6 +5336,13 @@ "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", "license": "MIT" }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -5968,7 +5980,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, "license": "Apache-2.0", "optional": true, "engines": { @@ -6319,6 +6330,19 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/errno": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", @@ -6421,7 +6445,6 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -7120,6 +7143,19 @@ "node": ">=6.9.0" } }, + "node_modules/get-east-asian-width": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -7425,6 +7461,22 @@ "ms": "^2.0.0" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmmirror.com/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/i18next": { "version": "20.6.1", "resolved": "https://registry.npmjs.org/i18next/-/i18next-20.6.1.tgz", @@ -7521,7 +7573,7 @@ "version": "5.1.5", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/import-cwd": { @@ -8106,6 +8158,137 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, + "node_modules/lint-staged": { + "version": "16.4.0", + "resolved": "https://registry.npmmirror.com/lint-staged/-/lint-staged-16.4.0.tgz", + "integrity": "sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^14.0.3", + "listr2": "^9.0.5", + "picomatch": "^4.0.3", + "string-argv": "^0.3.2", + "tinyexec": "^1.0.4", + "yaml": "^2.8.2" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmmirror.com/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/lint-staged/node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmmirror.com/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/loader-utils": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", @@ -8216,6 +8399,115 @@ "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", "license": "MIT" }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -8382,6 +8674,19 @@ "node": ">= 0.6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -8660,7 +8965,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "dev": true, "license": "MIT", "optional": true }, @@ -8798,6 +9102,22 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -10060,7 +10380,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 14.18.0" @@ -10126,6 +10446,23 @@ "node": ">=4" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -10181,6 +10518,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rollup": { + "version": "2.80.0", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-2.80.0.tgz", + "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", + "license": "MIT", + "peer": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/rollup-plugin-postcss": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/rollup-plugin-postcss/-/rollup-plugin-postcss-4.0.2.tgz", @@ -10310,7 +10663,7 @@ "version": "1.97.3", "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "chokidar": "^4.0.0", @@ -10609,6 +10962,52 @@ "slate": ">=0.65.3" } }, + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/snabbdom": { "version": "3.6.3", "resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.6.3.tgz", @@ -10778,6 +11177,16 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-hash": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", @@ -11163,6 +11572,23 @@ "node": ">=8.10.0" } }, + "node_modules/tailwindcss/node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/terser": { "version": "5.46.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", @@ -11233,6 +11659,16 @@ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", "license": "MIT" }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -11353,7 +11789,7 @@ "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -11515,7 +11951,6 @@ "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", - "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.21.3", @@ -11599,7 +12034,6 @@ "version": "4.59.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 9b82115..25d3e5f 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -139,7 +139,7 @@ importers: version: 3.2.0(vue@3.5.31(typescript@4.9.5)) tailwindcss: specifier: ^3.2.7 - version: 3.4.19 + version: 3.4.19(yaml@2.8.3) ts-md5: specifier: ^1.3.1 version: 1.3.1 @@ -216,6 +216,12 @@ importers: eslint-plugin-vue: specifier: ^9.27.0 version: 9.33.0(eslint@8.57.1) + husky: + specifier: ^9.1.7 + version: 9.1.7 + lint-staged: + specifier: ^16.4.0 + version: 16.4.0 prettier: specifier: ^2.8.1 version: 2.8.8 @@ -1668,6 +1674,10 @@ packages: resolution: {integrity: sha512-FipRmyd16Pr/tEey/YaaQ/24Pc3HEpLM9S1DRakEuXlSLXNIJnu1oJtHM53eVYpvW3dXapSjrip3xylZUTIZVQ==} engines: {node: '>=8'} + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} + engines: {node: '>=18'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1830,6 +1840,14 @@ packages: claygl@1.3.0: resolution: {integrity: sha512-+gGtJjT6SSHD2l2yC3MCubW/sCV40tZuSs5opdtn79vFSGUgp/lH139RNEQ6Jy078/L0aV8odCw8RSrUcMfLaQ==} + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-truncate@5.2.0: + resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} + engines: {node: '>=20'} + clipboard@2.0.11: resolution: {integrity: sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==} @@ -1851,10 +1869,17 @@ packages: colord@2.9.3: resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -2150,6 +2175,9 @@ packages: peerDependencies: vue: ^2.6.11 || ^3.2.0 + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -2170,6 +2198,10 @@ packages: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + errno@0.1.8: resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==} hasBin: true @@ -2284,6 +2316,9 @@ packages: eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + ext@1.7.0: resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} @@ -2407,6 +2442,10 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-east-asian-width@1.5.0: + resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -2491,6 +2530,11 @@ packages: humanize-ms@1.2.1: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + i18next@20.6.1: resolution: {integrity: sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==} @@ -2579,6 +2623,10 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} + engines: {node: '>=18'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -2735,6 +2783,15 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + lint-staged@16.4.0: + resolution: {integrity: sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==} + engines: {node: '>=20.17'} + hasBin: true + + listr2@9.0.5: + resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==} + engines: {node: '>=20.0.0'} + loader-utils@3.3.1: resolution: {integrity: sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==} engines: {node: '>= 12.13.0'} @@ -2790,6 +2847,10 @@ packages: lodash@4.18.0: resolution: {integrity: sha512-l1mfj2atMqndAHI3ls7XqPxEjV2J9ZkcNyHpoZA3r2T1LLwDB69jgkMWh71YKwhBbK0G2f4WSn05ahmQXVxupA==} + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -2862,6 +2923,10 @@ packages: engines: {node: '>=4.0.0'} hasBin: true + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} @@ -2992,6 +3057,10 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -3492,6 +3561,10 @@ packages: engines: {node: '>= 0.4'} hasBin: true + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -3631,6 +3704,14 @@ packages: slate@0.72.8: resolution: {integrity: sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==} + slice-ansi@7.1.2: + resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} + engines: {node: '>=18'} + + slice-ansi@8.0.0: + resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} + engines: {node: '>=20'} + snabbdom@3.6.3: resolution: {integrity: sha512-W2lHLLw2qR2Vv0DcMmcxXqcfdBaIcoN+y/86SmHv8fn4DazEQSH6KN3TjZcWvwujW56OHiiirsbHWZb4vx/0fg==} engines: {node: '>=12.17.0'} @@ -3692,6 +3773,10 @@ packages: resolution: {integrity: sha512-gHFfL3px0Kctd6Po0M8TzEvt3De/xu6cnRrjlfYNhwbhLPLwigI2t1nc6jrzNuaYg5C4YF78PPFuQPzRiqn9ew==} engines: {node: '>=4.0.0'} + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + string-hash@1.1.3: resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==} @@ -3706,6 +3791,14 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.2.0: + resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==} + engines: {node: '>=20'} + string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} @@ -3786,6 +3879,10 @@ packages: tiny-warning@1.0.3: resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + tinyexec@1.0.4: + resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} + engines: {node: '>=18'} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -4042,6 +4139,10 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -4106,6 +4207,11 @@ packages: resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} engines: {node: '>= 6'} + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + yarn@1.22.22: resolution: {integrity: sha512-prL3kGtyG7o9Z9Sv8IPfBNrWTDmXB4Qbes8A9rEzt6wkJV8mUvoirjU0Mp3GGAU06Y0XQyA3/2/RQFVuK7MTfg==} engines: {node: '>=4.0.0'} @@ -6088,6 +6194,10 @@ snapshots: - proxy-agent - supports-color + ansi-escapes@7.3.0: + dependencies: + environment: 1.1.0 + ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} @@ -6268,6 +6378,15 @@ snapshots: claygl@1.3.0: {} + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-truncate@5.2.0: + dependencies: + slice-ansi: 8.0.0 + string-width: 8.2.0 + clipboard@2.0.11: dependencies: good-listener: 1.2.2 @@ -6286,10 +6405,14 @@ snapshots: colord@2.9.3: {} + colorette@2.0.20: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 + commander@14.0.3: {} + commander@2.20.3: {} commander@4.1.1: {} @@ -6626,6 +6749,8 @@ snapshots: dependencies: vue: 3.5.31(typescript@4.9.5) + emoji-regex@10.6.0: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -6640,6 +6765,8 @@ snapshots: entities@7.0.1: {} + environment@1.1.0: {} + errno@0.1.8: dependencies: prr: 1.0.1 @@ -6819,6 +6946,8 @@ snapshots: eventemitter3@4.0.7: {} + eventemitter3@5.0.4: {} + ext@1.7.0: dependencies: type: 2.7.3 @@ -6933,6 +7062,8 @@ snapshots: gensync@1.0.0-beta.2: {} + get-east-asian-width@1.5.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -7040,6 +7171,8 @@ snapshots: dependencies: ms: 2.1.3 + husky@9.1.7: {} + i18next@20.6.1: dependencies: '@babel/runtime': 7.29.2 @@ -7113,6 +7246,10 @@ snapshots: is-fullwidth-code-point@3.0.0: {} + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.5.0 + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -7252,6 +7389,24 @@ snapshots: lines-and-columns@1.2.4: {} + lint-staged@16.4.0: + dependencies: + commander: 14.0.3 + listr2: 9.0.5 + picomatch: 4.0.4 + string-argv: 0.3.2 + tinyexec: 1.0.4 + yaml: 2.8.3 + + listr2@9.0.5: + dependencies: + cli-truncate: 5.2.0 + colorette: 2.0.20 + eventemitter3: 5.0.4 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.2 + loader-utils@3.3.1: {} locate-path@6.0.0: @@ -7290,6 +7445,14 @@ snapshots: lodash@4.18.0: {} + log-update@6.1.0: + dependencies: + ansi-escapes: 7.3.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.2 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + lru-cache@10.4.3: {} lru-cache@5.1.1: @@ -7350,6 +7513,8 @@ snapshots: mime@2.6.0: {} + mimic-function@5.0.1: {} + mimic-response@3.1.0: {} minimatch@3.1.5: @@ -7483,6 +7648,10 @@ snapshots: dependencies: wrappy: 1.0.2 + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -7643,12 +7812,13 @@ snapshots: optionalDependencies: postcss: 8.5.8 - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.8): + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.8)(yaml@2.8.3): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 1.21.7 postcss: 8.5.8 + yaml: 2.8.3 postcss-merge-longhand@5.1.7(postcss@8.5.8): dependencies: @@ -7930,6 +8100,11 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + reusify@1.1.0: {} rfdc@1.4.1: {} @@ -8117,6 +8292,16 @@ snapshots: is-plain-object: 5.0.0 tiny-warning: 1.0.3 + slice-ansi@7.1.2: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + slice-ansi@8.0.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + snabbdom@3.6.3: {} sortablejs@1.14.0: {} @@ -8166,6 +8351,8 @@ snapshots: stream-wormhole@1.1.0: {} + string-argv@0.3.2: {} + string-hash@1.1.3: {} string-split-by@1.0.0: @@ -8184,6 +8371,17 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.2.0 + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.5.0 + strip-ansi: 7.2.0 + + string-width@8.2.0: + dependencies: + get-east-asian-width: 1.5.0 + strip-ansi: 7.2.0 + string_decoder@1.1.1: dependencies: safe-buffer: 5.1.2 @@ -8240,7 +8438,7 @@ snapshots: sax: 1.6.0 stable: 0.1.8 - tailwindcss@3.4.19: + tailwindcss@3.4.19(yaml@2.8.3): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -8259,7 +8457,7 @@ snapshots: postcss: 8.5.8 postcss-import: 15.1.0(postcss@8.5.8) postcss-js: 4.1.0(postcss@8.5.8) - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.8) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.8)(yaml@2.8.3) postcss-nested: 6.2.0(postcss@8.5.8) postcss-selector-parser: 6.1.2 resolve: 1.22.11 @@ -8291,6 +8489,8 @@ snapshots: tiny-warning@1.0.3: {} + tinyexec@1.0.4: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.4) @@ -8531,6 +8731,12 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.2.0 + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrappy@1.0.2: {} ws@7.5.10: {} @@ -8568,6 +8774,8 @@ snapshots: yaml@1.10.3: {} + yaml@2.8.3: {} + yarn@1.22.22: {} yocto-queue@0.1.0: {} diff --git a/web/src/stores/modules/dashboard.ts b/web/src/stores/modules/dashboard.ts index d430923..d84613f 100644 --- a/web/src/stores/modules/dashboard.ts +++ b/web/src/stores/modules/dashboard.ts @@ -2,71 +2,73 @@ import { defineStore } from 'pinia'; import { getDashboard } from '/@/api/pisadmin/dashboard'; interface BuyerDashboard { - kpi: { - total_inquiries: number; - pending_inquiries: number; - completed_quotes: number; - total_suppliers: number; - } | null; - tasks: Array<{ - id: number; - title: string; - inquiry_no: string; - status: string; - created_at: string; - }>; - messages: Array<{ - id: number; - title: string; - content: string; - is_read: boolean; - created_at: string; - }>; - trend: Array<{ month: string; count: number }>; + kpi: { + total_inquiries: number; + pending_inquiries: number; + completed_quotes: number; + total_suppliers: number; + quote_timely_rate: number; + } | null; + tasks: Array<{ + id: number; + title: string; + inquiry_no: string; + status: string; + created_at: string; + }>; + messages: Array<{ + id: number; + title: string; + content: string; + is_read: boolean; + created_at: string; + }>; + trend: Array<{ month: string; count: number }>; } interface SupplierDashboard { - kpi: { - total_quotes: number; - pending_quotes: number; - won_quotes: number; - conversion_rate: number; - } | null; - pending_quotes: Array<{ - id: number; - inquiry_no: string; - item_name: string; - quantity: number; - unit: string; - deadline: string; - }>; - messages: Array<{ - id: number; - title: string; - content: string; - is_read: boolean; - created_at: string; - }>; - trend: Array<{ month: string; quotes: number; won: number }>; + kpi: { + total_quotes: number; + pending_quotes: number; + won_quotes: number; + conversion_rate: number; + } | null; + pending_quotes: Array<{ + id: number; + inquiry_no: string; + item_name: string; + quantity: number; + unit: string; + deadline: string; + status: number; + }>; + messages: Array<{ + id: number; + title: string; + content: string; + is_read: boolean; + created_at: string; + }>; + trend: Array<{ month: string; quotes: number; won: number }>; } export const useDashboardStore = defineStore('dashboard', { - state: () => ({ - buyer: null as BuyerDashboard | null, - supplier: null as SupplierDashboard | null, - loading: false, - }), - actions: { - async fetchDashboard() { - this.loading = true; - try { - const res: any = await getDashboard(); - // 后端直接返回 {buyer, supplier} 结构 - this.buyer = res?.buyer || null; - this.supplier = res?.supplier || null; - } finally { - this.loading = false; - } - }, - }, + state: () => ({ + buyer: null as BuyerDashboard | null, + supplier: null as SupplierDashboard | null, + loading: false, + }), + actions: { + async fetchDashboard() { + this.loading = true; + try { + const res: any = await getDashboard(); + // 后端直接返回 {buyer, supplier} 结构 + this.buyer = res?.buyer || null; + this.supplier = res?.supplier || null; + } finally { + this.loading = false; + } + }, + }, }); diff --git a/web/src/views/pisadmin/basicinfo/company/crud.tsx b/web/src/views/pisadmin/basicinfo/company/crud.tsx index f346b55..cb084eb 100644 --- a/web/src/views/pisadmin/basicinfo/company/crud.tsx +++ b/web/src/views/pisadmin/basicinfo/company/crud.tsx @@ -88,6 +88,9 @@ export const createCrudOptions = function ({ crudExpose }: Partial request({ url: apiPrefix, method: 'get', params }) diff --git a/web/src/views/pisadmin/miscprocurement/misc_stations/crud.tsx b/web/src/views/pisadmin/miscprocurement/misc_stations/crud.tsx index 609485c..f33d173 100644 --- a/web/src/views/pisadmin/miscprocurement/misc_stations/crud.tsx +++ b/web/src/views/pisadmin/miscprocurement/misc_stations/crud.tsx @@ -135,6 +135,9 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp type: 'input', search: { show: true, component: { props: { placeholder: '请输入工站代码', clearable: true } } }, form: { rules: [{ required: true, message: '请输入工站代码' }] }, + editForm: { + component: { props: { disabled: true } } + }, column: { minWidth: 140, showOverflowTooltip: true } }, stationname: { diff --git a/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/api.ts b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/api.ts index 87b3ef1..d69ecad 100644 --- a/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/api.ts +++ b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/api.ts @@ -20,6 +20,9 @@ export const SubmitPriceAuditObj = (id: string | number) => request({ url: `${ba /** 询价单操作日志(按主键,返回该单 inquiry_no 下全部记录,时间倒序) */ export const GetOperationLogs = (id: string | number) => request({ url: `${baseUrl}${id}/operation_logs/`, method: 'get' }) +/** 受邀供应商报价单一览(与发布生成的报价主表一致,操作日志嵌套表用) */ +export const GetInvitedSupplierQuotations = (id: string | number) => + request({ url: `${baseUrl}${id}/invited_supplier_quotations/`, method: 'get' }) /** 查询杂采议价记录(比价议价价格存此表,非报价明细「中标价格」) */ export const GetNegotiationRecordsObj = (id: string | number, params?: { part_id?: string }) => request({ url: `${baseUrl}${id}/negotiation_records/`, method: 'get', params: params ?? {} }) diff --git a/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/comparePrice.vue b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/comparePrice.vue index e5e3d58..eff2daa 100644 --- a/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/comparePrice.vue +++ b/web/src/views/pisadmin/miscprocurement/rfqmiscellaneous/comparePrice.vue @@ -18,11 +18,11 @@
询价单号{{ displayTextEmpty(comparisonDialog.baseInfo.code) }}
采购件料号{{ displayTextEmpty(comparisonDialog.baseInfo.partNo) }}
采购件名称{{ displayTextEmpty(comparisonDialog.baseInfo.partName) }}
-
目标价格{{ displayNumericEmpty(comparisonDialog.baseInfo.targetPrice) }}
+
目标价格{{ compareNumericDisplay(comparisonDialog.baseInfo.targetPrice) }}
交易币别{{ displayTextEmpty(comparisonDialog.baseInfo.currency) }}
税率{{ displayPercentRate(comparisonDialog.baseInfo.taxRate) }}
-
当前成交价{{ displayNumericEmpty(comparisonDialog.baseInfo.dealPrice) }}
-
制程最低价{{ displayNumericEmpty(comparisonDialog.baseInfo.lowestProcessPrice) }}
+
当前成交价{{ compareNumericDisplay(comparisonDialog.baseInfo.dealPrice) }}
+
制程最低价{{ compareNumericDisplay(comparisonDialog.baseInfo.lowestProcessPrice) }}
-