refactor(admin): 三张手写表格改用库里的 Table,文字按钮/开关行收进库
- 分组管理 / 数据管理 / 内容卡片 / 本地服务 四处手写 <table> 全部换成 Table size='sm' - 行内操作(新建子分组 / 编辑 / 删除)改用 TextButton,删除统一 danger 色 - 侧边栏、页头共 7 处开关行改用 ToggleRow - 空状态改用 Empty - 删掉随之失效的 CSS:.group-table* / .entry-table* / .cards-table* / .group-action* / .sidebar-toggle-row / .admin-item-title 等,App.css 净减 144 行 视觉:表格外框、表头底色、行 hover 统一由 @stash/ui 提供,与接口页一致
This commit is contained in:
+23
-167
@@ -795,15 +795,7 @@
|
|||||||
gap: var(--su-space-2);
|
gap: var(--su-space-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-item-row > :first-child:not(.admin-item-title) {
|
|
||||||
grid-column: span 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-item-title {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-nav-row {
|
.admin-nav-row {
|
||||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||||
@@ -927,10 +919,6 @@
|
|||||||
padding-left: var(--su-space-6);
|
padding-left: var(--su-space-6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-nav-group-row .admin-item-title {
|
|
||||||
color: var(--su-text-sub);
|
|
||||||
font-size: var(--su-font-size-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 归类页:一行一个分区,标题 / 类型 / 图标 / 色条 / 条数 / 保存 / 删除 */
|
/* 归类页:一行一个分区,标题 / 类型 / 图标 / 色条 / 条数 / 保存 / 删除 */
|
||||||
.admin-category-row {
|
.admin-category-row {
|
||||||
@@ -977,62 +965,15 @@
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-table-wrap {
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid var(--su-card-border);
|
|
||||||
border-radius: var(--su-radius-md);
|
|
||||||
background-color: var(--su-bg-card);
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
font-size: var(--su-font-size-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-table th {
|
|
||||||
padding: var(--su-space-3) var(--su-space-4);
|
|
||||||
background-color: var(--su-bg-subtle);
|
|
||||||
border-bottom: 1px solid var(--su-card-border);
|
|
||||||
color: var(--su-text-muted);
|
|
||||||
font-size: var(--su-font-size-sm);
|
|
||||||
font-weight: 600;
|
|
||||||
text-align: left;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-table td {
|
|
||||||
padding: var(--su-space-3) var(--su-space-4);
|
|
||||||
border-bottom: 1px solid var(--su-card-border);
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-table tbody tr:last-child td {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-table tbody tr:hover {
|
|
||||||
background-color: var(--su-bg-subtle);
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-col-sort {
|
|
||||||
width: 110px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-col-count {
|
|
||||||
width: 120px;
|
|
||||||
text-align: center !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-col-status {
|
|
||||||
width: 90px;
|
|
||||||
text-align: center !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-col-actions {
|
|
||||||
width: 190px;
|
|
||||||
text-align: right !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-name-cell {
|
.group-name-cell {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1183,49 +1124,12 @@
|
|||||||
font-size: var(--su-font-size-sm);
|
font-size: var(--su-font-size-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-sort {
|
|
||||||
color: var(--su-text-muted);
|
|
||||||
font-family: var(--su-font-mono);
|
|
||||||
font-size: var(--su-font-size-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-count {
|
|
||||||
color: var(--su-text-sub);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: var(--su-space-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-action {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--su-space-1);
|
|
||||||
padding: 0;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
color: var(--su-color-primary);
|
|
||||||
font-size: var(--su-font-size-sm);
|
|
||||||
cursor: pointer;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-action:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-action-danger {
|
|
||||||
color: var(--su-color-danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-empty {
|
|
||||||
padding: var(--su-space-6) 0 !important;
|
|
||||||
color: var(--su-text-muted);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-icon-field {
|
.group-icon-field {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1251,14 +1155,7 @@
|
|||||||
gap: var(--su-space-3);
|
gap: var(--su-space-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 内容页:与分组管理一致的列表 */
|
|
||||||
.entry-table .entry-col-href {
|
|
||||||
width: 36%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry-table .entry-col-format {
|
|
||||||
width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry-format {
|
.entry-format {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -1269,9 +1166,6 @@
|
|||||||
font-size: var(--su-font-size-sm);
|
font-size: var(--su-font-size-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.entry-table .entry-col-group {
|
|
||||||
width: 180px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry-title {
|
.entry-title {
|
||||||
color: var(--su-text-main);
|
color: var(--su-text-main);
|
||||||
@@ -1303,19 +1197,7 @@ a.entry-href:hover {
|
|||||||
font-size: var(--su-font-size-sm);
|
font-size: var(--su-font-size-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 侧边栏勾选:标题在左,开关在右 */
|
|
||||||
.sidebar-toggle-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: var(--su-space-3);
|
|
||||||
padding: var(--su-space-1) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-toggle-row .admin-item-title {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 首页:左右栏共用一个滚动区,左侧吸顶交给 CSS sticky */
|
/* 首页:左右栏共用一个滚动区,左侧吸顶交给 CSS sticky */
|
||||||
#page-scroll {
|
#page-scroll {
|
||||||
@@ -1500,44 +1382,12 @@ a.entry-href:hover {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cards-table-head {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--su-space-2);
|
|
||||||
margin-bottom: var(--su-space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cards-table-wrap {
|
|
||||||
margin-top: var(--su-space-2);
|
|
||||||
/* 列多了以后表格会比面板宽,横向滚动而不是把名字挤成两三行 */
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cards-table th,
|
|
||||||
.cards-table td {
|
|
||||||
padding: var(--su-space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cards-col-limit {
|
|
||||||
width: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 分组名不换行,超长省略;列宽固定,保证五列都落在面板内 */
|
|
||||||
.cards-table {
|
|
||||||
table-layout: fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cards-table td:first-child .group-name {
|
|
||||||
display: block;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cards-table th,
|
|
||||||
.cards-table td {
|
|
||||||
padding: var(--su-space-2) var(--su-space-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 首页 Tab:侧边栏设置 + 首页卡片上下两段 */
|
/* 首页 Tab:侧边栏设置 + 首页卡片上下两段 */
|
||||||
.admin-home {
|
.admin-home {
|
||||||
@@ -1552,14 +1402,7 @@ a.entry-href:hover {
|
|||||||
gap: var(--su-space-3);
|
gap: var(--su-space-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 首页卡片:分组展示顺序(表格里的「顺序」列,上下箭头即时保存) */
|
|
||||||
.cards-col-state {
|
|
||||||
width: 72px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cards-col-order {
|
|
||||||
width: 112px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-order-cell {
|
.card-order-cell {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1682,17 +1525,7 @@ a.entry-href:hover {
|
|||||||
border-bottom: 1px solid var(--su-border);
|
border-bottom: 1px solid var(--su-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 删除按钮:可删的用危险色,不可删的用弱化色(都取主题令牌) */
|
|
||||||
.admin-item-actions .admin-remove,
|
|
||||||
.cards-table-head .admin-remove {
|
|
||||||
color: var(--su-color-danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-item-actions .admin-remove:not(:disabled):hover,
|
|
||||||
.cards-table-head .admin-remove:not(:disabled):hover {
|
|
||||||
color: var(--su-color-danger);
|
|
||||||
background-color: color-mix(in srgb, var(--su-color-danger) 10%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-item-actions .admin-remove:disabled {
|
.admin-item-actions .admin-remove:disabled {
|
||||||
color: var(--su-text-muted);
|
color: var(--su-text-muted);
|
||||||
@@ -1700,3 +1533,26 @@ a.entry-href:hover {
|
|||||||
opacity: 0.45;
|
opacity: 0.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 后台表格里的操作列与卡片标题栏(其余表格样式已交给 @stash/ui 的 Table) */
|
||||||
|
.group-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: var(--su-space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards-table-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--su-space-2);
|
||||||
|
margin-bottom: var(--su-space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards-table-head .admin-remove {
|
||||||
|
color: var(--su-color-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards-table-head .admin-remove:not(:disabled):hover {
|
||||||
|
color: var(--su-color-danger);
|
||||||
|
background-color: color-mix(in srgb, var(--su-color-danger) 10%, transparent);
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
Brand,
|
Brand,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
|
Empty,
|
||||||
FormField,
|
FormField,
|
||||||
Icon,
|
Icon,
|
||||||
ICON_NAMES,
|
ICON_NAMES,
|
||||||
@@ -14,11 +15,14 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
Space,
|
Space,
|
||||||
Switch,
|
Switch,
|
||||||
|
Table,
|
||||||
Tabs,
|
Tabs,
|
||||||
Tag,
|
Tag,
|
||||||
Text,
|
Text,
|
||||||
Textarea,
|
Textarea,
|
||||||
|
TextButton,
|
||||||
Title,
|
Title,
|
||||||
|
ToggleRow,
|
||||||
Toaster,
|
Toaster,
|
||||||
toast,
|
toast,
|
||||||
type IconName,
|
type IconName,
|
||||||
@@ -578,92 +582,75 @@ function GroupsPanel({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="group-table-wrap">
|
<Table
|
||||||
<table className="group-table">
|
size="sm"
|
||||||
<thead>
|
rowKey="key"
|
||||||
<tr>
|
columns={[
|
||||||
<th>分组名称</th>
|
{ key: 'name', title: '分组名称' },
|
||||||
<th className="group-col-sort">排序权重</th>
|
{ key: 'sort', title: '排序权重', width: 110 },
|
||||||
<th className="group-col-count">关联网页数</th>
|
{ key: 'count', title: '关联网页数', width: 120 },
|
||||||
<th className="group-col-status">状态</th>
|
{ key: 'status', title: '状态', width: 90 },
|
||||||
<th className="group-col-actions">操作</th>
|
{ key: 'actions', title: '操作', width: 260, align: 'right' },
|
||||||
</tr>
|
]}
|
||||||
</thead>
|
rows={rows.map(({ node, level }) => {
|
||||||
<tbody>
|
const hasChildren = node.children.length > 0
|
||||||
{rows.map(({ node, level }) => {
|
const open = !collapsed.has(node.key)
|
||||||
const hasChildren = node.children.length > 0
|
return {
|
||||||
const open = !collapsed.has(node.key)
|
key: node.key,
|
||||||
return (
|
name: (
|
||||||
<tr key={node.key}>
|
<div className="group-name-cell" style={{ paddingLeft: level * 20 }}>
|
||||||
<td>
|
{hasChildren ? (
|
||||||
<div className="group-name-cell" style={{ paddingLeft: level * 20 }}>
|
<button
|
||||||
{hasChildren ? (
|
type="button"
|
||||||
<button
|
className={`group-caret${open ? ' group-caret-open' : ''}`}
|
||||||
type="button"
|
aria-label={open ? `折叠 ${node.title}` : `展开 ${node.title}`}
|
||||||
className={`group-caret${open ? ' group-caret-open' : ''}`}
|
aria-expanded={open}
|
||||||
aria-label={open ? `折叠 ${node.title}` : `展开 ${node.title}`}
|
onClick={() => toggle(node.key)}
|
||||||
aria-expanded={open}
|
/>
|
||||||
onClick={() => toggle(node.key)}
|
) : (
|
||||||
/>
|
<span className="group-caret group-caret-empty" aria-hidden="true" />
|
||||||
) : (
|
)}
|
||||||
<span className="group-caret group-caret-empty" aria-hidden="true" />
|
<GroupMark icon={node.icon} />
|
||||||
)}
|
<span className="group-name">{node.title}</span>
|
||||||
<GroupMark icon={node.icon} />
|
</div>
|
||||||
<span className="group-name">{node.title}</span>
|
),
|
||||||
</div>
|
sort: node.sort,
|
||||||
</td>
|
count: countGroupEntries(node),
|
||||||
<td className="group-sort">{node.sort}</td>
|
status: (
|
||||||
<td className="group-count">{countGroupEntries(node)}</td>
|
<Switch
|
||||||
<td>
|
checked={node.enabled !== false}
|
||||||
<Switch
|
onChange={(next) => void toggleEnabled(node, next)}
|
||||||
checked={node.enabled !== false}
|
label={`启用 ${node.title}`}
|
||||||
onChange={(next) => void toggleEnabled(node, next)}
|
size="sm"
|
||||||
label={`启用 ${node.title}`}
|
/>
|
||||||
size="sm"
|
),
|
||||||
/>
|
actions: (
|
||||||
</td>
|
<div className="group-actions">
|
||||||
<td>
|
<TextButton
|
||||||
<div className="group-actions">
|
icon={<Icon name="add_line" />}
|
||||||
<button
|
onClick={() => setDialog({ mode: 'child', parent: node })}
|
||||||
type="button"
|
>
|
||||||
className="group-action"
|
新建子分组
|
||||||
onClick={() => setDialog({ mode: 'child', parent: node })}
|
</TextButton>
|
||||||
>
|
<TextButton
|
||||||
<Icon name="add_line" />
|
icon={<Icon name="edit_4_line" />}
|
||||||
新建子分组
|
onClick={() => setDialog({ mode: 'edit', group: node })}
|
||||||
</button>
|
>
|
||||||
<button
|
编辑
|
||||||
type="button"
|
</TextButton>
|
||||||
className="group-action"
|
<TextButton
|
||||||
onClick={() => setDialog({ mode: 'edit', group: node })}
|
tone="danger"
|
||||||
>
|
icon={<Icon name="delete_2_line" />}
|
||||||
<Icon name="edit_4_line" />
|
onClick={() => onDelete(node.key, node.title)}
|
||||||
编辑
|
>
|
||||||
</button>
|
删除
|
||||||
<button
|
</TextButton>
|
||||||
type="button"
|
</div>
|
||||||
className="group-action group-action-danger"
|
),
|
||||||
onClick={() => onDelete(node.key, node.title)}
|
}
|
||||||
>
|
})}
|
||||||
<Icon name="delete_2_line" />
|
emptyText={<Empty text="暂无分组数据,点击上方「新建根分组」" />}
|
||||||
删除
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
|
|
||||||
{rows.length === 0 && (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={5} className="group-empty">
|
|
||||||
暂无分组数据,点击左侧「新建根分组」
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<GroupDialog
|
<GroupDialog
|
||||||
state={dialog}
|
state={dialog}
|
||||||
@@ -1019,75 +1006,54 @@ function ContentPanel({ groups, onReload }: { groups: GroupNode[]; onReload: ()
|
|||||||
{group?.title ?? ''} 下共 {rows.length} 条(含子分组)
|
{group?.title ?? ''} 下共 {rows.length} 条(含子分组)
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<div className="group-table-wrap">
|
<Table
|
||||||
<table className="group-table entry-table">
|
size="sm"
|
||||||
<thead>
|
rowKey="key"
|
||||||
<tr>
|
columns={[
|
||||||
<th>标题</th>
|
{ key: 'title', title: '标题' },
|
||||||
<th className="entry-col-format">格式</th>
|
{ key: 'format', title: '格式', width: 80 },
|
||||||
<th className="entry-col-href">跳转链接</th>
|
{ key: 'href', title: '跳转链接', width: '36%' },
|
||||||
<th className="entry-col-group">所属分组</th>
|
{ key: 'group', title: '所属分组', width: 180 },
|
||||||
<th className="group-col-actions">操作</th>
|
{ key: 'actions', title: '操作', width: 160, align: 'right' },
|
||||||
</tr>
|
]}
|
||||||
</thead>
|
rows={rows.map(({ entry, group: owner }) => ({
|
||||||
<tbody>
|
key: entry.id,
|
||||||
{rows.map(({ entry, group: owner }) => (
|
title: <span className="entry-title">{entry.name ?? entry.title ?? entry.id}</span>,
|
||||||
<tr key={entry.id}>
|
format: (
|
||||||
<td>
|
<span className="entry-format">
|
||||||
<span className="entry-title">{entry.name ?? entry.title ?? entry.id}</span>
|
{FORMAT_LABEL[(entry.format ?? 'apps') as GroupFormat]}
|
||||||
</td>
|
</span>
|
||||||
<td>
|
),
|
||||||
<span className="entry-format">
|
href: <EntryLink href={entry.href} />,
|
||||||
{FORMAT_LABEL[(entry.format ?? 'apps') as GroupFormat]}
|
group: <span className="entry-group">{owner.title}</span>,
|
||||||
</span>
|
actions: (
|
||||||
</td>
|
<div className="group-actions">
|
||||||
<td>
|
<TextButton
|
||||||
<EntryLink href={entry.href} />
|
icon={<Icon name="edit_4_line" />}
|
||||||
</td>
|
onClick={() => setDialog({ key: entry.id, groupKey: owner.key })}
|
||||||
<td>
|
>
|
||||||
<span className="entry-group">{owner.title}</span>
|
编辑
|
||||||
</td>
|
</TextButton>
|
||||||
<td>
|
<TextButton
|
||||||
<div className="group-actions">
|
tone="danger"
|
||||||
<button
|
icon={<Icon name="delete_2_line" />}
|
||||||
type="button"
|
onClick={async () => {
|
||||||
className="group-action"
|
const result = await deleteItem(entry.id)
|
||||||
onClick={() => setDialog({ key: entry.id, groupKey: owner.key })}
|
if (!result.ok) {
|
||||||
>
|
toast.error(result.error)
|
||||||
<Icon name="edit_4_line" />
|
return
|
||||||
编辑
|
}
|
||||||
</button>
|
toast.success('条目已删除')
|
||||||
<button
|
onReload()
|
||||||
type="button"
|
}}
|
||||||
className="group-action group-action-danger"
|
>
|
||||||
onClick={async () => {
|
删除
|
||||||
const result = await deleteItem(entry.id)
|
</TextButton>
|
||||||
if (!result.ok) {
|
</div>
|
||||||
toast.error(result.error)
|
),
|
||||||
return
|
}))}
|
||||||
}
|
emptyText={<Empty text="该分组下还没有内容" />}
|
||||||
toast.success('条目已删除')
|
/>
|
||||||
onReload()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon name="delete_2_line" />
|
|
||||||
删除
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{rows.length === 0 && (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={5} className="group-empty">
|
|
||||||
该分组下还没有内容
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<EntryDialog
|
<EntryDialog
|
||||||
open={dialog !== null}
|
open={dialog !== null}
|
||||||
@@ -1371,33 +1337,27 @@ function SidebarPanel({
|
|||||||
<Text size="sm" strong>
|
<Text size="sm" strong>
|
||||||
侧栏卡片
|
侧栏卡片
|
||||||
</Text>
|
</Text>
|
||||||
<div className="sidebar-toggle-row">
|
<ToggleRow
|
||||||
<Text className="admin-item-title">本地管理员</Text>
|
label="本地管理员"
|
||||||
<Switch
|
checked={value.show_profile !== false}
|
||||||
checked={value.show_profile !== false}
|
onChange={(visible) => onToggleCard('profile', visible)}
|
||||||
onChange={(visible) => onToggleCard('profile', visible)}
|
switchLabel="显示本地管理员卡片"
|
||||||
label="显示本地管理员卡片"
|
disabled={saving}
|
||||||
disabled={saving}
|
/>
|
||||||
/>
|
<ToggleRow
|
||||||
</div>
|
label="本地服务"
|
||||||
<div className="sidebar-toggle-row">
|
checked={value.show_service !== false}
|
||||||
<Text className="admin-item-title">本地服务</Text>
|
onChange={(visible) => onToggleCard('service', visible)}
|
||||||
<Switch
|
switchLabel="显示本地服务卡片"
|
||||||
checked={value.show_service !== false}
|
disabled={saving}
|
||||||
onChange={(visible) => onToggleCard('service', visible)}
|
/>
|
||||||
label="显示本地服务卡片"
|
<ToggleRow
|
||||||
disabled={saving}
|
label="分组导航"
|
||||||
/>
|
checked={value.show_nav !== false}
|
||||||
</div>
|
onChange={(visible) => onToggleCard('nav', visible)}
|
||||||
<div className="sidebar-toggle-row">
|
switchLabel="显示分组导航卡片"
|
||||||
<Text className="admin-item-title">分组导航</Text>
|
disabled={saving}
|
||||||
<Switch
|
/>
|
||||||
checked={value.show_nav !== false}
|
|
||||||
onChange={(visible) => onToggleCard('nav', visible)}
|
|
||||||
label="显示分组导航卡片"
|
|
||||||
disabled={saving}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Space>
|
</Space>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1634,42 +1594,23 @@ function LocalServiceTable({ projects }: { projects: Project[] }) {
|
|||||||
本地服务卡片由托管目录自动生成,随项目扫描更新;后台只能查看,不能删除或改名。
|
本地服务卡片由托管目录自动生成,随项目扫描更新;后台只能查看,不能删除或改名。
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<div className="group-table-wrap cards-table-wrap">
|
<Table
|
||||||
<table className="group-table cards-table">
|
size="sm"
|
||||||
<thead>
|
rowKey="name"
|
||||||
<tr>
|
columns={[
|
||||||
<th>项目名称</th>
|
{ key: 'name', title: '项目名称' },
|
||||||
<th className="entry-col-href">访问地址</th>
|
{ key: 'href', title: '访问地址', width: '50%' },
|
||||||
</tr>
|
]}
|
||||||
</thead>
|
rows={projects.map((project) => ({
|
||||||
<tbody>
|
name: <span className="group-name">{project.name}</span>,
|
||||||
{projects.map((project) => (
|
href: (
|
||||||
<tr key={project.name}>
|
<a className="entry-href" href={project.route_path} target="_blank" rel="noreferrer">
|
||||||
<td>
|
{project.route_path}
|
||||||
<span className="group-name">{project.name}</span>
|
</a>
|
||||||
</td>
|
),
|
||||||
<td>
|
}))}
|
||||||
<a
|
emptyText={<Empty text="还没有托管项目" />}
|
||||||
className="entry-href"
|
/>
|
||||||
href={project.route_path}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
{project.route_path}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
{projects.length === 0 && (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={2} className="group-empty">
|
|
||||||
还没有托管项目
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1795,88 +1736,80 @@ function CardTable({
|
|||||||
说明:开关和条数修改后即时保存;条数留空使用默认。
|
说明:开关和条数修改后即时保存;条数留空使用默认。
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<div className="group-table-wrap cards-table-wrap">
|
<Table
|
||||||
<table className="group-table cards-table">
|
size="sm"
|
||||||
<thead>
|
rowKey="key"
|
||||||
<tr>
|
columns={[
|
||||||
<th>分组名称</th>
|
{ key: 'name', title: '分组名称' },
|
||||||
<th className="cards-col-state">状态</th>
|
{ key: 'state', title: '状态', width: 90 },
|
||||||
<th className="cards-col-limit">预览条数</th>
|
{ key: 'limit', title: '预览条数', width: 110 },
|
||||||
<th className="cards-col-order">顺序</th>
|
{ key: 'order', title: '顺序', width: 120 },
|
||||||
<th className="group-col-status">展示</th>
|
{ key: 'show', title: '展示', width: 90 },
|
||||||
</tr>
|
]}
|
||||||
</thead>
|
rows={options.map((option) => {
|
||||||
<tbody>
|
const checked = option.key in selected
|
||||||
{options.map((option) => {
|
return {
|
||||||
const checked = option.key in selected
|
key: option.key,
|
||||||
return (
|
name: (
|
||||||
<tr key={option.key}>
|
<div style={{ paddingLeft: option.depth * 18 }}>
|
||||||
<td>
|
<span className="group-name">{option.title}</span>
|
||||||
<div style={{ paddingLeft: option.depth * 18 }}>
|
</div>
|
||||||
<span className="group-name">{option.title}</span>
|
),
|
||||||
</div>
|
state: (
|
||||||
</td>
|
<span className={option.enabled ? 'card-state card-state-on' : 'card-state card-state-off'}>
|
||||||
<td>
|
{option.enabled ? '启用' : '已停用'}
|
||||||
<span className={option.enabled ? 'card-state card-state-on' : 'card-state card-state-off'}>
|
</span>
|
||||||
{option.enabled ? '启用' : '已停用'}
|
),
|
||||||
</span>
|
limit: checked ? (
|
||||||
</td>
|
<Input
|
||||||
<td>
|
type="number"
|
||||||
{checked ? (
|
value={selected[option.key]}
|
||||||
<Input
|
placeholder="默认"
|
||||||
type="number"
|
aria-label={`${option.title} 条数`}
|
||||||
value={selected[option.key]}
|
onChange={(event) =>
|
||||||
placeholder="默认"
|
setSelected((current) => ({
|
||||||
aria-label={`${option.title} 条数`}
|
...current,
|
||||||
onChange={(event) =>
|
[option.key]: event.target.value,
|
||||||
setSelected((current) => ({
|
}))
|
||||||
...current,
|
}
|
||||||
[option.key]: event.target.value,
|
onBlur={() => void persistGroups(selected, order)}
|
||||||
}))
|
/>
|
||||||
}
|
) : (
|
||||||
onBlur={() => void persistGroups(selected, order)}
|
<span className="entry-href entry-href-empty">—</span>
|
||||||
/>
|
),
|
||||||
) : (
|
order: checked ? (
|
||||||
<span className="entry-href entry-href-empty">—</span>
|
<div className="card-order-cell">
|
||||||
)}
|
<span className="card-order-index">{order.indexOf(option.key) + 1}</span>
|
||||||
</td>
|
<IconButton
|
||||||
<td>
|
label={`${option.title} 上移`}
|
||||||
{checked ? (
|
icon={<Icon name="arrow_up_line" />}
|
||||||
<div className="card-order-cell">
|
size="xs"
|
||||||
<span className="card-order-index">{order.indexOf(option.key) + 1}</span>
|
disabled={busy || order.indexOf(option.key) === 0}
|
||||||
<IconButton
|
onClick={() => moveGroup(option.key, -1)}
|
||||||
label={`${option.title} 上移`}
|
/>
|
||||||
icon={<Icon name="arrow_up_line" />}
|
<IconButton
|
||||||
size="xs"
|
label={`${option.title} 下移`}
|
||||||
disabled={busy || order.indexOf(option.key) === 0}
|
icon={<Icon name="arrow_down_line" />}
|
||||||
onClick={() => moveGroup(option.key, -1)}
|
size="xs"
|
||||||
/>
|
disabled={busy || order.indexOf(option.key) === order.length - 1}
|
||||||
<IconButton
|
onClick={() => moveGroup(option.key, 1)}
|
||||||
label={`${option.title} 下移`}
|
/>
|
||||||
icon={<Icon name="arrow_down_line" />}
|
</div>
|
||||||
size="xs"
|
) : (
|
||||||
disabled={busy || order.indexOf(option.key) === order.length - 1}
|
<span className="entry-href entry-href-empty">—</span>
|
||||||
onClick={() => moveGroup(option.key, 1)}
|
),
|
||||||
/>
|
show: (
|
||||||
</div>
|
<Switch
|
||||||
) : (
|
checked={checked}
|
||||||
<span className="entry-href entry-href-empty">—</span>
|
onChange={(next) => toggle(option.key, next)}
|
||||||
)}
|
label={`卡片包含 ${option.title}`}
|
||||||
</td>
|
disabled={busy || !option.enabled}
|
||||||
<td>
|
/>
|
||||||
<Switch
|
),
|
||||||
checked={checked}
|
}
|
||||||
onChange={(next) => toggle(option.key, next)}
|
})}
|
||||||
label={`卡片包含 ${option.title}`}
|
emptyText={<Empty text="还没有可选的分组" />}
|
||||||
disabled={busy || !option.enabled}
|
/>
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1993,59 +1926,48 @@ function HeaderPanel({
|
|||||||
<Text size="sm" tone="muted">
|
<Text size="sm" tone="muted">
|
||||||
说明:这里控制页头右侧显示哪些图标按钮;关掉的按钮不再出现,也不影响其它页面。
|
说明:这里控制页头右侧显示哪些图标按钮;关掉的按钮不再出现,也不影响其它页面。
|
||||||
</Text>
|
</Text>
|
||||||
<div className="sidebar-toggle-row">
|
<ToggleRow
|
||||||
<Text>明暗切换</Text>
|
label="明暗切换"
|
||||||
<Switch
|
checked={site.actions.theme}
|
||||||
checked={site.actions.theme}
|
onChange={(next) =>
|
||||||
onChange={(next) =>
|
onChangeSite({ ...site, actions: { ...site.actions, theme: next } })
|
||||||
onChangeSite({ ...site, actions: { ...site.actions, theme: next } })
|
}
|
||||||
}
|
switchLabel="页头显示明暗切换按钮"
|
||||||
label="页头显示明暗切换按钮"
|
/>
|
||||||
/>
|
<ToggleRow
|
||||||
</div>
|
label="刷新"
|
||||||
<div className="sidebar-toggle-row">
|
checked={site.actions.refresh}
|
||||||
<Text>刷新</Text>
|
onChange={(next) =>
|
||||||
<Switch
|
onChangeSite({ ...site, actions: { ...site.actions, refresh: next } })
|
||||||
checked={site.actions.refresh}
|
}
|
||||||
onChange={(next) =>
|
switchLabel="页头显示刷新按钮"
|
||||||
onChangeSite({ ...site, actions: { ...site.actions, refresh: next } })
|
/>
|
||||||
}
|
<ToggleRow
|
||||||
label="页头显示刷新按钮"
|
label="站点设置"
|
||||||
/>
|
checked={site.actions.settings}
|
||||||
</div>
|
onChange={(next) =>
|
||||||
<div className="sidebar-toggle-row">
|
onChangeSite({ ...site, actions: { ...site.actions, settings: next } })
|
||||||
<Text>站点设置</Text>
|
}
|
||||||
<Switch
|
switchLabel="页头显示站点设置按钮"
|
||||||
checked={site.actions.settings}
|
/>
|
||||||
onChange={(next) =>
|
<ToggleRow
|
||||||
onChangeSite({ ...site, actions: { ...site.actions, settings: next } })
|
label="登录 / 退出"
|
||||||
}
|
checked={site.actions.session}
|
||||||
label="页头显示站点设置按钮"
|
onChange={(next) =>
|
||||||
/>
|
onChangeSite({ ...site, actions: { ...site.actions, session: next } })
|
||||||
</div>
|
}
|
||||||
<div className="sidebar-toggle-row">
|
switchLabel="页头显示登录或退出按钮"
|
||||||
<Text>登录 / 退出</Text>
|
/>
|
||||||
<Switch
|
|
||||||
checked={site.actions.session}
|
|
||||||
onChange={(next) =>
|
|
||||||
onChangeSite({ ...site, actions: { ...site.actions, session: next } })
|
|
||||||
}
|
|
||||||
label="页头显示登录或退出按钮"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Space>
|
</Space>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tab === 'ticker' && (
|
{tab === 'ticker' && (
|
||||||
<Space direction="column" gap="md" align="stretch">
|
<Space direction="column" gap="md" align="stretch">
|
||||||
<div className="sidebar-toggle-row">
|
<ToggleRow
|
||||||
<Text>显示页头滚动信息</Text>
|
label="显示页头滚动信息"
|
||||||
<Switch
|
checked={value.enabled}
|
||||||
checked={value.enabled}
|
onChange={(enabled) => onChange({ ...value, enabled })}
|
||||||
onChange={(enabled) => onChange({ ...value, enabled })}
|
/>
|
||||||
label="显示页头滚动信息"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<FormField label="滚动速度" htmlFor="header-speed" hint="秒数越大越慢,范围 8–120">
|
<FormField label="滚动速度" htmlFor="header-speed" hint="秒数越大越慢,范围 8–120">
|
||||||
<Input
|
<Input
|
||||||
id="header-speed"
|
id="header-speed"
|
||||||
|
|||||||
Reference in New Issue
Block a user