You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

484 lines
26 KiB

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CRM 联调参照 Demo(PROTOTYPE — 一次性验证页)</title>
<style>
:root { --bd:#d9d9d9; --bg:#f5f6f8; --pri:#1664ff; --err:#d4380d; --ok:#389e0d; }
* { box-sizing:border-box; }
body { margin:0; font:13px/1.6 "Microsoft YaHei",sans-serif; background:var(--bg); color:#222; }
header { background:#1f2733; color:#fff; padding:10px 16px; display:flex; gap:12px; align-items:center; flex-wrap:wrap; position:sticky; top:0; z-index:9; }
header b { font-size:15px; }
header .tag { background:#d4380d; border-radius:3px; padding:1px 6px; font-size:11px; }
header input { padding:4px 8px; border:0; border-radius:4px; width:280px; }
header .tok { font-size:12px; opacity:.9; max-width:360px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
nav { display:flex; gap:2px; background:#fff; padding:6px 12px 0; border-bottom:1px solid var(--bd); flex-wrap:wrap; position:sticky; top:46px; z-index:8; }
nav button { border:1px solid transparent; background:none; padding:7px 14px; cursor:pointer; font-size:13px; border-radius:6px 6px 0 0; }
nav button.on { border-color:var(--bd); border-bottom-color:#fff; background:var(--bg); color:var(--pri); font-weight:bold; }
main { padding:14px 16px; }
section.tab { display:none; } section.tab.on { display:block; }
.card { background:#fff; border:1px solid var(--bd); border-radius:6px; padding:14px; margin-bottom:14px; }
.card h3 { margin:0 0 10px; font-size:14px; }
label { display:inline-block; margin:4px 10px 4px 0; }
input[type=text],input[type=number],select,textarea { padding:4px 8px; border:1px solid var(--bd); border-radius:4px; font:inherit; }
textarea { width:100%; min-height:56px; }
button.act { background:var(--pri); color:#fff; border:0; border-radius:4px; padding:5px 14px; cursor:pointer; margin:4px 6px 4px 0; }
button.act.gray { background:#8a919c; } button.act.red { background:var(--err); }
table { border-collapse:collapse; width:100%; background:#fff; }
th,td { border:1px solid var(--bd); padding:5px 8px; text-align:left; font-size:12px; vertical-align:top; }
th { background:#f0f2f5; white-space:nowrap; }
td button { border:1px solid var(--bd); background:#fff; border-radius:3px; padding:1px 6px; cursor:pointer; margin:1px; font-size:12px; }
pre.raw { background:#10233a; color:#bfe3ff; padding:8px; border-radius:4px; max-height:260px; overflow:auto; font-size:11px; white-space:pre-wrap; word-break:break-all; margin:6px 0 0; }
pre.raw.bad { color:#ffb3a0; }
.grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(240px,1fr)); gap:6px 14px; }
.st { font-weight:bold; } .st.ok { color:var(--ok); } .st.bad { color:var(--err); }
#logPanel pre { max-height:420px; }
.hint { color:#8a919c; font-size:12px; margin:2px 0 8px; }
.rowact { display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin:6px 0; }
</style>
</head>
<body>
<header>
<b>CRM 联调参照 Demo</b><span class="tag">PROTOTYPE 一次性</span>
<label>后端 BASE <input id="baseUrl" placeholder="如 http://localhost:8081"></label>
<button class="act gray" onclick="saveBase()">保存</button>
<span class="tok" id="tokState">未登录</span>
<button class="act gray" onclick="logout()">清除token</button>
</header>
<nav id="tabs"></nav>
<main>
<section class="tab" id="tab-login"><div class="card">
<h3>① 登录 / Token 存取</h3>
<p class="hint">正式登录为钉钉扫码(/api/auth/login/dingtalk),demo 不走 JSAPI;测试环境(verify profile)用调试端点按 userId 直接签发,或手工粘贴任意有效 token。token 存 localStorage。</p>
<div class="rowact">
<label>userId <input type="text" id="loginUserId" value="739564171091247104" style="width:220px"></label>
<button class="act" onclick="debugToken()">调试端点签发</button>
</div>
<label>手工粘贴 token</label><br>
<textarea id="manualToken" placeholder="eyJ0eXAi..."></textarea><br>
<button class="act" onclick="saveManual()">保存 token</button>
<pre class="raw" id="raw-login">(尚无请求)</pre>
</div></section>
<section class="tab" id="tab-region"><div class="card">
<h3>② 行政区划三级级联(/api/rule/region/*)</h3>
<div class="rowact">
<label><select id="rg-prov" onchange="loadCities()"></select></label>
<label><select id="rg-city" onchange="loadDists()"></select></label>
<label><select id="rg-dist"></select></label>
<span id="rg-result" class="st"></span>
</div>
<pre class="raw" id="raw-region">(尚无请求)</pre>
</div></section>
<section class="tab" id="tab-dict"><div class="card">
<h3>③ 字典下拉(/api/dict/item/enabled-list)</h3>
<button class="act" onclick="loadDicts()">加载四组字典</button>
<div class="grid" style="margin-top:8px">
<label>渠道 lead_source <select id="dict-lead_source"></select></label>
<label>品牌 brand <select id="dict-brand"></select></label>
<label>需求产品 product <select id="dict-product"></select></label>
<label>需求场景 scene <select id="dict-scene"></select></label>
</div>
<pre class="raw" id="raw-dict">(尚无请求)</pre>
</div></section>
<section class="tab" id="tab-create"><div class="card">
<h3>④ 新增线索(POST /api/lead/create)</h3>
<p class="hint">必填(后端最小校验=DB NOT NULL):省、市、渠道、品牌、需求产品、咨询内容。可连续新增(提交后表单保留)。</p>
<div class="grid">
<label>线索名称 <input type="text" id="c-leadName" value="演示线索"></label>
<label>联系电话 <input type="text" id="c-phone"></label>
<label>微信号 <input type="text" id="c-wechat"></label>
<label>邮箱 <input type="text" id="c-email"></label>
<label><select id="c-prov" onchange="loadCCities()"></select></label>
<label>市/区 <select id="c-city"></select></label>
<label>渠道 <select id="c-channel"></select></label>
<label>品牌 <select id="c-brand"></select></label>
<label>需求产品 <select id="c-product"></select></label>
<label>需求场景 <select id="c-scene"></select></label>
<label>所属公海池 <select id="c-pool"></select></label>
<label>是否加急 <select id="c-urgent"><option value="0"></option><option value="1"></option></select></label>
</div>
<label>详细地址</label><input type="text" id="c-address" style="width:60%">
<label>咨询内容</label><textarea id="c-content">demo 页面新增的测试咨询内容</textarea>
<div class="rowact">
<label><input type="checkbox" id="c-claim"> 创建即领取(claimOnCreate)</label>
<label><input type="checkbox" id="c-follow"> 创建即关注(followOnCreate)</label>
<button class="act" onclick="createLead()">提交新增</button>
</div>
<pre class="raw" id="raw-create">(尚无请求)</pre>
</div></section>
<section class="tab" id="tab-list"><div class="card">
<h3>⑤ 四列表(POST /api/lead/page viewType=…)+ 流转操作</h3>
<div class="rowact">
<label>视图 <select id="l-view" onchange="loadLeads()">
<option>MANAGE</option><option>PUBLIC_POOL</option><option>MY_LEAD</option><option>MY_FOLLOW</option></select></label>
<label>状态 <span id="l-status"></span></label>
<label>公海池 <select id="l-pool" onchange="loadLeads()"></select></label>
<label>关键字 <input type="text" id="l-kw"></label>
<button class="act" onclick="loadLeads()">查询</button>
<span id="l-total" class="st"></span>
</div>
<div class="rowact">
<button class="act gray" onclick="page(-1)">上一页</button>
<span id="l-pageno"></span>
<button class="act gray" onclick="page(1)">下一页</button>
</div>
<table><thead><tr><th>ID</th><th>名称</th><th>电话</th><th>状态</th><th>公海池</th><th>领取人</th><th>省/市</th><th>渠道</th><th>创建时间</th><th>操作</th></tr></thead>
<tbody id="l-body"></tbody></table>
<pre class="raw" id="raw-list">(尚无请求)</pre>
</div></section>
<section class="tab" id="tab-pool"><div class="card">
<h3>⑥ 公海池配置(/api/rule/pool/*)</h3>
<button class="act" onclick="loadPools()">刷新列表</button>
<table style="margin-top:8px"><thead><tr><th>ID</th><th>名称</th><th>部门</th><th>领取规则</th><th>回收/失效天</th><th>负责人</th><th>操作</th></tr></thead>
<tbody id="p-body"></tbody></table>
</div>
<div class="card">
<h3>新建/编辑公海池</h3>
<input type="hidden" id="p-id">
<div class="grid">
<label>池名称 <input type="text" id="p-name"></label>
<label>归属部门 <select id="p-dept"></select></label>
<label>领取规则 <select id="p-rule"><option value="1">1 成员可领+管理员分配</option><option value="2">2 仅管理员分配</option></select></label>
<label>超时回收天数 <input type="number" id="p-recycle" value="7"></label>
<label>线索失效天数 <input type="number" id="p-expire" value="180"></label>
<label>每日领取上限 <input type="number" id="p-daily" value="10"></label>
<label>个人持有上限 <input type="number" id="p-hold" value="200"></label>
<label>负责人 <select id="p-owner"></select></label>
</div>
<label>协作人(多选)<select id="p-collab" multiple size="3" style="min-width:220px"></select></label>
<div class="rowact">
<label>负责省份(多选)<select id="p-provs" multiple size="4" style="min-width:220px" onchange="loadPoolCities()"></select></label>
<label>负责市/区(多选,随首个选中省份加载)<select id="p-cities" multiple size="4" style="min-width:220px"></select></label>
</div>
<button class="act" onclick="savePool()">保存公海池</button>
<button class="act gray" onclick="resetPoolForm()">清空表单</button>
<pre class="raw" id="raw-pool">(尚无请求)</pre>
</div></section>
<section class="tab" id="tab-log"><div class="card" id="logPanel">
<h3>⑦ 全量请求日志(新→旧,原文)</h3>
<button class="act gray" onclick="document.getElementById('log').textContent=''">清空</button>
<pre class="raw" id="log"></pre>
</div></section>
</main>
<script>
"use strict";
/* ================= PROTOTYPE:CRM 前端联调参照 demo =================
* 零构建单文件,纯 fetch 直调后端;每个请求/响应原文渲染到对应面板 + 全量日志。
* 用完即弃:联调完成后本页无维护义务(frontend-integration 票据 06)。 */
const $ = id => document.getElementById(id);
const STATUS = {1:"未分发",2:"待领取",3:"已领取",4:"跟进中",5:"已转商机",6:"过期失效",7:"线索作废"};
let page1 = 1;
function base() { return ($("baseUrl").value || location.origin || "http://localhost:8081").replace(/\/$/,""); }
function saveBase(){ localStorage.setItem("demoBase", base()); showTok(); }
function token(){ return localStorage.getItem("demoToken") || ""; }
function showTok(){ const t = token(); $("tokState").textContent = t ? ("token: " + t.slice(0,28) + "…") : "未登录"; }
function logout(){ localStorage.removeItem("demoToken"); showTok(); }
async function api(method, path, {params, form, rawId, auth=true} = {}) {
let url = base() + path;
if (params) url += "?" + new URLSearchParams(params);
let body;
if (form !== undefined) {
const sp = new URLSearchParams();
for (const [k,v] of Object.entries(form)) {
if (v === null || v === undefined || v === "") continue;
if (Array.isArray(v)) v.forEach(x => sp.append(k, x));
else sp.append(k, v);
}
body = sp;
}
const headers = {};
if (auth && token()) headers["Authorization"] = "Bearer " + token();
const reqDesc = `${method} ${url}\n${body ? "BODY: " + body.toString() : "(无body)"}`;
let text, httpStatus;
try {
const resp = await fetch(url, {method, body, headers});
httpStatus = resp.status;
text = await resp.text();
} catch (e) {
text = "FETCH 失败: " + e.message; httpStatus = 0;
}
let json = null; try { json = JSON.parse(text); } catch (e) {}
const ok = json && json.success === true;
const stamp = new Date().toLocaleTimeString();
const block = `[${stamp}] HTTP ${httpStatus} ${ok===false?"✗":"✓"}\n${reqDesc}\n--RESP--\n${pretty(text)}\n`;
$("log").textContent = block + $("log").textContent;
if (rawId) {
const el = $(rawId);
el.textContent = `HTTP ${httpStatus} ${ok ? "success=true" : "success=false/非JSON"}\n${pretty(text)}`;
el.classList.toggle("bad", !ok);
}
if (!ok) throw new Error(`HTTP ${httpStatus} code=${json && json.code} ${json && json.message || text.slice(0,120)}`);
return json.data;
}
function pretty(t){ try { return JSON.stringify(JSON.parse(t), null, 1); } catch(e){ return t; } }
function showErr(rawId, e){ if (rawId) { const el=$(rawId); el.textContent = "错误:" + e.message; el.classList.add("bad"); } }
/* ---------------- ① 登录 ---------------- */
async function debugToken(){
try {
const t = await api("GET","/api/auth/debug/token",{params:{userId:$("loginUserId").value}, rawId:"raw-login", auth:false});
localStorage.setItem("demoToken", t); showTok();
} catch(e){ showErr("raw-login", e); }
}
function saveManual(){ localStorage.setItem("demoToken", $("manualToken").value.trim()); showTok(); }
/* ---------------- ② 区划 ---------------- */
function selFill(sel, items, txt){ sel.innerHTML = ""; (items||[]).forEach(it=>{const o=document.createElement("option");o.value=it.code;o.textContent=txt(it);sel.appendChild(o);}); }
async function loadProvinces(){
try {
const ps = await api("GET","/api/rule/region/level",{params:{level:1}, rawId:"raw-region"});
window._provById = {}; ps.forEach(p=>window._provById[p.id]=p);
selFill($("rg-prov"), ps, p=>p.name); selFill($("c-prov"), ps, p=>p.name);
$("rg-prov").dataset.ids = JSON.stringify(ps.map(p=>({id:p.id,code:p.code})));
$("c-prov").dataset.ids = JSON.stringify(ps.map(p=>({id:p.id,code:p.code})));
loadCities(); loadCCities();
} catch(e){ showErr("raw-region", e); }
}
function provIdOf(sel){ const arr = JSON.parse(sel.dataset.ids||"[]"); const hit = arr.find(x=>x.code===sel.value); return hit && hit.id; }
async function loadCities(){
const pid = provIdOf($("rg-prov")); if(!pid) return;
try {
const cs = await api("GET","/api/rule/region/list",{params:{parentId:pid}, rawId:"raw-region"});
selFill($("rg-city"), cs, c=>c.name); $("rg-city").dataset.ids = JSON.stringify(cs.map(c=>({id:c.id,code:c.code})));
loadDists();
$("rg-result").textContent = "";
} catch(e){ showErr("raw-region", e); }
}
async function loadDists(){
const arr = JSON.parse($("rg-city").dataset.ids||"[]"); const hit = arr.find(x=>x.code===$("rg-city").value);
if(!hit) return;
try {
const ds = await api("GET","/api/rule/region/list",{params:{parentId:hit.id}, rawId:"raw-region"});
selFill($("rg-dist"), ds, d=>d.name);
$("rg-result").textContent = "✓ 级联完成"; $("rg-result").className = "st ok";
} catch(e){ showErr("raw-region", e); }
}
async function loadCCities(){
const pid = provIdOf($("c-prov")); if(!pid) return;
try { const cs = await api("GET","/api/rule/region/list",{params:{parentId:pid}}); selFill($("c-city"), cs, c=>c.name); }
catch(e){ console.warn(e); }
}
/* ---------------- ③ 字典 ---------------- */
const DICTS = ["lead_source","brand","product","scene"];
async function loadDicts(){
for (const g of DICTS) {
try {
const items = await api("GET","/api/dict/item/enabled-list",{params:{groupCode:g}, rawId:"raw-dict"});
window["_dict_"+g] = items;
const sel = $("dict-"+g); sel.innerHTML = "";
items.forEach(it=>{const o=document.createElement("option");o.value=it.code;o.textContent=it.name+" ("+it.code+")";sel.appendChild(o);});
fillDictSelect("c-channel","lead_source"); fillDictSelect("c-brand","brand");
fillDictSelect("c-product","product"); fillDictSelect("c-scene","scene");
} catch(e){ showErr("raw-dict", e); return; }
}
}
function fillDictSelect(selId, group){
const sel = $(selId); sel.innerHTML = "";
(window["_dict_"+group]||[]).forEach(it=>{const o=document.createElement("option");o.value=it.code;o.textContent=it.name;sel.appendChild(o);});
}
/* ---------------- ④ 新增线索 ---------------- */
async function createLead(){
try {
const id = await api("POST","/api/lead/create",{
params:{claimOnCreate:$("c-claim").checked, followOnCreate:$("c-follow").checked},
form:{leadName:$("c-leadName").value, phone:$("c-phone").value, wechat:$("c-wechat").value,
email:$("c-email").value, provinceCode:$("c-prov").value, cityCode:$("c-city").value,
address:$("c-address").value, channelCode:$("c-channel").value, brandCode:$("c-brand").value,
productCode:$("c-product").value, sceneCode:$("c-scene").value,
consultContent:$("c-content").value, isUrgent:$("c-urgent").value,
poolId:$("c-pool").value || null},
rawId:"raw-create"});
$("c-leadName").value = $("c-leadName").value + "+"; // 连续新增:改个名避免重名混淆
} catch(e){ showErr("raw-create", e); }
}
/* ---------------- ⑤ 列表 + 流转 ---------------- */
const LSTATUS = [2,3,4,5,6,7,1];
function initStatusChecks(){
$("l-status").innerHTML = LSTATUS.map(s=>`<label style="white-space:nowrap"><input type="checkbox" class="l-st" value="${s}">${STATUS[s]}</label>`).join("");
}
async function loadLeads(){
const statusIn = [...document.querySelectorAll(".l-st:checked")].map(c=>c.value);
try {
const r = await api("POST","/api/lead/page",{
form:{current:page1, size:10, viewType:$("l-view").value, keyword:$("l-kw").value||null,
poolId:$("l-pool").value||null, statusIn:statusIn.length?statusIn:null},
rawId:"raw-list"});
$("l-total").textContent = "total=" + (r.total!=null?r.total:(r.totalCount!=null?r.totalCount:"?"));
$("l-pageno").textContent = "第 " + page1 + " 页";
$("l-body").innerHTML = (r.content||[]).map(l=>`<tr>
<td>${l.id}</td><td>${l.leadName||""}</td><td>${l.phone||""}</td>
<td class="st">${STATUS[l.status]||l.status}</td><td>${l.poolNameSnapshot||"-"}</td>
<td>${l.ownerNameSnapshot||"-"}</td><td>${l.provinceName||l.provinceCode||""} ${l.cityName||l.cityCode||""}</td>
<td>${l.channelCode||""}</td><td>${(l.createTime||"").slice(0,16)}</td>
<td>${rowOps(l)}</td></tr>`).join("");
} catch(e){ showErr("raw-list", e); }
}
function page(d){ page1 = Math.max(1, page1+d); loadLeads(); }
function rowOps(l){
const b = [];
if (l.status===2) b.push(`<button onclick="doClaim(${l.id})">领取</button>`);
b.push(`<button onclick="doFollow(${l.id},${l.followed})">${l.followed?"取关":"关注"}</button>`);
if (l.status===3||l.status===4) {
b.push(`<button onclick="doFeedback(${l.id})">反馈</button>`);
b.push(`<button onclick="doRelease(${l.id})">释放</button>`);
b.push(`<button onclick="doConvert(${l.id})">转商机</button>`);
}
if (l.status===1) {
b.push(`<button onclick="doAssignPool(${l.id})">分池</button>`);
b.push(`<button onclick="doAssignUser(${l.id})">分人</button>`);
}
if (l.status===6) b.push(`<button onclick="doActivate(${l.id})">激活</button>`);
b.push(`<button onclick="doDetail(${l.id})">详情</button>`);
b.push(`<button onclick="doDelete(${l.id})">删除</button>`);
return b.join("");
}
async function simple(path, params, msg){
try { await api("POST", path, {params, form:{}, rawId:"raw-list"}); loadLeads(); }
catch(e){ showErr("raw-list", e); }
}
const doClaim = id => simple("/api/lead/claim",{id});
const doRelease = id => simple("/api/lead/release",{id});
const doActivate = id => simple("/api/lead/activate",{id});
const doDelete = id => { if(confirm("确认删除?")) simple("/api/lead/delete",{id}); };
async function doFollow(id, followed){
try { await api("POST", followed?"/api/lead/unfollow":"/api/lead/follow",{params:{id}, form:{}, rawId:"raw-list"}); loadLeads(); }
catch(e){ showErr("raw-list", e); }
}
function doAssignPool(id){ const poolId = prompt("poolId(见公海池页签)"); if(poolId) simple("/api/lead/assign-pool",{id,poolId}); }
function doAssignUser(id){ const userId = prompt("userId"); if(userId) simple("/api/lead/assign-user",{id,userId}); }
async function doFeedback(id){
const content = prompt("反馈内容","客户有意向"); if(!content) return;
const invalid = confirm("确定反馈【有效】?\n确定=有效(跟进中) 取消=无效(线索作废)");
try {
await api("POST","/api/lead/feedback-submit",{form:{leadId:id, feedbackStatus:invalid?1:2, content}, rawId:"raw-list"});
loadLeads();
} catch(e){ showErr("raw-list", e); }
}
async function doConvert(id){
const name = prompt("商机名称","测试商机"); if(!name) return;
try {
await api("POST","/api/lead/convert",{params:{id}, form:{opportunityName:name,
industryCode: prompt("行业code","it")||"it", intendedCustomer: prompt("意向客户","客户")||"客户",
regionCode: prompt("地区国标code","440100")||"440100"}, rawId:"raw-list"});
loadLeads();
} catch(e){ showErr("raw-list", e); }
}
async function doDetail(id){
try { await api("GET","/api/lead/detail",{params:{id}, rawId:"raw-list"}); }
catch(e){ showErr("raw-list", e); }
}
/* ---------------- ⑥ 公海池 ---------------- */
async function loadPools(){
try {
const r = await api("POST","/api/rule/pool/page",{form:{current:1,size:50}, rawId:"raw-pool"});
window._pools = r.content||[];
$("p-body").innerHTML = window._pools.map(p=>`<tr><td>${p.id}</td><td>${p.poolName}</td>
<td>${p.deptName||p.deptId}</td><td>${p.claimRule}</td>
<td>${p.recycleDays}/${p.expireDays}</td><td>${p.ownerUserName||"-"}</td>
<td><button onclick="editPool(${p.id})">编辑</button>
<button onclick="delPool(${p.id})">删除</button></td></tr>`).join("");
const sel = $("l-pool"); sel.innerHTML = '<option value="">全部</option>';
const csel = $("c-pool"); csel.innerHTML = '<option value="">(不归池=未分发)</option>';
window._pools.forEach(p=>{
sel.insertAdjacentHTML("beforeend",`<option value="${p.id}">${p.poolName}</option>`);
csel.insertAdjacentHTML("beforeend",`<option value="${p.id}">${p.poolName}</option>`);
});
} catch(e){ showErr("raw-pool", e); }
}
async function delPool(id){
if(!confirm("确认删除公海池 "+id+" ?")) return;
try { await api("POST","/api/rule/pool/delete",{params:{id}, form:{}, rawId:"raw-pool"}); loadPools(); }
catch(e){ showErr("raw-pool", e); }
}
async function editPool(id){
try {
const d = await api("GET","/api/rule/pool/detail",{params:{id}, rawId:"raw-pool"});
$("p-id").value=d.id; $("p-name").value=d.poolName||""; $("p-dept").value=d.deptId||"";
$("p-rule").value=d.claimRule; $("p-recycle").value=d.recycleDays; $("p-expire").value=d.expireDays;
$("p-daily").value=d.dailyClaimLimit; $("p-hold").value=d.holdLimit;
loadPoolCities().then(()=>{ [...$("p-cities").options].forEach(o=>o.selected=(d.cityRegionIds||[]).map(String).includes(o.value)); });
[...$("p-provs").options].forEach(o=>o.selected=(d.provinceRegionIds||[]).map(String).includes(o.value));
[...$("p-collab").options].forEach(o=>o.selected=(d.collaboratorUserIds||[]).map(String).includes(o.value));
$("p-owner").value = d.ownerUserId||"";
} catch(e){ showErr("raw-pool", e); }
}
function resetPoolForm(){ $("p-id").value=""; ["p-name"].forEach(i=>$(i).value=""); }
async function loadDepts(){
try {
const tree = await api("GET","/api/system/depts/tree",{});
const flat = []; (function walk(ns){ (ns||[]).forEach(n=>{flat.push(n); walk(n.children);}); })(tree);
window._depts = flat;
$("p-dept").innerHTML = flat.map(d=>`<option value="${d.id}">${d.deptName}</option>`).join("");
} catch(e){ console.warn("部门树加载失败", e); }
}
async function loadUsers(){
try {
const r = await api("POST","/api/system/users/page",{form:{current:1,size:200}});
window._users = r.content||[];
const opts = window._users.map(u=>`<option value="${u.id}">${u.username||u.account||u.id}</option>`).join("");
$("p-owner").innerHTML = opts; $("p-collab").innerHTML = opts;
} catch(e){ console.warn("用户列表加载失败(需ADMIN)", e); }
}
async function loadProvOptions(){
try {
const ps = await api("GET","/api/rule/region/level",{params:{level:1}});
window._provs = ps;
$("p-provs").innerHTML = ps.map(p=>`<option value="${p.id}">${p.name}</option>`).join("");
} catch(e){ console.warn(e); }
}
async function loadPoolCities(){
const selIds = [...$("p-provs").selectedOptions].map(o=>o.value);
if (!selIds.length) { $("p-cities").innerHTML=""; return; }
try {
const cs = await api("GET","/api/rule/region/list",{params:{parentId:selIds[0]}, rawId:"raw-pool"});
$("p-cities").innerHTML = cs.map(c=>`<option value="${c.id}">${c.name}</option>`).join("");
} catch(e){ showErr("raw-pool", e); }
}
async function savePool(){
try {
await api("POST","/api/rule/pool/saveOrUpdate",{form:{
id:$("p-id").value||null, poolName:$("p-name").value, deptId:$("p-dept").value,
claimRule:$("p-rule").value, recycleDays:$("p-recycle").value, expireDays:$("p-expire").value,
dailyClaimLimit:$("p-daily").value, holdLimit:$("p-hold").value, ownerUserId:$("p-owner").value||null,
collaboratorUserIds:[...$("p-collab").selectedOptions].map(o=>o.value),
provinceRegionIds:[...$("p-provs").selectedOptions].map(o=>o.value),
cityRegionIds:[...$("p-cities").selectedOptions].map(o=>o.value)}, rawId:"raw-pool"});
loadPools();
} catch(e){ showErr("raw-pool", e); }
}
/* ---------------- 页签 ---------------- */
const TABS = [["login","登录"],["region","区划级联"],["dict","字典"],["create","新增线索"],["list","四列表"],["pool","公海池"],["log","请求日志"]];
function initTabs(){
$("tabs").innerHTML = TABS.map(([k,t],i)=>`<button id="tb-${k}" onclick="go('${k}')" class="${i===0?"on":""}">${t}</button>`).join("");
}
function go(k){
document.querySelectorAll("nav button").forEach(b=>b.classList.remove("on"));
document.querySelectorAll("section.tab").forEach(s=>s.classList.remove("on"));
$("tb-"+k).classList.add("on"); $("tab-"+k).classList.add("on");
}
window.addEventListener("load", () => {
$("baseUrl").value = localStorage.getItem("demoBase") || (location.protocol.startsWith("http") ? location.origin : "http://localhost:8081");
showTok(); initTabs(); initStatusChecks();
loadProvinces(); loadDicts(); loadPools(); loadDepts(); loadUsers(); loadProvOptions();
});
</script>
</body>
</html>