Compare commits
2 Commits
e384318046
...
codex/list
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
064eeb614f | ||
|
|
b383244a29 |
2672
server/uv.lock
generated
Normal file
2672
server/uv.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -241,7 +241,8 @@
|
||||
.main.settings-main {
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
}
|
||||
.main.audit-detail-main {
|
||||
.main.audit-detail-main,
|
||||
.main.digital-employees-detail-main {
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
}
|
||||
.workarea { min-height: 0; overflow: auto; padding: 24px; }
|
||||
|
||||
@@ -1,69 +1,31 @@
|
||||
.digital-work-records {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-rows: auto auto minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.work-records-head {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(420px, 0.8fr);
|
||||
.digital-work-records .work-records-head {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.work-records-head h3 {
|
||||
.digital-work-records .work-records-head h3 {
|
||||
margin: 0;
|
||||
color: #0f172a;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.work-records-head p {
|
||||
.digital-work-records .work-records-head p {
|
||||
margin: 6px 0 0;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.work-records-kpis {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
justify-self: end;
|
||||
width: min(100%, 480px);
|
||||
}
|
||||
|
||||
.work-record-kpi {
|
||||
min-height: 58px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #dfe7ef;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.work-record-kpi span {
|
||||
display: block;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.work-record-kpi strong {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
color: #0f172a;
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.work-record-kpi.success strong {
|
||||
color: var(--success-active);
|
||||
}
|
||||
|
||||
.work-record-kpi.danger strong {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.work-records-toolbar {
|
||||
.digital-work-records .work-records-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -72,12 +34,129 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.work-records-toolbar button {
|
||||
min-height: 34px;
|
||||
.digital-work-records .work-records-table-wrap.is-empty {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.digital-work-records .work-records-table-wrap {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.digital-work-records .filter-set {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.digital-work-records .list-search {
|
||||
position: relative;
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.digital-work-records .list-search .mdi {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #64748b;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.digital-work-records .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;
|
||||
}
|
||||
|
||||
.digital-work-records .list-search input::placeholder {
|
||||
color: #8da0b4;
|
||||
}
|
||||
|
||||
.digital-work-records .list-search input:focus {
|
||||
border-color: var(--theme-primary);
|
||||
box-shadow: 0 0 0 3px rgba(58, 124, 165, 0.14);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.digital-work-records .filter-btn {
|
||||
min-height: 38px;
|
||||
min-width: 120px;
|
||||
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;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.digital-work-records .filter-btn:hover {
|
||||
border-color: rgba(58, 124, 165, .32);
|
||||
color: var(--theme-primary-active);
|
||||
}
|
||||
|
||||
.digital-work-records .work-records-filter {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.digital-work-records .work-records-filter-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
left: 0;
|
||||
min-width: 150px;
|
||||
max-height: 280px;
|
||||
padding: 6px;
|
||||
z-index: 40;
|
||||
border: 1px solid #d7e0ea;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 16px 32px rgba(15, 23, 42, 0.12);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.digital-work-records .work-records-filter-menu button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 36px;
|
||||
padding: 0 12px;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: #334155;
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.digital-work-records .work-records-filter-menu button:hover,
|
||||
.digital-work-records .work-records-filter-menu button.active {
|
||||
background: rgba(58, 124, 165, 0.1);
|
||||
color: var(--theme-primary-active);
|
||||
}
|
||||
|
||||
.digital-work-records .refresh-btn {
|
||||
min-height: 38px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 12px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid #d8e1eb;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
@@ -87,45 +166,44 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.work-records-toolbar button:disabled {
|
||||
.digital-work-records .refresh-btn:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.work-records-table-wrap {
|
||||
min-height: 400px;
|
||||
overflow: auto;
|
||||
border: 1px solid #edf2f7;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(180deg, #fcfefd 0%, #f4f8f6 100%);
|
||||
}
|
||||
|
||||
.work-records-table-wrap.is-empty {
|
||||
display: grid;
|
||||
.digital-work-records .list-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-content: flex-end;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.digital-work-records-table {
|
||||
.digital-work-records .page-summary {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.digital-work-records .digital-work-records-table {
|
||||
width: 100%;
|
||||
min-width: 1180px;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.digital-work-records-table .col-time { width: 14%; }
|
||||
.digital-work-records .digital-work-records-table .col-time { width: 14%; }
|
||||
|
||||
.digital-work-records-table .col-module { width: 12%; }
|
||||
.digital-work-records .digital-work-records-table .col-module { width: 12%; }
|
||||
|
||||
.digital-work-records-table .col-source { width: 10%; }
|
||||
.digital-work-records .digital-work-records-table .col-source { width: 10%; }
|
||||
|
||||
.digital-work-records-table .col-status { width: 17%; }
|
||||
.digital-work-records .digital-work-records-table .col-status { width: 17%; }
|
||||
|
||||
.digital-work-records-table .col-summary { width: 31%; }
|
||||
.digital-work-records .digital-work-records-table .col-summary { width: 31%; }
|
||||
|
||||
.digital-work-records-table .col-trace { width: 16%; }
|
||||
.digital-work-records .digital-work-records-table .col-trace { width: 16%; }
|
||||
|
||||
.digital-work-records-table thead th {
|
||||
.digital-work-records .digital-work-records-table thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
@@ -140,7 +218,7 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.digital-work-records-table tbody td {
|
||||
.digital-work-records .digital-work-records-table tbody td {
|
||||
padding: 13px 12px;
|
||||
border-bottom: 1px solid #edf2f7;
|
||||
color: #24324a;
|
||||
@@ -150,56 +228,73 @@
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.digital-work-records-table tbody tr {
|
||||
.digital-work-records .digital-work-records-table tbody tr {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.digital-work-records-table tbody tr:hover,
|
||||
.digital-work-records-table tbody tr:focus-visible {
|
||||
.digital-work-records .digital-work-records-table tbody tr:hover,
|
||||
.digital-work-records .digital-work-records-table tbody tr:focus-visible {
|
||||
background: linear-gradient(90deg, rgba(58, 124, 165, .08), rgba(58, 124, 165, .03));
|
||||
}
|
||||
|
||||
.digital-work-records-table tbody tr:focus-visible {
|
||||
.digital-work-records .digital-work-records-table tbody tr:focus-visible {
|
||||
box-shadow: inset 0 0 0 2px rgba(58, 124, 165, .28);
|
||||
}
|
||||
|
||||
.digital-work-records-table tbody tr:last-child td {
|
||||
.digital-work-records .digital-work-records-table tbody tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.work-record-status-stack {
|
||||
.digital-work-records .work-records-table {
|
||||
min-width: 1180px;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.digital-work-records .work-records-table .col-time { width: 14%; }
|
||||
|
||||
.digital-work-records .work-records-table .col-module { width: 13%; }
|
||||
|
||||
.digital-work-records .work-records-table .col-source { width: 10%; }
|
||||
|
||||
.digital-work-records .work-records-table .col-status { width: 16%; }
|
||||
|
||||
.digital-work-records .work-records-table .col-summary { width: 31%; }
|
||||
|
||||
.digital-work-records .work-records-table .col-trace { width: 16%; }
|
||||
|
||||
.digital-work-records .work-record-status-stack {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.work-record-status-stack > span:last-child {
|
||||
.digital-work-records .work-record-status-stack > span:last-child {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.work-record-summary-cell {
|
||||
.digital-work-records .work-record-summary-cell {
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.work-record-summary-cell strong,
|
||||
.work-record-summary-cell span,
|
||||
.work-record-summary-cell em {
|
||||
.digital-work-records .work-record-summary-cell strong,
|
||||
.digital-work-records .work-record-summary-cell span,
|
||||
.digital-work-records .work-record-summary-cell em {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.work-record-summary-cell strong {
|
||||
.digital-work-records .work-record-summary-cell strong {
|
||||
color: #0f172a;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.work-record-summary-cell span {
|
||||
.digital-work-records .work-record-summary-cell span {
|
||||
margin-top: 4px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
@@ -207,7 +302,7 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.work-record-summary-cell em {
|
||||
.digital-work-records .work-record-summary-cell em {
|
||||
margin-top: 6px;
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
@@ -215,12 +310,12 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.work-record-trace-cell {
|
||||
.digital-work-records .work-record-trace-cell {
|
||||
color: #2563eb !important;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
.digital-work-records .status-pill {
|
||||
min-height: 24px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -233,38 +328,38 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-pill.success {
|
||||
.digital-work-records .status-pill.success {
|
||||
border-color: var(--success-line);
|
||||
background: var(--success-soft);
|
||||
color: var(--success-active);
|
||||
}
|
||||
|
||||
.status-pill.warning {
|
||||
.digital-work-records .status-pill.warning {
|
||||
border-color: #fed7aa;
|
||||
background: #fff7ed;
|
||||
color: #f97316;
|
||||
}
|
||||
|
||||
.status-pill.danger {
|
||||
.digital-work-records .status-pill.danger {
|
||||
border-color: #fecaca;
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.status-pill.info {
|
||||
.digital-work-records .status-pill.info {
|
||||
border-color: #bfdbfe;
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.status-pill.muted {
|
||||
.digital-work-records .status-pill.muted {
|
||||
border-color: #cbd5e1;
|
||||
background: #f8fafc;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.table-state,
|
||||
.work-records-empty {
|
||||
.digital-work-records .table-state,
|
||||
.digital-work-records .work-records-empty {
|
||||
width: 100%;
|
||||
min-height: 260px;
|
||||
display: grid;
|
||||
@@ -276,83 +371,73 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.table-state.error {
|
||||
.digital-work-records .table-state.error {
|
||||
background: linear-gradient(180deg, #fffdfd 0%, #fff6f6 100%);
|
||||
}
|
||||
|
||||
.table-state.error .mdi {
|
||||
.digital-work-records .table-state.error .mdi {
|
||||
color: #ef4444;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.table-state.error strong {
|
||||
.digital-work-records .table-state.error strong {
|
||||
color: #0f172a;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.table-state.error p {
|
||||
.digital-work-records .table-state.error p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.work-record-detail-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2800;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
background: rgba(15, 23, 42, .28);
|
||||
.digital-work-records.is-detail {
|
||||
display: grid;
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.work-record-detail-panel {
|
||||
width: min(720px, calc(100vw - 32px));
|
||||
.digital-work-records .work-record-detail-page {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
border-left: 1px solid #dfe7ef;
|
||||
background: #fff;
|
||||
box-shadow: -18px 0 42px rgba(15, 23, 42, .18);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.work-record-detail-head {
|
||||
min-height: 76px;
|
||||
.digital-work-records .work-record-detail-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
padding: 16px 18px;
|
||||
border-bottom: 1px solid #edf2f7;
|
||||
min-height: 44px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.work-record-detail-head span {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.work-record-detail-head h3 {
|
||||
margin: 5px 0 0;
|
||||
color: #0f172a;
|
||||
font-size: 17px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.work-record-detail-head button {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: grid;
|
||||
flex: 0 0 auto;
|
||||
place-items: center;
|
||||
.digital-work-records .work-record-detail-toolbar .back-action,
|
||||
.digital-work-records .work-record-detail-toolbar .refresh-btn {
|
||||
height: 36px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid #d8e1eb;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
color: #64748b;
|
||||
color: #0f172a;
|
||||
font-size: 13px;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.work-record-detail-head button:hover {
|
||||
.digital-work-records .work-record-detail-toolbar button:hover:not(:disabled) {
|
||||
border-color: rgba(var(--theme-primary-rgb, 58, 124, 165), .34);
|
||||
background: #f5fbff;
|
||||
color: var(--theme-primary-active);
|
||||
}
|
||||
|
||||
.work-record-detail-body {
|
||||
.digital-work-records .work-record-detail-shell {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.digital-work-records .work-record-detail-body {
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
align-content: start;
|
||||
@@ -362,18 +447,24 @@
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.work-record-detail-section,
|
||||
.work-record-detail-state {
|
||||
.digital-work-records .work-record-detail-body.inline-detail {
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.digital-work-records .work-record-detail-section,
|
||||
.digital-work-records .work-record-detail-state {
|
||||
border: 1px solid #e5edf5;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.work-record-detail-section {
|
||||
.digital-work-records .work-record-detail-section {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.work-record-detail-state {
|
||||
.digital-work-records .work-record-detail-state {
|
||||
min-height: 100%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
@@ -383,20 +474,20 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.work-record-detail-state.error .mdi {
|
||||
.digital-work-records .work-record-detail-state.error .mdi {
|
||||
color: #dc2626;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.work-record-detail-state.error strong {
|
||||
.digital-work-records .work-record-detail-state.error strong {
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.work-record-detail-state.error p {
|
||||
.digital-work-records .work-record-detail-state.error p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.work-record-detail-state.error button {
|
||||
.digital-work-records .work-record-detail-state.error button {
|
||||
height: 34px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid #fecaca;
|
||||
@@ -406,7 +497,7 @@
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.work-record-section-head {
|
||||
.digital-work-records .work-record-section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -414,25 +505,25 @@
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.work-record-section-head h4 {
|
||||
.digital-work-records .work-record-section-head h4 {
|
||||
margin: 0;
|
||||
color: #0f172a;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.work-record-section-head > span:not(.status-pill) {
|
||||
.digital-work-records .work-record-section-head > span:not(.status-pill) {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.work-record-info-grid {
|
||||
.digital-work-records .work-record-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.work-record-info-grid div {
|
||||
.digital-work-records .work-record-info-grid div {
|
||||
min-width: 0;
|
||||
padding: 10px;
|
||||
border: 1px solid #edf2f7;
|
||||
@@ -440,14 +531,14 @@
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.work-record-info-grid span {
|
||||
.digital-work-records .work-record-info-grid span {
|
||||
display: block;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.work-record-info-grid strong {
|
||||
.digital-work-records .work-record-info-grid strong {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
overflow-wrap: anywhere;
|
||||
@@ -455,16 +546,16 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.work-record-result-text,
|
||||
.work-record-error-text,
|
||||
.work-record-inline-empty {
|
||||
.digital-work-records .work-record-result-text,
|
||||
.digital-work-records .work-record-error-text,
|
||||
.digital-work-records .work-record-inline-empty {
|
||||
margin: 0;
|
||||
color: #475569;
|
||||
font-size: 13px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.work-record-error-text {
|
||||
.digital-work-records .work-record-error-text {
|
||||
margin-top: 10px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #fecaca;
|
||||
@@ -473,12 +564,12 @@
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.work-record-tool-list {
|
||||
.digital-work-records .work-record-tool-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.work-record-tool-list article {
|
||||
.digital-work-records .work-record-tool-list article {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -489,17 +580,17 @@
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.work-record-tool-list strong {
|
||||
.digital-work-records .work-record-tool-list strong {
|
||||
color: #0f172a;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.work-record-tool-list span {
|
||||
.digital-work-records .work-record-tool-list span {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.work-record-code-block {
|
||||
.digital-work-records .work-record-code-block {
|
||||
max-height: 320px;
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
@@ -512,37 +603,13 @@
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.work-record-detail-enter-active,
|
||||
.work-record-detail-leave-active {
|
||||
transition: opacity 180ms ease;
|
||||
}
|
||||
|
||||
.work-record-detail-enter-active .work-record-detail-panel,
|
||||
.work-record-detail-leave-active .work-record-detail-panel {
|
||||
transition: transform 220ms ease;
|
||||
}
|
||||
|
||||
.work-record-detail-enter-from,
|
||||
.work-record-detail-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.work-record-detail-enter-from .work-record-detail-panel,
|
||||
.work-record-detail-leave-to .work-record-detail-panel {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.work-records-head {
|
||||
.digital-work-records .work-records-head {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.work-records-kpis {
|
||||
justify-self: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.work-record-info-grid {
|
||||
.digital-work-records .work-record-info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
438
web/src/assets/styles/components/enterprise-page-shell.css
Normal file
438
web/src/assets/styles/components/enterprise-page-shell.css
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
}
|
||||
|
||||
.digital-work-records-section {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article class="detail-card panel json-risk-flow-card digital-worker-source-card">
|
||||
<article class="detail-card panel json-risk-summary-card digital-worker-source-card">
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<h3>Skills Markdown 源文件</h3>
|
||||
|
||||
337
web/src/components/audit/DigitalEmployeeListPanel.vue
Normal file
337
web/src/components/audit/DigitalEmployeeListPanel.vue
Normal file
@@ -0,0 +1,337 @@
|
||||
<template>
|
||||
<section class="digital-employee-list-panel">
|
||||
<div class="list-toolbar">
|
||||
<div class="filter-set">
|
||||
<label class="search-filter">
|
||||
<i class="mdi mdi-magnify"></i>
|
||||
<input
|
||||
:value="keyword"
|
||||
type="search"
|
||||
placeholder="搜索数字员工技能、编号、执行计划或维护人"
|
||||
@input="emit('update:keyword', $event.target.value)"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<AuditPickerFilter
|
||||
id="status"
|
||||
title="选择资产状态"
|
||||
close-label="关闭资产状态选择"
|
||||
:active-filter-popover="activeFilterPopover"
|
||||
:label="selectedStatusLabel"
|
||||
:options="statusOptions"
|
||||
:selected-value="selectedStatus"
|
||||
@toggle="emit('toggle-filter-popover', $event)"
|
||||
@close="emit('close-filter-popover')"
|
||||
@select="selectFilter('status', $event)"
|
||||
/>
|
||||
|
||||
<AuditPickerFilter
|
||||
id="enabled"
|
||||
title="选择启动状态"
|
||||
close-label="关闭启动状态选择"
|
||||
:active-filter-popover="activeFilterPopover"
|
||||
:label="selectedEnabledLabel"
|
||||
:options="enabledStateOptions"
|
||||
:selected-value="selectedEnabledState"
|
||||
@toggle="emit('toggle-filter-popover', $event)"
|
||||
@close="emit('close-filter-popover')"
|
||||
@select="selectFilter('enabled', $event)"
|
||||
/>
|
||||
|
||||
<AuditPickerFilter
|
||||
id="executionMode"
|
||||
title="选择执行方式"
|
||||
close-label="关闭执行方式选择"
|
||||
:active-filter-popover="activeFilterPopover"
|
||||
:label="selectedExecutionModeLabel"
|
||||
:options="executionModeOptions"
|
||||
:selected-value="selectedExecutionMode"
|
||||
@toggle="emit('toggle-filter-popover', $event)"
|
||||
@close="emit('close-filter-popover')"
|
||||
@select="selectFilter('executionMode', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-actions">
|
||||
<button
|
||||
v-if="keyword || activeFilterTokens.length"
|
||||
class="ghost-filter-btn"
|
||||
type="button"
|
||||
@click="emit('reset-filters')"
|
||||
>
|
||||
<i class="mdi mdi-filter-remove-outline"></i>
|
||||
<span>清空筛选</span>
|
||||
</button>
|
||||
<button
|
||||
class="create-btn digital-refresh-action"
|
||||
type="button"
|
||||
:disabled="loading"
|
||||
@click="emit('load-employees')"
|
||||
>
|
||||
<i class="mdi mdi-refresh"></i>
|
||||
<span>{{ loading ? '刷新中...' : '刷新' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="hint">
|
||||
<i class="mdi mdi-information-outline"></i>
|
||||
归集后台自动执行的数字员工技能,可查看技能内容、执行计划、启动状态和最近版本。
|
||||
</p>
|
||||
|
||||
<div v-if="activeFilterTokens.length" class="active-filter-strip">
|
||||
<span v-for="token in activeFilterTokens" :key="token" class="active-filter-chip">
|
||||
{{ token }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="table-wrap digital-table-wrap"
|
||||
:class="{ 'is-empty': !loading && !errorMessage && !visibleEmployees.length }"
|
||||
>
|
||||
<div v-if="loading" class="table-state">
|
||||
<TableLoadingState
|
||||
variant="panel"
|
||||
title="数字员工资产同步中"
|
||||
message="正在加载数字员工资产"
|
||||
icon="mdi mdi-view-list-outline"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="errorMessage" class="table-state error">
|
||||
<i class="mdi mdi-alert-circle-outline"></i>
|
||||
<p>{{ errorMessage }}</p>
|
||||
</div>
|
||||
|
||||
<TableEmptyState
|
||||
v-else-if="!visibleEmployees.length"
|
||||
eyebrow="数字员工"
|
||||
title="暂无匹配的数字员工"
|
||||
description="当前没有符合搜索条件的后台执行技能。"
|
||||
icon="mdi mdi-account-cog-outline"
|
||||
tone="theme"
|
||||
art-label="STAFF"
|
||||
:tips="['数字员工已从规则中心拆出为独立入口', '运行与定时操作统一进入详情后处理']"
|
||||
/>
|
||||
|
||||
<table v-else class="digital-employees-table">
|
||||
<colgroup>
|
||||
<col class="col-skill">
|
||||
<col class="col-skill-type">
|
||||
<col class="col-owner">
|
||||
<col class="col-schedule">
|
||||
<col class="col-mode">
|
||||
<col class="col-status">
|
||||
<col class="col-enabled">
|
||||
<col class="col-updated">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>技能名称</th>
|
||||
<th>技能类型</th>
|
||||
<th>维护归口</th>
|
||||
<th>执行计划</th>
|
||||
<th>触发方式</th>
|
||||
<th>资产状态</th>
|
||||
<th>启动状态</th>
|
||||
<th>最近更新</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="employee in pagedEmployees"
|
||||
:key="employee.id"
|
||||
@click="emit('open-employee-detail', employee)"
|
||||
>
|
||||
<td>
|
||||
<div class="skill-name-cell">
|
||||
<span class="skill-avatar" :class="employee.badgeTone">{{ employee.short }}</span>
|
||||
<div>
|
||||
<strong>{{ employee.name }}</strong>
|
||||
<span class="skill-list-subtitle">{{ employee.summary || employee.code }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><span class="scope-pill skill-type-pill">{{ employee.skillCategory }}</span></td>
|
||||
<td>{{ employee.owner }}</td>
|
||||
<td><span class="scope-pill">{{ employee.scope }}</span></td>
|
||||
<td>{{ employee.executionMode }}</td>
|
||||
<td>
|
||||
<span :class="['status-pill', employee.statusTone]">{{ employee.status }}</span>
|
||||
</td>
|
||||
<td><span :class="['status-pill', employee.enabledTone]">{{ employee.enabledLabel }}</span></td>
|
||||
<td>{{ employee.updatedAt || '-' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<footer v-if="!loading && !errorMessage && visibleEmployees.length" class="list-foot digital-employee-pagination">
|
||||
<span class="page-summary">共 {{ visibleEmployees.length }} 条,目前第 {{ currentPage }} / {{ totalPages }} 页</span>
|
||||
<div class="pager" aria-label="员工技能分页">
|
||||
<button class="page-nav" type="button" :disabled="currentPage === 1" @click="currentPage--">
|
||||
<i class="mdi mdi-chevron-left"></i>
|
||||
</button>
|
||||
<button
|
||||
v-for="page in pageNumbers"
|
||||
:key="page"
|
||||
class="page-number"
|
||||
:class="{ active: currentPage === page }"
|
||||
type="button"
|
||||
@click="currentPage = page"
|
||||
>
|
||||
{{ page }}
|
||||
</button>
|
||||
<button class="page-nav" type="button" :disabled="currentPage === totalPages" @click="currentPage++">
|
||||
<i class="mdi mdi-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import AuditPickerFilter from './AuditPickerFilter.vue'
|
||||
import TableEmptyState from '../shared/TableEmptyState.vue'
|
||||
import TableLoadingState from '../shared/TableLoadingState.vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'DigitalEmployeeListPanel'
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
keyword: { type: String, default: '' },
|
||||
activeFilterPopover: { type: String, default: '' },
|
||||
selectedStatus: { type: String, default: '' },
|
||||
selectedStatusLabel: { type: String, default: '' },
|
||||
statusOptions: { type: Array, default: () => [] },
|
||||
selectedEnabledState: { type: String, default: '' },
|
||||
selectedEnabledLabel: { type: String, default: '' },
|
||||
enabledStateOptions: { type: Array, default: () => [] },
|
||||
selectedExecutionMode: { type: String, default: '' },
|
||||
selectedExecutionModeLabel: { type: String, default: '' },
|
||||
executionModeOptions: { type: Array, default: () => [] },
|
||||
activeFilterTokens: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false },
|
||||
errorMessage: { type: String, default: '' },
|
||||
visibleEmployees: { type: Array, default: () => [] }
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:keyword',
|
||||
'toggle-filter-popover',
|
||||
'close-filter-popover',
|
||||
'select-filter',
|
||||
'reset-filters',
|
||||
'load-employees',
|
||||
'open-employee-detail'
|
||||
])
|
||||
|
||||
const currentPage = ref(1)
|
||||
const pageSize = 20
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(props.visibleEmployees.length / pageSize)))
|
||||
const pagedEmployees = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize
|
||||
return props.visibleEmployees.slice(start, start + pageSize)
|
||||
})
|
||||
const pageNumbers = computed(() => {
|
||||
const total = totalPages.value
|
||||
if (total <= 7) {
|
||||
return Array.from({ length: total }, (_, index) => index + 1)
|
||||
}
|
||||
const start = Math.max(1, Math.min(currentPage.value - 3, total - 6))
|
||||
return Array.from({ length: 7 }, (_, index) => start + index)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => [props.keyword, props.selectedStatus, props.selectedEnabledState, props.selectedExecutionMode],
|
||||
() => {
|
||||
currentPage.value = 1
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.visibleEmployees.length,
|
||||
() => {
|
||||
currentPage.value = Math.min(currentPage.value, totalPages.value)
|
||||
if (currentPage.value < 1) {
|
||||
currentPage.value = 1
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function selectFilter(type, value) {
|
||||
emit('select-filter', type, value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped src="../../assets/styles/views/audit-view.css"></style>
|
||||
|
||||
<style scoped>
|
||||
.digital-employee-list-panel {
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.digital-employee-list-panel .digital-table-wrap {
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.digital-employee-list-panel .digital-employee-pagination {
|
||||
flex: 0 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.digital-employee-list-panel .digital-employee-pagination .page-summary {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.digital-employee-list-panel .pager {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 4px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.digital-employee-list-panel .pager button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 0;
|
||||
border-radius: 9px;
|
||||
background: transparent;
|
||||
color: #334155;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.digital-employee-list-panel .pager button:hover:not(.active):not(:disabled) {
|
||||
background: #fff;
|
||||
color: var(--theme-primary-active);
|
||||
box-shadow: 0 1px 4px rgba(15, 23, 42, .08);
|
||||
}
|
||||
|
||||
.digital-employee-list-panel .pager button.active {
|
||||
background: var(--theme-primary-active);
|
||||
color: #fff;
|
||||
box-shadow: 0 8px 16px var(--theme-primary-shadow);
|
||||
}
|
||||
|
||||
.digital-employee-list-panel .pager button:disabled {
|
||||
color: #cbd5e1;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
@@ -1,206 +1,330 @@
|
||||
<template>
|
||||
<section class="digital-work-records">
|
||||
<header class="work-records-head">
|
||||
<div>
|
||||
<h3>工作记录</h3>
|
||||
<p>查看数字员工近期执行记录、状态和结果摘要。</p>
|
||||
</div>
|
||||
|
||||
<div class="work-records-kpis" aria-label="工作记录统计">
|
||||
<article class="work-record-kpi">
|
||||
<span>日志总数</span>
|
||||
<strong>{{ totalCount }}</strong>
|
||||
</article>
|
||||
<article class="work-record-kpi success">
|
||||
<span>成功数量</span>
|
||||
<strong>{{ successCount }}</strong>
|
||||
</article>
|
||||
<article class="work-record-kpi danger">
|
||||
<span>失败数量</span>
|
||||
<strong>{{ failedCount }}</strong>
|
||||
</article>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="work-records-toolbar">
|
||||
<span>{{ loading ? '正在同步工作记录' : `当前展示 ${visibleRuns.length} 条记录` }}</span>
|
||||
<button type="button" :disabled="loading" @click="loadWorkRecords(true)">
|
||||
<i class="mdi mdi-refresh"></i>
|
||||
<span>{{ loading ? '刷新中...' : '刷新' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap work-records-table-wrap" :class="{ 'is-empty': !loading && !runs.length }">
|
||||
<div v-if="loading && !runs.length" class="table-state">
|
||||
<TableLoadingState
|
||||
variant="panel"
|
||||
title="工作记录同步中"
|
||||
message="正在读取数字员工近期执行记录"
|
||||
icon="mdi mdi-clipboard-text-clock-outline"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="errorMessage" class="table-state error">
|
||||
<i class="mdi mdi-alert-circle-outline"></i>
|
||||
<strong>工作记录加载失败</strong>
|
||||
<p>{{ errorMessage }}</p>
|
||||
</div>
|
||||
|
||||
<table v-else-if="runs.length" class="digital-work-records-table">
|
||||
<colgroup>
|
||||
<col class="col-time">
|
||||
<col class="col-module">
|
||||
<col class="col-source">
|
||||
<col class="col-status">
|
||||
<col class="col-summary">
|
||||
<col class="col-trace">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>执行时间</th>
|
||||
<th>工作模块</th>
|
||||
<th>触发来源</th>
|
||||
<th>状态</th>
|
||||
<th>摘要</th>
|
||||
<th>Run ID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="run in visibleRuns"
|
||||
:key="run.run_id"
|
||||
class="work-record-row"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@click="openWorkRecordDetail(run)"
|
||||
@keydown.enter.prevent="openWorkRecordDetail(run)"
|
||||
>
|
||||
<td>{{ formatWorkRecordDateTime(run.started_at) }}</td>
|
||||
<td>{{ resolveWorkRecordModuleLabel(run) }}</td>
|
||||
<td>{{ resolveWorkRecordSourceLabel(run.source) }}</td>
|
||||
<td>
|
||||
<div class="work-record-status-stack">
|
||||
<span class="status-pill" :class="resolveWorkRecordStatusTone(run)">
|
||||
{{ resolveWorkRecordStatusLabel(run) }}
|
||||
</span>
|
||||
<span>{{ resolveWorkRecordStatusNote(run) }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="work-record-summary-cell">
|
||||
<strong>{{ resolveWorkRecordTitle(run) }}</strong>
|
||||
<span>{{ formatWorkRecordSummary(run.result_summary) }}</span>
|
||||
<em>{{ resolveWorkRecordSummaryMeta(run) }}</em>
|
||||
</td>
|
||||
<td class="work-record-trace-cell">{{ run.run_id }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div v-else class="work-records-empty">
|
||||
当前还没有数字员工工作记录。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Teleport to="body">
|
||||
<Transition name="work-record-detail">
|
||||
<div
|
||||
v-if="detailOpen"
|
||||
class="work-record-detail-mask"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="工作记录详情"
|
||||
@click.self="closeWorkRecordDetail"
|
||||
>
|
||||
<aside class="work-record-detail-panel">
|
||||
<header class="work-record-detail-head">
|
||||
<div>
|
||||
<span>工作记录详情</span>
|
||||
<h3>{{ selectedRunDetail ? resolveWorkRecordTitle(selectedRunDetail) : '工作记录' }}</h3>
|
||||
</div>
|
||||
<button type="button" aria-label="关闭工作记录详情" @click="closeWorkRecordDetail">
|
||||
<i class="mdi mdi-close"></i>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div v-if="detailLoading" class="work-record-detail-state">
|
||||
<TableLoadingState
|
||||
variant="panel"
|
||||
title="详情加载中"
|
||||
message="正在读取该次工作记录的完整执行信息"
|
||||
icon="mdi mdi-clipboard-text-search-outline"
|
||||
<section class="digital-employee-list-panel digital-work-records">
|
||||
<Transition name="skill-view" mode="out-in">
|
||||
<!-- 列表视图 -->
|
||||
<div v-if="!selectedRunDetail" key="list" class="digital-work-records-list-stage">
|
||||
<div class="list-toolbar">
|
||||
<div class="filter-set">
|
||||
<label class="search-filter">
|
||||
<i class="mdi mdi-magnify"></i>
|
||||
<input
|
||||
v-model="listKeyword"
|
||||
type="search"
|
||||
placeholder="搜索摘要、Run ID..."
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div v-else-if="detailError" class="work-record-detail-state error">
|
||||
<i class="mdi mdi-alert-circle-outline"></i>
|
||||
<strong>工作记录详情加载失败</strong>
|
||||
<p>{{ detailError }}</p>
|
||||
<button type="button" @click="reloadSelectedDetail">重新加载</button>
|
||||
</div>
|
||||
<AuditPickerFilter
|
||||
id="module"
|
||||
title="选择工作模块"
|
||||
close-label="关闭选择"
|
||||
:active-filter-popover="activeFilterPopover"
|
||||
:label="activeModule === '全部' ? '工作模块' : activeModule"
|
||||
:options="modulePickerOptions"
|
||||
:selected-value="activeModule"
|
||||
@toggle="toggleFilterPopover"
|
||||
@close="closeFilterPopover"
|
||||
@select="selectModule"
|
||||
/>
|
||||
|
||||
<div v-else-if="selectedRunDetail" class="work-record-detail-body">
|
||||
<section class="work-record-detail-section">
|
||||
<div class="work-record-section-head">
|
||||
<h4>基本信息</h4>
|
||||
<span class="status-pill" :class="resolveWorkRecordStatusTone(selectedRunDetail)">
|
||||
{{ resolveWorkRecordStatusLabel(selectedRunDetail) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="work-record-info-grid">
|
||||
<div><span>Run ID</span><strong>{{ selectedRunDetail.run_id }}</strong></div>
|
||||
<div><span>工作模块</span><strong>{{ resolveWorkRecordModuleLabel(selectedRunDetail) }}</strong></div>
|
||||
<div><span>触发来源</span><strong>{{ resolveWorkRecordSourceLabel(selectedRunDetail.source) }}</strong></div>
|
||||
<div><span>开始时间</span><strong>{{ formatWorkRecordDateTime(selectedRunDetail.started_at) }}</strong></div>
|
||||
<div><span>结束时间</span><strong>{{ formatWorkRecordDateTime(selectedRunDetail.finished_at) }}</strong></div>
|
||||
<div><span>状态说明</span><strong>{{ resolveWorkRecordStatusNote(selectedRunDetail) }}</strong></div>
|
||||
</div>
|
||||
</section>
|
||||
<AuditPickerFilter
|
||||
id="status"
|
||||
title="选择执行状态"
|
||||
close-label="关闭选择"
|
||||
:active-filter-popover="activeFilterPopover"
|
||||
:label="activeStatus === '全部' ? '执行状态' : activeStatus"
|
||||
:options="statusPickerOptions"
|
||||
:selected-value="activeStatus"
|
||||
@toggle="toggleFilterPopover"
|
||||
@close="closeFilterPopover"
|
||||
@select="selectStatus"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<section class="work-record-detail-section">
|
||||
<div class="work-record-section-head">
|
||||
<h4>执行摘要</h4>
|
||||
<span>{{ resolveWorkRecordSummaryMeta(selectedRunDetail) }}</span>
|
||||
</div>
|
||||
<p class="work-record-result-text">
|
||||
{{ selectedRunDetail.result_summary || '暂无执行摘要。' }}
|
||||
</p>
|
||||
<p v-if="selectedRunDetail.error_message" class="work-record-error-text">
|
||||
{{ selectedRunDetail.error_message }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="work-record-detail-section">
|
||||
<div class="work-record-section-head">
|
||||
<h4>工具调用</h4>
|
||||
<span>{{ (selectedRunDetail.tool_calls || []).length }} 条</span>
|
||||
</div>
|
||||
<div v-if="(selectedRunDetail.tool_calls || []).length" class="work-record-tool-list">
|
||||
<article v-for="toolCall in selectedRunDetail.tool_calls" :key="toolCall.id">
|
||||
<strong>{{ toolCall.tool_name }}</strong>
|
||||
<span>{{ toolCall.tool_type || 'tool' }} · {{ toolCall.status || 'unknown' }}</span>
|
||||
</article>
|
||||
</div>
|
||||
<div v-else class="work-record-inline-empty">当前暂无工具调用明细。</div>
|
||||
</section>
|
||||
|
||||
<section class="work-record-detail-section">
|
||||
<div class="work-record-section-head">
|
||||
<h4>执行上下文</h4>
|
||||
<span>JSON</span>
|
||||
</div>
|
||||
<pre class="work-record-code-block">{{ formatJson(selectedRunDetail.route_json) }}</pre>
|
||||
</section>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="toolbar-actions">
|
||||
<button
|
||||
v-if="listKeyword || activeFilterTokens.length"
|
||||
class="ghost-filter-btn"
|
||||
type="button"
|
||||
@click="resetFilters"
|
||||
>
|
||||
<i class="mdi mdi-filter-remove-outline"></i>
|
||||
<span>清空筛选</span>
|
||||
</button>
|
||||
<button
|
||||
class="create-btn digital-refresh-action"
|
||||
type="button"
|
||||
:disabled="loading"
|
||||
@click="loadWorkRecords(true)"
|
||||
>
|
||||
<i class="mdi mdi-refresh"></i>
|
||||
<span>{{ loading ? '刷新中...' : '刷新' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
|
||||
<p class="hint">
|
||||
<i class="mdi mdi-information-outline"></i>
|
||||
查看数字员工近期执行记录、状态和结果摘要。
|
||||
</p>
|
||||
|
||||
<div v-if="activeFilterTokens.length" class="active-filter-strip">
|
||||
<span v-for="token in activeFilterTokens" :key="token" class="active-filter-chip">
|
||||
{{ token }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap digital-table-wrap" :class="{ 'is-empty': !loading && !errorMessage && !visibleRuns.length }">
|
||||
<div v-if="loading && !runs.length" class="table-state">
|
||||
<TableLoadingState
|
||||
variant="panel"
|
||||
title="工作记录同步中"
|
||||
message="正在读取数字员工近期执行记录"
|
||||
icon="mdi mdi-clipboard-text-clock-outline"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="errorMessage" class="table-state error">
|
||||
<i class="mdi mdi-alert-circle-outline"></i>
|
||||
<p>{{ errorMessage }}</p>
|
||||
</div>
|
||||
|
||||
<TableEmptyState
|
||||
v-else-if="!visibleRuns.length"
|
||||
eyebrow="工作记录"
|
||||
title="暂无匹配的工作记录"
|
||||
description="当前没有符合搜索条件的数字员工工作记录。"
|
||||
icon="mdi mdi-clipboard-text-clock-outline"
|
||||
tone="theme"
|
||||
art-label="RECORDS"
|
||||
/>
|
||||
|
||||
<table v-else class="digital-employees-table digital-work-records-table">
|
||||
<colgroup>
|
||||
<col class="col-time">
|
||||
<col class="col-module">
|
||||
<col class="col-source">
|
||||
<col class="col-status">
|
||||
<col class="col-summary">
|
||||
<col class="col-trace">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>执行时间</th>
|
||||
<th>工作模块</th>
|
||||
<th>触发来源</th>
|
||||
<th>状态</th>
|
||||
<th>摘要</th>
|
||||
<th>Run ID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="run in visibleRuns"
|
||||
:key="run.run_id"
|
||||
class="work-record-row"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@click="openWorkRecordDetail(run)"
|
||||
@keydown.enter.prevent="openWorkRecordDetail(run)"
|
||||
>
|
||||
<td>{{ formatWorkRecordDateTime(run.started_at) }}</td>
|
||||
<td>{{ resolveWorkRecordModuleLabel(run) }}</td>
|
||||
<td>{{ resolveWorkRecordSourceLabel(run.source) }}</td>
|
||||
<td>
|
||||
<div class="work-record-status-stack">
|
||||
<span class="status-pill" :class="resolveWorkRecordStatusTone(run)">
|
||||
{{ resolveWorkRecordStatusLabel(run) }}
|
||||
</span>
|
||||
<span>{{ resolveWorkRecordStatusNote(run) }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="work-record-summary-cell">
|
||||
<strong>{{ resolveWorkRecordTitle(run) }}</strong>
|
||||
<span>{{ formatWorkRecordSummary(run.result_summary) }}</span>
|
||||
<em>{{ resolveWorkRecordSummaryMeta(run) }}</em>
|
||||
</td>
|
||||
<td class="work-record-trace-cell">{{ run.run_id }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<footer v-if="!loading && !errorMessage && visibleRuns.length" class="list-foot digital-employee-pagination">
|
||||
<span class="page-summary">共 {{ filteredRuns.length }} 条,目前第 {{ currentPage }} / {{ totalPages }} 页</span>
|
||||
<div class="pager" aria-label="工作记录分页">
|
||||
<button class="page-nav" type="button" :disabled="currentPage === 1" @click="currentPage--">
|
||||
<i class="mdi mdi-chevron-left"></i>
|
||||
</button>
|
||||
<button
|
||||
v-for="page in pageNumbers"
|
||||
:key="page"
|
||||
class="page-number"
|
||||
:class="{ active: currentPage === page }"
|
||||
type="button"
|
||||
@click="currentPage = page"
|
||||
>
|
||||
{{ page }}
|
||||
</button>
|
||||
<button class="page-nav" type="button" :disabled="currentPage === totalPages" @click="currentPage++">
|
||||
<i class="mdi mdi-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- 详情视图 (全屏样式,参考 AuditJsonRiskRuleDetail) -->
|
||||
<div v-else key="detail" class="json-risk-editor-shell panel work-records-detail-stage">
|
||||
<header class="json-risk-editor-head asset-detail-topbar list-toolbar">
|
||||
<div class="json-risk-editor-title asset-detail-topbar-main filter-set">
|
||||
<div class="json-risk-head-copy">
|
||||
<div class="json-risk-head-title-row">
|
||||
<h2>{{ resolveWorkRecordTitle(selectedRunDetail) }}</h2>
|
||||
</div>
|
||||
<p class="json-risk-head-subtitle">
|
||||
执行工作流:{{ resolveWorkRecordModuleLabel(selectedRunDetail) }}
|
||||
</p>
|
||||
<div class="json-risk-head-meta">
|
||||
<span>Run ID:{{ selectedRunDetail.run_id }}</span>
|
||||
<span>触发来源:{{ resolveWorkRecordSourceLabel(selectedRunDetail.source) }}</span>
|
||||
<span>开始时间:{{ formatWorkRecordDateTime(selectedRunDetail.started_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="json-risk-score-ring"
|
||||
:class="selectedRunDetail.status"
|
||||
>
|
||||
<strong style="font-size: 16px; font-weight: 900;">{{ resolveWorkRecordStatusLabel(selectedRunDetail) }}</strong>
|
||||
<span>运行状态</span>
|
||||
<em>{{ resolveWorkRecordStatusNote(selectedRunDetail) || '执行完毕' }}</em>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div v-if="detailLoading" class="work-record-detail-state panel" style="min-height: 200px; display: grid; place-items: center; border: 0;">
|
||||
<TableLoadingState
|
||||
variant="panel"
|
||||
title="详情加载中"
|
||||
message="正在读取该次工作记录的完整执行信息"
|
||||
icon="mdi mdi-clipboard-text-search-outline"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="detailError" class="work-record-detail-state error panel" style="min-height: 200px; display: grid; place-items: center; text-align: center; border: 0; color: #dc2626;">
|
||||
<i class="mdi mdi-alert-circle-outline" style="font-size: 32px; margin-bottom: 8px;"></i>
|
||||
<strong>工作记录详情加载失败</strong>
|
||||
<p>{{ detailError }}</p>
|
||||
<button class="minor-action" type="button" @click="reloadSelectedDetail" style="margin-top: 12px;">重新加载</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="json-risk-editor-body work-record-detail-shell">
|
||||
<section class="json-risk-main-stage work-record-detail-body inline-detail">
|
||||
<!-- 卡片1:基本信息 -->
|
||||
<article class="detail-card panel json-risk-summary-card">
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<h3>基本信息</h3>
|
||||
<p>此次运行的执行周期、触发来源、标识信息与最终状态。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="json-risk-meta-grid">
|
||||
<div class="json-risk-meta-item">
|
||||
<span class="json-risk-meta-label">Run ID</span>
|
||||
<span class="json-risk-meta-value">{{ selectedRunDetail.run_id }}</span>
|
||||
</div>
|
||||
<div class="json-risk-meta-item">
|
||||
<span class="json-risk-meta-label">工作模块</span>
|
||||
<span class="json-risk-meta-value">{{ resolveWorkRecordModuleLabel(selectedRunDetail) }}</span>
|
||||
</div>
|
||||
<div class="json-risk-meta-item">
|
||||
<span class="json-risk-meta-label">触发来源</span>
|
||||
<span class="json-risk-meta-value">{{ resolveWorkRecordSourceLabel(selectedRunDetail.source) }}</span>
|
||||
</div>
|
||||
<div class="json-risk-meta-item">
|
||||
<span class="json-risk-meta-label">开始时间</span>
|
||||
<span class="json-risk-meta-value">{{ formatWorkRecordDateTime(selectedRunDetail.started_at) }}</span>
|
||||
</div>
|
||||
<div class="json-risk-meta-item">
|
||||
<span class="json-risk-meta-label">结束时间</span>
|
||||
<span class="json-risk-meta-value">{{ formatWorkRecordDateTime(selectedRunDetail.finished_at) }}</span>
|
||||
</div>
|
||||
<div class="json-risk-meta-item">
|
||||
<span class="json-risk-meta-label">状态说明</span>
|
||||
<span class="json-risk-meta-value">{{ resolveWorkRecordStatusNote(selectedRunDetail) || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- 卡片2:执行摘要 -->
|
||||
<article class="detail-card panel json-risk-description-card">
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<h3>执行摘要</h3>
|
||||
<p>本次数字员工工作流的执行内容与结果摘要。</p>
|
||||
</div>
|
||||
<span class="edit-badge">{{ resolveWorkRecordSummaryMeta(selectedRunDetail) }}</span>
|
||||
</div>
|
||||
<p class="json-risk-description-text" style="padding: 0 12px 12px; margin: 0;">{{ selectedRunDetail.result_summary || '暂无执行摘要。' }}</p>
|
||||
<p v-if="selectedRunDetail.error_message" class="work-record-error-text" style="margin: 0 12px 12px; padding: 10px 12px; border: 1px solid #fecaca; border-radius: 4px; background: #fef2f2; color: #b91c1c;">
|
||||
{{ selectedRunDetail.error_message }}
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<!-- 卡片3:工具调用 -->
|
||||
<article class="detail-card panel">
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<h3>工具调用</h3>
|
||||
<p>此任务在执行期间调用的外部系统/工具细节与执行状态。</p>
|
||||
</div>
|
||||
<span class="edit-badge">{{ (selectedRunDetail.tool_calls || []).length }} 次调用</span>
|
||||
</div>
|
||||
<div v-if="(selectedRunDetail.tool_calls || []).length" class="work-record-tool-list" style="padding: 0 12px 12px; display: grid; gap: 8px;">
|
||||
<article v-for="toolCall in selectedRunDetail.tool_calls" :key="toolCall.id" style="display: flex; align-items: center; justify-content: space-between; padding: 10px 12px; border: 1px solid #edf2f7; border-radius: 4px; background: #f8fafc;">
|
||||
<strong style="color: #0f172a; font-size: 13px;">{{ toolCall.tool_name }}</strong>
|
||||
<span style="color: #64748b; font-size: 12px;">{{ toolCall.tool_type || 'tool' }} · {{ toolCall.status || 'unknown' }}</span>
|
||||
</article>
|
||||
</div>
|
||||
<div v-else class="work-record-inline-empty" style="padding: 0 12px 12px; color: #94a3b8; font-size: 13px;">当前暂无工具调用明细。</div>
|
||||
</article>
|
||||
|
||||
<!-- 卡片4:执行上下文 -->
|
||||
<article class="detail-card panel">
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<h3>执行上下文</h3>
|
||||
<p>后台调度的运行时配置与状态信息(JSON 格式)。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding: 0 12px 12px;">
|
||||
<pre class="work-record-code-block" style="max-height: 320px; margin: 0; padding: 12px; overflow: auto; border: 1px solid #e2e8f0; border-radius: 4px; background: #0f172a; color: #e2e8f0; font-size: 12px; line-height: 1.55;">{{ formatJson(selectedRunDetail.route_json) }}</pre>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer class="detail-actions">
|
||||
<button class="back-action" type="button" @click="closeWorkRecordDetail">
|
||||
<i class="mdi mdi-arrow-left"></i>
|
||||
<span>返回工作记录列表</span>
|
||||
</button>
|
||||
<div class="detail-action-group">
|
||||
<button class="minor-action" type="button" :disabled="detailLoading" @click="reloadSelectedDetail">
|
||||
<i class="mdi mdi-refresh"></i>
|
||||
<span>{{ detailLoading ? '刷新中...' : '刷新详情' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</Transition>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import AuditPickerFilter from './AuditPickerFilter.vue'
|
||||
import TableEmptyState from '../shared/TableEmptyState.vue'
|
||||
import TableLoadingState from '../shared/TableLoadingState.vue'
|
||||
import { fetchAgentRunDetail, fetchAgentRuns } from '../../services/agentAssets.js'
|
||||
import { useToast } from '../../composables/useToast.js'
|
||||
@@ -221,7 +345,7 @@ defineOptions({
|
||||
name: 'DigitalEmployeeWorkRecords'
|
||||
})
|
||||
|
||||
const emit = defineEmits(['summary-change'])
|
||||
const emit = defineEmits(['summary-change', 'detail-open-change'])
|
||||
|
||||
const { toast } = useToast()
|
||||
const runs = ref([])
|
||||
@@ -232,12 +356,120 @@ const detailLoading = ref(false)
|
||||
const detailError = ref('')
|
||||
const selectedRunId = ref('')
|
||||
const selectedRunDetail = ref(null)
|
||||
|
||||
watch(detailOpen, (newVal) => {
|
||||
emit('detail-open-change', newVal)
|
||||
}, { immediate: true })
|
||||
let pollTimer = 0
|
||||
|
||||
const totalCount = computed(() => runs.value.length)
|
||||
const successCount = computed(() => runs.value.filter((run) => run.status === 'succeeded').length)
|
||||
const failedCount = computed(() => runs.value.filter((run) => run.status === 'failed').length)
|
||||
const visibleRuns = computed(() => runs.value.slice(0, 100))
|
||||
|
||||
const listKeyword = ref('')
|
||||
const activeModule = ref('全部')
|
||||
const activeStatus = ref('全部')
|
||||
const activeFilterPopover = ref('')
|
||||
|
||||
const modulePickerOptions = computed(() => {
|
||||
const set = new Set(runs.value.map((run) => resolveWorkRecordModuleLabel(run)))
|
||||
return [
|
||||
{ label: '全部工作模块', value: '全部' },
|
||||
...Array.from(set).map(m => ({ label: m, value: m }))
|
||||
]
|
||||
})
|
||||
|
||||
const statusPickerOptions = computed(() => {
|
||||
const set = new Set(runs.value.map((run) => resolveWorkRecordStatusLabel(run)))
|
||||
return [
|
||||
{ label: '全部执行状态', value: '全部' },
|
||||
...Array.from(set).map(s => ({ label: s, value: s }))
|
||||
]
|
||||
})
|
||||
|
||||
const activeFilterTokens = computed(() => {
|
||||
const tokens = []
|
||||
if (activeModule.value !== '全部') {
|
||||
tokens.push(activeModule.value)
|
||||
}
|
||||
if (activeStatus.value !== '全部') {
|
||||
tokens.push(activeStatus.value)
|
||||
}
|
||||
return tokens
|
||||
})
|
||||
|
||||
const filteredRuns = computed(() => {
|
||||
const keyword = listKeyword.value.trim().toLowerCase()
|
||||
return runs.value.filter((run) => {
|
||||
const moduleLabel = resolveWorkRecordModuleLabel(run)
|
||||
const statusLabel = resolveWorkRecordStatusLabel(run)
|
||||
const matchesKeyword = !keyword || [run.run_id, moduleLabel, statusLabel, run.result_summary].filter(Boolean).join(' ').toLowerCase().includes(keyword)
|
||||
const matchesModule = activeModule.value === '全部' || moduleLabel === activeModule.value
|
||||
const matchesStatus = activeStatus.value === '全部' || statusLabel === activeStatus.value
|
||||
return matchesKeyword && matchesModule && matchesStatus
|
||||
})
|
||||
})
|
||||
|
||||
const currentPage = ref(1)
|
||||
const pageSize = 20
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(filteredRuns.value.length / pageSize)))
|
||||
|
||||
const visibleRuns = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize
|
||||
return filteredRuns.value.slice(start, start + pageSize)
|
||||
})
|
||||
|
||||
const pageNumbers = computed(() => {
|
||||
const total = totalPages.value
|
||||
if (total <= 7) {
|
||||
return Array.from({ length: total }, (_, index) => index + 1)
|
||||
}
|
||||
const start = Math.max(1, Math.min(currentPage.value - 3, total - 6))
|
||||
return Array.from({ length: 7 }, (_, index) => start + index)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => [listKeyword.value, activeModule.value, activeStatus.value],
|
||||
() => {
|
||||
currentPage.value = 1
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => filteredRuns.value.length,
|
||||
() => {
|
||||
currentPage.value = Math.min(currentPage.value, totalPages.value)
|
||||
if (currentPage.value < 1) {
|
||||
currentPage.value = 1
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function toggleFilterPopover(id) {
|
||||
activeFilterPopover.value = activeFilterPopover.value === id ? '' : id
|
||||
}
|
||||
|
||||
function closeFilterPopover() {
|
||||
activeFilterPopover.value = ''
|
||||
}
|
||||
|
||||
function selectModule(val) {
|
||||
activeModule.value = val
|
||||
closeFilterPopover()
|
||||
}
|
||||
|
||||
function selectStatus(val) {
|
||||
activeStatus.value = val
|
||||
closeFilterPopover()
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
listKeyword.value = ''
|
||||
activeModule.value = '全部'
|
||||
activeStatus.value = '全部'
|
||||
closeFilterPopover()
|
||||
}
|
||||
|
||||
async function loadWorkRecords(showToast = false) {
|
||||
loading.value = true
|
||||
@@ -302,6 +534,7 @@ function reloadSelectedDetail() {
|
||||
|
||||
function closeWorkRecordDetail() {
|
||||
detailOpen.value = false
|
||||
selectedRunDetail.value = null
|
||||
detailError.value = ''
|
||||
}
|
||||
|
||||
@@ -329,4 +562,103 @@ onBeforeUnmount(() => {
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped src="../../assets/styles/views/audit-view.css"></style>
|
||||
<style scoped src="../../assets/styles/views/audit-view-part2.css"></style>
|
||||
<style scoped src="../../assets/styles/views/digital-employees-view.css"></style>
|
||||
<style scoped src="../../assets/styles/components/digital-employee-work-records.css"></style>
|
||||
|
||||
<style scoped>
|
||||
.digital-employee-list-panel {
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.digital-employee-list-panel .digital-table-wrap {
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.digital-employee-list-panel .digital-employee-pagination {
|
||||
flex: 0 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.digital-employee-list-panel .digital-employee-pagination .page-summary {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.digital-employee-list-panel .pager {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 4px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.digital-employee-list-panel .pager button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 0;
|
||||
border-radius: 9px;
|
||||
background: transparent;
|
||||
color: #334155;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.digital-employee-list-panel .pager button:hover:not(.active):not(:disabled) {
|
||||
background: #fff;
|
||||
color: var(--theme-primary-active);
|
||||
box-shadow: 0 1px 4px rgba(15, 23, 42, .08);
|
||||
}
|
||||
|
||||
.digital-employee-list-panel .pager button.active {
|
||||
background: var(--theme-primary-active);
|
||||
color: #fff;
|
||||
box-shadow: 0 8px 16px var(--theme-primary-shadow);
|
||||
}
|
||||
|
||||
.digital-employee-list-panel .pager button:disabled {
|
||||
color: #cbd5e1;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.digital-work-records-list-stage {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.work-records-detail-stage {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 风险环的成功、失败、执行中状态配色 */
|
||||
.json-risk-score-ring.succeeded {
|
||||
--score-ring: #16a34a;
|
||||
--score-ring-bg: #f0fdf4;
|
||||
}
|
||||
|
||||
.json-risk-score-ring.failed {
|
||||
--score-ring: #dc2626;
|
||||
--score-ring-bg: #fef2f2;
|
||||
}
|
||||
|
||||
.json-risk-score-ring.running {
|
||||
--score-ring: #2563eb;
|
||||
--score-ring-bg: #eff6ff;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -23,7 +23,6 @@ import { GridComponent, TooltipComponent } from 'echarts/components'
|
||||
import { use } from 'echarts/core'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
|
||||
import { useAnimationProgress } from '../../composables/useAnimationProgress.js'
|
||||
import { useEcharts } from '../../composables/useEcharts.js'
|
||||
import { resolveCssColor, useThemeColors } from '../../composables/useThemeColors.js'
|
||||
|
||||
@@ -34,7 +33,6 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const chartElement = shallowRef(null)
|
||||
const progress = useAnimationProgress([() => props.items], 980)
|
||||
const themeColors = useThemeColors()
|
||||
const resolvedItems = computed(() => {
|
||||
const fallback = themeColors.value.chartPrimary
|
||||
@@ -54,18 +52,14 @@ const ariaLabel = computed(() =>
|
||||
|
||||
const chartMaxValue = computed(() => Math.max(...resolvedItems.value.map((item) => item.value), 1))
|
||||
const chartAxisMax = computed(() => Math.ceil((chartMaxValue.value * 1.1) / 10000) * 10000)
|
||||
const animatedItems = computed(() =>
|
||||
resolvedItems.value.map((item) => ({
|
||||
...item,
|
||||
animatedValue: progress.value >= 0.999
|
||||
? item.value
|
||||
: Number((item.value * progress.value).toFixed(0))
|
||||
}))
|
||||
)
|
||||
|
||||
const chartOptions = computed(() => ({
|
||||
backgroundColor: 'transparent',
|
||||
animation: false,
|
||||
animation: true,
|
||||
animationDuration: 1200,
|
||||
animationDurationUpdate: 1200,
|
||||
animationEasing: 'linear',
|
||||
animationEasingUpdate: 'linear',
|
||||
grid: {
|
||||
top: 8,
|
||||
right: 62,
|
||||
@@ -116,9 +110,9 @@ const chartOptions = computed(() => ({
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
data: animatedItems.value.map((item) => ({
|
||||
data: resolvedItems.value.map((item) => ({
|
||||
name: item.name || item.shortName,
|
||||
value: item.animatedValue,
|
||||
value: item.value,
|
||||
itemStyle: { color: item.resolvedColor }
|
||||
})),
|
||||
barWidth: 14,
|
||||
@@ -133,6 +127,7 @@ const chartOptions = computed(() => ({
|
||||
label: {
|
||||
show: true,
|
||||
position: 'right',
|
||||
valueAnimation: true,
|
||||
color: '#64748b',
|
||||
fontSize: 11,
|
||||
fontWeight: 800,
|
||||
|
||||
@@ -24,7 +24,6 @@ import { TooltipComponent } from 'echarts/components'
|
||||
import { use } from 'echarts/core'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
|
||||
import { useAnimationProgress } from '../../composables/useAnimationProgress.js'
|
||||
import { useEcharts } from '../../composables/useEcharts.js'
|
||||
import { resolveCssColor, useThemeColors } from '../../composables/useThemeColors.js'
|
||||
|
||||
@@ -37,7 +36,6 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const chartElement = shallowRef(null)
|
||||
const progress = useAnimationProgress([() => props.items], 980)
|
||||
const themeColors = useThemeColors()
|
||||
const resolvedItems = computed(() => {
|
||||
const fallback = themeColors.value.chartPrimary
|
||||
@@ -55,7 +53,11 @@ const ariaLabel = computed(() =>
|
||||
|
||||
const chartOptions = computed(() => ({
|
||||
backgroundColor: 'transparent',
|
||||
animation: false,
|
||||
animation: true,
|
||||
animationDuration: 1200,
|
||||
animationDurationUpdate: 1200,
|
||||
animationEasing: 'linear',
|
||||
animationEasingUpdate: 'linear',
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
confine: true,
|
||||
@@ -78,7 +80,8 @@ const chartOptions = computed(() => ({
|
||||
radius: ['60%', '88%'],
|
||||
center: ['50%', '50%'],
|
||||
startAngle: 90,
|
||||
endAngle: 90 - Math.max(progress.value, 0.0001) * 360,
|
||||
animationType: 'expansion',
|
||||
animationTypeUpdate: 'expansion',
|
||||
avoidLabelOverlap: true,
|
||||
padAngle: 1.5,
|
||||
minAngle: 2,
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
<div class="gauge-chart">
|
||||
<div class="gauge-body">
|
||||
<div ref="chartElement" class="gauge-canvas" role="img" :aria-label="ariaLabel"></div>
|
||||
<div class="gauge-center">
|
||||
<strong>{{ animatedRatio }}%</strong>
|
||||
<span>已执行</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gauge-summary">
|
||||
<div>
|
||||
@@ -30,7 +26,6 @@ import { GaugeChart as EChartsGaugeChart } from 'echarts/charts'
|
||||
import { use } from 'echarts/core'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
|
||||
import { useAnimationProgress } from '../../composables/useAnimationProgress.js'
|
||||
import { useEcharts } from '../../composables/useEcharts.js'
|
||||
import { useThemeColors } from '../../composables/useThemeColors.js'
|
||||
|
||||
@@ -44,15 +39,8 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const chartElement = shallowRef(null)
|
||||
const progress = useAnimationProgress([() => props.ratio], 980)
|
||||
const themeColors = useThemeColors()
|
||||
const normalizedRatio = computed(() => Math.max(0, Math.min(100, Math.round(Number(props.ratio) || 0))))
|
||||
const animatedRatio = computed(() => {
|
||||
if (progress.value >= 0.999) {
|
||||
return normalizedRatio.value
|
||||
}
|
||||
return Math.round(normalizedRatio.value * progress.value)
|
||||
})
|
||||
const ariaLabel = computed(() => `预算执行率${normalizedRatio.value}%,预算总额${props.total},已执行${props.used},剩余${props.left}`)
|
||||
|
||||
const chartOptions = computed(() => {
|
||||
@@ -60,7 +48,11 @@ const chartOptions = computed(() => {
|
||||
|
||||
return {
|
||||
backgroundColor: 'transparent',
|
||||
animation: false,
|
||||
animation: true,
|
||||
animationDuration: 1200,
|
||||
animationDurationUpdate: 1200,
|
||||
animationEasing: 'linear',
|
||||
animationEasingUpdate: 'linear',
|
||||
series: [
|
||||
{
|
||||
type: 'gauge',
|
||||
@@ -90,8 +82,23 @@ const chartOptions = computed(() => {
|
||||
splitLine: { show: false },
|
||||
axisLabel: { show: false },
|
||||
anchor: { show: false },
|
||||
detail: { show: false },
|
||||
data: [{ value: animatedRatio.value }]
|
||||
detail: {
|
||||
show: true,
|
||||
valueAnimation: true,
|
||||
offsetCenter: [0, '22%'],
|
||||
formatter: '{value}%',
|
||||
color: primary,
|
||||
fontSize: 24,
|
||||
fontWeight: 850
|
||||
},
|
||||
title: {
|
||||
show: true,
|
||||
offsetCenter: [0, '46%'],
|
||||
color: '#64748b',
|
||||
fontSize: 11,
|
||||
fontWeight: 700
|
||||
},
|
||||
data: [{ value: normalizedRatio.value, name: '已执行' }]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -120,31 +127,6 @@ useEcharts(chartElement, chartOptions)
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.gauge-center {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
animation: gaugeCenterIn 620ms ease both;
|
||||
animation-delay: 360ms;
|
||||
}
|
||||
|
||||
.gauge-center strong {
|
||||
color: var(--chart-primary);
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.gauge-center span {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: #64748b;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.gauge-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
@@ -168,17 +150,6 @@ useEcharts(chartElement, chartOptions)
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@keyframes gaugeCenterIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes gaugeSummaryIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
@@ -191,7 +162,6 @@ useEcharts(chartElement, chartOptions)
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.gauge-center,
|
||||
.gauge-summary {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import { GridComponent, TooltipComponent } from 'echarts/components'
|
||||
import { use } from 'echarts/core'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
|
||||
import { useAnimationProgress } from '../../composables/useAnimationProgress.js'
|
||||
import { useEcharts } from '../../composables/useEcharts.js'
|
||||
import { useThemeColors } from '../../composables/useThemeColors.js'
|
||||
|
||||
@@ -30,12 +29,6 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const chartElement = shallowRef(null)
|
||||
const progress = useAnimationProgress([
|
||||
() => props.labels,
|
||||
() => props.applications,
|
||||
() => props.approved,
|
||||
() => props.avgHours
|
||||
], 1100)
|
||||
const themeColors = useThemeColors()
|
||||
const chartColors = computed(() => ({
|
||||
primary: themeColors.value.chartPrimary,
|
||||
@@ -49,18 +42,13 @@ const ariaLabel = computed(() =>
|
||||
)).join(';')
|
||||
)
|
||||
|
||||
const scaleSeries = (series, decimals = 0) =>
|
||||
series.map((value) => {
|
||||
const number = Number(value || 0)
|
||||
if (progress.value >= 0.999) {
|
||||
return number
|
||||
}
|
||||
return Number((number * progress.value).toFixed(decimals))
|
||||
})
|
||||
|
||||
const chartOptions = computed(() => ({
|
||||
backgroundColor: 'transparent',
|
||||
animation: false,
|
||||
animation: true,
|
||||
animationDuration: 1200,
|
||||
animationDurationUpdate: 1200,
|
||||
animationEasing: 'linear',
|
||||
animationEasingUpdate: 'linear',
|
||||
grid: {
|
||||
top: 18,
|
||||
right: 38,
|
||||
@@ -125,7 +113,7 @@ const chartOptions = computed(() => ({
|
||||
{
|
||||
name: '申请量(单)',
|
||||
type: 'bar',
|
||||
data: scaleSeries(props.applications),
|
||||
data: props.applications,
|
||||
barWidth: 12,
|
||||
barGap: '28%',
|
||||
itemStyle: {
|
||||
@@ -136,7 +124,7 @@ const chartOptions = computed(() => ({
|
||||
{
|
||||
name: '审批完成量(单)',
|
||||
type: 'bar',
|
||||
data: scaleSeries(props.approved),
|
||||
data: props.approved,
|
||||
barWidth: 12,
|
||||
itemStyle: {
|
||||
color: chartColors.value.blue,
|
||||
@@ -147,7 +135,7 @@ const chartOptions = computed(() => ({
|
||||
name: '平均审批时长(小时)',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: scaleSeries(props.avgHours, 1),
|
||||
data: props.avgHours,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 7,
|
||||
|
||||
@@ -135,19 +135,34 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="isLogs">
|
||||
<div class="kpi-chips">
|
||||
<div v-for="kpi in logsKpis" :key="kpi.label" class="kpi-chip" :style="{ '--chip-color': kpi.color }">
|
||||
<span class="chip-value">{{ kpi.value }}<small>{{ kpi.unit }}</small></span>
|
||||
<span class="chip-label">{{ kpi.label }}</span>
|
||||
<span class="chip-delta" :class="kpi.trend">{{ kpi.meta }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="isApproval">
|
||||
<div class="kpi-chips">
|
||||
<div v-for="kpi in approvalKpis" :key="kpi.label" class="kpi-chip" :style="{ '--chip-color': kpi.color }">
|
||||
<template v-else-if="isLogs">
|
||||
<div class="kpi-chips">
|
||||
<div v-for="kpi in logsKpis" :key="kpi.label" class="kpi-chip" :style="{ '--chip-color': kpi.color }">
|
||||
<span class="chip-value">{{ kpi.value }}<small>{{ kpi.unit }}</small></span>
|
||||
<span class="chip-label">{{ kpi.label }}</span>
|
||||
<span class="chip-delta" :class="kpi.trend">{{ kpi.meta }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="showDigitalEmployeeWorkRecordKpis">
|
||||
<div class="kpi-chips">
|
||||
<div
|
||||
v-for="kpi in digitalEmployeeWorkRecordKpis"
|
||||
:key="kpi.label"
|
||||
class="kpi-chip"
|
||||
:style="{ '--chip-color': kpi.color }"
|
||||
>
|
||||
<span class="chip-value">{{ kpi.value }}<small>条</small></span>
|
||||
<span class="chip-label">{{ kpi.label }}</span>
|
||||
<span class="chip-delta" :class="kpi.trend">{{ kpi.delta }} <i :class="kpi.arrow"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="isApproval">
|
||||
<div class="kpi-chips">
|
||||
<div v-for="kpi in approvalKpis" :key="kpi.label" class="kpi-chip" :style="{ '--chip-color': kpi.color }">
|
||||
<span class="chip-value">{{ kpi.value }}<small>{{ kpi.unit }}</small></span>
|
||||
<span class="chip-label">{{ kpi.label }}</span>
|
||||
<span class="chip-delta" :class="kpi.trend">{{ kpi.meta }}</span>
|
||||
@@ -208,10 +223,14 @@ const props = defineProps({
|
||||
type: Object,
|
||||
default: () => null
|
||||
},
|
||||
documentSummary: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
},
|
||||
documentSummary: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
},
|
||||
digitalEmployeeSummary: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
},
|
||||
companyName: {
|
||||
type: String,
|
||||
default: ''
|
||||
@@ -249,8 +268,9 @@ const isWorkbench = computed(() => props.activeView === 'workbench')
|
||||
const isRequestDetail = computed(() => ['requests', 'documents'].includes(props.activeView) && props.detailMode)
|
||||
const isDocuments = computed(() => props.activeView === 'documents' && !props.detailMode)
|
||||
const isRequests = computed(() => props.activeView === 'requests')
|
||||
const isLogs = computed(() => props.activeView === 'logs' && !props.logDetailMode)
|
||||
const isApproval = computed(() => props.activeView === 'approval')
|
||||
const isLogs = computed(() => props.activeView === 'logs' && !props.logDetailMode)
|
||||
const isDigitalEmployees = computed(() => props.activeView === 'digitalEmployees')
|
||||
const isApproval = computed(() => props.activeView === 'approval')
|
||||
const isPolicies = computed(() => props.activeView === 'policies')
|
||||
const isEmployees = computed(() => props.activeView === 'employees')
|
||||
const displayCompanyName = computed(() => String(props.companyName || '远光软件股份有限公司').trim() || '远光软件股份有限公司')
|
||||
@@ -304,8 +324,47 @@ const logsKpis = computed(() => {
|
||||
{ label: '正常数量', value: info, unit: '条', meta: total ? `占比 ${Math.round((info / total) * 100)}%` : '等待数据', trend: 'up', color: 'var(--success)' }
|
||||
]
|
||||
})
|
||||
|
||||
const chatKpis = [
|
||||
|
||||
const showDigitalEmployeeWorkRecordKpis = computed(() => {
|
||||
const summary = props.digitalEmployeeSummary ?? {}
|
||||
return isDigitalEmployees.value && summary.section === 'workRecords'
|
||||
})
|
||||
|
||||
const digitalEmployeeWorkRecordKpis = computed(() => {
|
||||
const summary = props.digitalEmployeeSummary ?? {}
|
||||
const total = Number(summary.total ?? 0)
|
||||
const succeeded = Number(summary.succeeded ?? 0)
|
||||
const failed = Number(summary.failed ?? 0)
|
||||
|
||||
return [
|
||||
{
|
||||
label: '日志总数',
|
||||
value: total,
|
||||
delta: '当前',
|
||||
trend: 'up',
|
||||
arrow: 'mdi mdi-minus',
|
||||
color: 'var(--theme-primary)'
|
||||
},
|
||||
{
|
||||
label: '成功数量',
|
||||
value: succeeded,
|
||||
delta: total ? `占比 ${Math.round((succeeded / total) * 100)}%` : '等待数据',
|
||||
trend: 'up',
|
||||
arrow: succeeded > 0 ? 'mdi mdi-arrow-up' : 'mdi mdi-minus',
|
||||
color: 'var(--success)'
|
||||
},
|
||||
{
|
||||
label: '失败数量',
|
||||
value: failed,
|
||||
delta: failed > 0 ? '需要关注' : '暂无失败',
|
||||
trend: failed > 0 ? 'down' : 'up',
|
||||
arrow: failed > 0 ? 'mdi mdi-arrow-down' : 'mdi mdi-minus',
|
||||
color: '#ef4444'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const chatKpis = [
|
||||
{ label: '今日已问数', value: 86, unit: '次', meta: '较昨日 +18', trend: 'up', color: 'var(--theme-primary)' },
|
||||
{ label: '已解决问题', value: 72, unit: '条', meta: '解决率 83.7%', trend: 'up', color: '#3b82f6' },
|
||||
{ label: '知识命中率', value: '92.3', unit: '%', meta: '较昨日 +2.6%', trend: 'up', color: '#8b5cf6' },
|
||||
|
||||
22
web/src/components/shared/EnterpriseDetailCard.vue
Normal file
22
web/src/components/shared/EnterpriseDetailCard.vue
Normal 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>
|
||||
71
web/src/components/shared/EnterpriseDetailPage.vue
Normal file
71
web/src/components/shared/EnterpriseDetailPage.vue
Normal 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>
|
||||
197
web/src/components/shared/EnterpriseListPage.vue
Normal file
197
web/src/components/shared/EnterpriseListPage.vue
Normal 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>
|
||||
107
web/src/components/shared/EnterprisePagination.vue
Normal file
107
web/src/components/shared/EnterprisePagination.vue
Normal 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>
|
||||
@@ -5,6 +5,12 @@ export function useEcharts(chartElement, chartOptions) {
|
||||
let chartInstance = null
|
||||
let resizeObserver = null
|
||||
let renderFrame = 0
|
||||
let initialFrame = 0
|
||||
let initialTimer = 0
|
||||
let initialRendered = false
|
||||
let initialPending = false
|
||||
let latestOption = null
|
||||
const initialAnimationDelay = 180
|
||||
|
||||
function renderChart() {
|
||||
if (!chartElement.value) {
|
||||
@@ -14,7 +20,37 @@ export function useEcharts(chartElement, chartOptions) {
|
||||
chartInstance = init(chartElement.value, null, { renderer: 'canvas' })
|
||||
chartInstance.resize()
|
||||
}
|
||||
chartInstance.setOption(chartOptions.value, true)
|
||||
|
||||
if (!initialRendered) {
|
||||
initialRendered = true
|
||||
initialPending = true
|
||||
latestOption = chartOptions.value
|
||||
if (typeof window !== 'undefined') {
|
||||
initialTimer = window.setTimeout(() => {
|
||||
initialTimer = 0
|
||||
initialFrame = window.requestAnimationFrame(() => {
|
||||
initialFrame = 0
|
||||
initialPending = false
|
||||
chartInstance?.setOption(latestOption || chartOptions.value, {
|
||||
notMerge: true,
|
||||
lazyUpdate: false
|
||||
})
|
||||
latestOption = null
|
||||
})
|
||||
}, initialAnimationDelay)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (initialPending) {
|
||||
latestOption = chartOptions.value
|
||||
return
|
||||
}
|
||||
|
||||
chartInstance.setOption(chartOptions.value, {
|
||||
notMerge: false,
|
||||
lazyUpdate: false
|
||||
})
|
||||
}
|
||||
|
||||
function handleResize() {
|
||||
@@ -63,6 +99,14 @@ export function useEcharts(chartElement, chartOptions) {
|
||||
window.cancelAnimationFrame(renderFrame)
|
||||
renderFrame = 0
|
||||
}
|
||||
if (initialFrame && typeof window !== 'undefined') {
|
||||
window.cancelAnimationFrame(initialFrame)
|
||||
initialFrame = 0
|
||||
}
|
||||
if (initialTimer && typeof window !== 'undefined') {
|
||||
window.clearTimeout(initialTimer)
|
||||
initialTimer = 0
|
||||
}
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose()
|
||||
chartInstance = null
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
const KNOWLEDGE_JOB_TYPES = new Set(['knowledge_index_sync', 'llm_wiki_sync'])
|
||||
const KNOWLEDGE_JOB_TYPES = new Set([
|
||||
'knowledge_index_sync',
|
||||
'llm_wiki_sync',
|
||||
'llm_wiki_rule_formation',
|
||||
'finance_policy_knowledge_organize'
|
||||
])
|
||||
|
||||
const STATUS_LABELS = {
|
||||
running: '运行中',
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
const KNOWLEDGE_INGEST_JOB_TYPES = new Set(['knowledge_index_sync', 'llm_wiki_sync'])
|
||||
const KNOWLEDGE_INGEST_JOB_TYPES = new Set([
|
||||
'knowledge_index_sync',
|
||||
'llm_wiki_sync',
|
||||
'llm_wiki_rule_formation',
|
||||
'finance_policy_knowledge_organize'
|
||||
])
|
||||
|
||||
const STATUS_META = {
|
||||
queued: { label: '等待处理', tone: 'muted' },
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
'policies-main': activeView === 'policies',
|
||||
'audit-main': activeView === 'audit',
|
||||
'audit-detail-main': activeView === 'audit' && auditDetailOpen,
|
||||
'digital-employees-detail-main': activeView === 'digitalEmployees' && digitalEmployeeDetailOpen,
|
||||
'digital-employees-main': activeView === 'digitalEmployees',
|
||||
'logs-main': activeView === 'logs',
|
||||
'employees-main': activeView === 'employees',
|
||||
@@ -54,7 +55,7 @@
|
||||
}"
|
||||
>
|
||||
<TopBar
|
||||
v-if="activeView !== 'settings' && !(activeView === 'audit' && auditDetailOpen)"
|
||||
v-if="activeView !== 'settings' && !(activeView === 'audit' && auditDetailOpen) && !(activeView === 'digitalEmployees' && digitalEmployeeDetailOpen)"
|
||||
:current-view="topBarView"
|
||||
:search="search"
|
||||
:active-view="activeView"
|
||||
@@ -65,6 +66,7 @@
|
||||
:logs-summary="logsSummary"
|
||||
:request-summary="requestSummary"
|
||||
:document-summary="documentSummary"
|
||||
:digital-employee-summary="digitalEmployeeSummary"
|
||||
:company-name="ENTERPRISE_DISPLAY_NAME"
|
||||
:detail-mode="detailMode"
|
||||
:log-detail-mode="logDetailMode"
|
||||
@@ -144,7 +146,11 @@
|
||||
/>
|
||||
<PoliciesView v-else-if="activeView === 'policies'" @summary-change="knowledgeSummary = $event" />
|
||||
<AuditView v-else-if="activeView === 'audit'" @detail-open-change="auditDetailOpen = $event" />
|
||||
<DigitalEmployeesView v-else-if="activeView === 'digitalEmployees'" />
|
||||
<DigitalEmployeesView
|
||||
v-else-if="activeView === 'digitalEmployees'"
|
||||
@summary-change="digitalEmployeeSummary = $event"
|
||||
@detail-open-change="digitalEmployeeDetailOpen = $event"
|
||||
/>
|
||||
<LogDetailView v-else-if="activeView === 'logs' && logDetailMode" />
|
||||
<LogsView v-else-if="activeView === 'logs'" @summary-change="logsSummary = $event" />
|
||||
<EmployeeManagementView v-else-if="activeView === 'employees'" @overview-change="employeeSummary = $event" />
|
||||
@@ -197,7 +203,9 @@ const employeeSummary = ref(null)
|
||||
const knowledgeSummary = ref(null)
|
||||
const logsSummary = ref(null)
|
||||
const documentSummary = ref(null)
|
||||
const digitalEmployeeSummary = ref(null)
|
||||
const auditDetailOpen = ref(false)
|
||||
const digitalEmployeeDetailOpen = ref(false)
|
||||
const loginEntryAnimating = ref(false)
|
||||
const sidebarCollapsed = ref(false)
|
||||
const mobileSidebarOpen = ref(false)
|
||||
|
||||
@@ -10,66 +10,43 @@
|
||||
@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"
|
||||
>
|
||||
{{ 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"
|
||||
>
|
||||
<template #filters>
|
||||
<div class="list-search">
|
||||
<i class="mdi mdi-magnify"></i>
|
||||
<input v-model="listKeyword" type="search" placeholder="搜索单号、申请人、部门、报销类型..." />
|
||||
</div>
|
||||
|
||||
<button v-for="filter in filters" :key="filter" type="button" class="filter-btn">
|
||||
<span>{{ filter }}</span>
|
||||
<i class="mdi mdi-chevron-down"></i>
|
||||
</button>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<div class="list-toolbar">
|
||||
<div class="filter-set">
|
||||
<div class="list-search">
|
||||
<i class="mdi mdi-magnify"></i>
|
||||
<input v-model="listKeyword" type="search" placeholder="搜索单号、申请人、部门、报销类型..." />
|
||||
</div>
|
||||
<template #hint>
|
||||
<i class="mdi mdi-information-outline"></i>
|
||||
点击单据行查看审批详情
|
||||
</template>
|
||||
|
||||
<button v-for="filter in filters" :key="filter" type="button" class="filter-btn">
|
||||
<span>{{ filter }}</span>
|
||||
<i class="mdi mdi-chevron-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="hint"><i class="mdi mdi-information-outline"></i> 点击单据行查看审批详情</p>
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
@@ -9,91 +9,68 @@
|
||||
@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"
|
||||
>
|
||||
<template #filters>
|
||||
<div class="list-search">
|
||||
<i class="mdi mdi-magnify"></i>
|
||||
<input v-model="listKeyword" type="search" placeholder="搜索单号、申请人、部门、归档类型..." />
|
||||
</div>
|
||||
|
||||
<el-dropdown
|
||||
v-for="menu in filterMenus"
|
||||
:key="menu.key"
|
||||
class="archive-filter-control"
|
||||
trigger="click"
|
||||
placement="bottom-start"
|
||||
popper-class="archive-filter-menu"
|
||||
@command="selectFilterValue(menu.key, $event)"
|
||||
>
|
||||
{{ tab }}
|
||||
</button>
|
||||
</nav>
|
||||
<button class="filter-btn archive-filter-trigger" type="button">
|
||||
<span>{{ menu.label }}</span>
|
||||
<i class="mdi mdi-chevron-down"></i>
|
||||
</button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="option in menu.options"
|
||||
:key="`${menu.key}-${option.value}`"
|
||||
:command="option.value"
|
||||
:class="{ 'is-active': menu.activeValue === option.value }"
|
||||
class="archive-filter-option"
|
||||
:aria-current="menu.activeValue === option.value ? 'true' : undefined"
|
||||
>
|
||||
<i v-if="menu.activeValue === option.value" class="mdi mdi-check"></i>
|
||||
<span>{{ option.label }}</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<div class="list-toolbar">
|
||||
<div class="filter-set">
|
||||
<div class="list-search">
|
||||
<i class="mdi mdi-magnify"></i>
|
||||
<input v-model="listKeyword" type="search" placeholder="搜索单号、申请人、部门、归档类型..." />
|
||||
</div>
|
||||
<template #hint>
|
||||
<i class="mdi mdi-information-outline"></i>
|
||||
归档中心保存公司已付款或已归档的报销数据,点击单据行查看详情
|
||||
</template>
|
||||
|
||||
<el-dropdown
|
||||
v-for="menu in filterMenus"
|
||||
:key="menu.key"
|
||||
class="archive-filter-control"
|
||||
trigger="click"
|
||||
placement="bottom-start"
|
||||
popper-class="archive-filter-menu"
|
||||
@command="selectFilterValue(menu.key, $event)"
|
||||
>
|
||||
<button class="filter-btn archive-filter-trigger" type="button">
|
||||
<span>{{ menu.label }}</span>
|
||||
<i class="mdi mdi-chevron-down"></i>
|
||||
</button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="option in menu.options"
|
||||
:key="`${menu.key}-${option.value}`"
|
||||
:command="option.value"
|
||||
:class="{ 'is-active': menu.activeValue === option.value }"
|
||||
class="archive-filter-option"
|
||||
:aria-current="menu.activeValue === option.value ? 'true' : undefined"
|
||||
>
|
||||
<i v-if="menu.activeValue === option.value" class="mdi mdi-check"></i>
|
||||
<span>{{ option.label }}</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="hint"><i class="mdi mdi-information-outline"></i> 归档中心保存公司已付款或已归档的报销数据,点击单据行查看详情</p>
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<article
|
||||
v-if="selectedEmployee"
|
||||
key="detail"
|
||||
class="skill-detail digital-employee-detail"
|
||||
class="skill-detail digital-employee-detail json-risk-skill-detail"
|
||||
>
|
||||
<div class="detail-scroll">
|
||||
<section v-if="detailError" class="detail-inline-state panel error">
|
||||
@@ -74,8 +74,13 @@
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
<article v-else key="list" class="skill-list panel digital-employees-list">
|
||||
<nav class="status-tabs" aria-label="数字员工页签">
|
||||
<article
|
||||
v-else
|
||||
key="list"
|
||||
class="skill-list digital-employees-list"
|
||||
:class="{ 'panel': !workRecordDetailOpen }"
|
||||
>
|
||||
<nav v-if="!workRecordDetailOpen" class="status-tabs" aria-label="数字员工页签">
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: activeSection === 'skills' }"
|
||||
@@ -260,6 +265,8 @@
|
||||
<DigitalEmployeeWorkRecords
|
||||
v-else
|
||||
class="digital-work-records-section"
|
||||
@summary-change="emit('summary-change', $event)"
|
||||
@detail-open-change="workRecordDetailOpen = $event"
|
||||
/>
|
||||
</article>
|
||||
</Transition>
|
||||
@@ -279,7 +286,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import AuditDigitalEmployeeDetail from '../components/audit/AuditDigitalEmployeeDetail.vue'
|
||||
import AuditPickerFilter from '../components/audit/AuditPickerFilter.vue'
|
||||
@@ -322,11 +329,18 @@ import {
|
||||
|
||||
const { currentUser } = useSystemState()
|
||||
const { toast } = useToast()
|
||||
const emit = defineEmits(['summary-change', 'detail-open-change'])
|
||||
|
||||
const employees = ref([])
|
||||
const selectedEmployee = ref(null)
|
||||
const selectedEmployeeId = ref('')
|
||||
const activeSection = ref('skills')
|
||||
const workRecordDetailOpen = ref(false)
|
||||
const isDetailOpen = computed(() => Boolean(selectedEmployee.value) || (activeSection.value === 'workRecords' && workRecordDetailOpen.value))
|
||||
|
||||
watch(isDetailOpen, (newVal) => {
|
||||
emit('detail-open-change', newVal)
|
||||
}, { immediate: true })
|
||||
const keyword = ref('')
|
||||
const selectedStatus = ref('')
|
||||
const selectedEnabledState = ref('')
|
||||
|
||||
@@ -6,112 +6,124 @@
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<article class="system-logs-list panel">
|
||||
<div class="document-toolbar">
|
||||
<div class="filter-set">
|
||||
<div class="list-search">
|
||||
<i class="mdi mdi-magnify"></i>
|
||||
<input
|
||||
v-model.trim="systemSearchKeyword"
|
||||
type="search"
|
||||
placeholder="搜索摘要、模块、请求路径或 Request ID"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="document-filter status-dropdown-filter" :class="{ open: openFilterKey === 'level' }">
|
||||
<button
|
||||
class="filter-btn status-filter-trigger"
|
||||
type="button"
|
||||
:aria-expanded="openFilterKey === 'level'"
|
||||
@click="toggleFilter('level')"
|
||||
>
|
||||
<i class="mdi mdi-filter-variant"></i>
|
||||
<span>{{ systemLevelFilterLabel }}</span>
|
||||
<i class="mdi mdi-chevron-down"></i>
|
||||
</button>
|
||||
<div
|
||||
v-if="openFilterKey === 'level'"
|
||||
class="document-filter-menu status-filter-menu"
|
||||
role="listbox"
|
||||
aria-label="日志级别"
|
||||
>
|
||||
<button
|
||||
v-for="option in systemLevelFilterOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
role="option"
|
||||
:aria-selected="systemLevelFilter === option.value"
|
||||
:class="{ active: systemLevelFilter === option.value }"
|
||||
@click="selectLevelFilter(option.value)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="document-filter" :class="{ open: openFilterKey === 'eventType' }">
|
||||
<button
|
||||
class="filter-btn"
|
||||
type="button"
|
||||
:aria-expanded="openFilterKey === 'eventType'"
|
||||
@click="toggleFilter('eventType')"
|
||||
>
|
||||
<span>{{ systemEventTypeFilterLabel }}</span>
|
||||
<i class="mdi mdi-chevron-down"></i>
|
||||
</button>
|
||||
<div
|
||||
v-if="openFilterKey === 'eventType'"
|
||||
class="document-filter-menu"
|
||||
role="listbox"
|
||||
aria-label="事件类型"
|
||||
>
|
||||
<button
|
||||
v-for="option in systemEventTypeFilterOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
role="option"
|
||||
:aria-selected="systemEventTypeFilter === option.value"
|
||||
:class="{ active: systemEventTypeFilter === option.value }"
|
||||
@click="selectEventTypeFilter(option.value)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="document-actions">
|
||||
<button v-if="hasActiveFilters" class="create-request-btn secondary" type="button" @click="resetFilters">
|
||||
<i class="mdi mdi-filter-remove-outline"></i>
|
||||
<span>清空筛选</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="create-request-btn"
|
||||
:disabled="systemLogLoading"
|
||||
@click="loadSystemLogs(true)"
|
||||
>
|
||||
<i class="mdi mdi-refresh"></i>
|
||||
<span>{{ systemLogLoading ? '刷新中...' : '刷新日志' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="hint">
|
||||
<i class="mdi mdi-information-outline"></i>
|
||||
点击任意行可查看系统日志详情
|
||||
</p>
|
||||
|
||||
<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"
|
||||
<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
|
||||
v-model.trim="systemSearchKeyword"
|
||||
type="search"
|
||||
placeholder="搜索摘要、模块、请求路径或 Request ID"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<table v-else-if="visibleSystemLogEntries.length" class="system-log-table">
|
||||
<div class="document-filter status-dropdown-filter" :class="{ open: openFilterKey === 'level' }">
|
||||
<button
|
||||
class="filter-btn status-filter-trigger"
|
||||
type="button"
|
||||
:aria-expanded="openFilterKey === 'level'"
|
||||
@click="toggleFilter('level')"
|
||||
>
|
||||
<i class="mdi mdi-filter-variant"></i>
|
||||
<span>{{ systemLevelFilterLabel }}</span>
|
||||
<i class="mdi mdi-chevron-down"></i>
|
||||
</button>
|
||||
<div
|
||||
v-if="openFilterKey === 'level'"
|
||||
class="document-filter-menu status-filter-menu"
|
||||
role="listbox"
|
||||
aria-label="日志级别"
|
||||
>
|
||||
<button
|
||||
v-for="option in systemLevelFilterOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
role="option"
|
||||
:aria-selected="systemLevelFilter === option.value"
|
||||
:class="{ active: systemLevelFilter === option.value }"
|
||||
@click="selectLevelFilter(option.value)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="document-filter" :class="{ open: openFilterKey === 'eventType' }">
|
||||
<button
|
||||
class="filter-btn"
|
||||
type="button"
|
||||
:aria-expanded="openFilterKey === 'eventType'"
|
||||
@click="toggleFilter('eventType')"
|
||||
>
|
||||
<span>{{ systemEventTypeFilterLabel }}</span>
|
||||
<i class="mdi mdi-chevron-down"></i>
|
||||
</button>
|
||||
<div
|
||||
v-if="openFilterKey === 'eventType'"
|
||||
class="document-filter-menu"
|
||||
role="listbox"
|
||||
aria-label="事件类型"
|
||||
>
|
||||
<button
|
||||
v-for="option in systemEventTypeFilterOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
role="option"
|
||||
:aria-selected="systemEventTypeFilter === option.value"
|
||||
:class="{ active: systemEventTypeFilter === option.value }"
|
||||
@click="selectEventTypeFilter(option.value)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="create-request-btn"
|
||||
:disabled="systemLogLoading"
|
||||
@click="loadSystemLogs(true)"
|
||||
>
|
||||
<i class="mdi mdi-refresh"></i>
|
||||
<span>{{ systemLogLoading ? '刷新中...' : '刷新日志' }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<template #hint>
|
||||
<i class="mdi mdi-information-outline"></i>
|
||||
点击任意行可查看系统日志详情
|
||||
</template>
|
||||
|
||||
<template #empty>
|
||||
<div class="inline-empty">
|
||||
当前筛选条件下没有系统日志记录。
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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>
|
||||
</template>
|
||||
</EnterpriseListPage>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -1,96 +1,81 @@
|
||||
<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"
|
||||
>
|
||||
{{ tab }}
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="list-toolbar">
|
||||
<div class="filter-set">
|
||||
<div class="list-search">
|
||||
<i class="mdi mdi-magnify"></i>
|
||||
<input v-model="listKeyword" type="search" placeholder="搜索单号、事由、报销类型..." />
|
||||
</div>
|
||||
|
||||
<div class="date-range-filter" :class="{ open: datePopover }">
|
||||
<button class="filter-btn date-range-trigger" type="button" @click="datePopover = !datePopover">
|
||||
<span class="date-range-label">{{ dateRangeLabel }}</span>
|
||||
<i class="mdi mdi-calendar"></i>
|
||||
</button>
|
||||
<div v-if="datePopover" class="date-range-popover" role="dialog" aria-label="选择时间段">
|
||||
<header>
|
||||
<strong>选择时间段</strong>
|
||||
<button type="button" aria-label="关闭" @click="datePopover = false"><i class="mdi mdi-close"></i></button>
|
||||
</header>
|
||||
<div class="date-range-fields">
|
||||
<label>
|
||||
<span>开始日期</span>
|
||||
<input v-model="rangeStart" type="date" />
|
||||
</label>
|
||||
<label>
|
||||
<span>结束日期</span>
|
||||
<input v-model="rangeEnd" type="date" />
|
||||
</label>
|
||||
</div>
|
||||
<footer>
|
||||
<button class="ghost-btn" type="button" @click="datePopover = false">取消</button>
|
||||
<button class="apply-btn" type="button" :disabled="!rangeStart || !rangeEnd" @click="applyDateRange">应用</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button v-for="filter in filters" :key="filter" type="button" class="filter-btn">
|
||||
<span>{{ filter }}</span>
|
||||
<i class="mdi mdi-chevron-down"></i>
|
||||
</button>
|
||||
<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"
|
||||
>
|
||||
<template #filters>
|
||||
<div class="list-search">
|
||||
<i class="mdi mdi-magnify"></i>
|
||||
<input v-model="listKeyword" type="search" placeholder="搜索单号、事由、报销类型..." />
|
||||
</div>
|
||||
|
||||
<div class="date-range-filter" :class="{ open: datePopover }">
|
||||
<button class="filter-btn date-range-trigger" type="button" @click="datePopover = !datePopover">
|
||||
<span class="date-range-label">{{ dateRangeLabel }}</span>
|
||||
<i class="mdi mdi-calendar"></i>
|
||||
</button>
|
||||
<div v-if="datePopover" class="date-range-popover" role="dialog" aria-label="选择时间段">
|
||||
<header>
|
||||
<strong>选择时间段</strong>
|
||||
<button type="button" aria-label="关闭" @click="datePopover = false"><i class="mdi mdi-close"></i></button>
|
||||
</header>
|
||||
<div class="date-range-fields">
|
||||
<label>
|
||||
<span>开始日期</span>
|
||||
<input v-model="rangeStart" type="date" />
|
||||
</label>
|
||||
<label>
|
||||
<span>结束日期</span>
|
||||
<input v-model="rangeEnd" type="date" />
|
||||
</label>
|
||||
</div>
|
||||
<footer>
|
||||
<button class="ghost-btn" type="button" @click="datePopover = false">取消</button>
|
||||
<button class="apply-btn" type="button" :disabled="!rangeStart || !rangeEnd" @click="applyDateRange">应用</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button v-for="filter in filters" :key="filter" type="button" class="filter-btn">
|
||||
<span>{{ filter }}</span>
|
||||
<i class="mdi mdi-chevron-down"></i>
|
||||
</button>
|
||||
</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>
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -14,6 +14,7 @@ const SOURCE_LABELS = {
|
||||
const KNOWLEDGE_JOB_TYPES = new Set([
|
||||
'knowledge_index_sync',
|
||||
'llm_wiki_sync',
|
||||
'llm_wiki_rule_formation',
|
||||
'finance_policy_knowledge_organize'
|
||||
])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user