Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in / Register
Toggle navigation
S
sumeiqiao
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
sumeiqiao
Commits
4baa296e
Commit
4baa296e
authored
Jul 23, 2026
by
xuchentao
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add article list publishing actions
parent
c5cd0ab3
Pipeline
#406
canceled with stages
Changes
2
Pipelines
1
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
95 additions
and
3 deletions
+95
-3
app.js
public/admin/app.js
+88
-2
style.css
public/admin/style.css
+7
-1
No files found.
public/admin/app.js
View file @
4baa296e
...
...
@@ -71,16 +71,97 @@ async function loadArticles() {
for
(
const
article
of
articles
)
{
const
card
=
document
.
createElement
(
"article"
);
card
.
className
=
"article-row"
;
card
.
innerHTML
=
'<div class="article-summary"><h2></h2><p><span class="category"></span><span class="updated"></span><span class="status"></span></p></div><
button class="ghost small edit-button">编辑</button
>'
;
card
.
innerHTML
=
'<div class="article-summary"><h2></h2><p><span class="category"></span><span class="updated"></span><span class="status"></span></p></div><
div class="row-actions"></div
>'
;
card
.
querySelector
(
"h2"
).
textContent
=
article
.
title
;
card
.
querySelector
(
".category"
).
textContent
=
article
.
category
;
card
.
querySelector
(
".updated"
).
textContent
=
`更新于
${
article
.
updated
}
`
;
applyStatus
(
card
.
querySelector
(
".status"
),
article
.
publishStatus
);
card
.
querySelector
(
".edit-button"
).
onclick
=
()
=>
openEditor
(
article
.
slug
);
const
actions
=
card
.
querySelector
(
".row-actions"
);
const
edit
=
listAction
(
"编辑"
,
"ghost small"
,
()
=>
openEditor
(
article
.
slug
));
const
publish
=
listAction
(
"发布"
,
"primary small"
,
()
=>
publishFromList
(
article
));
publish
.
disabled
=
article
.
publishStatus
===
"live"
;
publish
.
title
=
publish
.
disabled
?
"文章当前已上线"
:
"发布到官网"
;
actions
.
append
(
edit
,
publish
,
listMoreActions
(
article
));
list
.
append
(
card
);
}
}
function
listAction
(
label
,
className
,
action
)
{
const
button
=
document
.
createElement
(
"button"
);
button
.
type
=
"button"
;
button
.
className
=
className
;
button
.
textContent
=
label
;
button
.
onclick
=
action
;
return
button
;
}
function
listMenuAction
(
label
,
className
,
action
)
{
return
listAction
(
label
,
`menu-action
${
className
}
`
.
trim
(),
async
(
event
)
=>
{
event
.
currentTarget
.
closest
(
"details"
).
open
=
false
;
await
action
();
});
}
function
listMoreActions
(
article
)
{
const
details
=
document
.
createElement
(
"details"
);
details
.
className
=
"more-actions row-more-actions"
;
const
summary
=
document
.
createElement
(
"summary"
);
summary
.
textContent
=
"更多"
;
summary
.
setAttribute
(
"aria-label"
,
`
${
article
.
title
}
的更多操作`
);
const
menu
=
document
.
createElement
(
"div"
);
menu
.
className
=
"action-menu"
;
if
(
article
.
publishStatus
===
"edited-draft"
)
{
menu
.
append
(
listMenuAction
(
"放弃未发布修改"
,
""
,
()
=>
discardFromList
(
article
)));
}
if
([
"live"
,
"edited-draft"
].
includes
(
article
.
publishStatus
))
{
menu
.
append
(
listMenuAction
(
"下架文章"
,
"danger"
,
()
=>
unpublishFromList
(
article
)));
}
menu
.
append
(
listMenuAction
(
"删除文章"
,
"danger"
,
()
=>
deleteFromList
(
article
)));
details
.
append
(
summary
,
menu
);
details
.
addEventListener
(
"toggle"
,
()
=>
{
if
(
!
details
.
open
)
return
;
document
.
querySelectorAll
(
".row-more-actions[open]"
).
forEach
((
item
)
=>
{
if
(
item
!==
details
)
item
.
open
=
false
;
});
});
return
details
;
}
async
function
publishFromList
(
article
)
{
try
{
await
triggerBuild
(
`/articles/
${
article
.
slug
}
/publish`
);
await
loadArticles
();
}
catch
{}
}
async
function
discardFromList
(
article
)
{
if
(
!
confirm
(
`确定放弃《
${
article
.
title
}
》未发布的修改,恢复为当前线上版本吗?`
))
return
;
try
{
await
api
(
`/articles/
${
article
.
slug
}
/draft`
,
{
method
:
"DELETE"
});
await
loadArticles
();
}
catch
(
error
)
{
alert
(
error
.
message
);
}
}
async
function
unpublishFromList
(
article
)
{
const
detail
=
article
.
publishStatus
===
"edited-draft"
?
"未发布的修改会继续保留为草稿。"
:
"文章内容会继续保留为草稿。"
;
if
(
!
confirm
(
`确定将《
${
article
.
title
}
》从官网下架吗?
${
detail
}
`
))
return
;
try
{
await
triggerBuild
(
`/articles/
${
article
.
slug
}
/unpublish`
,
{
action
:
"下架"
});
await
loadArticles
();
}
catch
{}
}
async
function
deleteFromList
(
article
)
{
const
prompt
=
article
.
publishStatus
===
"new-draft"
?
`确定彻底删除草稿《
${
article
.
title
}
》吗?删除后无法恢复。`
:
`确定彻底删除《
${
article
.
title
}
》吗?线上版本和草稿都会删除,且无法恢复。`
;
if
(
!
confirm
(
prompt
))
return
;
try
{
await
triggerBuild
(
`/articles/
${
article
.
slug
}
`
,
{
action
:
"删除"
,
method
:
"DELETE"
});
await
loadArticles
();
}
catch
{}
}
function
fillCategories
(
selected
)
{
const
current
=
selected
||
$
(
"#category"
).
value
||
categories
[
0
]
||
""
;
const
options
=
$
(
"#category-options"
);
...
...
@@ -130,6 +211,11 @@ function toggleCategoryMenu() {
$
(
"#category-trigger"
).
addEventListener
(
"click"
,
(
event
)
=>
{
event
.
stopPropagation
();
toggleCategoryMenu
();
});
$
(
"#category-menu"
).
addEventListener
(
"click"
,
(
event
)
=>
event
.
stopPropagation
());
document
.
addEventListener
(
"click"
,
closeCategoryMenu
);
document
.
addEventListener
(
"click"
,
(
event
)
=>
{
document
.
querySelectorAll
(
".row-more-actions[open]"
).
forEach
((
details
)
=>
{
if
(
!
details
.
contains
(
event
.
target
))
details
.
open
=
false
;
});
});
document
.
addEventListener
(
"keydown"
,
(
event
)
=>
{
if
(
event
.
key
===
"Escape"
)
closeCategoryMenu
();
});
async
function
createCategory
()
{
...
...
public/admin/style.css
View file @
4baa296e
...
...
@@ -44,6 +44,7 @@ input:focus, textarea:focus, select:focus { outline: 2px solid rgba(22,174,156,.
.view-actions
{
display
:
flex
;
gap
:
9px
;
}
.article-list
{
display
:
grid
;
gap
:
10px
;
}
.article-row
{
padding
:
18px
20px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
gap
:
20px
;
background
:
white
;
border
:
1px
solid
var
(
--border
);
border-radius
:
14px
;
}
.article-summary
{
min-width
:
0
;
flex
:
1
;
}
.article-row
h2
{
margin
:
0
0
8px
;
font-size
:
17px
;
}
.article-row
p
{
margin
:
0
;
display
:
flex
;
flex-wrap
:
wrap
;
gap
:
8px
13px
;
align-items
:
center
;
color
:
var
(
--muted
);
font-size
:
12px
;
}
.article-row
code
{
color
:
#8d9995
;
}
...
...
@@ -51,7 +52,10 @@ input:focus, textarea:focus, select:focus { outline: 2px solid rgba(22,174,156,.
.status.live
{
color
:
#188367
;
background
:
#e2f6ef
;
}
.status.new-draft
{
color
:
#916300
;
background
:
#fff2c7
;
}
.status.edited-draft
{
color
:
#9c4c22
;
background
:
#ffeadc
;
}
.row-actions
{
display
:
flex
;
gap
:
8px
;
}
.row-actions
{
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
flex
:
0
0
auto
;
}
.row-actions
button
:disabled
{
cursor
:
default
;
opacity
:
.48
;
transform
:
none
;
}
.row-more-actions
summary
{
min-height
:
34px
;
padding-inline
:
13px
;
font-size
:
12px
;
}
.row-more-actions
.action-menu
{
top
:
calc
(
100%
+
8px
);
bottom
:
auto
;
z-index
:
12
;
}
.empty
{
padding
:
70px
;
color
:
var
(
--muted
);
text-align
:
center
;
background
:
white
;
border
:
1px
dashed
var
(
--border
);
border-radius
:
14px
;
}
#edit-form
{
display
:
grid
;
gap
:
18px
;
}
.field-panel
,
.editor-panel
{
padding
:
24px
;
background
:
white
;
border
:
1px
solid
var
(
--border
);
border-radius
:
16px
;
}
...
...
@@ -148,6 +152,8 @@ input:focus, textarea:focus, select:focus { outline: 2px solid rgba(22,174,156,.
.field-row
,
.editor-grid
{
grid-template-columns
:
1
fr
;
}
.view
{
width
:
min
(
100%
-
28px
,
1120px
);
padding-top
:
28px
;
}
.row-actions
{
width
:
100%
;
}
.row-actions
>
button
,
.row-more-actions
{
flex
:
1
;
}
.row-more-actions
summary
{
width
:
100%
;
justify-content
:
center
;
}
.edit-actions
{
flex-wrap
:
wrap
;
}
#message
{
width
:
100%
;
margin
:
4px
0
0
;
}
.login-card
{
padding
:
30px
24px
;
}
...
...
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