Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in / Register
Toggle navigation
Q
qiyouxue
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
xuchentao
qiyouxue
Commits
a1cfaaed
Commit
a1cfaaed
authored
Aug 13, 2026
by
tao355667
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: refine admin data tables
parent
3d8d4534
Changes
3
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
138 additions
and
126 deletions
+138
-126
app.js
public/admin/app.js
+89
-79
style.css
public/admin/style.css
+34
-27
index.astro
src/pages/admin/index.astro
+15
-20
No files found.
public/admin/app.js
View file @
a1cfaaed
...
@@ -14,7 +14,6 @@ let captchaRequest = 0;
...
@@ -14,7 +14,6 @@ let captchaRequest = 0;
let
currentUser
=
null
;
let
currentUser
=
null
;
let
consultationTeachers
=
[];
let
consultationTeachers
=
[];
let
staffAccounts
=
[];
let
staffAccounts
=
[];
let
activeConsultationNote
=
null
;
let
activeConsultationUser
=
null
;
let
activeConsultationUser
=
null
;
const
consultationStatusLabels
=
{
unprocessed
:
"未处理"
,
added
:
"已添加"
,
not_added
:
"未添加"
};
const
consultationStatusLabels
=
{
unprocessed
:
"未处理"
,
added
:
"已添加"
,
not_added
:
"未添加"
};
...
@@ -102,7 +101,6 @@ async function enterApp(user) {
...
@@ -102,7 +101,6 @@ async function enterApp(user) {
document
.
querySelectorAll
(
".content-role"
).
forEach
((
element
)
=>
element
.
classList
.
toggle
(
"hidden"
,
customerService
));
document
.
querySelectorAll
(
".content-role"
).
forEach
((
element
)
=>
element
.
classList
.
toggle
(
"hidden"
,
customerService
));
$
(
"#account-role"
).
textContent
=
admin
?
"管理员账号"
:
operator
?
"运营账号"
:
"客服账号"
;
$
(
"#account-role"
).
textContent
=
admin
?
"管理员账号"
:
operator
?
"运营账号"
:
"客服账号"
;
$
(
"#account-name"
).
textContent
=
user
?.
name
||
""
;
$
(
"#account-name"
).
textContent
=
user
?.
name
||
""
;
$
(
"#consultation-scope"
).
textContent
=
admin
?
"管理员可查看全部客服的用户线索"
:
"这里只显示分配给你的用户线索"
;
await
showAdminView
(
operator
?
"articles"
:
"consultations"
);
await
showAdminView
(
operator
?
"articles"
:
"consultations"
);
if
(
admin
||
operator
)
{
if
(
admin
||
operator
)
{
await
loadCategories
();
await
loadCategories
();
...
@@ -140,6 +138,46 @@ const formatTime = (value) => {
...
@@ -140,6 +138,46 @@ const formatTime = (value) => {
return
`
${
date
.
getFullYear
()}
-
${
pad
(
date
.
getMonth
()
+
1
)}
-
${
pad
(
date
.
getDate
())}
${
pad
(
date
.
getHours
())}
:
${
pad
(
date
.
getMinutes
())}
`
;
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
)
{
function
updateConsultationMetrics
(
records
)
{
$
(
"#consultation-count"
).
textContent
=
records
.
length
;
$
(
"#consultation-count"
).
textContent
=
records
.
length
;
$
(
"#metric-total"
).
textContent
=
records
.
length
;
$
(
"#metric-total"
).
textContent
=
records
.
length
;
...
@@ -148,51 +186,6 @@ function updateConsultationMetrics(records) {
...
@@ -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
()
{
async
function
loadConsultations
()
{
const
result
=
await
api
(
"/consultations"
).
catch
((
error
)
=>
{
alert
(
error
.
message
);
return
{
consultations
:
[]
};
});
const
result
=
await
api
(
"/consultations"
).
catch
((
error
)
=>
{
alert
(
error
.
message
);
return
{
consultations
:
[]
};
});
const
records
=
result
.
consultations
||
[];
const
records
=
result
.
consultations
||
[];
...
@@ -208,35 +201,41 @@ async function loadConsultations() {
...
@@ -208,35 +201,41 @@ async function loadConsultations() {
list
.
innerHTML
=
'<div class="empty">暂时没有用户线索。官网用户提交手机号后会显示在这里。</div>'
;
list
.
innerHTML
=
'<div class="empty">暂时没有用户线索。官网用户提交手机号后会显示在这里。</div>'
;
return
;
return
;
}
}
const
head
=
document
.
createElement
(
"div"
);
const
tableRows
=
[];
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
);
records
.
forEach
((
record
)
=>
{
records
.
forEach
((
record
)
=>
{
const
row
=
document
.
createElement
(
"article"
);
const
row
=
document
.
createElement
(
"article"
);
row
.
className
=
"data-row consultation-row"
;
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
.
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
||
"-"
;
const
values
=
[
row
.
querySelector
(
".user-name"
).
textContent
=
record
.
name
||
"未填写"
;
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"
);
const
phone
=
row
.
querySelector
(
".phone-link"
);
phone
.
textContent
=
record
.
phone
;
phone
.
textContent
=
record
.
phone
;
phone
.
href
=
`tel:
${
record
.
phone
}
`
;
phone
.
href
=
`tel:
${
record
.
phone
}
`
;
row
.
querySelector
(
".location"
).
textContent
=
record
.
location
||
"-"
;
row
.
querySelector
(
".location"
).
textContent
=
values
[
3
];
row
.
querySelector
(
".teacher-name"
).
textContent
=
record
.
teacherName
||
"未分配"
;
row
.
querySelector
(
".teacher-name"
).
textContent
=
values
[
4
];
row
.
querySelector
(
".consultation-status"
).
textContent
=
consultationStatusLabels
[
record
.
status
]
||
"未处理"
;
const
status
=
row
.
querySelector
(
".consultation-status"
);
row
.
querySelector
(
".unadded-reason"
).
textContent
=
record
.
unaddedReason
||
"-"
;
status
.
textContent
=
values
[
5
];
row
.
querySelector
(
".note-summary"
).
textContent
=
record
.
note
||
"-"
;
status
.
classList
.
add
(
`consultation-status--
${
record
.
status
}
`
);
row
.
querySelector
(
".call-count"
).
textContent
=
record
.
callCount
??
0
;
row
.
querySelector
(
".unadded-reason"
).
textContent
=
values
[
6
];
row
.
querySelector
(
".created"
).
textContent
=
formatTime
(
record
.
createdAt
);
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"
);
const
actions
=
row
.
querySelector
(
".row-actions"
);
actions
.
append
(
listAction
(
"编辑"
,
"
ghost small
"
,
async
()
=>
{
if
(
currentUser
?.
role
===
"admin"
&&
!
consultationTeachers
.
length
)
await
loadTeachers
();
openConsultationUserModal
(
record
);
}));
actions
.
append
(
listAction
(
"编辑"
,
"
text-action
"
,
async
()
=>
{
if
(
currentUser
?.
role
===
"admin"
&&
!
consultationTeachers
.
length
)
await
loadTeachers
();
openConsultationUserModal
(
record
);
}));
if
(
currentUser
?.
role
===
"admin"
)
actions
.
append
(
listAction
(
"删除"
,
"
ghost danger small
"
,
async
()
=>
{
if
(
currentUser
?.
role
===
"admin"
)
actions
.
append
(
listAction
(
"删除"
,
"
text-action danger
"
,
async
()
=>
{
if
(
!
confirm
(
`确定删除用户“
${
record
.
phone
}
”吗?`
))
return
;
if
(
!
confirm
(
`确定删除用户“
${
record
.
phone
}
”吗?`
))
return
;
try
{
await
api
(
`/consultations/
${
record
.
id
}
`
,
{
method
:
"DELETE"
});
await
loadConsultations
();
}
catch
(
error
)
{
alert
(
error
.
message
);
}
try
{
await
api
(
`/consultations/
${
record
.
id
}
`
,
{
method
:
"DELETE"
});
await
loadConsultations
();
}
catch
(
error
)
{
alert
(
error
.
message
);
}
}));
}));
actions
.
append
(
listAction
(
"查看全部备注"
,
"ghost small"
,
()
=>
openConsultationNoteModal
(
record
,
records
,
null
)));
tableRows
.
push
({
element
:
row
,
values
});
list
.
append
(
row
);
});
});
list
.
append
(
filterableTableHead
(
"consultation-row"
,
[
"ID"
,
"姓名"
,
"手机号"
,
"归属地"
,
"分配客服"
,
"状态"
,
"未添加原因"
,
"备注"
,
"打电话次数"
,
"提交时间"
],
tableRows
));
tableRows
.
forEach
((
item
)
=>
list
.
append
(
item
.
element
));
}
}
$
(
"#refresh-consultations"
).
addEventListener
(
"click"
,
loadConsultations
);
$
(
"#refresh-consultations"
).
addEventListener
(
"click"
,
loadConsultations
);
...
@@ -257,6 +256,7 @@ function openConsultationUserModal(record = null) {
...
@@ -257,6 +256,7 @@ function openConsultationUserModal(record = null) {
:
`<option value="
${
record
?.
teacherId
||
currentUser
?.
id
||
""
}
">${record?.teacherName || currentUser?.name || "
当前客服
"}</option>`;
:
`<option value="
${
record
?.
teacherId
||
currentUser
?.
id
||
""
}
">${record?.teacherName || currentUser?.name || "
当前客服
"}</option>`;
$("
#
consultation
-
user
-
form
").reset();
$("
#
consultation
-
user
-
form
").reset();
$("
#
consultation
-
user
-
title
").textContent = record ? "
编辑用户
" : "
录入用户
";
$("
#
consultation
-
user
-
title
").textContent = record ? "
编辑用户
" : "
录入用户
";
$("
#
consultation
-
user
-
description
").textContent = record ? `用户 ID ${record.sequence || "
-
"} · 提交于 ${formatTime(record.createdAt)}` : "
一次完成用户信息、咨询状态和跟进备注录入
";
$("
#
consultation
-
user
-
id
").value = record?.id || "";
$("
#
consultation
-
user
-
id
").value = record?.id || "";
$("
#
consultation
-
user
-
name
").value = record?.name || "";
$("
#
consultation
-
user
-
name
").value = record?.name || "";
$("
#
consultation
-
user
-
phone
").value = record?.phone || "";
$("
#
consultation
-
user
-
phone
").value = record?.phone || "";
...
@@ -267,6 +267,7 @@ function openConsultationUserModal(record = null) {
...
@@ -267,6 +267,7 @@ function openConsultationUserModal(record = null) {
$("
#
consultation
-
user
-
reason
").value = record?.unaddedReason || "";
$("
#
consultation
-
user
-
reason
").value = record?.unaddedReason || "";
$("
#
consultation
-
user
-
calls
").value = record?.callCount ?? 0;
$("
#
consultation
-
user
-
calls
").value = record?.callCount ?? 0;
$("
#
consultation
-
user
-
note
").value = record?.note || "";
$("
#
consultation
-
user
-
note
").value = record?.note || "";
$("
#
consultation
-
user
-
note
-
count
").textContent = $("
#
consultation
-
user
-
note
").value.length;
$("
#
consultation
-
user
-
error
").textContent = "";
$("
#
consultation
-
user
-
error
").textContent = "";
show("
#
consultation
-
user
-
modal
");
show("
#
consultation
-
user
-
modal
");
$("
#
consultation
-
user
-
name
").focus();
$("
#
consultation
-
user
-
name
").focus();
...
@@ -278,6 +279,7 @@ $("#new-consultation").addEventListener("click", async () => {
...
@@ -278,6 +279,7 @@ $("#new-consultation").addEventListener("click", async () => {
});
});
$("
#
close
-
consultation
-
user
-
modal
").addEventListener("
click
", closeConsultationUserModal);
$("
#
close
-
consultation
-
user
-
modal
").addEventListener("
click
", closeConsultationUserModal);
$("
#
cancel
-
consultation
-
user
").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) => {
$("
#
consultation
-
user
-
form
").addEventListener("
submit
", async (event) => {
event.preventDefault();
event.preventDefault();
const id = $("
#
consultation
-
user
-
id
").value;
const id = $("
#
consultation
-
user
-
id
").value;
...
@@ -308,33 +310,41 @@ async function loadTeachers() {
...
@@ -308,33 +310,41 @@ async function loadTeachers() {
list.innerHTML = '<div class="
empty
">还没有后台账号。添加客服或运营账号后会显示在这里。</div>';
list.innerHTML = '<div class="
empty
">还没有后台账号。添加客服或运营账号后会显示在这里。</div>';
return;
return;
}
}
const head = document.createElement("
div
");
const tableRows = [];
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);
staffAccounts.forEach((teacher) => {
staffAccounts.forEach((teacher) => {
const row = document.createElement("
article
");
const row = document.createElement("
article
");
row.className = "
data
-
row
teacher
-
row
";
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.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>';
row.querySelector("
img
").src = teacher.qrCodeUrl || "
/
favicon
.
png
";
const values = [
row.querySelector("
.
teacher
-
identity
strong
").textContent = teacher.name;
teacher.name, teacher.username, teacher.role === "
operator
" ? "
运营
" : "
客服
",
row.querySelector("
.
teacher
-
identity
small
").textContent = teacher.role === "
operator
" ? "
学习资讯运营
" : teacher.title;
teacher.role === "
operator
" ? "
-
" : teacher.title || "
-
",
row.querySelector("
.
username
").textContent = teacher.username;
teacher.role === "
operator
" ? "
-
" : teacher.wechat || "
-
",
row.querySelector("
.
sort
").textContent = teacher.role === "
operator
" ? "
运营
" : "
客服
";
teacher.role === "
operator
" ? "
-
" : String(teacher.sort),
row.querySelector("
.
assigned
").textContent = teacher.role === "
operator
" ? "
-
" : `${teacher.assignedCount} 次`;
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
");
const state = row.querySelector("
.
teacher
-
state
");
state.textContent =
teacher.enabled ? "
启用
" : "
停用
"
;
state.textContent =
values[7]
;
state.className = `teacher-state ${teacher.enabled ? "
enabled
" : "
disabled
"}`;
state.className = `teacher-state ${teacher.enabled ? "
enabled
" : "
disabled
"}`;
row.querySelector("
.
row
-
actions
").append(
row.querySelector("
.
row
-
actions
").append(
listAction("
编辑
", "
ghost
small
", () => openTeacherModal(teacher)),
listAction("
编辑
", "
text
-
action
", () => openTeacherModal(teacher)),
listAction("
删除
", "
ghost
danger
small
", async () => {
listAction("
删除
", "
text
-
action
danger
", async () => {
if (!confirm(`确定删除账号“${teacher.name}”吗?历史数据仍会保留。`)) return;
if (!confirm(`确定删除账号“${teacher.name}”吗?历史数据仍会保留。`)) return;
try { await api(`/consultation-teachers/${teacher.id}`, { method: "
DELETE
" }); await loadTeachers(); }
try { await api(`/consultation-teachers/${teacher.id}`, { method: "
DELETE
" }); await loadTeachers(); }
catch (error) { alert(error.message); }
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 = ""; }
function closeTeacherModal() { hide("
#
teacher
-
modal
"); $("
#
teacher
-
form
").reset(); $("
#
teacher
-
error
").textContent = ""; }
...
...
public/admin/style.css
View file @
a1cfaaed
...
@@ -42,6 +42,7 @@ input:focus, textarea:focus, select:focus { outline: 2px solid rgba(20,115,230,.
...
@@ -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
;
}
.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
{
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-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
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-head
h1
{
margin
:
0
;
font-size
:
30px
;
}
.view-actions
{
display
:
flex
;
gap
:
9px
;
}
.view-actions
{
display
:
flex
;
gap
:
9px
;
}
...
@@ -185,12 +186,17 @@ input:focus, textarea:focus, select:focus { outline: 2px solid rgba(20,115,230,.
...
@@ -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
{
height
:
9px
;
}
.table-card
::-webkit-scrollbar-track
{
background
:
#f2f5f8
;
}
.table-card
::-webkit-scrollbar-track
{
background
:
#f2f5f8
;
}
.table-card
::-webkit-scrollbar-thumb
{
background
:
#b8c7d8
;
border
:
2px
solid
#f2f5f8
;
border-radius
:
999px
;
}
.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
{
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-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
;
}
.data-head
{
min-height
:
61px
;
padding-block
:
8px
;
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
;
}
.column-filter
{
min-width
:
0
;
display
:
grid
;
gap
:
4px
;
}
.teacher-row
{
grid-template-columns
:
1.45
fr
.85
fr
.45
fr
.65
fr
.55
fr
.9
fr
;
}
.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
,
1
fr
)
68px
102px
86px
;
}
#teacher-list
{
min-width
:
1050px
;
}
.teacher-row
{
grid-template-columns
:
.75
fr
.9
fr
.55
fr
1.35
fr
1
fr
.65
fr
.75
fr
.65
fr
86px
;
}
.data-row
small
{
display
:
block
;
margin-top
:
4px
;
color
:
var
(
--muted
);
font-size
:
11px
;
}
.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
;
}
.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
;
}
.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,.
...
@@ -198,10 +204,16 @@ input:focus, textarea:focus, select:focus { outline: 2px solid rgba(20,115,230,.
.created
{
color
:
var
(
--muted
);
}
.created
{
color
:
var
(
--muted
);
}
.sequence
,
.call-count
{
color
:
var
(
--muted
);
font-size
:
12px
;
}
.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
;
}
.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-status
{
font-weight
:
700
;
}
.consultation-row.data-head
>
:last-child
{
z-index
:
3
;
background
:
#f7f9fb
;
}
.consultation-status--unprocessed
{
color
:
#1769aa
;
}
.consultation-row
.row-actions
{
gap
:
5px
;
flex-wrap
:
nowrap
;
}
.consultation-status--added
{
color
:
#0b805b
;
}
.consultation-row
.row-actions
button
{
min-height
:
29px
;
padding-inline
:
9px
;
font-size
:
11px
;
}
.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
{
display
:
contents
;
}
.inline-follow-form
select
{
padding
:
8px
10px
;
font-size
:
13px
;
}
.inline-follow-form
select
{
padding
:
8px
10px
;
font-size
:
13px
;
}
.inline-note
{
min-width
:
0
;
display
:
grid
;
gap
:
4px
;
}
.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,.
...
@@ -210,20 +222,8 @@ input:focus, textarea:focus, select:focus { outline: 2px solid rgba(20,115,230,.
.note-toggle
:hover
{
text-decoration
:
underline
;
}
.note-toggle
:hover
{
text-decoration
:
underline
;
}
.inline-follow-actions
{
display
:
grid
;
justify-items
:
start
;
gap
:
2px
;
}
.inline-follow-actions
{
display
:
grid
;
justify-items
:
start
;
gap
:
2px
;
}
.inline-follow-actions
.save-state
{
min-height
:
14px
;
margin
:
0
;
white-space
:
nowrap
;
}
.inline-follow-actions
.save-state
{
min-height
:
14px
;
margin
:
0
;
white-space
:
nowrap
;
}
.consultation-note-card
{
width
:
min
(
680px
,
100%
);
}
.staff-name
{
overflow
:
hidden
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
font-size
:
12px
;
}
.consultation-note-form
{
padding
:
22px
;
display
:
grid
;
gap
:
18px
;
}
.staff-title
,
.staff-wechat
,
.username
{
overflow
:
hidden
;
color
:
var
(
--muted
);
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
font-size
:
12px
;
}
.note-card-summary
{
display
:
grid
;
grid-template-columns
:
1
fr
1
fr
;
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
1
fr
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
;
}
.teacher-state
{
width
:
max-content
;
padding
:
4px
9px
;
border-radius
:
999px
;
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.enabled
{
color
:
#087a56
;
background
:
#dff8ee
;
}
.teacher-state.disabled
{
color
:
#7b8190
;
background
:
#edf0f4
;
}
.teacher-state.disabled
{
color
:
#7b8190
;
background
:
#edf0f4
;
}
...
@@ -235,7 +235,14 @@ input:focus, textarea:focus, select:focus { outline: 2px solid rgba(20,115,230,.
...
@@ -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
{
width
:
8px
;
}
.teacher-form
::-webkit-scrollbar-track
{
background
:
#f3f6f9
;
}
.teacher-form
::-webkit-scrollbar-track
{
background
:
#f3f6f9
;
}
.teacher-form
::-webkit-scrollbar-thumb
{
background
:
#bdc9d6
;
border
:
2px
solid
#f3f6f9
;
border-radius
:
999px
;
}
.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
:
1
fr
1
fr
;
gap
:
15px
;
}
.form-grid
{
display
:
grid
;
grid-template-columns
:
1
fr
1
fr
;
gap
:
15px
;
}
.teacher-form
label
,
.qr-field
{
display
:
grid
;
gap
:
7px
;
color
:
var
(
--muted
);
font-size
:
13px
;
}
.teacher-form
label
,
.qr-field
{
display
:
grid
;
gap
:
7px
;
color
:
var
(
--muted
);
font-size
:
13px
;
}
.teacher-form
label
small
{
font-size
:
11px
;
}
.teacher-form
label
small
{
font-size
:
11px
;
}
...
@@ -277,9 +284,9 @@ input:focus, textarea:focus, select:focus { outline: 2px solid rgba(20,115,230,.
...
@@ -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
{
padding
:
10px
;
place-items
:
start
center
;
}
.modal-card
{
max-height
:
calc
(
100
dvh
-
20px
);
}
.modal-card
{
max-height
:
calc
(
100
dvh
-
20px
);
}
.teacher-form
{
padding
:
17px
;
}
.teacher-form
{
padding
:
17px
;
}
.consultation-row
{
grid-template-columns
:
44px
76px
108px
76px
82px
66px
112px
150px
68px
102px
214
px
;
}
.consultation-row
{
grid-template-columns
:
44px
76px
108px
76px
82px
66px
112px
minmax
(
150px
,
1
fr
)
68px
102px
86
px
;
}
.
note-card-summary
{
grid-template-columns
:
1
fr
;
}
.
teacher-row
{
grid-template-columns
:
.75
fr
.9
fr
.55
fr
1.35
fr
1
fr
.65
fr
.75
fr
.65
fr
86px
;
}
.
note-card-footer
{
grid-template-columns
:
1
fr
1
fr
;
}
.
consultation-edit-section
{
padding
:
14px
;
}
.
note-card-footer
>
p
{
grid-column
:
1
/
-1
;
grid-row
:
2
;
}
.
consultation-edit-heading
{
align-items
:
flex-start
;
flex-direction
:
column
;
gap
:
3px
;
}
.settings-panel
{
align-items
:
stretch
;
flex-direction
:
column
;
}
.settings-panel
{
align-items
:
stretch
;
flex-direction
:
column
;
}
}
}
src/pages/admin/index.astro
View file @
a1cfaaed
...
@@ -50,7 +50,7 @@
...
@@ -50,7 +50,7 @@
</header>
</header>
<main
id=
"consultations-view"
class=
"view consultation-view consultation-role"
>
<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"
>
<div
class=
"metric-grid"
>
<article><small>
未处理
</small><strong
id=
"metric-unprocessed"
>
0
</strong></article>
<article><small>
未处理
</small><strong
id=
"metric-unprocessed"
>
0
</strong></article>
<article><small>
已添加
</small><strong
id=
"metric-added"
>
0
</strong></article>
<article><small>
已添加
</small><strong
id=
"metric-added"
>
0
</strong></article>
...
@@ -61,19 +61,18 @@
...
@@ -61,19 +61,18 @@
</main>
</main>
<main
id=
"teachers-view"
class=
"view hidden admin-only"
>
<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>
<div
class=
"table-card"
><div
id=
"teacher-list"
class=
"data-list"
></div></div>
</main>
</main>
<main
id=
"list-view"
class=
"view hidden content-role"
>
<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"
/>
<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>
<p
id=
"import-status"
class=
"import-status"
aria-live=
"polite"
></p>
<div
id=
"article-list"
class=
"article-list"
></div>
<div
id=
"article-list"
class=
"article-list"
></div>
</main>
</main>
<main
id=
"settings-view"
class=
"view hidden admin-only"
>
<main
id=
"settings-view"
class=
"view hidden admin-only"
>
<div
class=
"view-head"
><div><h1>
账号设置
</h1><span>
管理后台管理员账号的登录安全
</span></div></div>
<section
class=
"settings-panel"
>
<section
class=
"settings-panel"
>
<div><strong>
登录密码
</strong><p>
修改管理员账号 admin 的登录密码。环境变量中的主密码仍可用于登录和恢复。
</p></div>
<div><strong>
登录密码
</strong><p>
修改管理员账号 admin 的登录密码。环境变量中的主密码仍可用于登录和恢复。
</p></div>
<button
id=
"change-password"
class=
"ghost"
type=
"button"
>
修改密码
</button>
<button
id=
"change-password"
class=
"ghost"
type=
"button"
>
修改密码
</button>
...
@@ -157,33 +156,29 @@
...
@@ -157,33 +156,29 @@
</div>
</div>
</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
id=
"consultation-user-modal"
class=
"modal hidden"
>
<div
class=
"modal-card consultation-user-card"
>
<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"
>
<form
id=
"consultation-user-form"
class=
"teacher-form"
>
<input
id=
"consultation-user-id"
type=
"hidden"
/>
<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"
>
<div
class=
"form-grid"
>
<label>
姓名
<input
id=
"consultation-user-name"
maxlength=
"30"
/></label>
<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-phone"
inputmode=
"numeric"
maxlength=
"11"
required
/></label>
<label>
归属地
<input
id=
"consultation-user-location"
maxlength=
"80"
/></label>
<label>
归属地
<input
id=
"consultation-user-location"
maxlength=
"80"
/></label>
<label>
分配客服
<select
id=
"consultation-user-teacher"
required
></select></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>
状态
<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-reason"
maxlength=
"200"
/></label>
<label>
打电话次数
<input
id=
"consultation-user-calls"
type=
"number"
min=
"0"
max=
"9999"
value=
"0"
required
/></label>
<label>
打电话次数
<input
id=
"consultation-user-calls"
type=
"number"
min=
"0"
max=
"9999"
value=
"0"
required
/></label>
</div>
</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>
<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>
<div
class=
"password-actions"
><button
id=
"cancel-consultation-user"
class=
"ghost"
type=
"button"
>
取消
</button><button
class=
"primary"
type=
"submit"
>
保存用户
</button></div>
</form>
</form>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment