Commit a1cfaaed authored by tao355667's avatar tao355667

feat: refine admin data tables

parent 3d8d4534
......@@ -14,7 +14,6 @@ let captchaRequest = 0;
let currentUser = null;
let consultationTeachers = [];
let staffAccounts = [];
let activeConsultationNote = null;
let activeConsultationUser = null;
const consultationStatusLabels = { unprocessed: "未处理", added: "已添加", not_added: "未添加" };
......@@ -102,7 +101,6 @@ async function enterApp(user) {
document.querySelectorAll(".content-role").forEach((element) => element.classList.toggle("hidden", customerService));
$("#account-role").textContent = admin ? "管理员账号" : operator ? "运营账号" : "客服账号";
$("#account-name").textContent = user?.name || "";
$("#consultation-scope").textContent = admin ? "管理员可查看全部客服的用户线索" : "这里只显示分配给你的用户线索";
await showAdminView(operator ? "articles" : "consultations");
if (admin || operator) {
await loadCategories();
......@@ -140,6 +138,46 @@ const formatTime = (value) => {
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
};
function filterableTableHead(rowClass, columns, tableRows) {
const head = document.createElement("div");
head.className = `data-row data-head ${rowClass}`;
const filters = [];
columns.forEach((column, index) => {
const cell = document.createElement("label");
cell.className = "column-filter";
const label = document.createElement("span");
label.textContent = column;
const select = document.createElement("select");
select.setAttribute("aria-label", `${column}筛选`);
select.title = `筛选${column}`;
const all = document.createElement("option");
all.value = "";
all.textContent = "全部";
select.append(all);
[...new Set(tableRows.map((item) => item.values[index]))]
.sort((left, right) => String(left).localeCompare(String(right), "zh-CN", { numeric: true }))
.forEach((value) => {
const option = document.createElement("option");
option.value = value;
option.textContent = value;
select.append(option);
});
filters.push(select);
cell.append(label, select);
head.append(cell);
});
const operation = document.createElement("span");
operation.textContent = "操作";
head.append(operation);
const applyFilters = () => {
tableRows.forEach((item) => {
item.element.classList.toggle("hidden", filters.some((filter, index) => filter.value && item.values[index] !== filter.value));
});
};
filters.forEach((filter) => filter.addEventListener("change", applyFilters));
return head;
}
function updateConsultationMetrics(records) {
$("#consultation-count").textContent = records.length;
$("#metric-total").textContent = records.length;
......@@ -148,51 +186,6 @@ function updateConsultationMetrics(records) {
});
}
function closeConsultationNoteModal() {
hide("#consultation-note-modal");
activeConsultationNote = null;
$("#note-card-message").textContent = "";
}
function openConsultationNoteModal(record, records, form) {
activeConsultationNote = { record, records, form };
$("#note-card-phone").textContent = record.phone;
$("#note-card-teacher").textContent = record.teacherName;
$("#note-card-meta").textContent = `提交于 ${formatTime(record.createdAt)}`;
$("#note-card-status").value = record.status;
$("#note-card-content").value = record.note || "";
$("#note-card-count").textContent = $("#note-card-content").value.length;
$("#note-card-message").textContent = "";
show("#consultation-note-modal");
$("#note-card-content").focus();
}
$("#close-note-modal").addEventListener("click", closeConsultationNoteModal);
$("#cancel-note-modal").addEventListener("click", closeConsultationNoteModal);
$("#note-card-content").addEventListener("input", () => { $("#note-card-count").textContent = $("#note-card-content").value.length; });
$("#consultation-note-form").addEventListener("submit", async (event) => {
event.preventDefault();
if (!activeConsultationNote) return;
const { record, records, form } = activeConsultationNote;
const submit = event.submitter;
submit.disabled = true;
$("#note-card-message").textContent = "保存中…";
try {
const result = await api(`/consultations/${record.id}`, { method: "PATCH", body: { status: $("#note-card-status").value, note: $("#note-card-content").value } });
record.status = result.consultation.status;
record.note = result.consultation.note;
if (form) {
const select = form.querySelector("select");
const textarea = form.querySelector("textarea");
if (select) select.value = record.status;
if (textarea) textarea.value = record.note;
}
updateConsultationMetrics(records);
closeConsultationNoteModal();
} catch (error) { $("#note-card-message").textContent = error.message; }
finally { submit.disabled = false; }
});
async function loadConsultations() {
const result = await api("/consultations").catch((error) => { alert(error.message); return { consultations: [] }; });
const records = result.consultations || [];
......@@ -208,35 +201,41 @@ async function loadConsultations() {
list.innerHTML = '<div class="empty">暂时没有用户线索。官网用户提交手机号后会显示在这里。</div>';
return;
}
const head = document.createElement("div");
head.className = "data-row data-head consultation-row";
head.innerHTML = "<span>ID</span><span>姓名</span><span>手机号</span><span>归属地</span><span>分配客服</span><span>状态</span><span>未添加原因</span><span>备注</span><span>打电话次数</span><span>提交时间</span><span>操作</span>";
list.append(head);
const tableRows = [];
records.forEach((record) => {
const row = document.createElement("article");
row.className = "data-row consultation-row";
row.innerHTML = '<strong class="sequence"></strong><span class="user-name"></span><a class="phone-link"></a><span class="location"></span><span class="teacher-name"></span><span class="consultation-status"></span><span class="unadded-reason"></span><span class="note-summary"></span><span class="call-count"></span><time class="created"></time><div class="row-actions"></div>';
row.querySelector(".sequence").textContent = record.sequence || "-";
row.querySelector(".user-name").textContent = record.name || "未填写";
const values = [
String(record.sequence || "-"), record.name || "未填写", record.phone,
record.location || "-", record.teacherName || "未分配",
consultationStatusLabels[record.status] || "未处理", record.unaddedReason || "-",
record.note || "-", String(record.callCount ?? 0), formatTime(record.createdAt),
];
row.querySelector(".sequence").textContent = values[0];
row.querySelector(".user-name").textContent = values[1];
const phone = row.querySelector(".phone-link");
phone.textContent = record.phone;
phone.href = `tel:${record.phone}`;
row.querySelector(".location").textContent = record.location || "-";
row.querySelector(".teacher-name").textContent = record.teacherName || "未分配";
row.querySelector(".consultation-status").textContent = consultationStatusLabels[record.status] || "未处理";
row.querySelector(".unadded-reason").textContent = record.unaddedReason || "-";
row.querySelector(".note-summary").textContent = record.note || "-";
row.querySelector(".call-count").textContent = record.callCount ?? 0;
row.querySelector(".created").textContent = formatTime(record.createdAt);
row.querySelector(".location").textContent = values[3];
row.querySelector(".teacher-name").textContent = values[4];
const status = row.querySelector(".consultation-status");
status.textContent = values[5];
status.classList.add(`consultation-status--${record.status}`);
row.querySelector(".unadded-reason").textContent = values[6];
row.querySelector(".note-summary").textContent = values[7];
row.querySelector(".call-count").textContent = values[8];
row.querySelector(".created").textContent = values[9];
const actions = row.querySelector(".row-actions");
actions.append(listAction("编辑", "ghost small", async () => { if (currentUser?.role === "admin" && !consultationTeachers.length) await loadTeachers(); openConsultationUserModal(record); }));
if (currentUser?.role === "admin") actions.append(listAction("删除", "ghost danger small", async () => {
actions.append(listAction("编辑", "text-action", async () => { if (currentUser?.role === "admin" && !consultationTeachers.length) await loadTeachers(); openConsultationUserModal(record); }));
if (currentUser?.role === "admin") actions.append(listAction("删除", "text-action danger", async () => {
if (!confirm(`确定删除用户“${record.phone}”吗?`)) return;
try { await api(`/consultations/${record.id}`, { method: "DELETE" }); await loadConsultations(); } catch (error) { alert(error.message); }
}));
actions.append(listAction("查看全部备注", "ghost small", () => openConsultationNoteModal(record, records, null)));
list.append(row);
tableRows.push({ element: row, values });
});
list.append(filterableTableHead("consultation-row", ["ID", "姓名", "手机号", "归属地", "分配客服", "状态", "未添加原因", "备注", "打电话次数", "提交时间"], tableRows));
tableRows.forEach((item) => list.append(item.element));
}
$("#refresh-consultations").addEventListener("click", loadConsultations);
......@@ -257,6 +256,7 @@ function openConsultationUserModal(record = null) {
: `<option value="${record?.teacherId || currentUser?.id || ""}">${record?.teacherName || currentUser?.name || "当前客服"}</option>`;
$("#consultation-user-form").reset();
$("#consultation-user-title").textContent = record ? "编辑用户" : "录入用户";
$("#consultation-user-description").textContent = record ? `用户 ID ${record.sequence || "-"} · 提交于 ${formatTime(record.createdAt)}` : "一次完成用户信息、咨询状态和跟进备注录入";
$("#consultation-user-id").value = record?.id || "";
$("#consultation-user-name").value = record?.name || "";
$("#consultation-user-phone").value = record?.phone || "";
......@@ -267,6 +267,7 @@ function openConsultationUserModal(record = null) {
$("#consultation-user-reason").value = record?.unaddedReason || "";
$("#consultation-user-calls").value = record?.callCount ?? 0;
$("#consultation-user-note").value = record?.note || "";
$("#consultation-user-note-count").textContent = $("#consultation-user-note").value.length;
$("#consultation-user-error").textContent = "";
show("#consultation-user-modal");
$("#consultation-user-name").focus();
......@@ -278,6 +279,7 @@ $("#new-consultation").addEventListener("click", async () => {
});
$("#close-consultation-user-modal").addEventListener("click", closeConsultationUserModal);
$("#cancel-consultation-user").addEventListener("click", closeConsultationUserModal);
$("#consultation-user-note").addEventListener("input", () => { $("#consultation-user-note-count").textContent = $("#consultation-user-note").value.length; });
$("#consultation-user-form").addEventListener("submit", async (event) => {
event.preventDefault();
const id = $("#consultation-user-id").value;
......@@ -308,33 +310,41 @@ async function loadTeachers() {
list.innerHTML = '<div class="empty">还没有后台账号。添加客服或运营账号后会显示在这里。</div>';
return;
}
const head = document.createElement("div");
head.className = "data-row data-head teacher-row";
head.innerHTML = "<span>账号</span><span>登录账号</span><span>角色</span><span>累计分配</span><span>状态</span><span>操作</span>";
list.append(head);
const tableRows = [];
staffAccounts.forEach((teacher) => {
const row = document.createElement("article");
row.className = "data-row teacher-row";
row.innerHTML = '<div class="teacher-identity"><img alt="微信二维码" /><div><strong></strong><small></small></div></div><span class="username"></span><strong class="sort"></strong><span class="assigned"></span><span class="teacher-state"></span><div class="row-actions"></div>';
row.querySelector("img").src = teacher.qrCodeUrl || "/favicon.png";
row.querySelector(".teacher-identity strong").textContent = teacher.name;
row.querySelector(".teacher-identity small").textContent = teacher.role === "operator" ? "学习资讯运营" : teacher.title;
row.querySelector(".username").textContent = teacher.username;
row.querySelector(".sort").textContent = teacher.role === "operator" ? "运营" : "客服";
row.querySelector(".assigned").textContent = teacher.role === "operator" ? "-" : `${teacher.assignedCount} 次`;
row.innerHTML = '<strong class="staff-name"></strong><span class="username"></span><span class="staff-role"></span><span class="staff-title"></span><span class="staff-wechat"></span><span class="sort"></span><span class="assigned"></span><span class="teacher-state"></span><div class="row-actions"></div>';
const values = [
teacher.name, teacher.username, teacher.role === "operator" ? "运营" : "客服",
teacher.role === "operator" ? "-" : teacher.title || "-",
teacher.role === "operator" ? "-" : teacher.wechat || "-",
teacher.role === "operator" ? "-" : String(teacher.sort),
teacher.role === "operator" ? "-" : `${teacher.assignedCount} 次`,
teacher.enabled ? "启用" : "停用",
];
row.querySelector(".staff-name").textContent = values[0];
row.querySelector(".username").textContent = values[1];
row.querySelector(".staff-role").textContent = values[2];
row.querySelector(".staff-title").textContent = values[3];
row.querySelector(".staff-wechat").textContent = values[4];
row.querySelector(".sort").textContent = values[5];
row.querySelector(".assigned").textContent = values[6];
const state = row.querySelector(".teacher-state");
state.textContent = teacher.enabled ? "启用" : "停用";
state.textContent = values[7];
state.className = `teacher-state ${teacher.enabled ? "enabled" : "disabled"}`;
row.querySelector(".row-actions").append(
listAction("编辑", "ghost small", () => openTeacherModal(teacher)),
listAction("删除", "ghost danger small", async () => {
listAction("编辑", "text-action", () => openTeacherModal(teacher)),
listAction("删除", "text-action danger", async () => {
if (!confirm(`确定删除账号“${teacher.name}”吗?历史数据仍会保留。`)) return;
try { await api(`/consultation-teachers/${teacher.id}`, { method: "DELETE" }); await loadTeachers(); }
catch (error) { alert(error.message); }
}),
);
list.append(row);
tableRows.push({ element: row, values });
});
list.append(filterableTableHead("teacher-row", ["姓名", "登录账号", "角色", "展示称谓", "微信号", "轮询顺序", "累计分配", "状态"], tableRows));
tableRows.forEach((item) => list.append(item.element));
}
function closeTeacherModal() { hide("#teacher-modal"); $("#teacher-form").reset(); $("#teacher-error").textContent = ""; }
......
......@@ -42,6 +42,7 @@ input:focus, textarea:focus, select:focus { outline: 2px solid rgba(20,115,230,.
.pending-badge { padding: 5px 10px; color: #cfe2ff; background: rgba(20,115,230,.12); border: 1px solid rgba(20,115,230,.3); border-radius: 999px; }
.view { width: min(1120px, calc(100% - 40px)); margin: 0 auto; padding: 44px 0 70px; }
.view-head { margin-bottom: 26px; display: flex; align-items: center; justify-content: space-between; gap: 18px; }
.view-toolbar { min-height: 40px; margin-bottom: 18px; display: flex; justify-content: flex-end; }
.view-head p { margin: 0 0 5px; color: var(--primary-dark); font-size: 11px; font-weight: 700; letter-spacing: .16em; }
.view-head h1 { margin: 0; font-size: 30px; }
.view-actions { display: flex; gap: 9px; }
......@@ -185,12 +186,17 @@ input:focus, textarea:focus, select:focus { outline: 2px solid rgba(20,115,230,.
.table-card::-webkit-scrollbar { height: 9px; }
.table-card::-webkit-scrollbar-track { background: #f2f5f8; }
.table-card::-webkit-scrollbar-thumb { background: #b8c7d8; border: 2px solid #f2f5f8; border-radius: 999px; }
.data-list { min-width: 1320px; }
.data-list { min-width: 0; }
.data-row { display: grid; align-items: center; gap: 10px; padding: 11px 14px; border-bottom: 1px solid var(--border); font-size: 12px; }
.data-row:last-child { border-bottom: 0; }
.data-head { min-height: 44px; padding-block: 11px; color: var(--muted); background: #f7f9fb; font-size: 12px; font-weight: 600; }
.consultation-row { grid-template-columns: 44px 76px 108px 76px 82px 66px 112px 150px 68px 102px 214px; }
.teacher-row { grid-template-columns: 1.45fr .85fr .45fr .65fr .55fr .9fr; }
.data-head { min-height: 61px; padding-block: 8px; color: var(--muted); background: #f7f9fb; font-size: 12px; font-weight: 600; }
.column-filter { min-width: 0; display: grid; gap: 4px; }
.column-filter > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.column-filter select { min-height: 24px; padding: 2px 19px 2px 5px; color: #657587; background-color: #fff; border-color: #d9e1e9; border-radius: 5px; font-size: 10px; }
#consultation-list { min-width: 1100px; }
.consultation-row { grid-template-columns: 44px 76px 108px 76px 82px 66px 112px minmax(150px,1fr) 68px 102px 86px; }
#teacher-list { min-width: 1050px; }
.teacher-row { grid-template-columns: .75fr .9fr .55fr 1.35fr 1fr .65fr .75fr .65fr 86px; }
.data-row small { display: block; margin-top: 4px; color: var(--muted); font-size: 11px; }
.phone-link { color: var(--text); font-size: 12px; font-weight: 600; text-decoration: none; }
.page { max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
......@@ -198,10 +204,16 @@ input:focus, textarea:focus, select:focus { outline: 2px solid rgba(20,115,230,.
.created { color: var(--muted); }
.sequence,.call-count { color: var(--muted); font-size: 12px; }
.user-name,.location,.unadded-reason,.note-summary,.consultation-status { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
.consultation-row > :last-child { position: sticky; right: 0; z-index: 2; min-height: 100%; align-content: center; background: #fff; box-shadow: -10px 0 14px -14px rgba(19,48,79,.75); }
.consultation-row.data-head > :last-child { z-index: 3; background: #f7f9fb; }
.consultation-row .row-actions { gap: 5px; flex-wrap: nowrap; }
.consultation-row .row-actions button { min-height: 29px; padding-inline: 9px; font-size: 11px; }
.consultation-status { font-weight: 700; }
.consultation-status--unprocessed { color: #1769aa; }
.consultation-status--added { color: #0b805b; }
.consultation-status--not_added { color: #bd4141; }
.consultation-row > :last-child,.teacher-row > :last-child { position: sticky; right: 0; z-index: 2; min-height: 100%; align-content: center; justify-content: flex-end; background: #fff; box-shadow: -10px 0 14px -14px rgba(19,48,79,.75); }
.consultation-row.data-head > :last-child,.teacher-row.data-head > :last-child { z-index: 3; background: #f7f9fb; }
.consultation-row .row-actions,.teacher-row .row-actions { gap: 12px; flex-wrap: nowrap; }
.consultation-row .row-actions .text-action,.teacher-row .row-actions .text-action { min-height: auto; padding: 2px 0; color: var(--primary-dark); background: transparent; border: 0; border-radius: 0; font-size: 12px; line-height: 1.4; }
.consultation-row .row-actions .text-action:hover,.teacher-row .row-actions .text-action:hover { color: var(--deep); text-decoration: underline; transform: none; }
.consultation-row .row-actions .text-action.danger,.teacher-row .row-actions .text-action.danger { color: var(--danger); }
.inline-follow-form { display: contents; }
.inline-follow-form select { padding: 8px 10px; font-size: 13px; }
.inline-note { min-width: 0; display: grid; gap: 4px; }
......@@ -210,20 +222,8 @@ input:focus, textarea:focus, select:focus { outline: 2px solid rgba(20,115,230,.
.note-toggle:hover { text-decoration: underline; }
.inline-follow-actions { display: grid; justify-items:start; gap: 2px; }
.inline-follow-actions .save-state { min-height: 14px; margin: 0; white-space: nowrap; }
.consultation-note-card { width: min(680px, 100%); }
.consultation-note-form { padding: 22px; display: grid; gap: 18px; }
.note-card-summary { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.note-card-summary > div { padding: 14px 16px; display: grid; gap: 5px; background: #f5f9fd; border-radius: 9px; }
.note-card-summary small { color: var(--muted); font-size: 11px; }
.note-card-summary strong { font-size: 15px; }
.consultation-note-form > label { display: grid; gap: 7px; color: var(--muted); font-size: 13px; }
.consultation-note-form select { max-width: 180px; }
.consultation-note-form textarea { min-height: 230px; resize: vertical; line-height: 1.7; }
.note-card-footer { display: grid; grid-template-columns: auto 1fr auto auto; gap: 9px; align-items:center; }
.note-card-footer > span { color: var(--muted); font-size: 11px; }
.note-card-footer > p { margin: 0; color: var(--danger); font-size: 12px; }
.teacher-identity { display: flex; align-items: center; gap: 12px; }
.teacher-identity img { width: 48px; height: 48px; object-fit: cover; border: 1px solid var(--border); border-radius: 8px; }
.staff-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
.staff-title,.staff-wechat,.username { overflow: hidden; color: var(--muted); text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
.teacher-state { width: max-content; padding: 4px 9px; border-radius: 999px; font-size: 12px; }
.teacher-state.enabled { color: #087a56; background: #dff8ee; }
.teacher-state.disabled { color: #7b8190; background: #edf0f4; }
......@@ -235,7 +235,14 @@ input:focus, textarea:focus, select:focus { outline: 2px solid rgba(20,115,230,.
.teacher-form::-webkit-scrollbar { width: 8px; }
.teacher-form::-webkit-scrollbar-track { background: #f3f6f9; }
.teacher-form::-webkit-scrollbar-thumb { background: #bdc9d6; border: 2px solid #f3f6f9; border-radius: 999px; }
.teacher-form > .password-actions { margin: 0 -4px -4px; padding: 12px 4px 4px; position: sticky; bottom: 0; z-index: 2; background: linear-gradient(180deg,rgba(255,255,255,0),#fff 24%); }
.teacher-form > .password-actions { margin-top: 2px; }
.consultation-edit-section { padding: 17px; display: grid; gap: 15px; border: 1px solid var(--border); border-radius: 12px; background: #fbfcfe; }
.consultation-edit-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; }
.consultation-edit-heading strong { color: #203a57; font-size: 14px; }
.consultation-edit-heading span { color: var(--muted); font-size: 11px; }
.consultation-note-field { display: grid; gap: 7px; color: var(--muted); font-size: 13px; }
.consultation-note-field textarea { min-height: 170px; resize: vertical; line-height: 1.7; }
.consultation-note-field small { justify-self: end; color: var(--muted); font-size: 11px; }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
.teacher-form label, .qr-field { display: grid; gap: 7px; color: var(--muted); font-size: 13px; }
.teacher-form label small { font-size: 11px; }
......@@ -277,9 +284,9 @@ input:focus, textarea:focus, select:focus { outline: 2px solid rgba(20,115,230,.
.modal { padding: 10px; place-items: start center; }
.modal-card { max-height: calc(100dvh - 20px); }
.teacher-form { padding: 17px; }
.consultation-row { grid-template-columns: 44px 76px 108px 76px 82px 66px 112px 150px 68px 102px 214px; }
.note-card-summary { grid-template-columns: 1fr; }
.note-card-footer { grid-template-columns: 1fr 1fr; }
.note-card-footer > p { grid-column: 1 / -1; grid-row: 2; }
.consultation-row { grid-template-columns: 44px 76px 108px 76px 82px 66px 112px minmax(150px,1fr) 68px 102px 86px; }
.teacher-row { grid-template-columns: .75fr .9fr .55fr 1.35fr 1fr .65fr .75fr .65fr 86px; }
.consultation-edit-section { padding: 14px; }
.consultation-edit-heading { align-items: flex-start; flex-direction: column; gap: 3px; }
.settings-panel { align-items:stretch; flex-direction:column; }
}
......@@ -50,7 +50,7 @@
</header>
<main id="consultations-view" class="view consultation-view consultation-role">
<div class="view-head"><div><h1>用户线索列表</h1><span id="consultation-scope">管理员可查看全部客服的用户线索</span></div><div class="view-actions"><button id="refresh-consultations" class="ghost" type="button">刷新记录</button><button id="new-consultation" class="primary admin-only" type="button">录入用户</button></div></div>
<div class="view-toolbar"><div class="view-actions"><button id="refresh-consultations" class="ghost" type="button">刷新记录</button><button id="new-consultation" class="primary admin-only" type="button">录入用户</button></div></div>
<div class="metric-grid">
<article><small>未处理</small><strong id="metric-unprocessed">0</strong></article>
<article><small>已添加</small><strong id="metric-added">0</strong></article>
......@@ -61,19 +61,18 @@
</main>
<main id="teachers-view" class="view hidden admin-only">
<div class="view-head"><div><p>STAFF ACCOUNTS</p><h1>后台账号管理</h1><span>客服负责用户咨询并参与轮询;运营负责学习资讯发布。</span></div><button id="new-teacher" class="primary" type="button">添加账号</button></div>
<div class="view-toolbar"><button id="new-teacher" class="primary" type="button">添加账号</button></div>
<div class="table-card"><div id="teacher-list" class="data-list"></div></div>
</main>
<main id="list-view" class="view hidden content-role">
<div class="view-head"><div><p>CONTENT</p><h1>学习资讯</h1></div><div class="view-actions"><button id="manage-categories" class="ghost">管理分类</button><button id="import-article" class="ghost">导入文件</button><button id="new-article" class="primary">+ 新建文章</button></div></div>
<div class="view-toolbar"><div class="view-actions"><button id="manage-categories" class="ghost">管理分类</button><button id="import-article" class="ghost">导入文件</button><button id="new-article" class="primary">+ 新建文章</button></div></div>
<input id="article-file" class="hidden" type="file" accept=".md,.docx,text/markdown,application/vnd.openxmlformats-officedocument.wordprocessingml.document" />
<p id="import-status" class="import-status" aria-live="polite"></p>
<div id="article-list" class="article-list"></div>
</main>
<main id="settings-view" class="view hidden admin-only">
<div class="view-head"><div><h1>账号设置</h1><span>管理后台管理员账号的登录安全</span></div></div>
<section class="settings-panel">
<div><strong>登录密码</strong><p>修改管理员账号 admin 的登录密码。环境变量中的主密码仍可用于登录和恢复。</p></div>
<button id="change-password" class="ghost" type="button">修改密码</button>
......@@ -157,33 +156,29 @@
</div>
</div>
<div id="consultation-note-modal" class="modal hidden">
<div class="modal-card consultation-note-card">
<div class="modal-head"><div><strong>全部跟进备注</strong><small id="note-card-meta"></small></div><button id="close-note-modal" class="ghost small" type="button">关闭</button></div>
<form id="consultation-note-form" class="consultation-note-form">
<div class="note-card-summary"><div><small>家长手机号</small><strong id="note-card-phone"></strong></div><div><small>分配客服</small><strong id="note-card-teacher"></strong></div></div>
<label>咨询状态<select id="note-card-status"><option value="unprocessed">未处理</option><option value="added">已添加</option><option value="not_added">未添加</option></select></label>
<label>完整备注<textarea id="note-card-content" maxlength="500" rows="10" placeholder="记录沟通结果、家长需求或下次跟进时间"></textarea></label>
<div class="note-card-footer"><span><strong id="note-card-count">0</strong> / 500</span><p id="note-card-message"></p><button id="cancel-note-modal" class="ghost" type="button">取消</button><button class="primary" type="submit">保存备注</button></div>
</form>
</div>
</div>
<div id="consultation-user-modal" class="modal hidden">
<div class="modal-card consultation-user-card">
<div class="modal-head"><div><strong id="consultation-user-title">录入用户</strong><small>完善用户线索并分配客服</small></div><button id="close-consultation-user-modal" class="ghost small" type="button">关闭</button></div>
<div class="modal-head"><div><strong id="consultation-user-title">录入用户</strong><small id="consultation-user-description">一次完成用户信息、咨询状态和跟进备注录入</small></div><button id="close-consultation-user-modal" class="ghost small" type="button">关闭</button></div>
<form id="consultation-user-form" class="teacher-form">
<input id="consultation-user-id" type="hidden" />
<section class="consultation-edit-section">
<div class="consultation-edit-heading"><strong>用户信息</strong><span>联系方式与归属信息</span></div>
<div class="form-grid">
<label>姓名<input id="consultation-user-name" maxlength="30" /></label>
<label>手机号<input id="consultation-user-phone" inputmode="numeric" maxlength="11" required /></label>
<label>归属地<input id="consultation-user-location" maxlength="80" /></label>
<label>分配客服<select id="consultation-user-teacher" required></select></label>
</div>
</section>
<section class="consultation-edit-section">
<div class="consultation-edit-heading"><strong>咨询处理</strong><span>状态与本次联系结果</span></div>
<div class="form-grid">
<label>状态<select id="consultation-user-status"><option value="unprocessed">未处理</option><option value="added">已添加</option><option value="not_added">未添加</option></select></label>
<label>未添加原因<input id="consultation-user-reason" maxlength="200" /></label>
<label>打电话次数<input id="consultation-user-calls" type="number" min="0" max="9999" value="0" required /></label>
</div>
<label>跟进备注<textarea id="consultation-user-note" maxlength="500" rows="5"></textarea></label>
<label class="consultation-note-field">完整跟进备注<textarea id="consultation-user-note" maxlength="500" rows="8" placeholder="记录沟通结果、家长需求或下次跟进时间"></textarea><small><strong id="consultation-user-note-count">0</strong> / 500</small></label>
</section>
<p id="consultation-user-error" class="error"></p>
<div class="password-actions"><button id="cancel-consultation-user" class="ghost" type="button">取消</button><button class="primary" type="submit">保存用户</button></div>
</form>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment