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.
164 lines
5.6 KiB
164 lines
5.6 KiB
|
2 weeks ago
|
const API = 'http://localhost:8080';
|
||
|
|
let token = null;
|
||
|
|
let currentView = 'MINE';
|
||
|
|
|
||
|
|
const $ = (id) => document.getElementById(id);
|
||
|
|
|
||
|
|
function toast(msg) {
|
||
|
|
const t = $('toast');
|
||
|
|
t.textContent = msg;
|
||
|
|
t.classList.remove('hidden');
|
||
|
|
clearTimeout(t._h);
|
||
|
|
t._h = setTimeout(() => t.classList.add('hidden'), 2500);
|
||
|
|
}
|
||
|
|
|
||
|
|
function authHeaders(extra = {}) {
|
||
|
|
const h = { ...extra };
|
||
|
|
if (token) h['Authorization'] = 'Bearer ' + token;
|
||
|
|
return h;
|
||
|
|
}
|
||
|
|
|
||
|
|
const STATUS_TEXT = { 1: '待领取', 2: '推进中', 3: '暂缓中', 4: '已关闭', 5: '已转项目' };
|
||
|
|
|
||
|
|
// ---- 登录(调试 token)----
|
||
|
|
async function login() {
|
||
|
|
const userId = $('userId').value.trim();
|
||
|
|
try {
|
||
|
|
const res = await fetch(`${API}/api/auth/debug/token?userId=${encodeURIComponent(userId)}`);
|
||
|
|
const body = await res.json();
|
||
|
|
if (body.success && body.data) {
|
||
|
|
token = body.data;
|
||
|
|
$('tokenState').textContent = '已登录 uid=' + userId;
|
||
|
|
$('tokenState').className = 'pill pill-on';
|
||
|
|
toast('登录成功');
|
||
|
|
loadList();
|
||
|
|
} else {
|
||
|
|
toast('登录失败: ' + (body.message || res.status));
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
toast('登录请求异常: ' + e.message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---- 列表 ----
|
||
|
|
async function loadList() {
|
||
|
|
if (!token) { toast('请先签发调试 Token'); return; }
|
||
|
|
const params = new URLSearchParams({ viewType: currentView, current: '1', size: '20' });
|
||
|
|
try {
|
||
|
|
const res = await fetch(`${API}/api/opportunity/page`, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: authHeaders({ 'Content-Type': 'application/x-www-form-urlencoded' }),
|
||
|
|
body: params.toString(),
|
||
|
|
});
|
||
|
|
const body = await res.json();
|
||
|
|
if (!body.success) { renderRows([]); $('listMeta').textContent = '错误: ' + body.message; return; }
|
||
|
|
const page = body.data || {};
|
||
|
|
renderRows(page.content || []);
|
||
|
|
$('listMeta').textContent = `视图 ${currentView} · 共 ${page.total ?? 0} 条`;
|
||
|
|
} catch (e) {
|
||
|
|
toast('列表加载异常: ' + e.message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderRows(rows) {
|
||
|
|
const tbody = $('oppRows');
|
||
|
|
if (!rows.length) {
|
||
|
|
tbody.innerHTML = '<tr><td colspan="8" class="empty">无数据</td></tr>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
tbody.innerHTML = rows.map((r) => `
|
||
|
|
<tr data-id="${r.id}">
|
||
|
|
<td>${r.id ?? ''}</td>
|
||
|
|
<td>${esc(r.oppName)}</td>
|
||
|
|
<td>${esc(r.oppSourceName || r.oppSource || '')}</td>
|
||
|
|
<td class="status-${r.oppStatus}">${STATUS_TEXT[r.oppStatus] || r.oppStatus || ''}</td>
|
||
|
|
<td>${esc(r.currentStageName || '')}</td>
|
||
|
|
<td>${esc(r.ownerNameSnapshot || '')}</td>
|
||
|
|
<td>${esc(r.primaryCustomerNameSnapshot || '')}</td>
|
||
|
|
<td>${r.projectAmount ?? ''}</td>
|
||
|
|
</tr>`).join('');
|
||
|
|
}
|
||
|
|
|
||
|
|
function esc(s) {
|
||
|
|
return String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---- 新建商机 ----
|
||
|
|
async function openNewModal() {
|
||
|
|
if (!token) { toast('请先签发调试 Token'); return; }
|
||
|
|
$('newForm').reset();
|
||
|
|
$('formMsg').textContent = '';
|
||
|
|
onSourceChange();
|
||
|
|
$('newModal').classList.remove('hidden');
|
||
|
|
}
|
||
|
|
|
||
|
|
function closeNewModal() { $('newModal').classList.add('hidden'); }
|
||
|
|
|
||
|
|
async function onSourceChange() {
|
||
|
|
const isLead = $('f_oppSource').value === 'opp_source_01';
|
||
|
|
$('leadRow').classList.toggle('hidden', !isLead);
|
||
|
|
if (isLead) await loadConvertibleLeads();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadConvertibleLeads() {
|
||
|
|
const sel = $('f_sourceLeadId');
|
||
|
|
sel.innerHTML = '<option value="">(加载中…)</option>';
|
||
|
|
try {
|
||
|
|
const res = await fetch(`${API}/api/opportunity/convertible-leads`, { headers: authHeaders() });
|
||
|
|
const body = await res.json();
|
||
|
|
const list = (body.success && body.data) || [];
|
||
|
|
sel.innerHTML = '<option value="">请选择</option>' +
|
||
|
|
list.map((l) => `<option value="${l.id}">${esc(l.leadName || l.name || l.id)}</option>`).join('');
|
||
|
|
if (!list.length) sel.innerHTML = '<option value="">(无可关联线索)</option>';
|
||
|
|
} catch (e) {
|
||
|
|
sel.innerHTML = '<option value="">加载失败</option>';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function submitNew(ev) {
|
||
|
|
ev.preventDefault();
|
||
|
|
const msg = $('formMsg');
|
||
|
|
msg.textContent = '提交中…'; msg.className = 'msg';
|
||
|
|
const payload = {
|
||
|
|
oppSource: $('f_oppSource').value,
|
||
|
|
opportunityName: $('f_oppName').value.trim(),
|
||
|
|
partyA: $('f_partyA').value.trim() || null,
|
||
|
|
customerName: $('f_customerName').value.trim() || null,
|
||
|
|
remark: $('f_remark').value.trim() || null,
|
||
|
|
sourceLeadId: $('f_sourceLeadId').value ? Number($('f_sourceLeadId').value) : null,
|
||
|
|
};
|
||
|
|
try {
|
||
|
|
const res = await fetch(`${API}/api/opportunity`, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: authHeaders({ 'Content-Type': 'application/json' }),
|
||
|
|
body: JSON.stringify(payload),
|
||
|
|
});
|
||
|
|
const body = await res.json();
|
||
|
|
if (body.success) {
|
||
|
|
msg.textContent = '创建成功 id=' + body.data; msg.className = 'msg ok';
|
||
|
|
toast('商机已创建 id=' + body.data);
|
||
|
|
setTimeout(() => { closeNewModal(); loadList(); }, 700);
|
||
|
|
} else {
|
||
|
|
msg.textContent = '失败: ' + (body.message || res.status); msg.className = 'msg err';
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
msg.textContent = '异常: ' + e.message; msg.className = 'msg err';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---- 事件绑定 ----
|
||
|
|
$('btnLogin').addEventListener('click', login);
|
||
|
|
$('btnRefresh').addEventListener('click', loadList);
|
||
|
|
$('btnNew').addEventListener('click', openNewModal);
|
||
|
|
$('btnCancel').addEventListener('click', closeNewModal);
|
||
|
|
$('f_oppSource').addEventListener('change', onSourceChange);
|
||
|
|
$('newForm').addEventListener('submit', submitNew);
|
||
|
|
document.querySelectorAll('#viewTabs button').forEach((b) => {
|
||
|
|
b.addEventListener('click', () => {
|
||
|
|
document.querySelectorAll('#viewTabs button').forEach((x) => x.classList.remove('active'));
|
||
|
|
b.classList.add('active');
|
||
|
|
currentView = b.dataset.view;
|
||
|
|
loadList();
|
||
|
|
});
|
||
|
|
});
|