refactor(ui): introduce shared list detail shells

This commit is contained in:
caoxiaozhu
2026-05-28 22:49:58 +08:00
parent b383244a29
commit 064eeb614f
17 changed files with 1163 additions and 1095 deletions

View File

@@ -0,0 +1,438 @@
.enterprise-list-page {
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
padding: 16px 18px;
}
.enterprise-list-page .status-tabs {
flex: 0 0 auto;
display: flex;
gap: 28px;
margin-top: 14px;
border-bottom: 1px solid #dbe4ee;
overflow-x: auto;
}
.enterprise-list-page .status-tabs button {
position: relative;
min-height: 36px;
border: 0;
background: transparent;
color: #64748b;
font-size: 14px;
font-weight: 750;
white-space: nowrap;
}
.enterprise-list-page .status-tabs button.active {
color: var(--theme-primary-active);
}
.enterprise-list-page .status-tabs button.active::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: -1px;
height: 3px;
border-radius: 2px 2px 0 0;
background: var(--theme-primary);
}
.enterprise-list-page .list-toolbar {
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-top: 14px;
}
.enterprise-list-page .table-wrap {
flex: 1 1 auto;
min-height: 0;
}
.enterprise-list-page .toolbar-actions,
.enterprise-list-page .document-actions {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
margin-left: auto;
}
.enterprise-list-page .filter-set {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
min-width: 0;
}
.enterprise-list-page .list-search {
position: relative;
width: min(280px, 100%);
}
.enterprise-list-page .list-search .mdi {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: #64748b;
font-size: 15px;
}
.enterprise-list-page .list-search input {
width: 100%;
height: 38px;
padding: 0 12px 0 36px;
border: 1px solid #d7e0ea;
border-radius: 4px;
background: #fff;
color: #0f172a;
font-size: 13px;
transition: border-color 160ms ease, box-shadow 160ms ease;
}
.enterprise-list-page .list-search input::placeholder {
color: #8da0b4;
}
.enterprise-list-page .list-search input:focus {
border-color: var(--theme-primary);
box-shadow: 0 0 0 3px var(--theme-focus-ring);
outline: none;
}
.enterprise-list-page .filter-btn {
min-width: 104px;
min-height: 38px;
display: inline-flex;
align-items: center;
justify-content: space-between;
gap: 9px;
padding: 0 14px;
border: 1px solid #d7e0ea;
border-radius: 4px;
background: #fff;
color: #334155;
font-size: 14px;
font-weight: 750;
white-space: nowrap;
}
.enterprise-list-page .filter-btn:hover,
.enterprise-list-page .document-filter.open .filter-btn {
border-color: rgba(var(--theme-primary-rgb, 58, 124, 165), 0.32);
color: var(--theme-primary-active);
}
.enterprise-list-page .create-request-btn,
.enterprise-list-page .create-btn,
.enterprise-list-page .export-btn,
.enterprise-list-page .template-btn {
min-height: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 0 18px;
border: 0;
border-radius: 4px;
background: var(--theme-gradient-primary);
color: #fff;
font-size: 14px;
font-weight: 800;
white-space: nowrap;
box-shadow: 0 10px 24px var(--theme-primary-shadow);
transition: transform 160ms ease, box-shadow 160ms ease, filter 160ms ease;
}
.enterprise-list-page .create-request-btn.secondary,
.enterprise-list-page .ghost-filter-btn,
.enterprise-list-page .template-btn,
.enterprise-list-page .export-btn {
border: 1px solid #d7e0ea;
background: #fff;
color: #334155;
box-shadow: none;
}
.enterprise-list-page .create-request-btn:hover:not(:disabled),
.enterprise-list-page .create-btn:hover:not(:disabled),
.enterprise-list-page .export-btn:hover:not(:disabled),
.enterprise-list-page .template-btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 14px 28px var(--theme-primary-shadow);
filter: saturate(1.02);
}
.enterprise-list-page .create-request-btn.secondary:hover:not(:disabled),
.enterprise-list-page .ghost-filter-btn:hover:not(:disabled),
.enterprise-list-page .template-btn:hover:not(:disabled),
.enterprise-list-page .export-btn:hover:not(:disabled) {
border-color: rgba(var(--theme-primary-rgb, 58, 124, 165), 0.32);
color: var(--theme-primary-active);
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.08);
}
.enterprise-list-page .create-request-btn:disabled,
.enterprise-list-page .create-btn:disabled,
.enterprise-list-page .export-btn:disabled,
.enterprise-list-page .template-btn:disabled {
cursor: not-allowed;
opacity: 0.72;
transform: none;
}
.enterprise-list-page .hint {
display: inline-flex;
align-items: center;
gap: 7px;
margin-top: 10px;
color: #64748b;
font-size: 13px;
}
.enterprise-list-page .hint .mdi {
color: #64748b;
}
.enterprise-list-page .table-wrap {
min-height: 400px;
margin-top: 10px;
overflow: auto;
border: 1px solid #edf2f7;
border-radius: 4px;
background: linear-gradient(180deg, #fcfeff 0%, var(--theme-primary-light-9) 100%);
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: flex-start;
}
.enterprise-list-page .table-wrap.is-empty {
align-items: center;
justify-content: center;
}
.enterprise-list-page .table-state {
width: 100%;
min-height: 220px;
align-self: center;
}
.enterprise-list-page .table-state.error {
display: grid;
place-items: center;
gap: 10px;
padding: 32px;
text-align: center;
}
.enterprise-list-page .table-state.error > .mdi {
color: var(--danger);
font-size: 30px;
}
.enterprise-list-page .table-state.error strong {
color: var(--ink);
font-size: 15px;
}
.enterprise-list-page .table-state.error p {
max-width: 520px;
color: var(--muted);
font-size: 13px;
line-height: 1.6;
}
.enterprise-list-page .state-action,
.enterprise-list-page .retry-btn {
min-height: 34px;
padding: 0 12px;
border: 1px solid rgba(var(--theme-primary-rgb, 58, 124, 165), 0.22);
border-radius: 4px;
background: var(--theme-primary-soft);
color: var(--theme-primary-active);
font-size: 12px;
font-weight: 750;
}
.enterprise-list-page table {
width: 100%;
min-width: 1080px;
align-self: flex-start;
border-collapse: collapse;
table-layout: fixed;
}
.enterprise-list-page th,
.enterprise-list-page td {
padding: 13px 12px;
border-bottom: 1px solid #edf2f7;
color: #24324a;
font-size: 14px;
line-height: 1.35;
text-align: center;
vertical-align: middle;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.enterprise-list-page th {
position: sticky;
top: 0;
z-index: 1;
background: #f7fafc;
color: #64748b;
font-size: 13px;
font-weight: 800;
}
.enterprise-list-page tbody tr {
cursor: pointer;
}
.enterprise-list-page tbody tr:hover,
.enterprise-list-page tbody tr.spotlight {
background: linear-gradient(90deg, rgba(var(--theme-primary-rgb, 58, 124, 165), 0.08), rgba(var(--theme-primary-rgb, 58, 124, 165), 0.03));
}
.enterprise-list-page tbody tr:last-child td {
border-bottom: 0;
}
.enterprise-list-page .doc-id {
color: var(--theme-primary-active);
font-weight: 800;
}
.enterprise-pagination {
flex: 0 0 auto;
}
.enterprise-list-page .list-foot {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: 16px;
margin-top: 12px;
}
.enterprise-list-page .page-summary {
color: #64748b;
font-size: 14px;
font-weight: 650;
}
.enterprise-list-page .pager {
display: inline-flex;
justify-content: center;
gap: 6px;
padding: 4px;
border: 1px solid #e2e8f0;
border-radius: 4px;
background: #f8fafc;
}
.enterprise-list-page .pager button {
width: 32px;
height: 32px;
border: 0;
border-radius: 4px;
background: transparent;
color: #334155;
font-size: 14px;
font-weight: 800;
transition: background 160ms ease, color 160ms ease, box-shadow 160ms ease;
}
.enterprise-list-page .pager button:hover:not(.active) {
background: #fff;
color: var(--theme-primary-active);
box-shadow: 0 1px 4px rgba(15, 23, 42, 0.08);
}
.enterprise-list-page .pager button.active {
background: var(--theme-primary);
color: #fff;
box-shadow: 0 8px 16px var(--theme-primary-shadow);
}
.enterprise-list-page .pager button:disabled {
cursor: not-allowed;
opacity: 0.45;
box-shadow: none;
}
.enterprise-list-page .page-ellipsis,
.enterprise-list-page .pager span {
color: #64748b;
font-weight: 800;
}
.enterprise-list-page .page-size-select {
width: 112px;
justify-self: end;
}
.enterprise-detail-page {
min-height: 0;
height: 100%;
display: grid;
grid-template-rows: minmax(0, 1fr) auto;
}
.enterprise-detail-page .detail-scroll {
min-height: 0;
overflow: auto;
}
.enterprise-detail-card .card-head {
align-items: flex-start;
}
@media (max-width: 760px) {
.enterprise-list-page {
padding: 16px;
}
.enterprise-list-page .status-tabs {
gap: 18px;
}
.enterprise-list-page .toolbar-actions,
.enterprise-list-page .document-actions {
width: 100%;
margin-left: 0;
}
.enterprise-list-page .filter-set,
.enterprise-list-page .list-search,
.enterprise-list-page .filter-btn,
.enterprise-list-page .create-request-btn,
.enterprise-list-page .create-btn,
.enterprise-list-page .export-btn,
.enterprise-list-page .template-btn,
.enterprise-list-page .page-size-select {
width: 100%;
}
.enterprise-list-page .list-toolbar,
.enterprise-list-page .list-foot {
display: grid;
grid-template-columns: 1fr;
justify-items: stretch;
}
.enterprise-list-page .pager,
.enterprise-list-page .page-size-select {
justify-self: stretch;
}
}

View File

@@ -11,9 +11,6 @@
.approval-list {
min-height: 0;
display: grid;
grid-template-rows: auto auto auto minmax(0, 1fr) auto;
padding: 16px 18px;
overflow: hidden;
}

View File

@@ -1,16 +1,16 @@
.logs-view {
display: grid;
grid-template-rows: minmax(0, 1fr);
height: 100%;
min-height: 0;
display: grid;
grid-template-rows: minmax(0, 1fr);
background: transparent;
}
.logs-empty {
min-height: 0;
display: grid;
align-content: center;
justify-items: center;
min-height: 0;
padding: 28px 20px;
text-align: center;
}
@@ -29,9 +29,6 @@
.system-logs-list.panel {
min-height: 0;
display: grid;
grid-template-rows: auto auto minmax(0, 1fr) auto;
padding: 16px 18px;
overflow: hidden;
border: 0;
border-radius: var(--radius);
@@ -39,84 +36,14 @@
box-shadow: 0 1px 3px rgba(0, 0, 0, .10), 0 1px 2px rgba(0, 0, 0, .06);
}
.system-logs-list .document-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.system-logs-list .filter-set,
.system-logs-list .document-actions {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.system-logs-list .list-search {
position: relative;
width: 280px;
}
.system-logs-list .list-search .mdi {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: #64748b;
font-size: 15px;
}
.system-logs-list .list-search input {
width: 100%;
height: 38px;
padding: 0 12px 0 36px;
border: 1px solid #d7e0ea;
border-radius: 4px;
background: #fff;
color: #0f172a;
font-size: 13px;
transition: border-color 160ms ease, box-shadow 160ms ease;
}
.system-logs-list .list-search input::placeholder {
color: #8da0b4;
}
.system-logs-list .list-search input:focus {
border-color: var(--theme-primary);
box-shadow: 0 0 0 3px rgba(58, 124, 165, 0.14);
outline: none;
}
.system-logs-list .document-filter {
position: relative;
}
.system-logs-list .filter-btn {
min-width: 120px;
min-height: 38px;
display: inline-flex;
align-items: center;
justify-content: space-between;
gap: 9px;
padding: 0 14px;
border: 1px solid #d7e0ea;
border-radius: 4px;
background: #fff;
color: #334155;
font-size: 14px;
font-weight: 750;
white-space: nowrap;
}
.system-logs-list .filter-btn:hover,
.system-logs-list .document-filter.open .filter-btn {
border-color: rgba(58, 124, 165, .32);
color: var(--theme-primary-active);
}
.system-logs-list .status-dropdown-filter,
.system-logs-list .status-filter-trigger,
.system-logs-list .status-filter-menu {
@@ -143,9 +70,9 @@
}
.system-logs-list .document-filter-menu button {
display: block;
width: 100%;
min-height: 36px;
display: block;
padding: 0 12px;
border: 0;
border-radius: 4px;
@@ -159,148 +86,22 @@
.system-logs-list .document-filter-menu button:hover,
.system-logs-list .document-filter-menu button.active {
background: rgba(58, 124, 165, 0.1);
background: rgba(var(--theme-primary-rgb, 58, 124, 165), 0.1);
color: var(--theme-primary-active);
}
.system-logs-list .create-request-btn {
min-height: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 0 18px;
border: 0;
border-radius: 4px;
background: linear-gradient(135deg, var(--theme-primary), var(--theme-primary-active));
color: #fff;
font-size: 14px;
font-weight: 800;
white-space: nowrap;
box-shadow: 0 10px 24px var(--theme-primary-shadow);
transition: transform 160ms ease, box-shadow 160ms ease, filter 160ms ease;
}
.system-logs-list .create-request-btn.secondary {
border: 1px solid #d7e0ea;
background: #fff;
color: #334155;
box-shadow: none;
}
.system-logs-list .create-request-btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 14px 28px var(--theme-primary-shadow);
filter: saturate(1.02);
}
.system-logs-list .create-request-btn.secondary:hover:not(:disabled) {
border-color: rgba(58, 124, 165, .32);
color: var(--theme-primary-active);
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.08);
}
.system-logs-list .create-request-btn:disabled {
cursor: not-allowed;
opacity: .72;
transform: none;
}
.system-logs-list .hint {
display: inline-flex;
align-items: center;
gap: 7px;
margin: 10px 0 0;
color: #64748b;
font-size: 13px;
}
.system-logs-list .table-wrap {
min-height: 400px;
margin-top: 10px;
display: flex;
flex-direction: column;
overflow: auto;
border: 1px solid #edf2f7;
border-radius: 10px;
background: linear-gradient(180deg, #fcfefd 0%, #f4f8f6 100%);
}
.system-logs-list .table-wrap.is-empty {
align-items: center;
justify-content: center;
}
.system-logs-list .table-state {
width: 100%;
min-height: 260px;
display: grid;
place-items: center;
gap: 10px;
padding: 28px 20px;
text-align: center;
color: #64748b;
background: linear-gradient(180deg, #fcfffd 0%, #f5f9f7 100%);
}
.system-logs-list .system-log-table {
width: 100%;
min-width: 1260px;
align-self: flex-start;
border-collapse: collapse;
table-layout: fixed;
}
.system-logs-list .col-time { width: 13%; }
.system-logs-list .col-level { width: 8%; }
.system-logs-list .col-event { width: 13%; }
.system-logs-list .col-module { width: 15%; }
.system-logs-list .col-outcome { width: 8%; }
.system-logs-list .col-summary { width: 27%; }
.system-logs-list .col-request { width: 16%; }
.system-logs-list th,
.system-logs-list td {
padding: 13px 12px;
border-bottom: 1px solid #edf2f7;
color: #24324a;
font-size: 14px;
line-height: 1.35;
text-align: center;
vertical-align: middle;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.system-logs-list th {
position: sticky;
top: 0;
z-index: 1;
background: #f7fafc;
color: #64748b;
font-size: 13px;
font-weight: 800;
}
.system-logs-list tbody tr {
cursor: pointer;
}
.system-logs-list tbody tr:hover {
background: linear-gradient(90deg, rgba(58, 124, 165, .08), rgba(58, 124, 165, .03));
}
.system-logs-list tbody tr:last-child td {
border-bottom: 0;
}
.system-logs-list .summary-cell {
text-align: left;
white-space: normal;
@@ -342,7 +143,7 @@
justify-content: center;
padding: 0 9px;
border: 1px solid transparent;
border-radius: 6px;
border-radius: 4px;
font-size: 12px;
font-weight: 750;
white-space: nowrap;
@@ -389,111 +190,12 @@
text-align: center;
}
.system-logs-list .list-foot {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: 16px;
margin-top: 12px;
}
.system-logs-list .page-summary {
color: #64748b;
font-size: 14px;
font-weight: 650;
}
.system-logs-list .pager {
display: inline-flex;
justify-content: center;
gap: 6px;
padding: 4px;
border: 1px solid #e2e8f0;
border-radius: 12px;
background: #f8fafc;
}
.system-logs-list .pager button,
.system-logs-list .page-ellipsis {
width: 32px;
height: 32px;
display: inline-grid;
place-items: center;
}
.system-logs-list .pager button {
border: 0;
border-radius: 9px;
background: transparent;
color: #334155;
font-size: 14px;
font-weight: 800;
}
.system-logs-list .page-ellipsis {
color: #94a3b8;
font-size: 13px;
font-weight: 800;
}
.system-logs-list .pager button:hover:not(.active):not(:disabled) {
background: #fff;
color: var(--theme-primary-active);
box-shadow: 0 1px 4px rgba(15, 23, 42, .08);
}
.system-logs-list .pager button.active {
background: var(--theme-primary-active);
color: #fff;
box-shadow: 0 8px 16px var(--theme-primary-shadow);
}
.system-logs-list .pager button:disabled {
color: #cbd5e1;
cursor: not-allowed;
opacity: 1;
}
.system-logs-list .page-size-select {
width: 118px;
justify-self: end;
}
@media (max-width: 1200px) {
.system-logs-list .document-toolbar {
align-items: stretch;
flex-direction: column;
}
.system-logs-list .document-actions {
justify-content: flex-start;
}
.system-logs-list .list-foot {
grid-template-columns: 1fr;
}
}
@media (max-width: 760px) {
.logs-view {
padding: 0;
}
.system-logs-list.panel {
padding: 16px;
}
.system-logs-list .filter-set,
.system-logs-list .document-actions,
.system-logs-list .list-search,
.system-logs-list .filter-btn,
.system-logs-list .page-size-select {
width: 100%;
}
.system-logs-list .document-filter-menu {
width: 100%;
}
}

View File

@@ -4,146 +4,15 @@
display: grid;
grid-template-rows: minmax(0, 1fr);
gap: 14px;
animation: fadeUp 220ms var(--ease) both;
overflow: hidden;
animation: fadeUp 220ms var(--ease) both;
}
.travel-list {
min-height: 0;
display: grid;
grid-template-rows: auto auto auto minmax(0, 1fr) auto;
padding: 16px 18px;
overflow: hidden;
}
.list-search {
position: relative;
width: 220px;
}
.list-search .mdi {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: #64748b;
font-size: 15px;
}
.list-search input {
width: 100%;
height: 38px;
padding: 0 12px 0 36px;
border: 1px solid #d7e0ea;
border-radius: 8px;
background: #fff;
color: #0f172a;
font-size: 13px;
transition: border-color 160ms ease, box-shadow 160ms ease;
}
.list-search input::placeholder {
color: #8da0b4;
}
.list-search input:focus {
border-color: var(--theme-primary);
box-shadow: 0 0 0 3px var(--theme-focus-ring);
outline: none;
}
.status-tabs {
display: flex;
gap: 28px;
margin-top: 14px;
border-bottom: 1px solid #dbe4ee;
}
.status-tabs button {
position: relative;
min-height: 36px;
border: 0;
background: transparent;
color: #64748b;
font-size: 14px;
font-weight: 750;
}
.status-tabs button.active {
color: var(--theme-primary-active);
}
.status-tabs button.active::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: -1px;
height: 3px;
border-radius: 999px 999px 0 0;
background: var(--theme-primary);
}
.list-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-top: 14px;
}
.filter-set {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.create-request-btn {
min-height: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 0 18px;
border: 0;
border-radius: 10px;
background: var(--theme-gradient-primary);
color: #fff;
font-size: 14px;
font-weight: 800;
white-space: nowrap;
box-shadow: 0 10px 24px var(--theme-primary-shadow);
transition: transform 160ms ease, box-shadow 160ms ease, filter 160ms ease;
}
.create-request-btn:hover {
transform: translateY(-1px);
box-shadow: 0 14px 28px rgba(var(--theme-primary-rgb), 0.24);
filter: saturate(1.02);
}
.filter-btn {
min-height: 38px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 9px;
padding: 0 14px;
border-radius: 8px;
font-size: 14px;
font-weight: 750;
white-space: nowrap;
border: 1px solid #d7e0ea;
background: #fff;
color: #334155;
}
.filter-btn {
min-width: 120px;
justify-content: space-between;
}
.date-range-filter {
position: relative;
}
@@ -153,25 +22,25 @@
}
.date-range-label {
max-width: 110px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 110px;
}
.date-range-popover {
position: absolute;
top: calc(100% + 8px);
left: 0;
width: 320px;
z-index: 40;
width: 320px;
display: grid;
gap: 14px;
padding: 16px;
border: 1px solid #d7e0ea;
border-radius: 12px;
border-radius: 4px;
background: #fff;
box-shadow: 0 18px 42px rgba(15, 23, 42, .16);
box-shadow: 0 18px 42px rgba(15, 23, 42, 0.16);
}
.date-range-popover header,
@@ -193,7 +62,7 @@
display: grid;
place-items: center;
border: 0;
border-radius: 8px;
border-radius: 4px;
background: transparent;
color: #64748b;
}
@@ -225,7 +94,7 @@
height: 38px;
padding: 0 9px;
border: 1px solid #d7e0ea;
border-radius: 8px;
border-radius: 4px;
color: #0f172a;
font-size: 13px;
}
@@ -240,7 +109,7 @@
.apply-btn {
height: 36px;
padding: 0 14px;
border-radius: 8px;
border-radius: 4px;
font-size: 13px;
font-weight: 750;
}
@@ -262,158 +131,20 @@
background: #cbd5e1;
}
.filter-btn:hover {
border-color: rgba(var(--theme-primary-rgb), .32);
color: var(--theme-primary-active);
}
.hint {
display: inline-flex;
align-items: center;
gap: 7px;
margin-top: 10px;
color: #64748b;
font-size: 13px;
}
.hint .mdi {
color: #64748b;
}
.table-wrap {
min-height: 400px;
margin-top: 10px;
overflow-x: auto;
overflow-y: auto;
border: 1px solid #edf2f7;
border-radius: 10px;
background: linear-gradient(180deg, #fcfefd 0%, var(--theme-primary-light-9) 100%);
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: flex-start;
}
.table-wrap.is-empty {
align-items: center;
justify-content: center;
}
/* 让表格在flex容器中正常显示 */
.table-wrap table {
width: 100%;
align-self: flex-start;
}
.table-state {
width: 100%;
min-height: 260px;
display: grid;
place-items: center;
gap: 10px;
padding: 28px 20px;
text-align: center;
color: #64748b;
background: linear-gradient(180deg, #fcfffd 0%, var(--theme-primary-light-9) 100%);
align-self: center;
}
.table-state .mdi {
font-size: 28px;
color: var(--theme-primary);
}
.table-state strong {
color: #0f172a;
font-size: 15px;
}
.table-state p {
max-width: 420px;
margin: 0;
font-size: 13px;
line-height: 1.6;
}
.table-state.error {
background: linear-gradient(180deg, #fffdfd 0%, #fff6f6 100%);
}
.table-state.error .mdi {
color: #ef4444;
}
.retry-btn {
height: 36px;
padding: 0 14px;
border: 1px solid #f1c5c5;
border-radius: 8px;
background: #fff;
color: #b91c1c;
font-size: 13px;
font-weight: 750;
}
table {
width: 100%;
min-width: 1080px;
border-collapse: collapse;
table-layout: fixed;
}
colgroup col {
width: 10%;
}
th,
td {
padding: 13px 12px;
border-bottom: 1px solid #edf2f7;
color: #24324a;
font-size: 14px;
line-height: 1.35;
text-align: center;
vertical-align: middle;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
th {
position: sticky;
top: 0;
z-index: 1;
background: #f7fafc;
color: #64748b;
font-size: 13px;
font-weight: 800;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
tbody tr {
cursor: pointer;
}
tbody tr:hover {
background: linear-gradient(90deg, rgba(var(--theme-primary-rgb), .08), rgba(var(--theme-primary-rgb), .03));
}
tbody tr:last-child td {
border-bottom: 0;
}
.doc-id {
color: var(--theme-primary-active);
font-weight: 800;
}
.col-id { width: 13%; }
.col-type { width: 12%; }
.col-title { width: 19%; }
.col-occurred,
.col-apply { width: 13%; }
.col-amount { width: 11%; }
.col-node { width: 11%; }
.col-approval { width: 8%; }
.type-tag {
min-height: 26px;
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 26px;
padding: 0 10px;
border-radius: 999px;
font-size: 12px;
@@ -421,7 +152,9 @@ tbody tr:last-child td {
white-space: nowrap;
}
.type-tag.travel {
.type-tag.travel,
.type-tag.hotel,
.type-tag.transport {
background: var(--theme-primary-light-9);
color: var(--theme-primary-active);
}
@@ -431,12 +164,6 @@ tbody tr:last-child td {
color: #ea580c;
}
.type-tag.hotel,
.type-tag.transport {
background: var(--theme-primary-light-9);
color: var(--theme-primary-active);
}
.type-tag.meal {
background: #fef3c7;
color: #b45309;
@@ -464,7 +191,7 @@ tbody tr:last-child td {
align-items: center;
padding: 0 9px;
border: 1px solid transparent;
border-radius: 6px;
border-radius: 4px;
font-size: 12px;
font-weight: 750;
white-space: nowrap;
@@ -506,96 +233,13 @@ tbody tr:last-child td {
color: #475569;
}
.list-foot {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: 16px;
margin-top: 12px;
}
.page-summary {
color: #64748b;
font-size: 14px;
font-weight: 650;
}
.pager {
display: inline-flex;
justify-content: center;
gap: 6px;
padding: 4px;
border: 1px solid #e2e8f0;
border-radius: 12px;
background: #f8fafc;
}
.pager button {
width: 32px;
height: 32px;
border: 0;
border-radius: 9px;
background: transparent;
color: #334155;
font-size: 14px;
font-weight: 800;
transition: background 160ms ease, color 160ms ease, box-shadow 160ms ease;
}
.pager button:hover:not(.active) {
background: #fff;
color: var(--theme-primary-active);
box-shadow: 0 1px 4px rgba(15, 23, 42, .08);
}
.pager button.active {
background: var(--theme-primary);
color: #fff;
box-shadow: 0 8px 16px var(--theme-primary-shadow);
}
.page-nav {
color: #64748b;
}
.page-size-select {
width: 112px;
justify-self: end;
}
@media (max-width: 1200px) {
.list-toolbar,
.list-foot {
grid-template-columns: 1fr;
}
}
@media (max-width: 760px) {
.travel-list {
padding: 16px;
}
.status-tabs {
gap: 18px;
overflow-x: auto;
}
.filter-btn,
.page-size-select {
.date-range-filter,
.date-range-trigger {
width: 100%;
}
.filter-set {
width: 100%;
}
.list-foot {
display: grid;
justify-items: stretch;
}
.pager,
.page-size-select {
justify-self: stretch;
.date-range-popover {
width: min(320px, calc(100vw - 48px));
}
}

View File

@@ -0,0 +1,22 @@
<template>
<article class="detail-card panel enterprise-detail-card" :class="{ wide }">
<div v-if="title || description || $slots.actions" class="card-head">
<div>
<h3 v-if="title">{{ title }}</h3>
<p v-if="description">{{ description }}</p>
</div>
<slot name="actions"></slot>
</div>
<slot></slot>
</article>
</template>
<script setup>
defineProps({
description: { type: String, default: '' },
title: { type: String, default: '' },
wide: { type: Boolean, default: false }
})
</script>

View File

@@ -0,0 +1,71 @@
<template>
<article class="enterprise-detail-page" :class="variant">
<div class="detail-scroll">
<TableLoadingState
v-if="loading"
class="detail-loading-state panel"
variant="panel"
:title="loadingTitle"
:message="loadingMessage"
:icon="loadingIcon"
:show-skeleton="false"
/>
<section v-else-if="error" class="detail-inline-state panel error">
<slot name="error" :error="error">
<i class="mdi mdi-alert-circle-outline"></i>
<div>
<strong>{{ errorTitle }}</strong>
<p>{{ error }}</p>
</div>
</slot>
</section>
<template v-else>
<section v-if="$slots.hero" class="detail-hero panel">
<slot name="hero"></slot>
</section>
<slot></slot>
<div v-if="$slots.main || $slots.side" class="detail-grid">
<section v-if="$slots.main" class="detail-main">
<slot name="main"></slot>
</section>
<aside v-if="$slots.side" class="detail-side">
<slot name="side"></slot>
</aside>
</div>
</template>
</div>
<footer v-if="backLabel || $slots.actions" class="detail-actions">
<button v-if="backLabel" class="back-action" type="button" @click="emit('back')">
<i class="mdi mdi-arrow-left"></i>
<span>{{ backLabel }}</span>
</button>
<div v-if="$slots.actions" class="detail-action-group">
<slot name="actions"></slot>
</div>
</footer>
</article>
</template>
<script setup>
import TableLoadingState from './TableLoadingState.vue'
defineProps({
backLabel: { type: String, default: '' },
error: { type: String, default: '' },
errorTitle: { type: String, default: '详情加载失败' },
loading: { type: Boolean, default: false },
loadingIcon: { type: String, default: 'mdi mdi-file-document-outline' },
loadingMessage: { type: String, default: '' },
loadingTitle: { type: String, default: '正在加载详情' },
variant: { type: String, default: '' }
})
const emit = defineEmits(['back'])
</script>

View File

@@ -0,0 +1,197 @@
<template>
<article class="enterprise-list-page panel" :class="variant">
<slot name="before"></slot>
<nav v-if="hasTabs" class="status-tabs" :aria-label="tabsLabel">
<slot
name="tabs"
:tabs="normalizedTabs"
:active-tab="activeTab"
:select-tab="selectTab"
>
<button
v-for="tab in normalizedTabs"
:key="tab.value"
type="button"
:class="{ active: activeTab === tab.value }"
@click="selectTab(tab.value)"
>
<span>{{ tab.label }}</span>
<small v-if="tab.count !== null">{{ tab.count }}</small>
</button>
</slot>
</nav>
<div v-if="hasToolbar" class="list-toolbar">
<slot name="toolbar">
<div v-if="hasFilterRegion" class="filter-set">
<slot name="filters">
<label v-if="searchable" class="list-search">
<i class="mdi mdi-magnify"></i>
<input
:value="keyword"
type="search"
:placeholder="searchPlaceholder"
@input="emit('update:keyword', $event.target.value)"
/>
</label>
</slot>
</div>
<div v-if="$slots.actions" class="toolbar-actions">
<slot name="actions"></slot>
</div>
</slot>
</div>
<p v-if="hasHint" class="hint">
<slot name="hint">
<i v-if="hintIcon" :class="hintIcon"></i>
<span>{{ hint }}</span>
</slot>
</p>
<div class="table-wrap" :class="{ 'is-empty': empty, 'has-error': Boolean(error) }">
<div v-if="loading" class="table-state">
<TableLoadingState
:title="loadingTitle"
:message="loadingMessage"
:icon="loadingIcon"
/>
</div>
<div v-else-if="error" class="table-state error">
<slot name="error" :error="error">
<i class="mdi mdi-alert-circle-outline"></i>
<strong>{{ errorTitle }}</strong>
<p>{{ error }}</p>
<button v-if="retryLabel" class="state-action retry-btn" type="button" @click="emit('retry')">
{{ retryLabel }}
</button>
</slot>
</div>
<slot v-else-if="empty" name="empty">
<TableEmptyState
v-if="emptyState"
:eyebrow="emptyState.eyebrow"
:title="emptyState.title"
:description="emptyState.desc || emptyState.description"
:icon="emptyState.icon"
:action-label="emptyState.actionLabel"
:action-icon="emptyState.actionIcon"
:tone="emptyState.tone"
:art-label="emptyState.artLabel"
:tips="emptyState.tips || []"
@action="emit('empty-action')"
/>
</slot>
<slot v-else name="table"></slot>
</div>
<slot name="footer">
<EnterprisePagination
v-if="showPagination"
:current-page="currentPage"
:page-size="pageSize"
:page-size-options="pageSizeOptions"
:pages="pages"
:show-page-size="showPageSize"
:summary="summary"
:total="total"
:total-pages="totalPages"
@update:current-page="emit('update:currentPage', $event)"
@update:page-size="emit('update:pageSize', $event)"
@page-size-change="emit('page-size-change', $event)"
/>
</slot>
</article>
</template>
<script setup>
import { computed, useSlots } from 'vue'
import EnterprisePagination from './EnterprisePagination.vue'
import TableEmptyState from './TableEmptyState.vue'
import TableLoadingState from './TableLoadingState.vue'
const props = defineProps({
activeTab: { type: [String, Number], default: '' },
currentPage: { type: Number, default: 1 },
empty: { type: Boolean, default: false },
emptyState: { type: Object, default: null },
error: { type: String, default: '' },
errorTitle: { type: String, default: '列表加载失败' },
hint: { type: String, default: '' },
hintIcon: { type: String, default: 'mdi mdi-information-outline' },
keyword: { type: String, default: '' },
loading: { type: Boolean, default: false },
loadingIcon: { type: String, default: 'mdi mdi-file-document-outline' },
loadingMessage: { type: String, default: '' },
loadingTitle: { type: String, default: '数据同步中' },
pageSize: { type: Number, default: 10 },
pageSizeOptions: {
type: Array,
default: () => []
},
pages: {
type: Array,
default: () => []
},
retryLabel: { type: String, default: '重新加载' },
searchable: { type: Boolean, default: false },
searchPlaceholder: { type: String, default: '搜索' },
showPageSize: { type: Boolean, default: true },
showPagination: { type: Boolean, default: false },
summary: { type: String, default: '' },
tabs: {
type: Array,
default: () => []
},
tabsLabel: { type: String, default: '列表视图' },
total: { type: Number, default: 0 },
totalPages: { type: Number, default: 1 },
variant: { type: String, default: '' }
})
const emit = defineEmits([
'empty-action',
'page-size-change',
'retry',
'update:activeTab',
'update:currentPage',
'update:keyword',
'update:pageSize'
])
const slots = useSlots()
const normalizedTabs = computed(() =>
props.tabs.map((tab) => {
if (tab && typeof tab === 'object') {
const value = tab.value ?? tab.label ?? ''
return {
value,
label: String(tab.label ?? value),
count: tab.count ?? null
}
}
return {
value: tab,
label: String(tab),
count: null
}
})
)
const hasTabs = computed(() => normalizedTabs.value.length > 0 || Boolean(slots.tabs))
const hasToolbar = computed(() => props.searchable || Boolean(slots.filters || slots.actions || slots.toolbar))
const hasFilterRegion = computed(() => props.searchable || Boolean(slots.filters))
const hasHint = computed(() => Boolean(props.hint || slots.hint))
function selectTab(tab) {
emit('update:activeTab', tab)
}
</script>

View File

@@ -0,0 +1,107 @@
<template>
<footer class="list-foot enterprise-pagination">
<span class="page-summary">{{ summaryText }}</span>
<div class="pager" aria-label="分页">
<button
class="page-nav"
type="button"
:disabled="currentPage <= 1"
aria-label="上一页"
@click="setPage(currentPage - 1)"
>
<i class="mdi mdi-chevron-left"></i>
</button>
<template v-for="item in pageItems" :key="String(item)">
<span v-if="item === 'ellipsis'" class="page-ellipsis" aria-hidden="true">...</span>
<button
v-else
class="page-number"
:class="{ active: currentPage === item }"
type="button"
:aria-current="currentPage === item ? 'page' : undefined"
@click="setPage(item)"
>
{{ item }}
</button>
</template>
<button
class="page-nav"
type="button"
:disabled="currentPage >= totalPages"
aria-label="下一页"
@click="setPage(currentPage + 1)"
>
<i class="mdi mdi-chevron-right"></i>
</button>
</div>
<EnterpriseSelect
v-if="showPageSize"
class="page-size-select"
:model-value="pageSize"
:options="pageSizeOptions"
size="small"
@change="setPageSize"
/>
</footer>
</template>
<script setup>
import { computed } from 'vue'
import EnterpriseSelect from './EnterpriseSelect.vue'
const props = defineProps({
currentPage: { type: Number, default: 1 },
pageSize: { type: Number, default: 10 },
pageSizeOptions: {
type: Array,
default: () => []
},
pages: {
type: Array,
default: () => []
},
showPageSize: { type: Boolean, default: true },
summary: { type: String, default: '' },
total: { type: Number, default: 0 },
totalPages: { type: Number, default: 1 }
})
const emit = defineEmits(['update:currentPage', 'update:pageSize', 'page-size-change'])
const pageItems = computed(() => {
if (props.pages.length) {
return props.pages
}
return Array.from({ length: props.totalPages }, (_, index) => index + 1)
})
const summaryText = computed(() => {
if (props.summary) {
return props.summary
}
return `${props.total} 条,当前第 ${props.currentPage}`
})
function setPage(page) {
if (page === 'ellipsis') {
return
}
const nextPage = Math.min(Math.max(Number(page) || 1, 1), props.totalPages)
if (nextPage !== props.currentPage) {
emit('update:currentPage', nextPage)
}
}
function setPageSize(size) {
emit('update:pageSize', size)
emit('page-size-change', size)
}
</script>

View File

@@ -10,6 +10,7 @@ import { installThemeSkin } from './composables/useThemeSkin.js'
import { installSessionNavigation } from './composables/useSystemState.js'
import './assets/styles/element-plus-theme.css'
import './assets/styles/detail-page-corners.css'
import './assets/styles/components/enterprise-page-shell.css'
const app = createApp(App)

View File

@@ -10,21 +10,25 @@
@request-deleted="handleDetailDeleted"
/>
<article v-else class="approval-list panel">
<nav class="status-tabs" aria-label="审批状态">
<button
v-for="tab in tabs"
:key="tab"
type="button"
:class="{ active: activeTab === tab }"
@click="activeTab = tab"
<EnterpriseListPage
v-else
v-model:active-tab="activeTab"
class="approval-list"
:tabs="tabs"
tabs-label="审批状态"
:loading="loading"
:error="error"
:empty="showEmpty"
:empty-state="approvalEmptyState"
loading-title="审批待办同步中"
loading-message="正在加载当前可见的待审批报销单据"
loading-icon="mdi mdi-clipboard-check-outline"
error-title="审批列表加载失败"
:show-pagination="false"
@retry="reload"
@empty-action="handleEmptyAction"
>
{{ tab }}
</button>
</nav>
<div class="list-toolbar">
<div class="filter-set">
<template #filters>
<div class="list-search">
<i class="mdi mdi-magnify"></i>
<input v-model="listKeyword" type="search" placeholder="搜索单号、申请人、部门、报销类型..." />
@@ -34,42 +38,15 @@
<span>{{ filter }}</span>
<i class="mdi mdi-chevron-down"></i>
</button>
</div>
</div>
</template>
<p class="hint"><i class="mdi mdi-information-outline"></i> 点击单据行查看审批详情</p>
<template #hint>
<i class="mdi mdi-information-outline"></i>
点击单据行查看审批详情
</template>
<div class="table-wrap" :class="{ 'is-empty': showEmpty }">
<div v-if="loading" class="table-state">
<TableLoadingState
title="审批待办同步中"
message="正在加载当前可见的待审报销单据"
icon="mdi mdi-clipboard-check-outline"
/>
</div>
<div v-else-if="error" class="table-state error">
<i class="mdi mdi-alert-circle-outline"></i>
<strong>审批列表加载失败</strong>
<p>{{ error }}</p>
<button class="state-action" type="button" @click="reload">重新加载</button>
</div>
<TableEmptyState
v-else-if="showEmpty"
:eyebrow="approvalEmptyState.eyebrow"
:title="approvalEmptyState.title"
:description="approvalEmptyState.desc"
:icon="approvalEmptyState.icon"
:action-label="approvalEmptyState.actionLabel"
:action-icon="approvalEmptyState.actionIcon"
:tone="approvalEmptyState.tone"
:art-label="approvalEmptyState.artLabel"
:tips="approvalEmptyState.tips"
@action="handleEmptyAction"
/>
<table v-else>
<template #table>
<table>
<colgroup>
<col><col><col><col><col><col><col><col><col><col><col>
</colgroup>
@@ -113,8 +90,8 @@
</tr>
</tbody>
</table>
</div>
</article>
</template>
</EnterpriseListPage>
</section>
</template>

View File

@@ -9,21 +9,25 @@
@request-deleted="reload"
/>
<article v-else class="approval-list panel">
<nav class="status-tabs" aria-label="归档分类">
<button
v-for="tab in tabs"
:key="tab"
type="button"
:class="{ active: activeTab === tab }"
@click="activeTab = tab"
<EnterpriseListPage
v-else
v-model:active-tab="activeTab"
class="approval-list"
:tabs="tabs"
tabs-label="归档分类"
:loading="loading"
:error="error"
:empty="showEmpty"
:empty-state="archiveEmptyState"
loading-title="归档数据同步中"
loading-message="正在加载公司已归档的报销单据"
loading-icon="mdi mdi-archive-check-outline"
error-title="归档列表加载失败"
:show-pagination="false"
@retry="reload"
@empty-action="handleEmptyAction"
>
{{ tab }}
</button>
</nav>
<div class="list-toolbar">
<div class="filter-set">
<template #filters>
<div class="list-search">
<i class="mdi mdi-magnify"></i>
<input v-model="listKeyword" type="search" placeholder="搜索单号、申请人、部门、归档类型..." />
@@ -58,42 +62,15 @@
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<p class="hint"><i class="mdi mdi-information-outline"></i> 归档中心保存公司已付款或已归档的报销数据点击单据行查看详情</p>
<template #hint>
<i class="mdi mdi-information-outline"></i>
归档中心保存公司已付款或已归档的报销数据点击单据行查看详情
</template>
<div class="table-wrap" :class="{ 'is-empty': showEmpty }">
<div v-if="loading" class="table-state">
<TableLoadingState
title="归档数据同步中"
message="正在加载公司已归档的报销单据"
icon="mdi mdi-archive-check-outline"
/>
</div>
<div v-else-if="error" class="table-state error">
<i class="mdi mdi-alert-circle-outline"></i>
<strong>归档列表加载失败</strong>
<p>{{ error }}</p>
<button class="state-action" type="button" @click="reload">重新加载</button>
</div>
<TableEmptyState
v-else-if="showEmpty"
:eyebrow="archiveEmptyState.eyebrow"
:title="archiveEmptyState.title"
:description="archiveEmptyState.desc"
:icon="archiveEmptyState.icon"
:action-label="archiveEmptyState.actionLabel"
:action-icon="archiveEmptyState.actionIcon"
:tone="archiveEmptyState.tone"
:art-label="archiveEmptyState.artLabel"
:tips="archiveEmptyState.tips"
@action="handleEmptyAction"
/>
<table v-else>
<template #table>
<table>
<colgroup>
<col><col><col><col><col><col><col><col><col>
</colgroup>
@@ -129,8 +106,8 @@
</tr>
</tbody>
</table>
</div>
</article>
</template>
</EnterpriseListPage>
</section>
</template>

View File

@@ -6,9 +6,24 @@
</div>
<template v-else>
<article class="system-logs-list panel">
<div class="document-toolbar">
<div class="filter-set">
<EnterpriseListPage
v-model:current-page="currentPage"
v-model:page-size="pageSize"
class="system-logs-list"
:loading="systemLogLoading && !visibleSystemLogEntries.length"
:empty="!systemLogLoading && !visibleSystemLogEntries.length"
:total="totalCount"
:total-pages="totalPages"
:pages="visiblePageItems"
:page-size-options="pageSizeOptions"
:summary="`共 ${totalCount} 条系统日志,当前第 ${currentPage} 页`"
:show-pagination="!systemLogLoading && filteredSystemLogEntries.length > 0"
loading-title="系统日志同步中"
loading-message="正在加载系统运行日志记录"
loading-icon="mdi mdi-text-box-search-outline"
@page-size-change="changePageSize"
>
<template #filters>
<div class="list-search">
<i class="mdi mdi-magnify"></i>
<input
@@ -78,9 +93,9 @@
</button>
</div>
</div>
</div>
</template>
<div class="document-actions">
<template #actions>
<button v-if="hasActiveFilters" class="create-request-btn secondary" type="button" @click="resetFilters">
<i class="mdi mdi-filter-remove-outline"></i>
<span>清空筛选</span>
@@ -94,24 +109,21 @@
<i class="mdi mdi-refresh"></i>
<span>{{ systemLogLoading ? '刷新中...' : '刷新日志' }}</span>
</button>
</div>
</div>
</template>
<p class="hint">
<template #hint>
<i class="mdi mdi-information-outline"></i>
点击任意行可查看系统日志详情
</p>
</template>
<div class="table-wrap" :class="{ 'is-empty': !systemLogLoading && !filteredSystemLogEntries.length }">
<div v-if="systemLogLoading && !visibleSystemLogEntries.length" class="table-state">
<TableLoadingState
title="系统日志同步中"
message="正在加载系统运行日志记录"
icon="mdi mdi-text-box-search-outline"
/>
<template #empty>
<div class="inline-empty">
当前筛选条件下没有系统日志记录。
</div>
</template>
<table v-else-if="visibleSystemLogEntries.length" class="system-log-table">
<template #table>
<table class="system-log-table">
<colgroup>
<col class="col-time">
<col class="col-level">
@@ -155,48 +167,12 @@
<strong>{{ entry.summary || entry.message }}</strong>
<span>{{ formatSummary(entry.message) }}</span>
</td>
<td class="trace-cell">{{ entry.request_id || '—' }}</td>
<td class="trace-cell">{{ entry.request_id || '-' }}</td>
</tr>
</tbody>
</table>
<div v-else class="inline-empty">
当前筛选条件下没有系统日志记录
</div>
</div>
<footer v-if="!systemLogLoading && filteredSystemLogEntries.length" class="list-foot">
<span class="page-summary"> {{ totalCount }} 条系统日志目前第 {{ currentPage }} </span>
<div class="pager" aria-label="分页">
<button class="page-nav" type="button" :disabled="currentPage === 1" aria-label="上一页" @click="currentPage--">
<i class="mdi mdi-chevron-left"></i>
</button>
<template v-for="page in visiblePageItems" :key="page === 'ellipsis' ? 'ellipsis' : page">
<span v-if="page === 'ellipsis'" class="page-ellipsis" aria-hidden="true">...</span>
<button
v-else
class="page-number"
:class="{ active: currentPage === page }"
type="button"
:aria-current="currentPage === page ? 'page' : undefined"
@click="currentPage = page"
>
{{ page }}
</button>
</template>
<button class="page-nav" type="button" :disabled="currentPage === totalPages" aria-label="下一页" @click="currentPage++">
<i class="mdi mdi-chevron-right"></i>
</button>
</div>
<EnterpriseSelect
v-model="pageSize"
class="page-size-select"
:options="pageSizeOptions"
size="small"
@change="changePageSize"
/>
</footer>
</article>
</EnterpriseListPage>
</template>
</section>
</template>

View File

@@ -1,20 +1,29 @@
<template>
<section class="travel-page">
<article class="travel-list panel">
<nav class="status-tabs" aria-label="个人报销状态">
<button
v-for="tab in tabs"
:key="tab"
type="button"
:class="{ active: activeTab === tab }"
@click="activeTab = tab"
<EnterpriseListPage
v-model:active-tab="activeTab"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
class="travel-list"
:tabs="tabs"
tabs-label="个人报销状态"
:loading="loading"
:error="error"
:empty="showEmpty"
:empty-state="emptyState"
:total="totalCount"
:total-pages="totalPages"
:page-size-options="pageSizeOptions"
:show-pagination="showTable"
loading-title="真实报销数据同步中"
loading-message="正在加载后端返回的个人报销单据"
loading-icon="mdi mdi-file-document-outline"
error-title="报销列表加载失败"
@retry="emit('reload')"
@empty-action="handleEmptyAction"
@page-size-change="changePageSize"
>
{{ tab }}
</button>
</nav>
<div class="list-toolbar">
<div class="filter-set">
<template #filters>
<div class="list-search">
<i class="mdi mdi-magnify"></i>
<input v-model="listKeyword" type="search" placeholder="搜索单号、事由、报销类型..." />
@@ -51,46 +60,22 @@
<span>{{ filter }}</span>
<i class="mdi mdi-chevron-down"></i>
</button>
</div>
</template>
<template #actions>
<button class="create-request-btn" type="button" @click="emit('create-request')">
<i class="mdi mdi-plus-circle-outline"></i>
<span>发起报销</span>
</button>
</div>
</template>
<p class="hint"><i class="mdi mdi-information-outline"></i> 点击任意行可查看单据详情</p>
<template #hint>
<i class="mdi mdi-information-outline"></i>
点击任意行可查看单据详情
</template>
<div class="table-wrap" :class="{ 'is-empty': showEmpty }">
<div v-if="loading" class="table-state">
<TableLoadingState
title="真实报销数据同步中"
message="正在加载后端返回的个人报销单据"
icon="mdi mdi-file-document-outline"
/>
</div>
<div v-else-if="error" class="table-state error">
<i class="mdi mdi-alert-circle-outline"></i>
<strong>报销列表加载失败</strong>
<p>{{ error }}</p>
<button class="retry-btn" type="button" @click="emit('reload')">重新加载</button>
</div>
<TableEmptyState
v-else-if="showEmpty"
:eyebrow="emptyState.eyebrow"
:title="emptyState.title"
:description="emptyState.desc"
:icon="emptyState.icon"
:action-label="emptyState.actionLabel"
:action-icon="emptyState.actionIcon"
:tone="emptyState.tone"
:art-label="emptyState.artLabel"
:tips="emptyState.tips"
@action="handleEmptyAction"
/>
<table v-else>
<template #table>
<table>
<colgroup>
<col class="col-id">
<col class="col-type">
@@ -126,24 +111,8 @@
</tr>
</tbody>
</table>
</div>
<footer v-if="showTable" class="list-foot">
<span class="page-summary"> {{ totalCount }} 目前第 {{ currentPage }} </span>
<div class="pager" aria-label="分页">
<button class="page-nav" type="button" :disabled="currentPage === 1" aria-label="上一页" @click="currentPage--"><i class="mdi mdi-chevron-left"></i></button>
<button v-for="p in totalPages" :key="p" class="page-number" :class="{ active: currentPage === p }" type="button" :aria-current="currentPage === p ? 'page' : undefined" @click="currentPage = p">{{ p }}</button>
<button class="page-nav" type="button" :disabled="currentPage === totalPages" aria-label="下一页" @click="currentPage++"><i class="mdi mdi-chevron-right"></i></button>
</div>
<EnterpriseSelect
v-model="pageSize"
class="page-size-select"
:options="pageSizeOptions"
size="small"
@change="changePageSize"
/>
</footer>
</article>
</template>
</EnterpriseListPage>
</section>
</template>

View File

@@ -1,7 +1,6 @@
import { computed, ref, watch } from 'vue'
import TableLoadingState from '../../components/shared/TableLoadingState.vue'
import TableEmptyState from '../../components/shared/TableEmptyState.vue'
import EnterpriseListPage from '../../components/shared/EnterpriseListPage.vue'
import { useApprovalInbox } from '../../composables/useApprovalInbox.js'
import { useSystemState } from '../../composables/useSystemState.js'
import { fetchApprovalExpenseClaims } from '../../services/reimbursements.js'
@@ -115,9 +114,8 @@ function buildApprovalRow(request) {
export default {
name: 'ApprovalCenterView',
components: {
TravelRequestDetailView,
TableLoadingState,
TableEmptyState
EnterpriseListPage,
TravelRequestDetailView
},
setup() {
const { currentUser } = useSystemState()

View File

@@ -1,7 +1,6 @@
import { computed, ref } from 'vue'
import TableLoadingState from '../../components/shared/TableLoadingState.vue'
import TableEmptyState from '../../components/shared/TableEmptyState.vue'
import EnterpriseListPage from '../../components/shared/EnterpriseListPage.vue'
import { mapExpenseClaimToRequest } from '../../composables/useRequests.js'
import { fetchArchivedExpenseClaims } from '../../services/reimbursements.js'
import {
@@ -92,9 +91,8 @@ function resolveFilterLabel(options, activeValue, fallbackLabel) {
export default {
name: 'ArchiveCenterView',
components: {
TravelRequestDetailView,
TableLoadingState,
TableEmptyState
EnterpriseListPage,
TravelRequestDetailView
},
setup() {
const activeTab = ref(ARCHIVE_TAB_ALL)

View File

@@ -1,8 +1,7 @@
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import EnterpriseSelect from '../../components/shared/EnterpriseSelect.vue'
import TableLoadingState from '../../components/shared/TableLoadingState.vue'
import EnterpriseListPage from '../../components/shared/EnterpriseListPage.vue'
import { useSystemState } from '../../composables/useSystemState.js'
import { useToast } from '../../composables/useToast.js'
import { fetchSystemLogEntries } from '../../services/systemLogs.js'
@@ -62,8 +61,7 @@ function resolveSystemOutcomeTone(outcome) {
export default {
name: 'LogsView',
components: {
EnterpriseSelect,
TableLoadingState
EnterpriseListPage
},
emits: ['summary-change'],
setup(_, { emit }) {

View File

@@ -1,8 +1,6 @@
import { computed, ref, watch } from 'vue'
import EnterpriseSelect from '../../components/shared/EnterpriseSelect.vue'
import TableLoadingState from '../../components/shared/TableLoadingState.vue'
import TableEmptyState from '../../components/shared/TableEmptyState.vue'
import EnterpriseListPage from '../../components/shared/EnterpriseListPage.vue'
import { normalizeRequestForUi } from '../../utils/requestViewModel.js'
function extractRowDate(value) {
@@ -13,9 +11,7 @@ function extractRowDate(value) {
export default {
name: 'RequestsView',
components: {
EnterpriseSelect,
TableLoadingState,
TableEmptyState
EnterpriseListPage
},
props: {
filteredRequests: { type: Array, required: true },