Merge remote-tracking branch 'origin/feat/list-footer-multi-hint-20261012' into lmx202610121455
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
陆运选线路 H5 页。
|
陆运选线路 H5 页。
|
||||||
|
|
||||||
本文件职责:按完整线路列出各段成本与合计,提交所选下标。
|
本文件职责:按完整线路列出各段车型、成本与合计,提交所选下标。
|
||||||
样式对齐空运选线路页。禁止 URL userid;禁止本页查 TMS。
|
样式对齐空运选线路页。禁止 URL userid;禁止本页查 TMS。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -64,19 +64,40 @@ def render_land_route_ok() -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _segment_vehicle(leg: dict[str, Any] | None) -> str:
|
||||||
|
"""每段车型用 TMS 的 vehicleType 原文。没有台数,空值显示 -。"""
|
||||||
|
src = dict(leg or {})
|
||||||
|
raw = str(
|
||||||
|
src.get("vehicleType") or src.get("vehicle_type") or src.get("truckType") or ""
|
||||||
|
).strip()
|
||||||
|
if raw.lower() in {"none", "null"}:
|
||||||
|
raw = ""
|
||||||
|
return raw or "-"
|
||||||
|
|
||||||
|
|
||||||
def _route_card(index: int, route: dict[str, Any], quote: dict[str, Any] | None = None) -> str:
|
def _route_card(index: int, route: dict[str, Any], quote: dict[str, Any] | None = None) -> str:
|
||||||
"""一张线路:航线、各段路段+TMS 费目、合计。"""
|
"""一张线路:航线、各段路段、车型、TMS 费目、合计。"""
|
||||||
line = html.escape(route_line(route))
|
line = html.escape(route_line(route))
|
||||||
blocks = land_leg_fee_blocks_for_route(quote, route)
|
blocks = land_leg_fee_blocks_for_route(quote, route)
|
||||||
|
shown_legs = [
|
||||||
|
leg
|
||||||
|
for leg in list(route.get("legs") or [])
|
||||||
|
if str(leg.get("origin") or "").strip() or str(leg.get("destination") or "").strip()
|
||||||
|
]
|
||||||
etas = {i: d for i, (_, d) in enumerate(option_eta_bits(route), 1)}
|
etas = {i: d for i, (_, d) in enumerate(option_eta_bits(route), 1)}
|
||||||
fee_bits: list[str] = []
|
fee_bits: list[str] = []
|
||||||
items = ""
|
items = ""
|
||||||
if blocks:
|
if blocks:
|
||||||
for i, (title, fees, eta) in enumerate(blocks, 1):
|
for i, (title, fees, eta) in enumerate(blocks, 1):
|
||||||
|
vehicle = _segment_vehicle(shown_legs[i - 1] if i - 1 < len(shown_legs) else None)
|
||||||
items += (
|
items += (
|
||||||
f"<li class='leg'><span class='n'>{html.escape(title)}</span>"
|
f"<li class='leg'><span class='n'>{html.escape(title)}</span>"
|
||||||
"<span></span></li>"
|
"<span></span></li>"
|
||||||
)
|
)
|
||||||
|
items += (
|
||||||
|
f"<li><span class='n'>车型 {html.escape(vehicle)}</span>"
|
||||||
|
"<span></span></li>"
|
||||||
|
)
|
||||||
for fee in fees:
|
for fee in fees:
|
||||||
amount = _money(fee.get("amount"), str(fee.get("currency") or "").strip())
|
amount = _money(fee.get("amount"), str(fee.get("currency") or "").strip())
|
||||||
name = str(fee.get("name") or "").strip()
|
name = str(fee.get("name") or "").strip()
|
||||||
|
|||||||
@@ -2624,6 +2624,20 @@ def group_tms_confirm_hint(*, business_line: str = "") -> str:
|
|||||||
return f"请{who}回复“确认报价”确定采用TMS报价。\n{_GROUP_QUOTE_TAIL}"
|
return f"请{who}回复“确认报价”确定采用TMS报价。\n{_GROUP_QUOTE_TAIL}"
|
||||||
|
|
||||||
|
|
||||||
|
def multi_group_field_hint(*, includes_air: bool) -> str:
|
||||||
|
"""
|
||||||
|
多段群里每次带出各段询价字段时,贴在最后一行字段下面。
|
||||||
|
|
||||||
|
任一段是空运,喊航线或产品;没有空运只喊产品。成交跟进不走这里。
|
||||||
|
注里说的是附件报价单只存档,不改报价写入规则。
|
||||||
|
"""
|
||||||
|
who = "航线或产品人员" if includes_air else "产品人员"
|
||||||
|
return (
|
||||||
|
f"{who}可通过“纯文字”告知新报价,或直接将新报价excel表发给智能体。\n"
|
||||||
|
"(注:附件报价单仅作为文件存储于后台,智能体不读取且不将报价内容写进系统)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def group_no_tms_quote_hint(*, business_line: str = "") -> str:
|
def group_no_tms_quote_hint(*, business_line: str = "") -> str:
|
||||||
"""
|
"""
|
||||||
TMS 无价时写在「暂无匹配」下方:谁可以用文字或 Excel 另报。
|
TMS 无价时写在「暂无匹配」下方:谁可以用文字或 Excel 另报。
|
||||||
|
|||||||
@@ -385,6 +385,7 @@ def group_brief(
|
|||||||
|
|
||||||
结构对齐私聊核对:@销售和各段产品,下一行直接接段1询价字段。
|
结构对齐私聊核对:@销售和各段产品,下一行直接接段1询价字段。
|
||||||
不写 TMS,不写协同段。工单号已在私聊提示过,群里不再重复一行。
|
不写 TMS,不写协同段。工单号已在私聊提示过,群里不再重复一行。
|
||||||
|
每次拼这段字段都在底部加另报提示;成交跟进是另一条消息,不加。
|
||||||
"""
|
"""
|
||||||
_ = work_order_no
|
_ = work_order_no
|
||||||
names: list[str] = []
|
names: list[str] = []
|
||||||
@@ -406,6 +407,8 @@ def group_brief(
|
|||||||
body = _segment_inquiry_block(seg).strip()
|
body = _segment_inquiry_block(seg).strip()
|
||||||
if body:
|
if body:
|
||||||
lines.extend(body.splitlines())
|
lines.extend(body.splitlines())
|
||||||
|
# 紧挨最后一行字段,中间不空行。含空运与不含空运用不同开头。
|
||||||
|
lines.append(copy.multi_group_field_hint(includes_air=includes_air(segments)))
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -433,6 +433,41 @@ class LandRouteOptionsTests(unittest.TestCase):
|
|||||||
self.assertIn("CNY 7500", page)
|
self.assertIn("CNY 7500", page)
|
||||||
self.assertIn("成本价合计", page)
|
self.assertIn("成本价合计", page)
|
||||||
|
|
||||||
|
def test_h5_shows_vehicle_type_on_each_leg(self) -> None:
|
||||||
|
"""多条线路的 H5 卡片按段展示 TMS 车型,没有就写 -。"""
|
||||||
|
from agent.channel.h5.land_route_page import render_land_route_page
|
||||||
|
|
||||||
|
quote = _three_routes()
|
||||||
|
for leg in quote["landSegments"]:
|
||||||
|
dest = str(leg.get("destination") or "")
|
||||||
|
origin = str(leg.get("origin") or "")
|
||||||
|
if dest == "深圳":
|
||||||
|
leg["vehicleType"] = "9米6"
|
||||||
|
elif origin == "深圳":
|
||||||
|
leg["vehicleType"] = "13米"
|
||||||
|
elif dest == "广西":
|
||||||
|
leg["vehicleType"] = None
|
||||||
|
quote["fee_lines"] = _tms_land_fees(
|
||||||
|
(1, 1, "运输费", 7500),
|
||||||
|
(1, 2, "运输费", 9600),
|
||||||
|
(2, 1, "运输费", 7500),
|
||||||
|
(2, 2, "运输费", 9600),
|
||||||
|
(3, 1, "运输费", 7500),
|
||||||
|
(3, 2, "运输费", 9600),
|
||||||
|
)
|
||||||
|
page = render_land_route_page(
|
||||||
|
{"work_order_no": "WO202609220010", "quote": quote}
|
||||||
|
)
|
||||||
|
od = page.find("上海 → 深圳")
|
||||||
|
vehicle = page.find("车型 9米6", od)
|
||||||
|
fee = page.find("运输费", vehicle)
|
||||||
|
self.assertGreaterEqual(od, 0)
|
||||||
|
self.assertGreater(vehicle, od)
|
||||||
|
self.assertGreater(fee, vehicle)
|
||||||
|
self.assertIn("车型 13米", page)
|
||||||
|
self.assertIn("车型 -", page)
|
||||||
|
self.assertNotIn("9米6/1", page)
|
||||||
|
|
||||||
def test_h5_and_detail_use_tms_fees_when_java_sends_json_strings(self) -> None:
|
def test_h5_and_detail_use_tms_fees_when_java_sends_json_strings(self) -> None:
|
||||||
"""主账 quote 常是 JSON 串,且 fee_lines 可能没带段号。"""
|
"""主账 quote 常是 JSON 串,且 fee_lines 可能没带段号。"""
|
||||||
from agent.channel.h5.land_route_page import render_land_route_page
|
from agent.channel.h5.land_route_page import render_land_route_page
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ class QuoteTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(phase, "multi_deal_sent")
|
self.assertEqual(phase, "multi_deal_sent")
|
||||||
self.assertIn("120", self.client.texts[-1])
|
self.assertIn("120", self.client.texts[-1])
|
||||||
|
self.assertNotIn("纯文字", self.client.texts[-1])
|
||||||
self.assertNotIn("100", deal_text(
|
self.assertNotIn("100", deal_text(
|
||||||
work_order_no=self.ticket.work_order_no,
|
work_order_no=self.ticket.work_order_no,
|
||||||
segments=[Segment(index=1, mode="SEA"), Segment(index=2, mode="LAND")],
|
segments=[Segment(index=1, mode="SEA"), Segment(index=2, mode="LAND")],
|
||||||
@@ -198,6 +199,8 @@ class QuoteTests(unittest.TestCase):
|
|||||||
brief = self.client.texts[-1]
|
brief = self.client.texts[-1]
|
||||||
self.assertIn("段1:海运", brief)
|
self.assertIn("段1:海运", brief)
|
||||||
self.assertIn("段2:陆运", brief)
|
self.assertIn("段2:陆运", brief)
|
||||||
|
self.assertIn("产品人员可通过“纯文字”告知新报价", brief)
|
||||||
|
self.assertNotIn("航线或产品人员", brief)
|
||||||
self.assertNotIn("TMS报价", brief)
|
self.assertNotIn("TMS报价", brief)
|
||||||
self.assertNotIn("其他段报完", brief)
|
self.assertNotIn("其他段报完", brief)
|
||||||
self.assertEqual(self.ledger.get_ticket(work_order_no=no).collab_chat_id, "room-9")
|
self.assertEqual(self.ledger.get_ticket(work_order_no=no).collab_chat_id, "room-9")
|
||||||
@@ -265,6 +268,8 @@ class QuoteTests(unittest.TestCase):
|
|||||||
self.assertIn("段2:空运", brief)
|
self.assertIn("段2:空运", brief)
|
||||||
self.assertIn("目的地(必填):CRK", brief)
|
self.assertIn("目的地(必填):CRK", brief)
|
||||||
self.assertIn("贸易条款(非必填):DAP", brief)
|
self.assertIn("贸易条款(非必填):DAP", brief)
|
||||||
|
self.assertTrue(brief.endswith("(注:附件报价单仅作为文件存储于后台,智能体不读取且不将报价内容写进系统)"))
|
||||||
|
self.assertIn("航线或产品人员可通过“纯文字”告知新报价", brief)
|
||||||
self.assertNotIn("其他段报完", "\n".join(self.client.texts))
|
self.assertNotIn("其他段报完", "\n".join(self.client.texts))
|
||||||
self.assertNotIn("询价字段:", brief)
|
self.assertNotIn("询价字段:", brief)
|
||||||
|
|
||||||
|
|||||||
@@ -167,6 +167,10 @@ class SplitTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertIn("@吴季林@林绵助\n段1:", text)
|
self.assertIn("@吴季林@林绵助\n段1:", text)
|
||||||
self.assertNotIn("@林绵助\n\n段1", text)
|
self.assertNotIn("@林绵助\n\n段1", text)
|
||||||
|
self.assertTrue(
|
||||||
|
text.endswith(copy.multi_group_field_hint(includes_air=True)),
|
||||||
|
text,
|
||||||
|
)
|
||||||
|
|
||||||
def test_missing_fields_use_single_chat_examples(self) -> None:
|
def test_missing_fields_use_single_chat_examples(self) -> None:
|
||||||
outcome = split_segments("段1:海运\n起运港:广州港\n段2:空运\n目的地:CRK")
|
outcome = split_segments("段1:海运\n起运港:广州港\n段2:空运\n目的地:CRK")
|
||||||
@@ -694,6 +698,11 @@ class PullTests(unittest.TestCase):
|
|||||||
segments=flow._segments(flow.session_of("sales")),
|
segments=flow._segments(flow.session_of("sales")),
|
||||||
)
|
)
|
||||||
self.assertNotIn("TMS", brief_text)
|
self.assertNotIn("TMS", brief_text)
|
||||||
|
self.assertTrue(
|
||||||
|
brief_text.endswith(copy.multi_group_field_hint(includes_air=False)),
|
||||||
|
brief_text,
|
||||||
|
)
|
||||||
|
self.assertNotIn("航线或产品人员", brief_text)
|
||||||
|
|
||||||
|
|
||||||
class CardPullTests(unittest.TestCase):
|
class CardPullTests(unittest.TestCase):
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* 询价后台列表分页条:总数 + 每页条数 + 页码。
|
* 询价后台列表分页条:整组靠右,总数紧挨在「每页」左边,页码在后。
|
||||||
* 样式对齐 admin-fidelity.css 的 .pagination;翻页只发事件,不请求接口。
|
* 样式对齐 admin-fidelity.css 的 .pagination;翻页只发事件,不请求接口。
|
||||||
*/
|
*/
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
@@ -87,13 +87,13 @@ function onSizeChange(e: Event) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="pagination admin-pagination">
|
<div class="pagination admin-pagination">
|
||||||
<span class="admin-pagination__total">
|
|
||||||
共 <b>{{ total }}</b> {{ unit }}
|
|
||||||
<span v-if="total > 0" class="admin-pagination__range">
|
|
||||||
(第 {{ pageNo }} / {{ totalPages }} 页)
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<div class="admin-pagination__right">
|
<div class="admin-pagination__right">
|
||||||
|
<span class="admin-pagination__total">
|
||||||
|
共 <b>{{ total }}</b> {{ unit }}
|
||||||
|
<span v-if="total > 0" class="admin-pagination__range">
|
||||||
|
(第 {{ pageNo }} / {{ totalPages }} 页)
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
<label class="admin-pagination__size">
|
<label class="admin-pagination__size">
|
||||||
每页
|
每页
|
||||||
<select :value="pageSize" :disabled="disabled" @change="onSizeChange">
|
<select :value="pageSize" :disabled="disabled" @change="onSizeChange">
|
||||||
@@ -127,6 +127,11 @@ function onSizeChange(e: Event) {
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-pagination__total {
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-pagination__size {
|
.admin-pagination__size {
|
||||||
|
|||||||
@@ -918,6 +918,10 @@ body {
|
|||||||
color: #155eef !important;
|
color: #155eef !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pagination.admin-pagination {
|
||||||
|
justify-content: flex-end !important;
|
||||||
|
}
|
||||||
|
|
||||||
.pagination .pages {
|
.pagination .pages {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -830,7 +830,7 @@ async function downloadPreviewOrOriginal() {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="pagination">
|
<div class="pagination admin-pagination">
|
||||||
<span>
|
<span>
|
||||||
共 <b style="color: #0f172a">{{ filtered.length }}</b> 个模板
|
共 <b style="color: #0f172a">{{ filtered.length }}</b> 个模板
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user