优化开放 API 文档界面
This commit is contained in:
@@ -15,3 +15,737 @@ body,
|
|||||||
a {
|
a {
|
||||||
color: #1677ff;
|
color: #1677ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============ 后台主布局与侧边栏 ============ */
|
||||||
|
.app-shell {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-main {
|
||||||
|
min-width: 0;
|
||||||
|
background: #f5f8fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header {
|
||||||
|
height: 50px !important;
|
||||||
|
padding: 0 24px !important;
|
||||||
|
line-height: 50px !important;
|
||||||
|
background: #fff !important;
|
||||||
|
border-bottom: 1px solid #edf1f7;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-content {
|
||||||
|
min-width: 0;
|
||||||
|
margin: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-content__body {
|
||||||
|
min-height: calc(100vh - 50px - 32px);
|
||||||
|
padding: 20px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #edf1f7;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-main--flush .app-content {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-main--flush .app-content__body {
|
||||||
|
height: calc(100vh - 50px);
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
background: #fff;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar {
|
||||||
|
position: relative;
|
||||||
|
background: #f4f8fc !important;
|
||||||
|
border-right: 1px solid #e3eaf2;
|
||||||
|
box-shadow: inset -1px 0 0 rgba(225, 233, 242, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar .ant-layout-sider-children {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar__collapse {
|
||||||
|
position: absolute;
|
||||||
|
top: 42px;
|
||||||
|
right: -18px;
|
||||||
|
z-index: 2;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border: 1px solid #e8eef6;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #fff;
|
||||||
|
color: #9aa4b2;
|
||||||
|
box-shadow: 0 8px 18px rgba(42, 71, 105, 0.12);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.16s ease, box-shadow 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar__collapse:hover {
|
||||||
|
color: #1677ff;
|
||||||
|
box-shadow: 0 10px 22px rgba(22, 119, 255, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar__brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
height: 50px;
|
||||||
|
padding: 0 18px;
|
||||||
|
color: #1f2329;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar__brand-mark {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 0 0 28px;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: linear-gradient(135deg, #1677ff 0%, #39c5ff 100%);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar__brand-text {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar__nav {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 8px 14px 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar__group {
|
||||||
|
margin-bottom: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar__item,
|
||||||
|
.app-sidebar__child {
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #555f6d;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
text-align: left;
|
||||||
|
transition: background 0.16s ease, color 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar__item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 42px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar__item:hover {
|
||||||
|
color: #1677ff;
|
||||||
|
background: #edf5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar__item--active {
|
||||||
|
color: #1677ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar__item--disabled {
|
||||||
|
color: #9aa4b2;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar__icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 0 0 26px;
|
||||||
|
margin-right: 10px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar__label {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar__arrow {
|
||||||
|
margin-left: 8px;
|
||||||
|
color: currentColor;
|
||||||
|
font-size: 12px;
|
||||||
|
transform: rotate(0deg);
|
||||||
|
transition: transform 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar__arrow--open {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar__children {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
margin-top: 6px;
|
||||||
|
padding-left: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar__child {
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar__child:hover:not(:disabled) {
|
||||||
|
color: #1677ff;
|
||||||
|
background: #eef6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar__child--active {
|
||||||
|
color: #1677ff;
|
||||||
|
background: #dcecff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar__child--disabled {
|
||||||
|
color: #5f6875;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar__child:disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar--collapsed .app-sidebar__brand {
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar--collapsed .app-sidebar__nav {
|
||||||
|
padding: 8px 10px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar--collapsed .app-sidebar__group {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar--collapsed .app-sidebar__item {
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar--collapsed .app-sidebar__icon {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ 开放 API 对接文档 ============ */
|
||||||
|
.api-docs {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧目录 */
|
||||||
|
.api-docs__toc {
|
||||||
|
flex: 0 0 220px;
|
||||||
|
width: 220px;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-right: 1px solid #e7edf5;
|
||||||
|
background: #f8fbff;
|
||||||
|
padding: 16px 12px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__toc-tabs {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__toc-tab {
|
||||||
|
height: 34px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
color: #4e5969;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__toc-tab:hover,
|
||||||
|
.api-docs__toc-tab--active {
|
||||||
|
color: #1677ff;
|
||||||
|
background: #edf5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__nav {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__nav-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__nav-heading {
|
||||||
|
padding: 0 10px 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9aa4b2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__nav-row {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__nav-item,
|
||||||
|
.api-docs__nav-sub-item {
|
||||||
|
display: block;
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 7px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #4e5969;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 5px;
|
||||||
|
transition: all 0.15s;
|
||||||
|
user-select: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__nav-item:hover,
|
||||||
|
.api-docs__nav-sub-item:hover {
|
||||||
|
color: #1677ff;
|
||||||
|
background: #f2f7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__nav-item--active {
|
||||||
|
color: #1677ff;
|
||||||
|
background: #edf5ff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__nav-sub {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 2px 0 8px 12px;
|
||||||
|
padding-left: 12px;
|
||||||
|
border-left: 1px solid #e6edf6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__nav-sub-item {
|
||||||
|
min-height: 28px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: #86909c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__nav-sub-item--active {
|
||||||
|
color: #1677ff;
|
||||||
|
font-weight: 600;
|
||||||
|
background: #f4f8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右侧文档 */
|
||||||
|
.api-docs__content {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
background: #fff;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__inner {
|
||||||
|
width: min(1120px, calc(100% - 48px));
|
||||||
|
max-width: 1120px;
|
||||||
|
min-height: 100%;
|
||||||
|
margin: 0 auto 0 0;
|
||||||
|
padding: 28px 0 64px 32px;
|
||||||
|
background: #fff;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__section {
|
||||||
|
padding-top: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__section:first-child {
|
||||||
|
padding-top: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1d2129;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
scroll-margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__summary {
|
||||||
|
max-width: 1080px;
|
||||||
|
margin: 0 0 16px !important;
|
||||||
|
color: #4e5969 !important;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__endpoint-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__endpoint-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__endpoint {
|
||||||
|
color: #1f2329;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__notice {
|
||||||
|
margin: 0 0 2px;
|
||||||
|
padding: 13px 16px;
|
||||||
|
border-left: 4px solid #1890ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #f1f7ff;
|
||||||
|
color: #4e5969;
|
||||||
|
line-height: 1.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__notice-label {
|
||||||
|
color: #1f2329;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* EndpointDoc 子小节标题 */
|
||||||
|
.doc-section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1d2129;
|
||||||
|
margin: 22px 0 12px;
|
||||||
|
scroll-margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #4e5969;
|
||||||
|
margin: 10px 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__param-group {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__card {
|
||||||
|
margin-top: 16px;
|
||||||
|
border-color: #eef1f5 !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__card .ant-card-head {
|
||||||
|
min-height: 42px;
|
||||||
|
border-bottom-color: #eef1f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__card .ant-card-head-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1d2129;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__common {
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__pager {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 14px;
|
||||||
|
margin-top: 32px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #eef1f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__pager-btn {
|
||||||
|
min-height: 70px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid #e7edf5;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fbfdff;
|
||||||
|
color: #4e5969;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__pager-btn:hover:not(:disabled) {
|
||||||
|
border-color: #91caff;
|
||||||
|
background: #f2f8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__pager-btn:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__pager-btn span {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #86909c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__pager-btn strong {
|
||||||
|
display: block;
|
||||||
|
color: #1d2129;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__pager-btn--next {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__alert {
|
||||||
|
border-radius: 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__table {
|
||||||
|
border-top: 1px solid #edf1f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__table .ant-table {
|
||||||
|
color: #1f2329;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__table .ant-table-thead > tr > th {
|
||||||
|
background: #fafcff !important;
|
||||||
|
color: #1d2129 !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
border-bottom: 1px solid #edf1f7 !important;
|
||||||
|
border-right: 1px solid #f1f4f8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__table .ant-table-tbody > tr > td {
|
||||||
|
padding-top: 14px !important;
|
||||||
|
padding-bottom: 14px !important;
|
||||||
|
border-bottom: 1px solid #edf1f7 !important;
|
||||||
|
border-right: 1px solid #f3f6fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__table .ant-table-thead > tr > th:last-child,
|
||||||
|
.api-docs__table .ant-table-tbody > tr > td:last-child {
|
||||||
|
border-right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__table .ant-table-tbody > tr:hover > td {
|
||||||
|
background: #fbfdff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__field {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
color: #2454a6 !important;
|
||||||
|
background: transparent !important;
|
||||||
|
border: 0 !important;
|
||||||
|
padding: 0;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__type {
|
||||||
|
color: #1f2329;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__inline-example {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #2454a6 !important;
|
||||||
|
background: transparent !important;
|
||||||
|
border: 0 !important;
|
||||||
|
padding: 0;
|
||||||
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__required {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 20px;
|
||||||
|
height: 22px;
|
||||||
|
color: #cf1322;
|
||||||
|
background: #fff1f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__pre {
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: #f7f9fc;
|
||||||
|
border: 1px solid #e7edf5;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
line-height: 1.7;
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
color: #1f2329;
|
||||||
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__pre--plain {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__code,
|
||||||
|
.api-docs__endpoint-line {
|
||||||
|
margin: 0;
|
||||||
|
padding: 17px 18px;
|
||||||
|
background: #f7f9fc;
|
||||||
|
border: 1px solid #e7edf5;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #1f2329;
|
||||||
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.75;
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__endpoint-line {
|
||||||
|
min-height: 56px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__method {
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__path {
|
||||||
|
color: #202632;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* JSON 语法高亮配色 */
|
||||||
|
.json-key {
|
||||||
|
color: #2454a6;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-string {
|
||||||
|
color: #0f8a5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-number {
|
||||||
|
color: #b86b00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-boolean {
|
||||||
|
color: #1677ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-null {
|
||||||
|
color: #86909c;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.api-docs {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__toc {
|
||||||
|
position: relative;
|
||||||
|
flex: none;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 320px;
|
||||||
|
border-right: 0;
|
||||||
|
border-bottom: 1px solid #e7edf5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__content {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
min-height: calc(100vh - 50px - 320px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__inner {
|
||||||
|
padding: 22px 18px 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__endpoint-head {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__pager {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-docs__pager-btn--next {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,52 +1,180 @@
|
|||||||
import { useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom'
|
import { Outlet, useLocation, useNavigate } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
Layout,
|
Layout,
|
||||||
Menu,
|
|
||||||
theme,
|
|
||||||
Dropdown,
|
Dropdown,
|
||||||
Space,
|
Space,
|
||||||
Typography,
|
Typography,
|
||||||
Avatar,
|
Avatar,
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import {
|
import {
|
||||||
DashboardOutlined,
|
|
||||||
UserOutlined,
|
|
||||||
LogoutOutlined,
|
|
||||||
MenuFoldOutlined,
|
|
||||||
MenuUnfoldOutlined,
|
|
||||||
ApiOutlined,
|
ApiOutlined,
|
||||||
|
AppstoreOutlined,
|
||||||
|
DashboardOutlined,
|
||||||
|
DownOutlined,
|
||||||
|
LeftOutlined,
|
||||||
|
LogoutOutlined,
|
||||||
|
OrderedListOutlined,
|
||||||
|
RightOutlined,
|
||||||
ShopOutlined,
|
ShopOutlined,
|
||||||
BugOutlined,
|
TeamOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
WalletOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { useAuth } from '../store/auth'
|
import { useAuth } from '../store/auth'
|
||||||
import type { MenuProps } from 'antd'
|
import type { MenuProps } from 'antd'
|
||||||
|
|
||||||
const { Header, Sider, Content } = Layout
|
const { Header, Sider, Content } = Layout
|
||||||
|
|
||||||
|
interface SidebarChild {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
path?: string
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SidebarSection {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
icon: ReactNode
|
||||||
|
path?: string
|
||||||
|
disabled?: boolean
|
||||||
|
children?: SidebarChild[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminSections: SidebarSection[] = [
|
||||||
|
{
|
||||||
|
key: 'dashboard',
|
||||||
|
label: '工作台',
|
||||||
|
icon: <DashboardOutlined />,
|
||||||
|
path: '/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'products',
|
||||||
|
label: '商品管理',
|
||||||
|
icon: <AppstoreOutlined />,
|
||||||
|
children: [{ key: 'platform-products', label: '平台授权商品', disabled: true }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'shops',
|
||||||
|
label: '店铺管理',
|
||||||
|
icon: <ShopOutlined />,
|
||||||
|
children: [
|
||||||
|
{ key: 'shop-list', label: '店铺列表', path: '/platform-merchants' },
|
||||||
|
{ key: 'shop-products', label: '店铺商品绑定', disabled: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'orders',
|
||||||
|
label: '订单管理',
|
||||||
|
icon: <OrderedListOutlined />,
|
||||||
|
children: [
|
||||||
|
{ key: 'auto-orders', label: '自动发货订单', disabled: true },
|
||||||
|
{ key: 'manual-verify', label: '手动核销/反核销', disabled: true },
|
||||||
|
{ key: 'after-sales', label: '售后订单', disabled: true },
|
||||||
|
{ key: 'manual-orders', label: '手动处理订单', disabled: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'staff',
|
||||||
|
label: '员工管理',
|
||||||
|
icon: <TeamOutlined />,
|
||||||
|
children: [
|
||||||
|
{ key: 'staff-list', label: '员工列表', disabled: true },
|
||||||
|
{ key: 'positions', label: '岗位列表', disabled: true },
|
||||||
|
{ key: 'operation-logs', label: '操作日志', disabled: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'funds',
|
||||||
|
label: '资金',
|
||||||
|
icon: <WalletOutlined />,
|
||||||
|
children: [
|
||||||
|
{ key: 'point-details', label: '积分明细', disabled: true },
|
||||||
|
{ key: 'recharge-requests', label: '充值申请', disabled: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'open-api',
|
||||||
|
label: '开放 API',
|
||||||
|
icon: <ApiOutlined />,
|
||||||
|
children: [
|
||||||
|
{ key: 'api-keys', label: 'API 密钥', path: '/merchant-center' },
|
||||||
|
{ key: 'api-docs', label: '对接文档', path: '/open-api' },
|
||||||
|
{ key: 'api-debug', label: '调用调试', path: '/api-debug' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const merchantSections: SidebarSection[] = [
|
||||||
|
{
|
||||||
|
key: 'dashboard',
|
||||||
|
label: '工作台',
|
||||||
|
icon: <DashboardOutlined />,
|
||||||
|
path: '/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'open-api',
|
||||||
|
label: '开放 API',
|
||||||
|
icon: <ApiOutlined />,
|
||||||
|
children: [{ key: 'api-keys', label: 'API 密钥', path: '/merchant-center' }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function getSelectedKey(pathname: string) {
|
||||||
|
if (pathname === '/') return 'dashboard'
|
||||||
|
if (pathname.startsWith('/merchant-center')) return 'api-keys'
|
||||||
|
if (pathname.startsWith('/open-api')) return 'api-docs'
|
||||||
|
if (pathname.startsWith('/api-debug')) return 'api-debug'
|
||||||
|
if (pathname.startsWith('/platform-merchants')) return 'shop-list'
|
||||||
|
return 'dashboard'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInitialOpenKeys(sections: SidebarSection[], selectedKey: string) {
|
||||||
|
return sections
|
||||||
|
.filter((section) => section.children?.some((child) => child.key === selectedKey))
|
||||||
|
.map((section) => section.key)
|
||||||
|
}
|
||||||
|
|
||||||
export default function MainLayout() {
|
export default function MainLayout() {
|
||||||
const [collapsed, setCollapsed] = useState(false)
|
const [collapsed, setCollapsed] = useState(false)
|
||||||
const { user, logout, isAdmin } = useAuth()
|
const { user, logout, isAdmin } = useAuth()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const {
|
const isDocsPage = location.pathname.startsWith('/open-api')
|
||||||
token: { colorBgContainer, borderRadiusLG },
|
|
||||||
} = theme.useToken()
|
|
||||||
|
|
||||||
const menuItems: MenuProps['items'] = useMemo(() => {
|
const sections = useMemo(() => (isAdmin ? adminSections : merchantSections), [isAdmin])
|
||||||
const items: MenuProps['items'] = [
|
const selectedKey = getSelectedKey(location.pathname)
|
||||||
{ key: '/', icon: <DashboardOutlined />, label: '数据概览' },
|
const [openKeys, setOpenKeys] = useState<string[]>(() => getInitialOpenKeys(sections, selectedKey))
|
||||||
{ key: '/merchant-center', icon: <ShopOutlined />, label: '商户中心' },
|
|
||||||
]
|
useEffect(() => {
|
||||||
if (isAdmin) {
|
const nextOpenKeys = getInitialOpenKeys(sections, selectedKey)
|
||||||
items.push(
|
setOpenKeys((current) => Array.from(new Set([...current, ...nextOpenKeys])))
|
||||||
{ key: '/platform-merchants', icon: <ShopOutlined />, label: '商户管理' },
|
}, [sections, selectedKey])
|
||||||
{ key: '/open-api', icon: <ApiOutlined />, label: '开放接口' },
|
|
||||||
{ key: '/api-debug', icon: <BugOutlined />, label: 'API 调试' },
|
const toggleSection = (section: SidebarSection) => {
|
||||||
|
if (section.disabled) return
|
||||||
|
if (section.path) {
|
||||||
|
navigate(section.path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (collapsed) {
|
||||||
|
const firstEnabledChild = section.children?.find((child) => child.path && !child.disabled)
|
||||||
|
if (firstEnabledChild?.path) navigate(firstEnabledChild.path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setOpenKeys((current) =>
|
||||||
|
current.includes(section.key)
|
||||||
|
? current.filter((key) => key !== section.key)
|
||||||
|
: [...current, section.key],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return items
|
|
||||||
}, [isAdmin])
|
const handleChildClick = (child: SidebarChild) => {
|
||||||
|
if (!child.path || child.disabled) return
|
||||||
|
navigate(child.path)
|
||||||
|
}
|
||||||
|
|
||||||
const userMenu: MenuProps['items'] = [
|
const userMenu: MenuProps['items'] = [
|
||||||
{
|
{
|
||||||
@@ -61,46 +189,70 @@ export default function MainLayout() {
|
|||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout style={{ minHeight: '100vh' }}>
|
<Layout className="app-shell">
|
||||||
<Sider trigger={null} collapsible collapsed={collapsed} theme="dark">
|
<Sider
|
||||||
<div
|
className={`app-sidebar${collapsed ? ' app-sidebar--collapsed' : ''}`}
|
||||||
style={{
|
trigger={null}
|
||||||
height: 64,
|
collapsible
|
||||||
display: 'flex',
|
collapsed={collapsed}
|
||||||
alignItems: 'center',
|
width={230}
|
||||||
justifyContent: 'center',
|
collapsedWidth={74}
|
||||||
color: '#fff',
|
|
||||||
fontWeight: 700,
|
|
||||||
fontSize: collapsed ? 14 : 16,
|
|
||||||
letterSpacing: 1,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{collapsed ? '供货' : '皮肤供货平台'}
|
<button
|
||||||
</div>
|
className="app-sidebar__collapse"
|
||||||
<Menu
|
type="button"
|
||||||
theme="dark"
|
|
||||||
mode="inline"
|
|
||||||
selectedKeys={[location.pathname === '/' ? '/' : `/${location.pathname.split('/')[1]}`]}
|
|
||||||
items={menuItems}
|
|
||||||
onClick={({ key }) => navigate(key)}
|
|
||||||
/>
|
|
||||||
</Sider>
|
|
||||||
<Layout>
|
|
||||||
<Header
|
|
||||||
style={{
|
|
||||||
padding: '0 24px',
|
|
||||||
background: colorBgContainer,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
style={{ fontSize: 18, cursor: 'pointer' }}
|
|
||||||
onClick={() => setCollapsed(!collapsed)}
|
onClick={() => setCollapsed(!collapsed)}
|
||||||
|
aria-label={collapsed ? '展开侧边栏' : '收起侧边栏'}
|
||||||
|
>
|
||||||
|
{collapsed ? <RightOutlined /> : <LeftOutlined />}
|
||||||
|
</button>
|
||||||
|
<div className="app-sidebar__brand">
|
||||||
|
<span className="app-sidebar__brand-mark">S</span>
|
||||||
|
{!collapsed ? <span className="app-sidebar__brand-text">Skin Sales</span> : null}
|
||||||
|
</div>
|
||||||
|
<nav className="app-sidebar__nav">
|
||||||
|
{sections.map((section) => {
|
||||||
|
const isOpen = openKeys.includes(section.key)
|
||||||
|
const isSelected = selectedKey === section.key
|
||||||
|
const hasSelectedChild = section.children?.some((child) => child.key === selectedKey)
|
||||||
|
return (
|
||||||
|
<div className="app-sidebar__group" key={section.key}>
|
||||||
|
<button
|
||||||
|
className={`app-sidebar__item${isSelected || hasSelectedChild ? ' app-sidebar__item--active' : ''}${section.disabled ? ' app-sidebar__item--disabled' : ''}`}
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleSection(section)}
|
||||||
|
title={collapsed ? section.label : undefined}
|
||||||
|
>
|
||||||
|
<span className="app-sidebar__icon">{section.icon}</span>
|
||||||
|
{!collapsed ? <span className="app-sidebar__label">{section.label}</span> : null}
|
||||||
|
{!collapsed && section.children?.length ? (
|
||||||
|
<DownOutlined className={`app-sidebar__arrow${isOpen ? ' app-sidebar__arrow--open' : ''}`} />
|
||||||
|
) : null}
|
||||||
|
</button>
|
||||||
|
{!collapsed && section.children?.length && isOpen ? (
|
||||||
|
<div className="app-sidebar__children">
|
||||||
|
{section.children.map((child) => (
|
||||||
|
<button
|
||||||
|
className={`app-sidebar__child${selectedKey === child.key ? ' app-sidebar__child--active' : ''}${child.disabled ? ' app-sidebar__child--disabled' : ''}`}
|
||||||
|
key={child.key}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleChildClick(child)}
|
||||||
|
disabled={child.disabled}
|
||||||
|
>
|
||||||
|
{child.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
</Sider>
|
||||||
|
<Layout className={`app-main${isDocsPage ? ' app-main--flush' : ''}`}>
|
||||||
|
<Header
|
||||||
|
className="app-header"
|
||||||
>
|
>
|
||||||
{collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
|
||||||
</span>
|
|
||||||
<Dropdown menu={{ items: userMenu }}>
|
<Dropdown menu={{ items: userMenu }}>
|
||||||
<Space style={{ cursor: 'pointer' }}>
|
<Space style={{ cursor: 'pointer' }}>
|
||||||
<Avatar size="small" icon={<UserOutlined />} />
|
<Avatar size="small" icon={<UserOutlined />} />
|
||||||
@@ -111,15 +263,8 @@ export default function MainLayout() {
|
|||||||
</Space>
|
</Space>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</Header>
|
</Header>
|
||||||
<Content style={{ margin: 24 }}>
|
<Content className="app-content">
|
||||||
<div
|
<div className="app-content__body">
|
||||||
style={{
|
|
||||||
padding: 24,
|
|
||||||
minHeight: 360,
|
|
||||||
background: colorBgContainer,
|
|
||||||
borderRadius: borderRadiusLG,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</Content>
|
</Content>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { CSSProperties } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import { Alert, Card, Descriptions, Space, Table, Tag, Typography } from 'antd'
|
import { Space, Table, Tag, Typography } from 'antd'
|
||||||
import type { ColumnsType } from 'antd/es/table'
|
import type { ColumnsType } from 'antd/es/table'
|
||||||
import type { EndpointSpec, ParamSpec } from './types'
|
import type { EndpointSpec, ParamSpec } from './types'
|
||||||
|
import { errorCodes } from './endpoints'
|
||||||
|
|
||||||
const { Text, Paragraph } = Typography
|
const { Text, Paragraph } = Typography
|
||||||
|
|
||||||
@@ -13,119 +14,235 @@ const methodColor: Record<string, string> = {
|
|||||||
DELETE: 'red',
|
DELETE: 'red',
|
||||||
}
|
}
|
||||||
|
|
||||||
const preStyle: CSSProperties = {
|
const methodText: Record<string, string> = {
|
||||||
margin: 0,
|
GET: '#1677ff',
|
||||||
padding: 12,
|
POST: '#389e0d',
|
||||||
background: '#f6f8fa',
|
PUT: '#d46b08',
|
||||||
borderRadius: 6,
|
PATCH: '#d48806',
|
||||||
fontSize: 12.5,
|
DELETE: '#cf1322',
|
||||||
lineHeight: 1.6,
|
|
||||||
overflow: 'auto',
|
|
||||||
whiteSpace: 'pre-wrap',
|
|
||||||
wordBreak: 'break-all',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FieldName({ children }: { children: string }) {
|
||||||
|
return <span className="api-docs__field">{children}</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
function InlineExample({ children }: { children: string }) {
|
||||||
|
return <span className="api-docs__inline-example">{children}</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
function RequiredMark({ required }: { required?: boolean }) {
|
||||||
|
return required ? <span className="api-docs__required">是</span> : <Text type="secondary">否</Text>
|
||||||
|
}
|
||||||
|
|
||||||
|
// 轻量 JSON 语法高亮:把 JSON 字符串渲染成带颜色 span 的 HTML。
|
||||||
|
function highlightJson(json: string): string {
|
||||||
|
const esc = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||||
|
return esc.replace(
|
||||||
|
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+\.?\d*([eE][+-]?\d+)?)/g,
|
||||||
|
(match) => {
|
||||||
|
let cls = 'json-number'
|
||||||
|
if (/^"/.test(match)) {
|
||||||
|
cls = /:$/.test(match) ? 'json-key' : 'json-string'
|
||||||
|
} else if (/true|false/.test(match)) {
|
||||||
|
cls = 'json-boolean'
|
||||||
|
} else if (/null/.test(match)) {
|
||||||
|
cls = 'json-null'
|
||||||
|
}
|
||||||
|
return `<span class="${cls}">${match}</span>`
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function JsonBlock({ code }: { code: string }) {
|
||||||
|
return <pre className="api-docs__code" dangerouslySetInnerHTML={{ __html: highlightJson(code) }} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function MethodPathBlock({ method, path }: { method: string; path: string }) {
|
||||||
|
return (
|
||||||
|
<pre className="api-docs__endpoint-line">
|
||||||
|
<span className="api-docs__method" style={{ color: methodText[method] }}>{method}</span>
|
||||||
|
{' '}
|
||||||
|
<span className="api-docs__path">{path}</span>
|
||||||
|
</pre>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 请求参数表格:参数名 | 类型 | 必填 | 说明(示例并入说明列)
|
||||||
const paramColumns: ColumnsType<ParamSpec> = [
|
const paramColumns: ColumnsType<ParamSpec> = [
|
||||||
{
|
{
|
||||||
title: '字段',
|
title: '参数名',
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
width: 200,
|
width: 260,
|
||||||
render: (v: string) => <Text code style={{ whiteSpace: 'nowrap' }}>{v}</Text>,
|
render: (v: string) => <FieldName>{v}</FieldName>,
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '必填',
|
|
||||||
dataIndex: 'required',
|
|
||||||
width: 70,
|
|
||||||
render: (v?: boolean) =>
|
|
||||||
v ? <Tag color="red">是</Tag> : <Tag>否</Tag>,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '类型',
|
title: '类型',
|
||||||
dataIndex: 'type',
|
dataIndex: 'type',
|
||||||
width: 180,
|
width: 140,
|
||||||
render: (v: string) => <Text type="secondary" style={{ fontSize: 12 }}>{v}</Text>,
|
render: (v: string) => <Text className="api-docs__type">{v}</Text>,
|
||||||
},
|
},
|
||||||
{ title: '说明', dataIndex: 'desc' },
|
|
||||||
{
|
{
|
||||||
title: '示例',
|
title: '必填',
|
||||||
dataIndex: 'example',
|
dataIndex: 'required',
|
||||||
width: 160,
|
width: 72,
|
||||||
render: (v?: string) =>
|
render: (v?: boolean) => <RequiredMark required={v} />,
|
||||||
v ? <Text code style={{ fontSize: 12 }}>{v}</Text> : null,
|
},
|
||||||
|
{
|
||||||
|
title: '说明',
|
||||||
|
render: (_: unknown, record: ParamSpec) => (
|
||||||
|
<span>
|
||||||
|
{record.desc}
|
||||||
|
{record.example ? (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
<Text type="secondary" style={{ fontSize: 12 }}>示例</Text>{' '}
|
||||||
|
<InlineExample>{record.example}</InlineExample>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// 响应字段表格:字段 | 类型 | 说明(无必填列)
|
||||||
|
const responseColumns: ColumnsType<ParamSpec> = [
|
||||||
|
{
|
||||||
|
title: '字段',
|
||||||
|
dataIndex: 'name',
|
||||||
|
width: 280,
|
||||||
|
render: (v: string) => <FieldName>{v}</FieldName>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '类型',
|
||||||
|
dataIndex: 'type',
|
||||||
|
width: 140,
|
||||||
|
render: (v: string) => <Text className="api-docs__type">{v}</Text>,
|
||||||
|
},
|
||||||
|
{ title: '说明', dataIndex: 'desc' },
|
||||||
|
]
|
||||||
|
|
||||||
|
function SectionTitle({ id, children }: { id: string; children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<h4 id={id} className="doc-section-title">
|
||||||
|
{children}
|
||||||
|
</h4>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SubTitle({ children }: { children: ReactNode }) {
|
||||||
|
return <h5 className="doc-subtitle">{children}</h5>
|
||||||
|
}
|
||||||
|
|
||||||
function ParamTable({ title, params }: { title: string; params: ParamSpec[] }) {
|
function ParamTable({ title, params }: { title: string; params: ParamSpec[] }) {
|
||||||
return (
|
return (
|
||||||
<Card size="small" title={title} style={{ width: '100%' }}>
|
<div className="api-docs__param-group">
|
||||||
|
<SubTitle>{title}</SubTitle>
|
||||||
<Table
|
<Table
|
||||||
|
className="api-docs__table"
|
||||||
size="small"
|
size="small"
|
||||||
pagination={false}
|
pagination={false}
|
||||||
rowKey="name"
|
rowKey="name"
|
||||||
dataSource={params}
|
dataSource={params}
|
||||||
columns={paramColumns}
|
columns={paramColumns}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CodeBlock({ title, code }: { title: string; code: string }) {
|
/** 通用接口渲染:摘要 → 接口地址 → 请求方式 → 请求参数 → 响应参数 → 状态码 → 响应示例 → 注意事项。 */
|
||||||
return (
|
|
||||||
<Card size="small" title={title} style={{ width: '100%' }}>
|
|
||||||
<pre style={preStyle}>{code}</pre>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 通用接口渲染:方法+路径+权限 → 参数表 → 请求/响应示例 → 字段说明 → 注意事项。 */
|
|
||||||
export default function EndpointDoc({ spec }: { spec: EndpointSpec }) {
|
export default function EndpointDoc({ spec }: { spec: EndpointSpec }) {
|
||||||
|
const prefix = `ep-${spec.key}`
|
||||||
|
const hasParams =
|
||||||
|
!!spec.pathParams?.length || !!spec.queryParams?.length || !!spec.bodyParams?.length
|
||||||
return (
|
return (
|
||||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
<Space className="api-docs__endpoint" direction="vertical" size={20} style={{ width: '100%' }}>
|
||||||
<Card size="small">
|
<Paragraph className="api-docs__summary" type="secondary">
|
||||||
<Space wrap size="middle" align="center">
|
|
||||||
<Tag color={methodColor[spec.method]} style={{ margin: 0, fontSize: 13, fontWeight: 700 }}>
|
|
||||||
{spec.method}
|
|
||||||
</Tag>
|
|
||||||
<Text code copyable style={{ fontSize: 14 }}>
|
|
||||||
{spec.path}
|
|
||||||
</Text>
|
|
||||||
<Tag color="purple">{spec.scope}</Tag>
|
|
||||||
</Space>
|
|
||||||
<Paragraph type="secondary" style={{ margin: '8px 0 0' }}>
|
|
||||||
{spec.summary}
|
{spec.summary}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Card>
|
{spec.notes?.length ? (
|
||||||
|
<div className="api-docs__notice">
|
||||||
|
<span className="api-docs__notice-label">限制:</span>
|
||||||
|
{spec.notes.join(';')}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<SectionTitle id={`${prefix}-address`}>接口地址</SectionTitle>
|
||||||
|
<MethodPathBlock method={spec.method} path={spec.path} />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<SectionTitle id={`${prefix}-method`}>请求方式</SectionTitle>
|
||||||
|
<Space size="middle" align="center">
|
||||||
|
<Text code style={{ color: methodText[spec.method], fontWeight: 700 }}>
|
||||||
|
{spec.method}
|
||||||
|
</Text>
|
||||||
|
<Tag color={methodColor[spec.method]} style={{ margin: 0 }}>
|
||||||
|
{spec.scope}
|
||||||
|
</Tag>
|
||||||
|
</Space>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<SectionTitle id={`${prefix}-params`}>请求参数</SectionTitle>
|
||||||
{spec.pathParams?.length ? <ParamTable title="路径参数" params={spec.pathParams} /> : null}
|
{spec.pathParams?.length ? <ParamTable title="路径参数" params={spec.pathParams} /> : null}
|
||||||
{spec.queryParams?.length ? <ParamTable title="查询参数" params={spec.queryParams} /> : null}
|
{spec.queryParams?.length ? <ParamTable title="查询参数" params={spec.queryParams} /> : null}
|
||||||
{spec.bodyParams?.length ? <ParamTable title="请求体参数" params={spec.bodyParams} /> : null}
|
{spec.bodyParams?.length ? <ParamTable title="请求体参数" params={spec.bodyParams} /> : null}
|
||||||
|
{!hasParams ? <Text type="secondary">该接口无请求参数。</Text> : null}
|
||||||
|
{spec.requestExample ? (
|
||||||
|
<>
|
||||||
|
<SubTitle>请求示例</SubTitle>
|
||||||
|
<JsonBlock code={spec.requestExample} />
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</section>
|
||||||
|
|
||||||
{spec.requestExample ? <CodeBlock title="请求示例" code={spec.requestExample} /> : null}
|
<section>
|
||||||
<CodeBlock title="响应示例" code={spec.responseExample} />
|
<SectionTitle id={`${prefix}-response`}>响应参数</SectionTitle>
|
||||||
|
|
||||||
{spec.responseFields?.length ? (
|
{spec.responseFields?.length ? (
|
||||||
<ParamTable title="响应字段说明" params={spec.responseFields} />
|
<Table
|
||||||
) : null}
|
className="api-docs__table"
|
||||||
|
size="small"
|
||||||
{spec.notes?.length ? (
|
pagination={false}
|
||||||
<Alert
|
rowKey="name"
|
||||||
type="warning"
|
dataSource={spec.responseFields}
|
||||||
showIcon
|
columns={responseColumns}
|
||||||
message="注意事项"
|
|
||||||
description={
|
|
||||||
<ul style={{ margin: 0, paddingLeft: 20 }}>
|
|
||||||
{spec.notes.map((n, i) => (
|
|
||||||
<li key={i}>{n}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : (
|
||||||
|
<Text type="secondary">详见下方响应示例。</Text>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<SectionTitle id={`${prefix}-status`}>状态码</SectionTitle>
|
||||||
|
<Table
|
||||||
|
className="api-docs__table"
|
||||||
|
size="small"
|
||||||
|
pagination={false}
|
||||||
|
rowKey="name"
|
||||||
|
dataSource={errorCodes}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
title: 'HTTP 状态码',
|
||||||
|
dataIndex: 'name',
|
||||||
|
width: 120,
|
||||||
|
render: (v) => <Text code>{v}</Text>,
|
||||||
|
},
|
||||||
|
{ title: '含义', dataIndex: 'desc' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<SectionTitle id={`${prefix}-example`}>响应示例</SectionTitle>
|
||||||
|
<JsonBlock code={spec.responseExample} />
|
||||||
|
</section>
|
||||||
|
|
||||||
</Space>
|
</Space>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 通用响应说明卡片,供页面顶部展示统一响应格式与错误码。 */
|
/** 通用响应说明,供「概述」展示统一响应格式与错误码。 */
|
||||||
export function CommonResponseDoc({
|
export function CommonResponseDoc({
|
||||||
fields,
|
fields,
|
||||||
errors,
|
errors,
|
||||||
@@ -134,31 +251,45 @@ export function CommonResponseDoc({
|
|||||||
errors: ParamSpec[]
|
errors: ParamSpec[]
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
<Space direction="vertical" size={24} style={{ width: '100%' }}>
|
||||||
<Card size="small" title="统一响应格式">
|
<section>
|
||||||
<Descriptions size="small" column={1} bordered>
|
<SectionTitle id="common-response">统一响应格式</SectionTitle>
|
||||||
<Descriptions.Item label="结构">
|
<JsonBlock
|
||||||
<Text code>{`{ "code": 0, "message": "ok", "data": {} }`}</Text>
|
code={`{
|
||||||
</Descriptions.Item>
|
"code": 0,
|
||||||
{fields.map((f) => (
|
"message": "ok",
|
||||||
<Descriptions.Item key={f.name} label={<Text code>{f.name}</Text>}>
|
"data": {}
|
||||||
{f.desc}
|
}`}
|
||||||
</Descriptions.Item>
|
/>
|
||||||
))}
|
|
||||||
</Descriptions>
|
|
||||||
</Card>
|
|
||||||
<Card size="small" title="错误码">
|
|
||||||
<Table
|
<Table
|
||||||
|
className="api-docs__table"
|
||||||
|
size="small"
|
||||||
|
pagination={false}
|
||||||
|
rowKey="name"
|
||||||
|
dataSource={fields}
|
||||||
|
columns={responseColumns}
|
||||||
|
style={{ marginTop: 14 }}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<SectionTitle id="error-codes">错误码</SectionTitle>
|
||||||
|
<Table
|
||||||
|
className="api-docs__table"
|
||||||
size="small"
|
size="small"
|
||||||
pagination={false}
|
pagination={false}
|
||||||
rowKey="name"
|
rowKey="name"
|
||||||
dataSource={errors}
|
dataSource={errors}
|
||||||
columns={[
|
columns={[
|
||||||
{ title: 'HTTP/code', dataIndex: 'name', width: 120, render: (v) => <Text code>{v}</Text> },
|
{
|
||||||
|
title: 'HTTP/code',
|
||||||
|
dataIndex: 'name',
|
||||||
|
width: 120,
|
||||||
|
render: (v) => <Text code>{v}</Text>,
|
||||||
|
},
|
||||||
{ title: '含义', dataIndex: 'desc' },
|
{ title: '含义', dataIndex: 'desc' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</section>
|
||||||
</Space>
|
</Space>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ export const endpoints: EndpointSpec[] = [
|
|||||||
key: 'list-products',
|
key: 'list-products',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/api/client/v1/products',
|
path: '/api/client/v1/products',
|
||||||
|
title: '商品列表',
|
||||||
summary: '查询商户已授权且上架的商品列表',
|
summary: '查询商户已授权且上架的商品列表',
|
||||||
scope: 'products:read',
|
scope: 'products:read',
|
||||||
queryParams: [
|
queryParams: [
|
||||||
@@ -196,6 +197,7 @@ export const endpoints: EndpointSpec[] = [
|
|||||||
key: 'create-order',
|
key: 'create-order',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/api/client/v1/orders',
|
path: '/api/client/v1/orders',
|
||||||
|
title: '创建订单',
|
||||||
summary: '幂等创建订单并扣款(成功返回 201,重复请求返回 200 且 idempotent=true)',
|
summary: '幂等创建订单并扣款(成功返回 201,重复请求返回 200 且 idempotent=true)',
|
||||||
scope: 'orders:write',
|
scope: 'orders:write',
|
||||||
bodyParams: [
|
bodyParams: [
|
||||||
@@ -240,6 +242,7 @@ export const endpoints: EndpointSpec[] = [
|
|||||||
key: 'query-order',
|
key: 'query-order',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/api/client/v1/orders/{order_no}',
|
path: '/api/client/v1/orders/{order_no}',
|
||||||
|
title: '查询订单',
|
||||||
summary: '查询订单状态与详情',
|
summary: '查询订单状态与详情',
|
||||||
scope: 'orders:read',
|
scope: 'orders:read',
|
||||||
pathParams: [{ name: 'order_no', type: 'string', required: true, desc: '平台订单号', example: 'FO20260730000123' }],
|
pathParams: [{ name: 'order_no', type: 'string', required: true, desc: '平台订单号', example: 'FO20260730000123' }],
|
||||||
@@ -250,6 +253,7 @@ export const endpoints: EndpointSpec[] = [
|
|||||||
key: 'cancel-order',
|
key: 'cancel-order',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/api/client/v1/orders/{order_no}/cancel',
|
path: '/api/client/v1/orders/{order_no}/cancel',
|
||||||
|
title: '取消订单',
|
||||||
summary: '取消未履约订单并退款(仅 pending/failed 可取消)',
|
summary: '取消未履约订单并退款(仅 pending/failed 可取消)',
|
||||||
scope: 'orders:write',
|
scope: 'orders:write',
|
||||||
pathParams: [{ name: 'order_no', type: 'string', required: true, desc: '平台订单号', example: 'FO20260730000123' }],
|
pathParams: [{ name: 'order_no', type: 'string', required: true, desc: '平台订单号', example: 'FO20260730000123' }],
|
||||||
@@ -266,6 +270,7 @@ export const endpoints: EndpointSpec[] = [
|
|||||||
key: 'delivery-link',
|
key: 'delivery-link',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/api/client/v1/orders/{order_no}/delivery-link',
|
path: '/api/client/v1/orders/{order_no}/delivery-link',
|
||||||
|
title: '获取发货链接',
|
||||||
summary: '获取签名发货链接,适合直接跳转平台 Web 发货页',
|
summary: '获取签名发货链接,适合直接跳转平台 Web 发货页',
|
||||||
scope: 'orders:read + fulfillment:read',
|
scope: 'orders:read + fulfillment:read',
|
||||||
pathParams: [{ name: 'order_no', type: 'string', required: true, desc: '平台订单号', example: 'FO20260730000123' }],
|
pathParams: [{ name: 'order_no', type: 'string', required: true, desc: '平台订单号', example: 'FO20260730000123' }],
|
||||||
@@ -286,6 +291,7 @@ export const endpoints: EndpointSpec[] = [
|
|||||||
key: 'delivery-order',
|
key: 'delivery-order',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/api/client/v1/orders/{order_no}/delivery',
|
path: '/api/client/v1/orders/{order_no}/delivery',
|
||||||
|
title: '查询发货数据',
|
||||||
summary: '查询发货页结构化数据,适合商户自建发货界面',
|
summary: '查询发货页结构化数据,适合商户自建发货界面',
|
||||||
scope: 'orders:read + fulfillment:read',
|
scope: 'orders:read + fulfillment:read',
|
||||||
pathParams: [{ name: 'order_no', type: 'string', required: true, desc: '平台订单号', example: 'FO20260730000123' }],
|
pathParams: [{ name: 'order_no', type: 'string', required: true, desc: '平台订单号', example: 'FO20260730000123' }],
|
||||||
@@ -316,6 +322,7 @@ export const endpoints: EndpointSpec[] = [
|
|||||||
key: 'delivery-bind',
|
key: 'delivery-bind',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/api/client/v1/orders/{order_no}/delivery/bind',
|
path: '/api/client/v1/orders/{order_no}/delivery/bind',
|
||||||
|
title: '发起绑定',
|
||||||
summary: '发起绑定,返回绑定跳转链接与二维码地址',
|
summary: '发起绑定,返回绑定跳转链接与二维码地址',
|
||||||
scope: 'orders:write',
|
scope: 'orders:write',
|
||||||
pathParams: [{ name: 'order_no', type: 'string', required: true, desc: '平台订单号', example: 'FO20260730000123' }],
|
pathParams: [{ name: 'order_no', type: 'string', required: true, desc: '平台订单号', example: 'FO20260730000123' }],
|
||||||
@@ -340,6 +347,7 @@ export const endpoints: EndpointSpec[] = [
|
|||||||
key: 'delivery-submit',
|
key: 'delivery-submit',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/api/client/v1/orders/{order_no}/delivery/submit',
|
path: '/api/client/v1/orders/{order_no}/delivery/submit',
|
||||||
|
title: '提交发货',
|
||||||
summary: '提交发货到上游并进入履约中状态',
|
summary: '提交发货到上游并进入履约中状态',
|
||||||
scope: 'orders:write',
|
scope: 'orders:write',
|
||||||
pathParams: [{ name: 'order_no', type: 'string', required: true, desc: '平台订单号', example: 'FO20260730000123' }],
|
pathParams: [{ name: 'order_no', type: 'string', required: true, desc: '平台订单号', example: 'FO20260730000123' }],
|
||||||
@@ -369,6 +377,7 @@ export const endpoints: EndpointSpec[] = [
|
|||||||
key: 'get-wallet',
|
key: 'get-wallet',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/api/client/v1/wallet',
|
path: '/api/client/v1/wallet',
|
||||||
|
title: '余额查询',
|
||||||
summary: '查询商户钱包余额',
|
summary: '查询商户钱包余额',
|
||||||
scope: 'wallet:read',
|
scope: 'wallet:read',
|
||||||
responseExample: `{
|
responseExample: `{
|
||||||
|
|||||||
@@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
||||||
|
|
||||||
|
/** 每个接口下固定的子小节,供目录(TOC)构建与锚点跳转使用。 */
|
||||||
|
export const endpointSections = [
|
||||||
|
{ id: 'address', label: '接口地址' },
|
||||||
|
{ id: 'method', label: '请求方式' },
|
||||||
|
{ id: 'params', label: '请求参数' },
|
||||||
|
{ id: 'response', label: '响应参数' },
|
||||||
|
{ id: 'status', label: '状态码' },
|
||||||
|
{ id: 'example', label: '响应示例' },
|
||||||
|
] as const
|
||||||
|
|
||||||
/** 单个接口的完整描述。 */
|
/** 单个接口的完整描述。 */
|
||||||
export interface EndpointSpec {
|
export interface EndpointSpec {
|
||||||
/** 唯一 key,用于折叠面板与锚点 */
|
/** 唯一 key,用于折叠面板与锚点 */
|
||||||
@@ -10,6 +20,8 @@ export interface EndpointSpec {
|
|||||||
method: HttpMethod
|
method: HttpMethod
|
||||||
/** 接口路径,路径参数用 {name} 表示 */
|
/** 接口路径,路径参数用 {name} 表示 */
|
||||||
path: string
|
path: string
|
||||||
|
/** 中文接口名,用于目录与标题展示 */
|
||||||
|
title: string
|
||||||
/** 一句话说明 */
|
/** 一句话说明 */
|
||||||
summary: string
|
summary: string
|
||||||
/** 调用所需权限 scope */
|
/** 调用所需权限 scope */
|
||||||
|
|||||||
+310
-117
@@ -1,5 +1,6 @@
|
|||||||
import type { CSSProperties } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { Alert, Card, Collapse, Descriptions, Space, Table, Tabs, Tag, Typography } from 'antd'
|
import type { ReactNode } from 'react'
|
||||||
|
import { Alert, Card, Descriptions, Table, Tag, Typography } from 'antd'
|
||||||
import EndpointDoc, { CommonResponseDoc } from '../openapi/EndpointDoc'
|
import EndpointDoc, { CommonResponseDoc } from '../openapi/EndpointDoc'
|
||||||
import {
|
import {
|
||||||
commonResponseFields,
|
commonResponseFields,
|
||||||
@@ -8,26 +9,15 @@ import {
|
|||||||
orderStatusTable,
|
orderStatusTable,
|
||||||
paymentStatusTable,
|
paymentStatusTable,
|
||||||
} from '../openapi/endpoints'
|
} from '../openapi/endpoints'
|
||||||
|
import { endpointSections, type EndpointSpec } from '../openapi/types'
|
||||||
|
|
||||||
const { Title, Paragraph, Text } = Typography
|
const { Paragraph, Text } = Typography
|
||||||
|
|
||||||
const baseUrl =
|
const baseUrl =
|
||||||
typeof window !== 'undefined'
|
typeof window !== 'undefined'
|
||||||
? window.location.origin.replace(':5173', ':8080')
|
? window.location.origin.replace(':5173', ':8080')
|
||||||
: 'http://localhost:8080'
|
: 'http://localhost:8080'
|
||||||
|
|
||||||
const preStyle: CSSProperties = {
|
|
||||||
margin: 0,
|
|
||||||
padding: 12,
|
|
||||||
background: '#f6f8fa',
|
|
||||||
borderRadius: 6,
|
|
||||||
fontSize: 12.5,
|
|
||||||
lineHeight: 1.6,
|
|
||||||
overflow: 'auto',
|
|
||||||
whiteSpace: 'pre-wrap',
|
|
||||||
wordBreak: 'break-all',
|
|
||||||
}
|
|
||||||
|
|
||||||
const deliveryModes = [
|
const deliveryModes = [
|
||||||
{
|
{
|
||||||
mode: 'Web 发货页',
|
mode: 'Web 发货页',
|
||||||
@@ -42,58 +32,278 @@ const deliveryModes = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function OpenApiDocs() {
|
// ---------- TOC 目录构建 ----------
|
||||||
return (
|
interface TocLeaf {
|
||||||
<div>
|
id: string
|
||||||
<Title level={4} style={{ marginTop: 0 }}>
|
label: string
|
||||||
开放接口
|
level: 3 // 接口子小节(接口地址/请求方式/...)
|
||||||
</Title>
|
}
|
||||||
<Paragraph type="secondary">
|
interface TocItem {
|
||||||
面向商户系统调用的开放 API。凭证在「商户中心 / API 客户端」创建,
|
id: string
|
||||||
采用 <Text code>X-App-Key</Text> + HMAC-SHA256 签名鉴权。
|
label: string
|
||||||
发货链路同时支持「返回签名链接」和「返回结构化数据」两种模式。
|
children?: TocLeaf[]
|
||||||
<Text code>/api/open/v1</Text> 仍是上游回调链路,和本页的商户 API 分开。
|
}
|
||||||
</Paragraph>
|
interface TocGroup {
|
||||||
|
title: string
|
||||||
|
items: TocItem[]
|
||||||
|
}
|
||||||
|
|
||||||
<Tabs
|
function buildToc(): TocGroup[] {
|
||||||
tabPosition="left"
|
return [
|
||||||
style={{ minHeight: 680 }}
|
|
||||||
items={[
|
|
||||||
{
|
{
|
||||||
key: 'overview',
|
title: '开始',
|
||||||
label: '概览',
|
items: [
|
||||||
children: <OverviewTab />,
|
{ id: 'overview', label: '概述' },
|
||||||
|
{ id: 'auth', label: '鉴权与签名' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'auth',
|
title: '接口',
|
||||||
label: '鉴权与签名',
|
items: endpoints.map((ep) => ({
|
||||||
children: <AuthTab />,
|
id: `ep-${ep.key}`,
|
||||||
|
label: ep.title,
|
||||||
|
children: endpointSections.map((s) => ({
|
||||||
|
id: `ep-${ep.key}-${s.id}`,
|
||||||
|
label: s.label,
|
||||||
|
level: 3 as const,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'endpoints',
|
title: '附录',
|
||||||
label: '接口列表',
|
items: [{ id: 'status', label: '状态说明' }],
|
||||||
children: <EndpointsTab />,
|
|
||||||
},
|
},
|
||||||
{
|
]
|
||||||
key: 'delivery',
|
}
|
||||||
label: '发货模式',
|
|
||||||
children: <DeliveryModeTab />,
|
const toc = buildToc()
|
||||||
|
|
||||||
|
const pageItems = toc.flatMap((group) => group.items)
|
||||||
|
const pageIds = new Set(pageItems.map((item) => item.id))
|
||||||
|
|
||||||
|
function findPageId(id: string): string {
|
||||||
|
if (pageIds.has(id)) return id
|
||||||
|
for (const item of pageItems) {
|
||||||
|
if (item.children?.some((child) => child.id === id)) return item.id
|
||||||
|
}
|
||||||
|
return 'overview'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInitialHash() {
|
||||||
|
if (typeof window === 'undefined') return 'overview'
|
||||||
|
return decodeURIComponent(window.location.hash.replace(/^#/, '')) || 'overview'
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 锚点跳转 ----------
|
||||||
|
function scrollToId(id: string) {
|
||||||
|
const el = document.getElementById(id)
|
||||||
|
if (!el) return
|
||||||
|
const root = document.getElementById('docScroll')
|
||||||
|
if (!root) return
|
||||||
|
const top = root.scrollTop + el.getBoundingClientRect().top - root.getBoundingClientRect().top - 22
|
||||||
|
root.scrollTo({ top, behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function OpenApiDocs() {
|
||||||
|
const initialHash = useMemo(() => getInitialHash(), [])
|
||||||
|
const [activePageId, setActivePageId] = useState(() => findPageId(initialHash))
|
||||||
|
const [activeAnchorId, setActiveAnchorId] = useState(initialHash)
|
||||||
|
const activeEndpoint = useMemo(
|
||||||
|
() => endpoints.find((spec) => `ep-${spec.key}` === activePageId),
|
||||||
|
[activePageId],
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const syncHash = () => {
|
||||||
|
const nextHash = getInitialHash()
|
||||||
|
setActivePageId(findPageId(nextHash))
|
||||||
|
setActiveAnchorId(nextHash)
|
||||||
|
}
|
||||||
|
window.addEventListener('hashchange', syncHash)
|
||||||
|
return () => window.removeEventListener('hashchange', syncHash)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const root = document.getElementById('docScroll')
|
||||||
|
if (!root) return
|
||||||
|
|
||||||
|
if (activeAnchorId !== activePageId) {
|
||||||
|
requestAnimationFrame(() => scrollToId(activeAnchorId))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
root.scrollTo({ top: 0 })
|
||||||
|
}, [activeAnchorId, activePageId])
|
||||||
|
|
||||||
|
const activeIndex = useMemo(
|
||||||
|
() => pageItems.findIndex((item) => item.id === activePageId),
|
||||||
|
[activePageId],
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleNav = useCallback((id: string) => {
|
||||||
|
const pageId = findPageId(id)
|
||||||
|
setActivePageId(pageId)
|
||||||
|
setActiveAnchorId(id)
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.history.replaceState(null, '', `${window.location.pathname}${window.location.search}#${id}`)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleStep = useCallback(
|
||||||
|
(direction: -1 | 1) => {
|
||||||
|
const next = pageItems[activeIndex + direction]
|
||||||
|
if (next) handleNav(next.id)
|
||||||
},
|
},
|
||||||
{
|
[activeIndex, handleNav],
|
||||||
key: 'status',
|
)
|
||||||
label: '状态说明',
|
|
||||||
children: <StatusTab />,
|
return (
|
||||||
},
|
<div className="api-docs">
|
||||||
]}
|
{/* 左侧目录 */}
|
||||||
|
<aside className="api-docs__toc">
|
||||||
|
<div className="api-docs__toc-tabs">
|
||||||
|
<button
|
||||||
|
className={`api-docs__toc-tab${activePageId !== 'auth' ? ' api-docs__toc-tab--active' : ''}`}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleNav('overview')}
|
||||||
|
>
|
||||||
|
开放 API
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`api-docs__toc-tab${activePageId === 'auth' ? ' api-docs__toc-tab--active' : ''}`}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleNav('auth')}
|
||||||
|
>
|
||||||
|
API 密钥
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<nav className="api-docs__nav">
|
||||||
|
{toc.map((group) => (
|
||||||
|
<div className="api-docs__nav-group" key={group.title}>
|
||||||
|
<div className="api-docs__nav-heading">{group.title}</div>
|
||||||
|
{group.items.map((item) => {
|
||||||
|
const isActive = activePageId === item.id
|
||||||
|
return (
|
||||||
|
<div className="api-docs__nav-row" key={item.id}>
|
||||||
|
<a
|
||||||
|
className={`api-docs__nav-item${isActive ? ' api-docs__nav-item--active' : ''}`}
|
||||||
|
onClick={() => handleNav(item.id)}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</a>
|
||||||
|
{item.children?.length && isActive ? (
|
||||||
|
<div className="api-docs__nav-sub">
|
||||||
|
{item.children.map((c) => (
|
||||||
|
<a
|
||||||
|
key={c.id}
|
||||||
|
className={`api-docs__nav-sub-item${activeAnchorId === c.id ? ' api-docs__nav-sub-item--active' : ''}`}
|
||||||
|
onClick={() => handleNav(c.id)}
|
||||||
|
>
|
||||||
|
{c.label}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* 右侧文档 */}
|
||||||
|
<div id="docScroll" className="api-docs__content">
|
||||||
|
<div className="api-docs__inner">
|
||||||
|
{activePageId === 'overview' ? <OverviewSection /> : null}
|
||||||
|
{activePageId === 'auth' ? <AuthSection /> : null}
|
||||||
|
{activeEndpoint ? <EndpointSection spec={activeEndpoint} /> : null}
|
||||||
|
{activePageId === 'status' ? <StatusSection /> : null}
|
||||||
|
<PageStepper
|
||||||
|
currentIndex={activeIndex}
|
||||||
|
items={pageItems}
|
||||||
|
onStep={handleStep}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function OverviewTab() {
|
function PageStepper({
|
||||||
|
currentIndex,
|
||||||
|
items,
|
||||||
|
onStep,
|
||||||
|
}: {
|
||||||
|
currentIndex: number
|
||||||
|
items: TocItem[]
|
||||||
|
onStep: (direction: -1 | 1) => void
|
||||||
|
}) {
|
||||||
|
const prev = items[currentIndex - 1]
|
||||||
|
const next = items[currentIndex + 1]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
<div className="api-docs__pager">
|
||||||
|
<button
|
||||||
|
className="api-docs__pager-btn"
|
||||||
|
type="button"
|
||||||
|
disabled={!prev}
|
||||||
|
onClick={() => onStep(-1)}
|
||||||
|
>
|
||||||
|
<span>上一篇</span>
|
||||||
|
<strong>{prev?.label ?? '无'}</strong>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="api-docs__pager-btn api-docs__pager-btn--next"
|
||||||
|
type="button"
|
||||||
|
disabled={!next}
|
||||||
|
onClick={() => onStep(1)}
|
||||||
|
>
|
||||||
|
<span>下一篇</span>
|
||||||
|
<strong>{next?.label ?? '无'}</strong>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 各文档小节 ----------
|
||||||
|
function SectionWrap({
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
desc,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
desc?: ReactNode
|
||||||
|
children?: ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="api-docs__section">
|
||||||
|
<h2 id={id} className="api-docs__h2">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
{desc ? <Paragraph className="api-docs__summary" type="secondary">{desc}</Paragraph> : null}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function OverviewSection() {
|
||||||
|
return (
|
||||||
|
<SectionWrap
|
||||||
|
id="overview"
|
||||||
|
title="概述"
|
||||||
|
desc={
|
||||||
|
<>
|
||||||
|
面向商户系统调用的开放 API。凭证在「商户中心 / API 客户端」创建,
|
||||||
|
采用 <Text code>X-App-Key</Text> + HMAC-SHA256 签名鉴权。
|
||||||
|
发货链路同时支持「返回签名链接」和「返回结构化数据」两种模式。
|
||||||
|
<Text code>/api/open/v1</Text> 仍是上游回调链路,和本页的商户 API 分开。
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Alert
|
<Alert
|
||||||
|
className="api-docs__alert"
|
||||||
type="info"
|
type="info"
|
||||||
showIcon
|
showIcon
|
||||||
message="一句话上手"
|
message="一句话上手"
|
||||||
@@ -106,8 +316,9 @@ function OverviewTab() {
|
|||||||
</ol>
|
</ol>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Card size="small" title="发货链路概览">
|
<Card className="api-docs__card" size="small" title="发货链路概览">
|
||||||
<Table
|
<Table
|
||||||
|
className="api-docs__table"
|
||||||
size="small"
|
size="small"
|
||||||
pagination={false}
|
pagination={false}
|
||||||
rowKey="mode"
|
rowKey="mode"
|
||||||
@@ -118,14 +329,12 @@ function OverviewTab() {
|
|||||||
{
|
{
|
||||||
title: '相关接口',
|
title: '相关接口',
|
||||||
dataIndex: 'interfaces',
|
dataIndex: 'interfaces',
|
||||||
render: (v: string) => (
|
render: (v: string) => <pre className="api-docs__pre">{v}</pre>,
|
||||||
<pre style={preStyle}>{v}</pre>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
<Card size="small" title="环境信息">
|
<Card className="api-docs__card" size="small" title="环境信息">
|
||||||
<Descriptions size="small" column={1} bordered>
|
<Descriptions size="small" column={1} bordered>
|
||||||
<Descriptions.Item label="Base URL">
|
<Descriptions.Item label="Base URL">
|
||||||
<Text code>{baseUrl}</Text>
|
<Text code>{baseUrl}</Text>
|
||||||
@@ -140,42 +349,19 @@ function OverviewTab() {
|
|||||||
<Descriptions.Item label="请求体">POST 请求使用 <Text code>application/json</Text></Descriptions.Item>
|
<Descriptions.Item label="请求体">POST 请求使用 <Text code>application/json</Text></Descriptions.Item>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
</Card>
|
</Card>
|
||||||
|
<div className="api-docs__common">
|
||||||
<CommonResponseDoc fields={commonResponseFields} errors={errorCodes} />
|
<CommonResponseDoc fields={commonResponseFields} errors={errorCodes} />
|
||||||
</Space>
|
</div>
|
||||||
|
</SectionWrap>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DeliveryModeTab() {
|
function AuthSection() {
|
||||||
return (
|
return (
|
||||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
<SectionWrap id="auth" title="鉴权与签名">
|
||||||
<Card size="small" title="推荐用法">
|
<Card className="api-docs__card" size="small" title="鉴权头(每次请求必带)">
|
||||||
<Descriptions size="small" column={1} bordered>
|
|
||||||
<Descriptions.Item label="只想给用户一个链接">
|
|
||||||
用 <Text code>delivery-link</Text>,后端直接返回签名后的 Web 发货页地址,前端只负责跳转或复制。
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="想自建发货页面">
|
|
||||||
用 <Text code>delivery</Text> 获取订单数据,再按需调用 <Text code>bind</Text> 和 <Text code>submit</Text>。
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="取消作废">
|
|
||||||
取消作废属于商户后台操作,恢复后会重新签发新有效期和新签名。
|
|
||||||
</Descriptions.Item>
|
|
||||||
</Descriptions>
|
|
||||||
</Card>
|
|
||||||
<Alert
|
|
||||||
type="info"
|
|
||||||
showIcon
|
|
||||||
message="两种模式共用同一套底层履约逻辑"
|
|
||||||
description="不管是 Web 发货页还是商户自建页面,最终都会走同一套订单校验、绑定和提交逻辑,只是前端形态不同。"
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AuthTab() {
|
|
||||||
return (
|
|
||||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
|
||||||
<Card size="small" title="鉴权头(每次请求必带)">
|
|
||||||
<Table
|
<Table
|
||||||
|
className="api-docs__table"
|
||||||
size="small"
|
size="small"
|
||||||
pagination={false}
|
pagination={false}
|
||||||
rowKey="header"
|
rowKey="header"
|
||||||
@@ -187,13 +373,18 @@ function AuthTab() {
|
|||||||
]}
|
]}
|
||||||
columns={[
|
columns={[
|
||||||
{ title: 'Header', dataIndex: 'header', width: 160, render: (v) => <Text code>{v}</Text> },
|
{ title: 'Header', dataIndex: 'header', width: 160, render: (v) => <Text code>{v}</Text> },
|
||||||
{ title: '必填', dataIndex: 'required', width: 80, render: (v) => v ? <Tag color="red">是</Tag> : <Tag>否</Tag> },
|
{
|
||||||
|
title: '必填',
|
||||||
|
dataIndex: 'required',
|
||||||
|
width: 80,
|
||||||
|
render: (v) => v ? <span className="api-docs__required">是</span> : <Text type="secondary">否</Text>,
|
||||||
|
},
|
||||||
{ title: '说明', dataIndex: 'desc' },
|
{ title: '说明', dataIndex: 'desc' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card size="small" title="签名算法">
|
<Card className="api-docs__card" size="small" title="签名算法">
|
||||||
<Descriptions size="small" column={1} bordered>
|
<Descriptions size="small" column={1} bordered>
|
||||||
<Descriptions.Item label="参与参数">
|
<Descriptions.Item label="参与参数">
|
||||||
<Text code>app_key</Text>、<Text code>body_sha256</Text>、<Text code>method</Text>、<Text code>nonce</Text>、<Text code>path</Text>、<Text code>timestamp</Text>
|
<Text code>app_key</Text>、<Text code>body_sha256</Text>、<Text code>method</Text>、<Text code>nonce</Text>、<Text code>path</Text>、<Text code>timestamp</Text>
|
||||||
@@ -214,19 +405,21 @@ function AuthTab() {
|
|||||||
</Descriptions>
|
</Descriptions>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card size="small" title="签名示例(GET,body 为空)">
|
<Card className="api-docs__card" size="small" title="签名示例(GET,body 为空)">
|
||||||
<pre style={preStyle}>{`app_key=ak_xxx&body_sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855&method=GET&nonce=a1b2c3d4e5f67890&path=/api/client/v1/products×tamp=1721450000`}</pre>
|
<pre className="api-docs__pre">{`app_key=ak_xxx&body_sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855&method=GET&nonce=a1b2c3d4e5f67890&path=/api/client/v1/products×tamp=1721450000`}</pre>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card size="small" title="签名示例(POST,body 非空)">
|
<Card className="api-docs__card" size="small" title="签名示例(POST,body 非空)">
|
||||||
<pre style={preStyle}>{`# body = {"client_order_no":"shop-10001","sku":"suit_pink_sheep"}
|
<pre className="api-docs__pre">{`# body = {"client_order_no":"shop-10001","sku":"suit_pink_sheep"}
|
||||||
app_key=ak_xxx&body_sha256=<实际请求 body 字节的 SHA256 十六进制>&method=POST&nonce=a1b2c3d4e5f67890&path=/api/client/v1/orders×tamp=1721450000`}</pre>
|
app_key=ak_xxx&body_sha256=<实际请求 body 字节的 SHA256 十六进制>&method=POST&nonce=a1b2c3d4e5f67890&path=/api/client/v1/orders×tamp=1721450000`}</pre>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Alert
|
<Alert
|
||||||
|
className="api-docs__alert"
|
||||||
type="warning"
|
type="warning"
|
||||||
showIcon
|
showIcon
|
||||||
message="签名注意事项"
|
message="签名注意事项"
|
||||||
|
style={{ marginTop: 16 }}
|
||||||
description={
|
description={
|
||||||
<ul style={{ margin: 0, paddingLeft: 20 }}>
|
<ul style={{ margin: 0, paddingLeft: 20 }}>
|
||||||
<li>POST 签名用的 <Text code>body</Text> 必须与实际发送的 body <b>字节级一致</b>,不要签名后再改空格或字段顺序。</li>
|
<li>POST 签名用的 <Text code>body</Text> 必须与实际发送的 body <b>字节级一致</b>,不要签名后再改空格或字段顺序。</li>
|
||||||
@@ -236,36 +429,33 @@ app_key=ak_xxx&body_sha256=<实际请求 body 字节的 SHA256 十六进制>&met
|
|||||||
</ul>
|
</ul>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</SectionWrap>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function EndpointsTab() {
|
function EndpointSection({ spec }: { spec: EndpointSpec }) {
|
||||||
return (
|
return (
|
||||||
<Collapse
|
<div className="api-docs__section">
|
||||||
accordion
|
<div className="api-docs__endpoint-head">
|
||||||
items={endpoints.map((spec) => ({
|
<h2 id={`ep-${spec.key}`} className="api-docs__h2">
|
||||||
key: spec.key,
|
{spec.title}
|
||||||
label: (
|
</h2>
|
||||||
<Space size="small">
|
<div className="api-docs__endpoint-meta">
|
||||||
<Tag color={spec.method === 'GET' ? 'blue' : 'green'} style={{ margin: 0, fontWeight: 700 }}>
|
<Tag color={spec.method === 'GET' ? 'blue' : 'geekblue'}>{spec.method}</Tag>
|
||||||
{spec.method}
|
<Text code>{spec.scope}</Text>
|
||||||
</Tag>
|
</div>
|
||||||
<Text code style={{ fontSize: 13 }}>{spec.path}</Text>
|
</div>
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>{spec.summary}</Text>
|
<EndpointDoc spec={spec} />
|
||||||
</Space>
|
</div>
|
||||||
),
|
|
||||||
children: <EndpointDoc spec={spec} />,
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatusTab() {
|
function StatusSection() {
|
||||||
return (
|
return (
|
||||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
<SectionWrap id="status" title="状态说明">
|
||||||
<Card size="small" title="履约状态 fulfillment_status 与 can_fulfill">
|
<Card className="api-docs__card" size="small" title="履约状态 fulfillment_status 与 can_fulfill">
|
||||||
<Table
|
<Table
|
||||||
|
className="api-docs__table"
|
||||||
size="small"
|
size="small"
|
||||||
pagination={false}
|
pagination={false}
|
||||||
rowKey="name"
|
rowKey="name"
|
||||||
@@ -283,8 +473,9 @@ function StatusTab() {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
<Card size="small" title="支付状态 payment_status">
|
<Card className="api-docs__card" size="small" title="支付状态 payment_status">
|
||||||
<Table
|
<Table
|
||||||
|
className="api-docs__table"
|
||||||
size="small"
|
size="small"
|
||||||
pagination={false}
|
pagination={false}
|
||||||
rowKey="name"
|
rowKey="name"
|
||||||
@@ -296,11 +487,13 @@ function StatusTab() {
|
|||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
<Alert
|
<Alert
|
||||||
|
className="api-docs__alert"
|
||||||
type="info"
|
type="info"
|
||||||
showIcon
|
showIcon
|
||||||
message="推荐调用流程"
|
message="推荐调用流程"
|
||||||
|
style={{ marginTop: 16 }}
|
||||||
description={
|
description={
|
||||||
<pre style={{ ...preStyle, background: 'transparent', padding: 0 }}>{`拿到 sku(商品列表)
|
<pre className="api-docs__pre api-docs__pre--plain">{`拿到 sku(商品列表)
|
||||||
↓
|
↓
|
||||||
POST /orders 下单(幂等,Idempotency-Key = client_order_no)
|
POST /orders 下单(幂等,Idempotency-Key = client_order_no)
|
||||||
↓
|
↓
|
||||||
@@ -313,6 +506,6 @@ POST /orders 下单(幂等,Idempotency-Key = client_order_no)
|
|||||||
GET /orders/{order_no} 轮询,或接收 order.fulfillment.updated 回调`}</pre>
|
GET /orders/{order_no} 轮询,或接收 order.fulfillment.updated 回调`}</pre>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</SectionWrap>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user