落地私聊附件询价:发卡走 H5 收 Excel/PDF,认成口播再复用三线文字流程,避免附件卡在识别或退成纯文本。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,319 @@
|
||||
"""
|
||||
附件询价 H5 上传页。
|
||||
|
||||
本文件职责:渲染选文件页与过期/成功/拒收提示。URL 不含 userid。
|
||||
样式按企微内嵌、手机竖屏:卡片、虚线选区、已选文件列表;不改上传规则。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from html import escape
|
||||
import json
|
||||
|
||||
from agent.policy import inquiry_copy as copy
|
||||
|
||||
# 三页共用:浅底、白卡片、企微蓝主按钮。禁止在页面里写 userid。
|
||||
_PAGE_CSS = """
|
||||
:root {
|
||||
--bg: #eef3f9;
|
||||
--card: #ffffff;
|
||||
--text: #17233d;
|
||||
--muted: #5b6b7c;
|
||||
--line: #c5d4ea;
|
||||
--brand: #1677ff;
|
||||
--brand-press: #0f62d9;
|
||||
--danger: #d83931;
|
||||
--ok: #0f8a5f;
|
||||
--shadow: 0 10px 28px rgba(23, 51, 91, 0.10);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; min-height: 100%; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.wrap {
|
||||
max-width: 480px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 16px calc(24px + env(safe-area-inset-bottom));
|
||||
}
|
||||
.card {
|
||||
background: var(--card);
|
||||
border-radius: 16px;
|
||||
padding: 22px 20px 24px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.hero {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.badge {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
background: #e8f1ff;
|
||||
color: var(--brand);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.badge svg { width: 22px; height: 22px; }
|
||||
h1 { font-size: 20px; line-height: 1.3; margin: 0 0 4px; font-weight: 600; }
|
||||
.lead { margin: 0; color: var(--muted); font-size: 14px; line-height: 1.6; }
|
||||
.hint { margin: 12px 0 0; color: var(--muted); font-size: 13px; line-height: 1.7; }
|
||||
.pills { display: flex; gap: 8px; margin: 14px 0 0; }
|
||||
.pill {
|
||||
font-size: 12px;
|
||||
color: #2b5aa8;
|
||||
background: #eef4ff;
|
||||
border-radius: 999px;
|
||||
padding: 4px 10px;
|
||||
}
|
||||
.drop {
|
||||
position: relative;
|
||||
display: block;
|
||||
margin-top: 18px;
|
||||
padding: 22px 16px;
|
||||
border: 1.5px dashed var(--line);
|
||||
border-radius: 12px;
|
||||
background: #f7faff;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.drop:focus-within { border-color: var(--brand); box-shadow: 0 0 0 3px rgba(22, 119, 255, 0.16); }
|
||||
.drop input {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
.drop-title { display: block; font-size: 16px; font-weight: 600; }
|
||||
.drop-sub { display: block; margin-top: 6px; font-size: 13px; color: var(--muted); }
|
||||
.picked { list-style: none; margin: 14px 0 0; padding: 0; }
|
||||
.picked li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
margin-top: 8px;
|
||||
background: #f6f8fb;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.idx {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: #e8f1ff;
|
||||
color: var(--brand);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.name { word-break: break-all; }
|
||||
button[type="submit"], .btn {
|
||||
width: 100%;
|
||||
min-height: 48px;
|
||||
margin-top: 20px;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
background: var(--brand);
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
button[type="submit"]:active, .btn:active { background: var(--brand-press); }
|
||||
button[disabled] { background: #94a3b8; cursor: not-allowed; }
|
||||
.err {
|
||||
min-height: 1.4em;
|
||||
margin: 12px 0 0;
|
||||
color: var(--danger);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.rules { margin: 16px 0 0; padding-left: 18px; color: var(--muted); font-size: 13px; line-height: 1.8; }
|
||||
.tone-ok .badge { background: #e7f7ef; color: var(--ok); }
|
||||
.tone-warn .badge { background: #fdecea; color: var(--danger); }
|
||||
.tone-expired .badge { background: #eef1f5; color: var(--muted); }
|
||||
.body { margin: 8px 0 0; font-size: 15px; line-height: 1.7; }
|
||||
.back { margin: 10px 0 0; color: var(--muted); font-size: 14px; line-height: 1.6; }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* { transition: none !important; }
|
||||
}
|
||||
"""
|
||||
|
||||
_ICON_FILE = (
|
||||
'<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">'
|
||||
'<path d="M7 3h7l5 5v13a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z" stroke="currentColor" stroke-width="1.7"/>'
|
||||
'<path d="M14 3v5h5" stroke="currentColor" stroke-width="1.7"/>'
|
||||
'</svg>'
|
||||
)
|
||||
_ICON_OK = (
|
||||
'<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">'
|
||||
'<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.7"/>'
|
||||
'<path d="M8 12.5l2.5 2.5L16 9.5" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>'
|
||||
'</svg>'
|
||||
)
|
||||
_ICON_WARN = (
|
||||
'<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">'
|
||||
'<path d="M12 4l9 16H3L12 4z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/>'
|
||||
'<path d="M12 10v5M12 17.5h.01" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>'
|
||||
'</svg>'
|
||||
)
|
||||
_ICON_EXPIRED = (
|
||||
'<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">'
|
||||
'<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.7"/>'
|
||||
'<path d="M12 7v6l4 2" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>'
|
||||
'</svg>'
|
||||
)
|
||||
|
||||
|
||||
def _shell(*, title: str, body: str, tone: str = "ok") -> str:
|
||||
"""成功 / 拒绝 / 过期共用卡片壳,避免三页三种简陋白底。"""
|
||||
icons = {"ok": _ICON_OK, "warn": _ICON_WARN, "expired": _ICON_EXPIRED}
|
||||
icon = icons.get(tone, _ICON_WARN)
|
||||
extra = '<p class="back">请回企微查看识别结果。</p>' if tone == "ok" else ""
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>{title}</title>
|
||||
<style>{_PAGE_CSS}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap tone-{tone}">
|
||||
<div class="card">
|
||||
<div class="hero">
|
||||
<div class="badge">{icon}</div>
|
||||
<div>
|
||||
<h1>{title}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<p class="body">{body}</p>
|
||||
{extra}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
def render_upload_page() -> str:
|
||||
"""上传页:一次可选多个 Excel/PDF;图片请发回企微。"""
|
||||
reject = json.dumps(copy.ATTACH_H5_REJECT_TYPE, ensure_ascii=False)
|
||||
need = json.dumps(copy.ATTACH_H5_NEED_FILE, ensure_ascii=False)
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>上传询价附件</title>
|
||||
<style>{_PAGE_CSS}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="card">
|
||||
<div class="hero">
|
||||
<div class="badge">{_ICON_FILE}</div>
|
||||
<div>
|
||||
<h1>上传询价附件</h1>
|
||||
<p class="lead">这里只传 Excel、PDF。图片请直接发到当前企微会话。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pills"><span class="pill">Excel</span><span class="pill">PDF</span></div>
|
||||
<p class="hint">一次可选多个文件,顺序按你选中的顺序。提交后请回企微查看识别结果。</p>
|
||||
<form id="f" method="post" enctype="multipart/form-data">
|
||||
<label class="drop" for="files">
|
||||
<input id="files" name="files" type="file" multiple
|
||||
accept=".xls,.xlsx,.pdf,application/pdf,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"/>
|
||||
<span class="drop-title">选择 Excel 或 PDF</span>
|
||||
<span class="drop-sub">点这里选文件,可一次多选</span>
|
||||
</label>
|
||||
<ul class="picked" id="picked" hidden></ul>
|
||||
<button type="submit" id="go">上传</button>
|
||||
</form>
|
||||
<p class="err" id="err" role="alert"></p>
|
||||
<ul class="rules">
|
||||
<li>只接受 .xls / .xlsx / .pdf</li>
|
||||
<li>图片请发到当前企微会话,不要在本页上传</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
var REJECT = {reject};
|
||||
var NEED = {need};
|
||||
var input = document.getElementById("files");
|
||||
var picked = document.getElementById("picked");
|
||||
var err = document.getElementById("err");
|
||||
var go = document.getElementById("go");
|
||||
function esc(s) {{
|
||||
return String(s).replace(/[&<>"']/g, function (c) {{
|
||||
return ({{"&":"&","<":"<",">":">","\\"":""","'":"'"}})[c];
|
||||
}});
|
||||
}}
|
||||
function renderPicked() {{
|
||||
var files = input.files || [];
|
||||
err.textContent = "";
|
||||
if (!files.length) {{
|
||||
picked.innerHTML = "";
|
||||
picked.hidden = true;
|
||||
return;
|
||||
}}
|
||||
var html = "";
|
||||
for (var i = 0; i < files.length; i++) {{
|
||||
html += '<li><span class="idx">' + (i + 1) + '</span><span class="name">' + esc(files[i].name) + '</span></li>';
|
||||
}}
|
||||
picked.innerHTML = html;
|
||||
picked.hidden = false;
|
||||
}}
|
||||
input.addEventListener("change", renderPicked);
|
||||
document.getElementById("f").addEventListener("submit", function (ev) {{
|
||||
var files = input.files;
|
||||
err.textContent = "";
|
||||
if (!files || !files.length) {{
|
||||
ev.preventDefault();
|
||||
err.textContent = NEED;
|
||||
return;
|
||||
}}
|
||||
var okExt = /\\.(xlsx|xls|pdf)$/i;
|
||||
for (var i = 0; i < files.length; i++) {{
|
||||
if (!okExt.test(files[i].name)) {{
|
||||
ev.preventDefault();
|
||||
err.textContent = REJECT;
|
||||
return;
|
||||
}}
|
||||
}}
|
||||
go.disabled = true;
|
||||
go.textContent = "正在上传…";
|
||||
}});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
def render_upload_message(title: str, body: str, *, expired: bool = False) -> str:
|
||||
"""成功 / 拒绝 / 过期页。"""
|
||||
tone = "expired" if expired else ("ok" if title == "上传成功" else "warn")
|
||||
return _shell(title=escape(title), body=escape(body), tone=tone)
|
||||
|
||||
|
||||
def expired_upload_page() -> str:
|
||||
return render_upload_message("链接已过期", copy.ATTACH_H5_EXPIRED, expired=True)
|
||||
Reference in New Issue
Block a user