重构
This commit is contained in:
@@ -907,4 +907,956 @@ body {
|
||||
.el-time-panel { background: #1c1c1c !important; border-color: #333 !important; }
|
||||
.el-time-spinner__item { color: #aaa !important; }
|
||||
.el-time-spinner__item.is-active:not(.is-disabled) { color: #fff !important; }
|
||||
|
||||
/* ──────────────────────────────────────────────────────────────
|
||||
Admin light refresh
|
||||
The legacy admin theme was built as one dark surface. These
|
||||
overrides intentionally keep the current component system while
|
||||
giving every page clearer hierarchy, calmer colors and stronger
|
||||
table/form affordance.
|
||||
────────────────────────────────────────────────────────────── */
|
||||
:root {
|
||||
--accent: #1f2320;
|
||||
--accent-muted: #5f5b53;
|
||||
--accent-dim: #8c867c;
|
||||
--accent-subtle: #f0ede6;
|
||||
--accent-border: #ded8cc;
|
||||
--accent-hover: #f5f2eb;
|
||||
--accent-focus: rgba(31, 35, 32, 0.16);
|
||||
|
||||
--success-text: #346538;
|
||||
--success-bg: #edf3ec;
|
||||
--success-border: #d6e3d4;
|
||||
--warning-text: #956400;
|
||||
--warning-bg: #fbf3db;
|
||||
--warning-border: #eadcad;
|
||||
--danger-text: #9f2f2d;
|
||||
--danger-bg: #fdebec;
|
||||
--danger-border: #f3c5c7;
|
||||
--info-text: #1f6c9f;
|
||||
--info-bg: #e1f3fe;
|
||||
--info-border: #c7e4f6;
|
||||
|
||||
--gold-deep: var(--warning-text);
|
||||
--gold-mid: #b88928;
|
||||
--gold-bright: #8f6a18;
|
||||
--gold-glow: #8f6a18;
|
||||
--gold-surface: var(--warning-bg);
|
||||
--gold-border: var(--warning-border);
|
||||
--gold-text: var(--warning-text);
|
||||
--green-deep: var(--success-text);
|
||||
--green-mid: #477a4c;
|
||||
--green-bright: #346538;
|
||||
--green-glow: #346538;
|
||||
--green-surface: var(--success-bg);
|
||||
--green-border: var(--success-border);
|
||||
--green-text: var(--success-text);
|
||||
|
||||
--primary: #1f2320;
|
||||
--primary-dark: #111411;
|
||||
--primary-light: #363a35;
|
||||
--primary-link: #1f6c9f;
|
||||
--primary-on: #ffffff;
|
||||
--primary-grad: #1f2320;
|
||||
--primary-grad-hover: #363a35;
|
||||
--primary-shadow: none;
|
||||
|
||||
--bg-body: #f7f6f3;
|
||||
--bg-card: #ffffff;
|
||||
--bg-elevated: #fbfbfa;
|
||||
--bg-hover: #f1eee8;
|
||||
--text: #1f2320;
|
||||
--text-muted: #78746b;
|
||||
--border: #e7e2d8;
|
||||
--border-soft: #f0ece4;
|
||||
--radius: 8px;
|
||||
--radius-sm: 6px;
|
||||
--shadow: 0 8px 28px rgba(56, 49, 37, 0.05);
|
||||
|
||||
--el-bg-color: #ffffff;
|
||||
--el-bg-color-page: #f7f6f3;
|
||||
--el-bg-color-overlay: #ffffff;
|
||||
--el-text-color-primary: #1f2320;
|
||||
--el-text-color-regular: #3d413b;
|
||||
--el-text-color-secondary: #78746b;
|
||||
--el-text-color-placeholder:#aaa49a;
|
||||
--el-border-color: #e7e2d8;
|
||||
--el-border-color-light: #eee9df;
|
||||
--el-border-color-lighter: #f4f0e8;
|
||||
--el-fill-color: #f8f6f1;
|
||||
--el-fill-color-blank: #ffffff;
|
||||
--el-fill-color-light: #fbfaf7;
|
||||
--el-color-primary: #1f2320;
|
||||
--el-color-primary-light-3: #5d635c;
|
||||
--el-color-primary-light-5: #8d938b;
|
||||
--el-color-primary-light-7: #d9ddd6;
|
||||
--el-color-primary-light-9: #f4f6f2;
|
||||
--el-color-primary-dark-2: #111411;
|
||||
--el-color-success: var(--success-text);
|
||||
--el-color-success-light-9: var(--success-bg);
|
||||
--el-color-warning: var(--warning-text);
|
||||
--el-color-warning-light-9: var(--warning-bg);
|
||||
--el-color-danger: var(--danger-text);
|
||||
--el-color-danger-light-9: var(--danger-bg);
|
||||
--el-color-info: var(--info-text);
|
||||
--el-color-info-light-9: var(--info-bg);
|
||||
--el-table-bg-color: #ffffff;
|
||||
--el-table-tr-bg-color: #ffffff;
|
||||
--el-table-header-bg-color: #fbfaf7;
|
||||
--el-table-row-hover-bg-color: #f7f4ee;
|
||||
--el-table-border-color: #eee9df;
|
||||
--el-table-text-color: #363a35;
|
||||
--el-table-header-text-color: #6c675f;
|
||||
--el-card-bg-color: #ffffff;
|
||||
--el-card-border-color: #e7e2d8;
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
background: var(--bg-body) !important;
|
||||
color: var(--text) !important;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'SF Pro Display', 'Geist Sans', 'Helvetica Neue', 'PingFang SC',
|
||||
'Microsoft YaHei', Arial, sans-serif;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
*:focus-visible {
|
||||
outline: 2px solid rgba(31, 35, 32, 0.28);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #d5cfc3;
|
||||
border: 3px solid transparent;
|
||||
border-radius: 999px;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #bdb6a9;
|
||||
border: 3px solid transparent;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-color: #d5cfc3 transparent;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-link);
|
||||
}
|
||||
|
||||
.el-card {
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: var(--radius) !important;
|
||||
background: var(--bg-card) !important;
|
||||
box-shadow: var(--shadow) !important;
|
||||
color: var(--text) !important;
|
||||
}
|
||||
|
||||
.el-card__header {
|
||||
border-bottom: 1px solid var(--border-soft) !important;
|
||||
padding: 14px 18px !important;
|
||||
}
|
||||
|
||||
.el-card__body {
|
||||
color: var(--text) !important;
|
||||
}
|
||||
|
||||
.el-table {
|
||||
border-radius: var(--radius) !important;
|
||||
background: #ffffff !important;
|
||||
color: #363a35 !important;
|
||||
font-size: 13px;
|
||||
--el-table-border-color: var(--border-soft);
|
||||
}
|
||||
|
||||
.el-table::before,
|
||||
.el-table__inner-wrapper::before {
|
||||
background: var(--border-soft) !important;
|
||||
}
|
||||
|
||||
.el-table th.el-table__cell {
|
||||
height: 42px;
|
||||
background: #fbfaf7 !important;
|
||||
border-bottom: 1px solid var(--border) !important;
|
||||
color: #6c675f !important;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.el-table td.el-table__cell {
|
||||
border-bottom-color: var(--border-soft) !important;
|
||||
color: #363a35 !important;
|
||||
}
|
||||
|
||||
.el-table--striped .el-table__body tr.el-table__row--striped td {
|
||||
background: #fbfaf7 !important;
|
||||
}
|
||||
|
||||
.el-table__body tr:hover > td {
|
||||
background: #f7f4ee !important;
|
||||
}
|
||||
|
||||
.el-table .cell {
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.el-table__empty-block {
|
||||
min-height: 168px;
|
||||
}
|
||||
|
||||
.el-table__empty-text {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
.el-table__fixed-right,
|
||||
.el-table__fixed {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.el-table__fixed-right::before,
|
||||
.el-table__fixed::before {
|
||||
background: var(--border-soft) !important;
|
||||
}
|
||||
|
||||
.el-input__wrapper,
|
||||
.el-select__wrapper,
|
||||
.el-textarea__inner,
|
||||
.el-input-number .el-input__wrapper,
|
||||
.el-date-editor .el-input__wrapper {
|
||||
min-height: 34px;
|
||||
border-radius: 7px !important;
|
||||
background: #ffffff !important;
|
||||
box-shadow: 0 0 0 1px var(--border) inset !important;
|
||||
color: var(--text) !important;
|
||||
}
|
||||
|
||||
.el-input__wrapper:hover,
|
||||
.el-select__wrapper:hover,
|
||||
.el-textarea__inner:hover {
|
||||
box-shadow: 0 0 0 1px #d5cfc3 inset !important;
|
||||
}
|
||||
|
||||
.el-input__wrapper.is-focus,
|
||||
.el-select__wrapper.is-focused,
|
||||
.el-textarea__inner:focus {
|
||||
box-shadow: 0 0 0 1px #1f2320 inset, 0 0 0 3px rgba(31, 35, 32, 0.08) !important;
|
||||
}
|
||||
|
||||
.el-input-number {
|
||||
--el-input-number-controls-height: 15px;
|
||||
}
|
||||
|
||||
.el-input-number .el-input__wrapper {
|
||||
padding-right: 32px !important;
|
||||
}
|
||||
|
||||
.el-input-number .el-input__inner {
|
||||
padding: 0 4px !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.el-input-number.is-controls-right .el-input__wrapper {
|
||||
padding-left: 8px !important;
|
||||
padding-right: 32px !important;
|
||||
}
|
||||
|
||||
.el-input-number--small.is-controls-right .el-input__wrapper {
|
||||
padding-right: 28px !important;
|
||||
}
|
||||
|
||||
.el-input-number__increase,
|
||||
.el-input-number__decrease {
|
||||
background: #fbfaf7 !important;
|
||||
border-color: var(--border) !important;
|
||||
color: #6c675f !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.el-input-number__increase:hover,
|
||||
.el-input-number__decrease:hover {
|
||||
background: var(--accent-hover) !important;
|
||||
color: var(--text) !important;
|
||||
}
|
||||
|
||||
.el-input-number.is-controls-right .el-input-number__increase {
|
||||
top: 1px !important;
|
||||
right: 1px !important;
|
||||
width: 30px !important;
|
||||
height: calc((100% - 2px) / 2) !important;
|
||||
border-left: 1px solid var(--border) !important;
|
||||
border-bottom: 1px solid var(--border) !important;
|
||||
border-radius: 0 7px 0 0 !important;
|
||||
}
|
||||
|
||||
.el-input-number.is-controls-right .el-input-number__decrease {
|
||||
right: 1px !important;
|
||||
bottom: 1px !important;
|
||||
width: 30px !important;
|
||||
height: calc((100% - 2px) / 2) !important;
|
||||
border-left: 1px solid var(--border) !important;
|
||||
border-radius: 0 0 7px 0 !important;
|
||||
}
|
||||
|
||||
.el-input-number--small.is-controls-right .el-input-number__increase,
|
||||
.el-input-number--small.is-controls-right .el-input-number__decrease {
|
||||
width: 28px !important;
|
||||
}
|
||||
|
||||
.el-input-number__increase,
|
||||
.el-input-number__decrease {
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1 !important;
|
||||
}
|
||||
|
||||
.el-input-number:not(.is-controls-right) .el-input-number__decrease {
|
||||
border-right: 1px solid var(--border) !important;
|
||||
}
|
||||
|
||||
.el-input-number:not(.is-controls-right) .el-input-number__increase {
|
||||
border-left: 1px solid var(--border) !important;
|
||||
}
|
||||
|
||||
.el-input-number__increase.is-disabled,
|
||||
.el-input-number__decrease.is-disabled {
|
||||
background: #f4f0e8 !important;
|
||||
color: #b6afa3 !important;
|
||||
}
|
||||
|
||||
input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
input[type='number']::-webkit-outer-spin-button,
|
||||
input[type='number']::-webkit-inner-spin-button {
|
||||
margin: 0;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.el-input__inner,
|
||||
.el-select__placeholder,
|
||||
.el-date-editor .el-input__inner,
|
||||
.el-textarea__inner {
|
||||
color: var(--text) !important;
|
||||
}
|
||||
|
||||
.el-input__inner::placeholder,
|
||||
.el-textarea__inner::placeholder {
|
||||
color: #aaa49a !important;
|
||||
}
|
||||
|
||||
.el-input.is-disabled .el-input__wrapper,
|
||||
.el-select.is-disabled .el-select__wrapper,
|
||||
.el-textarea.is-disabled .el-textarea__inner {
|
||||
background: #f4f0e8 !important;
|
||||
box-shadow: 0 0 0 1px #ebe5da inset !important;
|
||||
}
|
||||
|
||||
.el-input.is-disabled .el-input__inner,
|
||||
.el-select.is-disabled .el-select__placeholder {
|
||||
color: #aaa49a !important;
|
||||
}
|
||||
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus {
|
||||
-webkit-box-shadow: 0 0 0 1000px #ffffff inset !important;
|
||||
-webkit-text-fill-color: var(--text) !important;
|
||||
caret-color: var(--text) !important;
|
||||
}
|
||||
|
||||
.el-form-item__label {
|
||||
color: #6c675f !important;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.el-form-item__error {
|
||||
color: var(--danger-text) !important;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
border-radius: 7px !important;
|
||||
border-color: var(--border) !important;
|
||||
background: #ffffff !important;
|
||||
color: var(--text) !important;
|
||||
font-weight: 650 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.el-button:hover,
|
||||
.el-button:focus {
|
||||
background: var(--accent-hover) !important;
|
||||
border-color: #d5cfc3 !important;
|
||||
color: var(--text) !important;
|
||||
}
|
||||
|
||||
.el-button--primary {
|
||||
background: var(--primary) !important;
|
||||
border-color: var(--primary) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.el-button--primary:hover,
|
||||
.el-button--primary:focus {
|
||||
background: var(--primary-light) !important;
|
||||
border-color: var(--primary-light) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.el-button--primary.is-plain,
|
||||
.el-button--info.is-plain {
|
||||
background: var(--info-bg) !important;
|
||||
border-color: var(--info-border) !important;
|
||||
color: var(--info-text) !important;
|
||||
}
|
||||
|
||||
.el-button--primary.is-plain:hover,
|
||||
.el-button--primary.is-plain:focus,
|
||||
.el-button--info.is-plain:hover,
|
||||
.el-button--info.is-plain:focus {
|
||||
background: #d5ecfb !important;
|
||||
border-color: #9fd0ed !important;
|
||||
color: #155b86 !important;
|
||||
}
|
||||
|
||||
.el-button--success {
|
||||
background: var(--success-text) !important;
|
||||
border-color: var(--success-text) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.el-button--success.is-plain {
|
||||
background: var(--success-bg) !important;
|
||||
border-color: var(--success-border) !important;
|
||||
color: var(--success-text) !important;
|
||||
}
|
||||
|
||||
.el-button--success.is-plain:hover,
|
||||
.el-button--success.is-plain:focus {
|
||||
background: #dcebd9 !important;
|
||||
border-color: #bad0b8 !important;
|
||||
color: #2f5d34 !important;
|
||||
}
|
||||
|
||||
.el-button--warning {
|
||||
background: var(--warning-text) !important;
|
||||
border-color: var(--warning-text) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.el-button--warning.is-plain {
|
||||
background: var(--warning-bg) !important;
|
||||
border-color: var(--warning-border) !important;
|
||||
color: var(--warning-text) !important;
|
||||
}
|
||||
|
||||
.el-button--warning.is-plain:hover,
|
||||
.el-button--warning.is-plain:focus {
|
||||
background: #f3e6bd !important;
|
||||
border-color: #d4bd75 !important;
|
||||
color: #7f5500 !important;
|
||||
}
|
||||
|
||||
.el-button--danger {
|
||||
background: var(--danger-text) !important;
|
||||
border-color: var(--danger-text) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.el-button--danger.is-plain {
|
||||
background: var(--danger-bg) !important;
|
||||
border-color: var(--danger-border) !important;
|
||||
color: var(--danger-text) !important;
|
||||
}
|
||||
|
||||
.el-button--danger.is-plain:hover,
|
||||
.el-button--danger.is-plain:focus {
|
||||
background: #f8d9dc !important;
|
||||
border-color: #e3a8ac !important;
|
||||
color: #842827 !important;
|
||||
}
|
||||
|
||||
.el-button--info {
|
||||
background: #eef1ec !important;
|
||||
border-color: #d9ddd6 !important;
|
||||
color: #4d524b !important;
|
||||
}
|
||||
|
||||
.el-button.is-link,
|
||||
.el-button--primary.is-link,
|
||||
.el-button--success.is-link,
|
||||
.el-button--warning.is-link,
|
||||
.el-button--danger.is-link {
|
||||
min-height: 0;
|
||||
padding: 0 4px !important;
|
||||
border-color: transparent !important;
|
||||
background: transparent !important;
|
||||
color: var(--primary-link) !important;
|
||||
}
|
||||
|
||||
.el-button--danger.is-link {
|
||||
color: var(--danger-text) !important;
|
||||
}
|
||||
|
||||
.el-button--warning.is-link {
|
||||
color: var(--warning-text) !important;
|
||||
}
|
||||
|
||||
.el-button.is-disabled,
|
||||
.el-button.is-disabled:hover {
|
||||
background: #f4f0e8 !important;
|
||||
border-color: #ebe5da !important;
|
||||
color: #aaa49a !important;
|
||||
}
|
||||
|
||||
.el-button.is-plain.is-disabled,
|
||||
.el-button.is-plain.is-disabled:hover,
|
||||
.el-button.is-plain.is-disabled:focus,
|
||||
.el-button.is-plain:disabled,
|
||||
.el-button--primary.is-plain.is-disabled,
|
||||
.el-button--primary.is-plain.is-disabled:hover,
|
||||
.el-button--primary.is-plain:disabled,
|
||||
.el-button--success.is-plain.is-disabled,
|
||||
.el-button--success.is-plain.is-disabled:hover,
|
||||
.el-button--success.is-plain:disabled,
|
||||
.el-button--warning.is-plain.is-disabled,
|
||||
.el-button--warning.is-plain.is-disabled:hover,
|
||||
.el-button--warning.is-plain:disabled,
|
||||
.el-button--danger.is-plain.is-disabled,
|
||||
.el-button--danger.is-plain.is-disabled:hover,
|
||||
.el-button--danger.is-plain:disabled {
|
||||
background: #f4f0e8 !important;
|
||||
border-color: #e3dcca !important;
|
||||
color: #aaa49a !important;
|
||||
}
|
||||
|
||||
.el-tag {
|
||||
border-radius: 999px !important;
|
||||
font-weight: 700 !important;
|
||||
letter-spacing: 0 !important;
|
||||
}
|
||||
|
||||
.el-tag--success {
|
||||
background: var(--success-bg) !important;
|
||||
border-color: var(--success-border) !important;
|
||||
color: var(--success-text) !important;
|
||||
}
|
||||
|
||||
.el-tag--warning {
|
||||
background: var(--warning-bg) !important;
|
||||
border-color: var(--warning-border) !important;
|
||||
color: var(--warning-text) !important;
|
||||
}
|
||||
|
||||
.el-tag--danger {
|
||||
background: var(--danger-bg) !important;
|
||||
border-color: var(--danger-border) !important;
|
||||
color: var(--danger-text) !important;
|
||||
}
|
||||
|
||||
.el-tag--info {
|
||||
background: #eef1ec !important;
|
||||
border-color: #d9ddd6 !important;
|
||||
color: #5d635c !important;
|
||||
}
|
||||
|
||||
.el-tag--primary {
|
||||
background: var(--accent-subtle) !important;
|
||||
border-color: var(--accent-border) !important;
|
||||
color: var(--accent) !important;
|
||||
}
|
||||
|
||||
.el-pagination {
|
||||
--el-pagination-bg-color: #ffffff;
|
||||
--el-pagination-button-bg-color: #ffffff;
|
||||
--el-pagination-hover-color: #1f2320;
|
||||
--el-pagination-text-color: #6c675f;
|
||||
--el-pagination-button-disabled-bg-color: #f4f0e8;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.el-pagination .btn-prev,
|
||||
.el-pagination .btn-next,
|
||||
.el-pager li {
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: 7px !important;
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
.el-pager li.is-active {
|
||||
background: var(--primary) !important;
|
||||
border-color: var(--primary) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.el-dropdown__popper,
|
||||
.el-select__popper,
|
||||
.el-popover.el-popper {
|
||||
border: 1px solid var(--border) !important;
|
||||
background: #ffffff !important;
|
||||
box-shadow: 0 18px 44px rgba(56, 49, 37, 0.12) !important;
|
||||
}
|
||||
|
||||
.el-dropdown-menu,
|
||||
.el-select-dropdown {
|
||||
background: #ffffff !important;
|
||||
}
|
||||
|
||||
.el-dropdown-menu__item,
|
||||
.el-select-dropdown__item {
|
||||
color: var(--text) !important;
|
||||
}
|
||||
|
||||
.el-dropdown-menu__item:hover,
|
||||
.el-select-dropdown__item.hover,
|
||||
.el-select-dropdown__item:hover {
|
||||
background: var(--accent-hover) !important;
|
||||
color: var(--text) !important;
|
||||
}
|
||||
|
||||
.el-select-dropdown__item.is-selected {
|
||||
color: var(--text) !important;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.el-dialog,
|
||||
.el-message-box {
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: 10px !important;
|
||||
background: #ffffff !important;
|
||||
box-shadow: 0 24px 64px rgba(56, 49, 37, 0.14) !important;
|
||||
}
|
||||
|
||||
.el-dialog__header,
|
||||
.el-message-box__header {
|
||||
border-bottom: 1px solid var(--border-soft) !important;
|
||||
}
|
||||
|
||||
.el-dialog__title,
|
||||
.el-message-box__title {
|
||||
color: var(--text) !important;
|
||||
font-weight: 750 !important;
|
||||
}
|
||||
|
||||
.el-dialog__body,
|
||||
.el-message-box__content {
|
||||
color: var(--text) !important;
|
||||
}
|
||||
|
||||
.el-dialog__footer,
|
||||
.el-message-box__btns {
|
||||
border-top: 1px solid var(--border-soft) !important;
|
||||
}
|
||||
|
||||
.el-overlay {
|
||||
background: rgba(31, 35, 32, 0.22) !important;
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
.el-loading-mask {
|
||||
background: rgba(247, 246, 243, 0.78) !important;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.el-loading-spinner .path {
|
||||
stroke: var(--primary) !important;
|
||||
}
|
||||
|
||||
.el-loading-spinner .el-loading-text {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
.el-message {
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: 8px !important;
|
||||
background: #ffffff !important;
|
||||
box-shadow: 0 12px 30px rgba(56, 49, 37, 0.12) !important;
|
||||
}
|
||||
|
||||
.el-alert {
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
color: var(--text-muted) !important;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.el-tabs__item.is-active,
|
||||
.el-tabs__item:hover {
|
||||
color: var(--text) !important;
|
||||
}
|
||||
|
||||
.el-tabs__active-bar {
|
||||
background: var(--primary) !important;
|
||||
}
|
||||
|
||||
.el-tabs__nav-wrap::after {
|
||||
background: var(--border-soft) !important;
|
||||
}
|
||||
|
||||
.el-statistic__head {
|
||||
color: var(--text-muted) !important;
|
||||
font-size: 12px !important;
|
||||
font-weight: 650 !important;
|
||||
letter-spacing: 0 !important;
|
||||
text-transform: none !important;
|
||||
}
|
||||
|
||||
.el-statistic__content .el-statistic__number {
|
||||
color: var(--text) !important;
|
||||
font-size: 24px !important;
|
||||
font-weight: 750 !important;
|
||||
}
|
||||
|
||||
.el-picker-panel,
|
||||
.el-time-panel {
|
||||
background: #ffffff !important;
|
||||
border-color: var(--border) !important;
|
||||
color: var(--text) !important;
|
||||
box-shadow: 0 18px 44px rgba(56, 49, 37, 0.12) !important;
|
||||
}
|
||||
|
||||
.el-picker-panel__footer {
|
||||
background: #fbfaf7 !important;
|
||||
border-top-color: var(--border-soft) !important;
|
||||
}
|
||||
|
||||
.el-date-picker__header-label,
|
||||
.el-date-table th,
|
||||
.el-date-table td .el-date-table-cell__text,
|
||||
.el-time-spinner__item {
|
||||
color: var(--text) !important;
|
||||
}
|
||||
|
||||
.el-date-table td.available:hover {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
|
||||
.el-date-table td.current:not(.disabled) .el-date-table-cell__text {
|
||||
background: var(--primary) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.el-time-spinner__item.is-active:not(.is-disabled) {
|
||||
color: var(--primary) !important;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.admin-list-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.list-chrome {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
min-height: 0;
|
||||
padding: 18px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.list-toolbar,
|
||||
.filter-bar,
|
||||
.filters,
|
||||
.list-panel-toolbar,
|
||||
.table-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.list-toolbar + .table-wrap,
|
||||
.filter-bar + .table-wrap,
|
||||
.filters + .table-wrap,
|
||||
.list-panel-toolbar + .table-wrap {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.table-wrap,
|
||||
.admin-table-wrap,
|
||||
.table-panel,
|
||||
.list-panel {
|
||||
min-width: 0;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-chrome > .table-wrap,
|
||||
.list-chrome > .admin-table-wrap,
|
||||
.list-chrome > .table-panel,
|
||||
.list-chrome > .list-panel {
|
||||
border-color: var(--border-soft);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.table-scroll,
|
||||
.admin-table-scroll,
|
||||
.el-table__body-wrapper {
|
||||
scrollbar-color: #d5cfc3 transparent;
|
||||
}
|
||||
|
||||
.pager,
|
||||
.list-pager,
|
||||
.table-pager,
|
||||
.pagination-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.muted,
|
||||
.hint,
|
||||
.subtext,
|
||||
.desc,
|
||||
.table-hint,
|
||||
.section-hint {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
.danger-text,
|
||||
.c-red {
|
||||
color: var(--danger-text) !important;
|
||||
}
|
||||
|
||||
.success-text,
|
||||
.c-green {
|
||||
color: var(--success-text) !important;
|
||||
}
|
||||
|
||||
.warning-text,
|
||||
.c-gold,
|
||||
.c-yellow {
|
||||
color: var(--warning-text) !important;
|
||||
}
|
||||
|
||||
.admin-page-state,
|
||||
.state-card,
|
||||
.empty-state,
|
||||
.error-state {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.error-state,
|
||||
.state-card.is-error {
|
||||
background: var(--danger-bg);
|
||||
border-color: var(--danger-border);
|
||||
}
|
||||
|
||||
.error-state .state-title,
|
||||
.state-card.is-error .state-title {
|
||||
color: var(--danger-text);
|
||||
}
|
||||
|
||||
.user-edit-dialog .edit-stats-panel,
|
||||
.agent-edit-dialog .edit-stats-panel,
|
||||
.detail-actions {
|
||||
border-top-color: var(--border-soft);
|
||||
}
|
||||
|
||||
.user-edit-dialog .password-field-label,
|
||||
.agent-edit-dialog .password-field-label,
|
||||
.user-edit-dialog .password-empty,
|
||||
.agent-edit-dialog .password-empty {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.user-edit-dialog .password-plain,
|
||||
.agent-edit-dialog .password-plain {
|
||||
color: var(--warning-text);
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.admin-list-page {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.list-chrome {
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.list-toolbar,
|
||||
.filter-bar,
|
||||
.filters,
|
||||
.list-panel-toolbar,
|
||||
.table-toolbar {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.list-toolbar > *,
|
||||
.filter-bar > *,
|
||||
.filters > *,
|
||||
.list-panel-toolbar > *,
|
||||
.table-toolbar > * {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.el-input,
|
||||
.el-select,
|
||||
.el-date-editor,
|
||||
.el-button {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.el-table {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pager,
|
||||
.list-pager,
|
||||
.table-pager,
|
||||
.pagination-bar {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.el-pagination {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ADMIN_LOCALE_STORAGE_KEY } from './i18n';
|
||||
|
||||
const api = axios.create({ baseURL: '/api' });
|
||||
|
||||
let handling401 = false;
|
||||
let handlingAuthInvalid = false;
|
||||
let handling403Portal = false;
|
||||
|
||||
const PORTAL_MISMATCH_CODES = new Set(['ADMIN_ACCESS_ONLY', 'AGENT_ACCESS_ONLY']);
|
||||
@@ -17,6 +17,33 @@ function requestPath(config: { url?: string; baseURL?: string } | undefined): st
|
||||
return `${base}${config.url}`;
|
||||
}
|
||||
|
||||
function isLoginRequest(config: { url?: string; baseURL?: string } | undefined): boolean {
|
||||
return requestPath(config).includes('/auth/login');
|
||||
}
|
||||
|
||||
function isInvalidCredentialsResponse(data: unknown): boolean {
|
||||
return (
|
||||
typeof data === 'object' &&
|
||||
data !== null &&
|
||||
'code' in data &&
|
||||
(data as { code?: unknown }).code === 'INVALID_CREDENTIALS'
|
||||
);
|
||||
}
|
||||
|
||||
async function redirectToLoginForInvalidSession() {
|
||||
if (handlingAuthInvalid) return;
|
||||
handlingAuthInvalid = true;
|
||||
try {
|
||||
clearStaffSession();
|
||||
resetStaffSessionHydration();
|
||||
if (router.currentRoute.value.path !== '/login') {
|
||||
await router.replace('/login');
|
||||
}
|
||||
} finally {
|
||||
handlingAuthInvalid = false;
|
||||
}
|
||||
}
|
||||
|
||||
api.interceptors.request.use((config) => {
|
||||
const t = localStorage.getItem('manage_token');
|
||||
if (t) config.headers.Authorization = `Bearer ${t}`;
|
||||
@@ -49,7 +76,18 @@ api.interceptors.request.use((config) => {
|
||||
});
|
||||
|
||||
api.interceptors.response.use(
|
||||
(res) => res,
|
||||
async (res) => {
|
||||
if (!isLoginRequest(res.config) && isInvalidCredentialsResponse(res.data)) {
|
||||
await redirectToLoginForInvalidSession();
|
||||
return Promise.reject(
|
||||
Object.assign(new Error((res.data as { error?: string }).error || 'Invalid credentials'), {
|
||||
response: res,
|
||||
config: res.config,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return res;
|
||||
},
|
||||
async (err) => {
|
||||
if (err.isPortalMismatch) {
|
||||
await ensureStaffSession();
|
||||
@@ -59,14 +97,11 @@ api.interceptors.response.use(
|
||||
return Promise.reject(err);
|
||||
}
|
||||
|
||||
if (err.response?.status === 401 && !handling401) {
|
||||
handling401 = true;
|
||||
clearStaffSession();
|
||||
resetStaffSessionHydration();
|
||||
if (router.currentRoute.value.path !== '/login') {
|
||||
await router.replace('/login');
|
||||
}
|
||||
handling401 = false;
|
||||
if (
|
||||
!isLoginRequest(err.config) &&
|
||||
(err.response?.status === 401 || isInvalidCredentialsResponse(err.response?.data))
|
||||
) {
|
||||
await redirectToLoginForInvalidSession();
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
@@ -44,15 +44,15 @@ defineProps<{
|
||||
|
||||
.admin-detail-item__label {
|
||||
font-size: 12px;
|
||||
color: #737373;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.admin-detail-item__value {
|
||||
font-size: 13px;
|
||||
color: #f5f5f5;
|
||||
color: var(--text);
|
||||
line-height: 1.35;
|
||||
min-width: 0;
|
||||
word-break: break-word;
|
||||
@@ -63,7 +63,7 @@ defineProps<{
|
||||
display: inline;
|
||||
margin-left: 6px;
|
||||
font-size: 11px;
|
||||
color: #737373;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,6 @@ const src = computed(() => `/flags/${countryCode.value}.svg`);
|
||||
flex-shrink: 0;
|
||||
border-radius: 2px;
|
||||
object-fit: cover;
|
||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.12);
|
||||
box-shadow: 0 0 0 1px var(--border);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -174,4 +174,47 @@ onUnmounted(() => document.removeEventListener('click', onDocClick));
|
||||
background: rgba(212, 175, 55, 0.1);
|
||||
color: #f0d875;
|
||||
}
|
||||
|
||||
/* Light admin refresh */
|
||||
.admin-locale-trigger {
|
||||
min-height: 32px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.admin-locale-menu {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 18px 44px rgba(56, 49, 37, 0.12);
|
||||
}
|
||||
|
||||
.admin-locale.admin .admin-locale-trigger,
|
||||
.admin-locale.gold .admin-locale-trigger {
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.admin-locale.admin .admin-locale-trigger:hover,
|
||||
.admin-locale.gold .admin-locale-trigger:hover {
|
||||
background: var(--accent-hover);
|
||||
border-color: #d5cfc3;
|
||||
}
|
||||
|
||||
.admin-locale.admin .admin-locale-menu,
|
||||
.admin-locale.gold .admin-locale-menu {
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.admin-locale.admin .admin-locale-option,
|
||||
.admin-locale.gold .admin-locale-option {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.admin-locale.admin .admin-locale-option:hover,
|
||||
.admin-locale.admin .admin-locale-option.active,
|
||||
.admin-locale.gold .admin-locale-option:hover,
|
||||
.admin-locale.gold .admin-locale-option.active {
|
||||
background: var(--accent-hover);
|
||||
color: var(--text);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -101,18 +101,28 @@ onBeforeUnmount(() => {
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px 6px;
|
||||
gap: 4px 6px;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.action-btns :deep(.el-button) {
|
||||
margin: 0;
|
||||
padding: 1px 4px;
|
||||
height: auto;
|
||||
font-size: 11px;
|
||||
line-height: 1.3;
|
||||
padding: 0 6px !important;
|
||||
min-height: 24px;
|
||||
height: 24px;
|
||||
border-radius: 6px !important;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-btns :deep(.el-button.is-link) {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.admin-responsive-actions.is-menu {
|
||||
min-width: 72px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -27,6 +27,21 @@ const { t } = useAdminLocale();
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.admin-row-actions :deep(.el-button) {
|
||||
min-height: 26px;
|
||||
padding: 0 9px !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: 7px !important;
|
||||
background: #ffffff !important;
|
||||
color: var(--text) !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
.admin-row-actions :deep(.el-button:hover) {
|
||||
background: var(--accent-hover) !important;
|
||||
border-color: #d5cfc3 !important;
|
||||
}
|
||||
|
||||
.admin-row-actions__caret {
|
||||
margin-left: 3px;
|
||||
font-size: 11px;
|
||||
@@ -36,7 +51,7 @@ const { t } = useAdminLocale();
|
||||
.admin-row-actions__menu :deep(.el-dropdown-menu__item) {
|
||||
font-size: 13px;
|
||||
line-height: 1.35;
|
||||
padding: 6px 16px;
|
||||
padding: 7px 16px;
|
||||
max-width: 300px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
@@ -53,18 +53,18 @@ defineProps<{
|
||||
line-height: 1.4;
|
||||
}
|
||||
.admin-subnav__link {
|
||||
color: #737373;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.admin-subnav__link:hover {
|
||||
color: #f5f5f5;
|
||||
color: var(--text);
|
||||
}
|
||||
.admin-subnav__current {
|
||||
color: #737373;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.admin-subnav__sep {
|
||||
color: #444;
|
||||
color: #b6afa3;
|
||||
user-select: none;
|
||||
}
|
||||
.admin-subnav__main {
|
||||
@@ -83,14 +83,14 @@ defineProps<{
|
||||
}
|
||||
.admin-subnav__title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #f5f5f5;
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.admin-subnav__subtitle {
|
||||
font-size: 13px;
|
||||
color: #777;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.admin-subnav__extra {
|
||||
display: flex;
|
||||
@@ -98,4 +98,21 @@ defineProps<{
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.admin-subnav {
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.admin-subnav__main,
|
||||
.admin-subnav__extra {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.admin-subnav__extra {
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -25,23 +25,29 @@ defineProps<{
|
||||
|
||||
<style scoped>
|
||||
.admin-table-empty {
|
||||
padding: 28px 16px;
|
||||
margin: 12px;
|
||||
padding: 34px 18px;
|
||||
border: 1px dashed var(--border);
|
||||
border-radius: 10px;
|
||||
background: #fbfaf7;
|
||||
text-align: center;
|
||||
}
|
||||
.admin-table-empty__icon {
|
||||
color: var(--green-text);
|
||||
opacity: 0.45;
|
||||
margin-bottom: 8px;
|
||||
color: var(--text-muted);
|
||||
opacity: 0.72;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.admin-table-empty__text {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 750;
|
||||
color: var(--text);
|
||||
}
|
||||
.admin-table-empty__hint {
|
||||
font-size: 12px;
|
||||
color: #555;
|
||||
margin-top: 4px;
|
||||
color: var(--text-muted);
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -105,7 +105,7 @@ function fmtFull(value: string | null | undefined) {
|
||||
.credit-context-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #bbb;
|
||||
color: var(--text);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.credit-context-alert {
|
||||
@@ -114,24 +114,24 @@ function fmtFull(value: string | null | undefined) {
|
||||
.credit-context-preview {
|
||||
margin-top: 12px;
|
||||
font-size: 13px;
|
||||
color: #aaa;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.level-tag {
|
||||
margin-left: 6px;
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.c-green {
|
||||
color: var(--gold-text);
|
||||
color: var(--success-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
.c-amber {
|
||||
color: #e6a23c;
|
||||
color: var(--warning-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
.amount-full-hint {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
color: var(--text-muted);
|
||||
margin-left: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -49,10 +49,10 @@ function isActive(path: string) {
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
margin-bottom: 16px;
|
||||
padding: 3px;
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
min-height: 40px;
|
||||
box-sizing: border-box;
|
||||
@@ -61,7 +61,7 @@ function isActive(path: string) {
|
||||
margin-bottom: 0;
|
||||
padding: 0 14px 0 0;
|
||||
border: none;
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-right: 1px solid var(--border);
|
||||
background: transparent;
|
||||
flex-shrink: 0;
|
||||
height: 44px;
|
||||
@@ -76,16 +76,27 @@ function isActive(path: string) {
|
||||
box-sizing: border-box;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #737373;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
transition: color 0.15s, background 0.15s;
|
||||
}
|
||||
.dashboard-subnav__item:hover {
|
||||
color: #d4d4d4;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: var(--text);
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
.dashboard-subnav__item--active {
|
||||
color: #f5f5f5;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #ffffff;
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.dashboard-subnav {
|
||||
overflow-x: auto;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.dashboard-subnav__item {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -280,14 +280,14 @@ const oldUrl = ref(props.modelValue);
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
flex-shrink: 0;
|
||||
border: 2px dashed #2a2a2a;
|
||||
border: 2px dashed #d5cfc3;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
background: #fbfaf7;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -296,7 +296,7 @@ const oldUrl = ref(props.modelValue);
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
color: #e55;
|
||||
color: var(--danger-text);
|
||||
cursor: pointer;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
@@ -306,7 +306,7 @@ const oldUrl = ref(props.modelValue);
|
||||
}
|
||||
|
||||
.delete-old-check:hover {
|
||||
background: rgba(238, 85, 85, 0.1);
|
||||
background: var(--danger-bg);
|
||||
}
|
||||
|
||||
.delete-old-check input[type="checkbox"] {
|
||||
@@ -318,13 +318,13 @@ const oldUrl = ref(props.modelValue);
|
||||
}
|
||||
|
||||
.drop-zone:hover {
|
||||
border-color: rgba(212, 175, 55, 0.4);
|
||||
background: rgba(212, 175, 55, 0.04);
|
||||
border-color: var(--primary);
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.drop-zone.is-dragging {
|
||||
border-color: var(--gold-mid);
|
||||
background: rgba(212, 175, 55, 0.1);
|
||||
border-color: var(--primary);
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.drop-preview {
|
||||
@@ -338,7 +338,7 @@ const oldUrl = ref(props.modelValue);
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #555;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.drop-icon {
|
||||
|
||||
@@ -15,16 +15,21 @@ withDefaults(
|
||||
const tabs = [
|
||||
{ path: '/matches', labelKey: 'nav.matches.fixtures' },
|
||||
{ path: '/matches/outrights', labelKey: 'nav.matches.outrights' },
|
||||
{ path: '/matches/market-templates', labelKey: 'nav.matches.market_templates' },
|
||||
];
|
||||
|
||||
function isActive(path: string) {
|
||||
if (path === '/matches/outrights') {
|
||||
return route.path === '/matches/outrights';
|
||||
}
|
||||
if (path === '/matches/market-templates') {
|
||||
return route.path === '/matches/market-templates';
|
||||
}
|
||||
return (
|
||||
route.path === '/matches' ||
|
||||
(route.path.startsWith('/matches/') &&
|
||||
!route.path.startsWith('/matches/outrights'))
|
||||
!route.path.startsWith('/matches/outrights') &&
|
||||
!route.path.startsWith('/matches/market-templates'))
|
||||
);
|
||||
}
|
||||
</script>
|
||||
@@ -53,10 +58,10 @@ function isActive(path: string) {
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-bottom: 16px;
|
||||
padding: 6px;
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
min-height: 48px;
|
||||
box-sizing: border-box;
|
||||
@@ -65,7 +70,7 @@ function isActive(path: string) {
|
||||
margin-bottom: 0;
|
||||
padding: 0 14px 0 0;
|
||||
border: none;
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-right: 1px solid var(--border);
|
||||
background: transparent;
|
||||
flex-shrink: 0;
|
||||
height: 44px;
|
||||
@@ -73,23 +78,34 @@ function isActive(path: string) {
|
||||
}
|
||||
.matches-subnav__item {
|
||||
padding: 0 14px;
|
||||
height: 36px;
|
||||
height: 34px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
border-radius: 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #888;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
transition: color 0.15s, background 0.15s;
|
||||
}
|
||||
.matches-subnav__item:hover {
|
||||
color: #ccc;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: var(--text);
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
.matches-subnav__item--active {
|
||||
color: var(--green-text);
|
||||
background: rgba(0, 200, 83, 0.1);
|
||||
color: #ffffff;
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.matches-subnav {
|
||||
overflow-x: auto;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.matches-subnav__item {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -22,18 +22,18 @@ function drawCaptcha() {
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
ctx.fillStyle = '#7c3aed';
|
||||
ctx.fillStyle = '#3d413b';
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
for (let i = 0; i < 28; i++) {
|
||||
ctx.fillStyle = `rgba(255,255,255,${0.15 + Math.random() * 0.35})`;
|
||||
ctx.fillStyle = `rgba(255,255,255,${0.2 + Math.random() * 0.32})`;
|
||||
ctx.beginPath();
|
||||
ctx.arc(Math.random() * w, Math.random() * h, Math.random() * 2.2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
ctx.strokeStyle = `rgba(255,255,255,${0.2 + Math.random() * 0.3})`;
|
||||
ctx.strokeStyle = `rgba(255,255,255,${0.22 + Math.random() * 0.26})`;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(Math.random() * w, Math.random() * h);
|
||||
@@ -98,16 +98,21 @@ defineExpose({ validate, refresh });
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 0 14px;
|
||||
border: none;
|
||||
border: 1px solid var(--border);
|
||||
border-right: none;
|
||||
border-radius: 8px 0 0 8px;
|
||||
background: #ffffff;
|
||||
color: #111;
|
||||
color: var(--text);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
font-weight: 650;
|
||||
outline: none;
|
||||
}
|
||||
.captcha-input::placeholder {
|
||||
color: #9ca3af;
|
||||
color: #aaa49a;
|
||||
}
|
||||
.captcha-input:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(31, 35, 32, 0.08);
|
||||
}
|
||||
.captcha-canvas {
|
||||
flex-shrink: 0;
|
||||
@@ -116,5 +121,8 @@ defineExpose({ validate, refresh });
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
border-radius: 0 8px 8px 0;
|
||||
border: 1px solid #3d413b;
|
||||
border-left: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -105,12 +105,12 @@ function limitLabel(value: string | null) {
|
||||
.transfer-context-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #bbb;
|
||||
color: var(--text);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.transfer-context-alert { margin-bottom: 0; }
|
||||
.c-green { color: var(--gold-text); font-weight: 600; }
|
||||
.c-amber { color: #e6a23c; font-weight: 600; }
|
||||
.amount-full-hint { font-size: 11px; color: #666; margin-left: 4px; }
|
||||
.field-hint { font-size: 12px; color: #666; }
|
||||
.c-green { color: var(--success-text); font-weight: 600; }
|
||||
.c-amber { color: var(--warning-text); font-weight: 600; }
|
||||
.amount-full-hint { font-size: 11px; color: var(--text-muted); margin-left: 4px; }
|
||||
.field-hint { font-size: 12px; color: var(--text-muted); }
|
||||
</style>
|
||||
|
||||
@@ -30,16 +30,16 @@ const style = computed(() => ({ height: props.height, width: '100%' }));
|
||||
<style scoped>
|
||||
.chart-panel {
|
||||
padding: 16px 18px 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #1e1e1e;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-soft);
|
||||
background: #ffffff;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.chart-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #ccc;
|
||||
color: var(--text);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.chart-canvas {
|
||||
|
||||
@@ -111,7 +111,8 @@ function onChange(code: string | undefined) {
|
||||
object-fit: cover;
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
background: #222;
|
||||
background: #f4f0e8;
|
||||
border: 1px solid var(--border-soft);
|
||||
}
|
||||
|
||||
.country-option {
|
||||
@@ -147,7 +148,7 @@ function onChange(code: string | undefined) {
|
||||
|
||||
.country-option-code {
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -218,6 +218,7 @@ const zh: Record<string, string> = {
|
||||
'page.agents.desc': '创建一级代理、调整授信额度、查看直属玩家与额度占用',
|
||||
'nav.matches.fixtures': '赛事配置',
|
||||
'nav.matches.outrights': '优胜赛配置(盘口)',
|
||||
'nav.matches.market_templates': '盘口模板',
|
||||
'page.matches.title': '赛事管理',
|
||||
'page.matches.desc': '草稿可编辑、删除;已发布可改开赛时间与热门;未结算可下架',
|
||||
'page.bets.title': '注单管理',
|
||||
@@ -485,6 +486,7 @@ const en: Record<string, string> = {
|
||||
'page.agents.desc': 'Tier-1 agents, credit limits, players, and usage',
|
||||
'nav.matches.fixtures': 'Fixtures',
|
||||
'nav.matches.outrights': 'Outright odds',
|
||||
'nav.matches.market_templates': 'Market templates',
|
||||
'page.matches.title': 'Matches',
|
||||
'page.matches.desc': 'Edit/delete drafts; adjust kickoff and featured when published; unpublish while unsettled',
|
||||
'page.bets.title': 'Bets',
|
||||
@@ -752,6 +754,7 @@ const ms: Record<string, string> = {
|
||||
'page.agents.desc': 'Ejen peringkat 1, had kredit, pemain dan penggunaan',
|
||||
'nav.matches.fixtures': 'Konfigurasi perlawanan',
|
||||
'nav.matches.outrights': 'Odds juara',
|
||||
'nav.matches.market_templates': 'Templat pasaran',
|
||||
'page.matches.title': 'Perlawanan',
|
||||
'page.matches.desc': 'Edit/padam draf; laraskan masa mula bila diterbitkan; nyahterbit jika belum selesai',
|
||||
'page.bets.title': 'Pertaruhan',
|
||||
|
||||
@@ -259,6 +259,7 @@ export const adminPagesMs: Record<string, string> = {
|
||||
'match.field.lang_en': 'EN',
|
||||
'match.field.lang_ms': 'MS',
|
||||
'match.field.kickoff': 'Masa mula',
|
||||
'match.timezone.platform_hint': 'Disimpan sebagai masa Malaysia (UTC+8)',
|
||||
'match.field.home_team': 'Pasukan tuan rumah',
|
||||
'match.field.away_team': 'Pasukan pelawat',
|
||||
'match.field.home_en': 'Tuan rumah (EN)',
|
||||
@@ -472,7 +473,8 @@ export const adminPagesMs: Record<string, string> = {
|
||||
'matchEditor.field.promo_label': 'Label promosi',
|
||||
'matchEditor.field.promo_label_optional': 'Label promosi (pilihan)',
|
||||
'matchEditor.field.line_value': 'Garisan',
|
||||
'matchEditor.ph.kickoff': 'Pilih tarikh & masa mula',
|
||||
'matchEditor.field.player_visible': 'Papar pemain',
|
||||
'matchEditor.ph.kickoff': 'Pilih tarikh & masa mula (masa Malaysia)',
|
||||
'matchEditor.group.league': 'Liga',
|
||||
'matchEditor.hint.league_readonly': 'Nama dan logo liga diselenggara dalam senarai kejohanan; paparan di sini hanya baca.',
|
||||
'matchEditor.group.home': 'Tuan rumah',
|
||||
@@ -492,7 +494,7 @@ export const adminPagesMs: Record<string, string> = {
|
||||
'matchEditor.market.HT_HANDICAP': 'HT handicap',
|
||||
'matchEditor.market.HT_OVER_UNDER': 'HT O/U',
|
||||
'matchEditor.market.FT_CORRECT_SCORE': 'FT skor tepat',
|
||||
'matchEditor.market.HT_CORRECT_SCORE': 'HT skor tepat',
|
||||
'matchEditor.market.HT_CORRECT_SCORE': '1H skor tepat',
|
||||
'matchEditor.market.SH_CORRECT_SCORE': '2H skor tepat',
|
||||
'matchEditor.period.FT': 'Sepenuh masa',
|
||||
'matchEditor.period.HT': 'Separuh masa',
|
||||
@@ -550,6 +552,13 @@ export const adminPagesMs: Record<string, string> = {
|
||||
'settlement.col.parlay_legs': 'Kaki parlay',
|
||||
'settlement.ht_score': 'Skor separuh masa',
|
||||
'settlement.ft_score': 'Skor penuh masa',
|
||||
'settlement.stats_facts': 'Fakta perlawanan',
|
||||
'settlement.corners': 'Sepakan sudut',
|
||||
'settlement.cards': 'Kad',
|
||||
'settlement.yellow_cards': 'Kad kuning',
|
||||
'settlement.red_cards': 'Kad merah',
|
||||
'settlement.home_stat': 'Tuan rumah',
|
||||
'settlement.away_stat': 'Pelawat',
|
||||
'settlement.record_score': 'Simpan skor',
|
||||
'settlement.preview_hint': 'Pratonton menukar perlawanan ke menunggu penyelesaian (skor disimpan selepas pengesahan; boleh buka semula sebelum itu)',
|
||||
'settlement.preview_btn': 'Pratonton penyelesaian',
|
||||
@@ -742,6 +751,7 @@ export const adminPagesMs: Record<string, string> = {
|
||||
'content.upload.pick_media_title': 'Pilih Imej Banner',
|
||||
'content.upload.no_media': 'Tiada imej banner dalam pustaka — muat naik dahulu',
|
||||
'content.upload.url_placeholder': 'Atau tampal URL imej',
|
||||
'content.upload.recommended_size': 'Saiz disyorkan: 860 x 360 px, atau imej nisbah 43:18. Karusel pemain memaparkan imej penuh dan mengisi ruang tambahan.',
|
||||
'content.link.none': 'Tiada pautan',
|
||||
'content.locale.zh-CN': 'Cina Ringkas',
|
||||
'content.locale.en-US': 'English',
|
||||
|
||||
@@ -281,6 +281,7 @@ export const adminPagesZh: Record<string, string> = {
|
||||
'match.field.lang_en': 'EN',
|
||||
'match.field.lang_ms': 'MS',
|
||||
'match.field.kickoff': '开赛时间',
|
||||
'match.timezone.platform_hint': '按马来西亚时间 (UTC+8) 保存',
|
||||
'match.field.home_team': '主队',
|
||||
'match.field.away_team': '客队',
|
||||
'match.field.home_en': '主队(英)',
|
||||
@@ -490,11 +491,11 @@ export const adminPagesZh: Record<string, string> = {
|
||||
'matchEditor.field.stage': '阶段',
|
||||
'matchEditor.field.group': '小组',
|
||||
'matchEditor.field.display_order': '排序',
|
||||
'matchEditor.field.correct_score_enabled': '波胆玩法',
|
||||
'matchEditor.field.promo_label': '促销标签',
|
||||
'matchEditor.field.promo_label_optional': '促销标签(可选)',
|
||||
'matchEditor.field.line_value': '盘口线',
|
||||
'matchEditor.ph.kickoff': '选择开赛日期与时间',
|
||||
'matchEditor.field.player_visible': '玩家端显示',
|
||||
'matchEditor.ph.kickoff': '选择开赛日期与时间(马来西亚时间)',
|
||||
'matchEditor.group.league': '联赛信息',
|
||||
'matchEditor.hint.league_readonly': '联赛名称与 Logo 请在赛事管理列表中点击「编辑」维护,此处仅展示。',
|
||||
'matchEditor.group.home': '主队',
|
||||
@@ -514,7 +515,7 @@ export const adminPagesZh: Record<string, string> = {
|
||||
'matchEditor.market.HT_HANDICAP': '半场让球',
|
||||
'matchEditor.market.HT_OVER_UNDER': '半场大小',
|
||||
'matchEditor.market.FT_CORRECT_SCORE': '全场波胆',
|
||||
'matchEditor.market.HT_CORRECT_SCORE': '半场波胆',
|
||||
'matchEditor.market.HT_CORRECT_SCORE': '上半场波胆',
|
||||
'matchEditor.market.SH_CORRECT_SCORE': '下半场波胆',
|
||||
'matchEditor.period.FT': '全场',
|
||||
'matchEditor.period.HT': '半场',
|
||||
@@ -574,6 +575,13 @@ export const adminPagesZh: Record<string, string> = {
|
||||
'settlement.col.parlay_legs': '串关腿数',
|
||||
'settlement.ht_score': '半场比分',
|
||||
'settlement.ft_score': '全场比分',
|
||||
'settlement.stats_facts': '统计事实',
|
||||
'settlement.corners': '角球',
|
||||
'settlement.cards': '罚牌',
|
||||
'settlement.yellow_cards': '黄牌',
|
||||
'settlement.red_cards': '红牌',
|
||||
'settlement.home_stat': '主队',
|
||||
'settlement.away_stat': '客队',
|
||||
'settlement.record_score': '录入比分',
|
||||
'settlement.preview_hint': '填写比分后点击生成预览,赛事将进入待结算并计算派彩(正式比分在确认结算后保存;未确认前仍可解除封盘)',
|
||||
'settlement.preview_btn': '生成结算预览',
|
||||
@@ -594,9 +602,9 @@ export const adminPagesZh: Record<string, string> = {
|
||||
'settlement.preview_items_scroll_hint': '笔数较多时可滚动查看',
|
||||
'settlement.preview_col.result': '本场结果',
|
||||
'settlement.preview_zero_parlay_hint':
|
||||
'预计派彩为 0:本场虽有 {legs} 条赢腿,但均在跨场串关内({pending} 笔待其他场次),须等其他比赛结算后才派彩。',
|
||||
'预计派彩为 0:{pending} 笔跨场串关注单仍须等其他比赛结算后,才会统一结算。',
|
||||
'settlement.preview_zero_lost_hint':
|
||||
'预计派彩为 0:{count} 笔串关在本场已有输腿,整单作废;其余单关/串关本场亦未中奖。',
|
||||
'预计派彩为 0:{count} 笔串关已满足整单结算条件但结果为输;其余单关/串关本场亦未中奖。',
|
||||
'settlement.preview_zero_default_hint': '预计派彩为 0:本场相关注单均未中奖或暂不满足派彩条件。',
|
||||
'settlement.preview.result.WIN': '赢',
|
||||
'settlement.preview.result.LOSE': '输',
|
||||
@@ -816,6 +824,7 @@ export const adminPagesZh: Record<string, string> = {
|
||||
'content.upload.pick_media_title': '选择 Banner 图片',
|
||||
'content.upload.no_media': '媒体库中暂无 Banner 图片,请先上传',
|
||||
'content.upload.url_placeholder': '或手动粘贴图片 URL',
|
||||
'content.upload.recommended_size': '建议尺寸:860 x 360 px,或 43:18 同比例图片;前台会完整显示并自动填充不合比例区域。',
|
||||
'content.link.none': '无跳转',
|
||||
'content.locale.zh-CN': '简体中文',
|
||||
'content.locale.en-US': 'English',
|
||||
@@ -1297,6 +1306,7 @@ export const adminPagesEn: Record<string, string> = {
|
||||
'match.field.lang_en': 'EN',
|
||||
'match.field.lang_ms': 'MS',
|
||||
'match.field.kickoff': 'Kickoff time',
|
||||
'match.timezone.platform_hint': 'Saved as Malaysia time (UTC+8)',
|
||||
'match.field.home_team': 'Home team',
|
||||
'match.field.away_team': 'Away team',
|
||||
'match.field.home_en': 'Home (EN)',
|
||||
@@ -1506,11 +1516,11 @@ export const adminPagesEn: Record<string, string> = {
|
||||
'matchEditor.field.stage': 'Stage',
|
||||
'matchEditor.field.group': 'Group',
|
||||
'matchEditor.field.display_order': 'Sort order',
|
||||
'matchEditor.field.correct_score_enabled': 'Correct score',
|
||||
'matchEditor.field.promo_label': 'Promo label',
|
||||
'matchEditor.field.promo_label_optional': 'Promo label (optional)',
|
||||
'matchEditor.field.line_value': 'Line',
|
||||
'matchEditor.ph.kickoff': 'Select kickoff date & time',
|
||||
'matchEditor.field.player_visible': 'Show on player',
|
||||
'matchEditor.ph.kickoff': 'Select kickoff date & time (Malaysia time)',
|
||||
'matchEditor.group.league': 'League',
|
||||
'matchEditor.hint.league_readonly': 'Edit league name and logo from the tournament list; shown here read-only.',
|
||||
'matchEditor.group.home': 'Home team',
|
||||
@@ -1530,7 +1540,7 @@ export const adminPagesEn: Record<string, string> = {
|
||||
'matchEditor.market.HT_HANDICAP': 'HT handicap',
|
||||
'matchEditor.market.HT_OVER_UNDER': 'HT O/U',
|
||||
'matchEditor.market.FT_CORRECT_SCORE': 'FT correct score',
|
||||
'matchEditor.market.HT_CORRECT_SCORE': 'HT correct score',
|
||||
'matchEditor.market.HT_CORRECT_SCORE': '1H correct score',
|
||||
'matchEditor.market.SH_CORRECT_SCORE': '2H correct score',
|
||||
'matchEditor.period.FT': 'Full time',
|
||||
'matchEditor.period.HT': 'Half time',
|
||||
@@ -1590,6 +1600,13 @@ export const adminPagesEn: Record<string, string> = {
|
||||
'settlement.col.parlay_legs': 'Parlay legs',
|
||||
'settlement.ht_score': 'Half-time score',
|
||||
'settlement.ft_score': 'Full-time score',
|
||||
'settlement.stats_facts': 'Match facts',
|
||||
'settlement.corners': 'Corners',
|
||||
'settlement.cards': 'Cards',
|
||||
'settlement.yellow_cards': 'Yellow cards',
|
||||
'settlement.red_cards': 'Red cards',
|
||||
'settlement.home_stat': 'Home',
|
||||
'settlement.away_stat': 'Away',
|
||||
'settlement.record_score': 'Save score',
|
||||
'settlement.preview_hint': 'Preview moves the match to pending settlement and calculates payouts (scores are saved on confirm; you can reopen betting before confirming)',
|
||||
'settlement.preview_btn': 'Preview settlement',
|
||||
@@ -1610,9 +1627,9 @@ export const adminPagesEn: Record<string, string> = {
|
||||
'settlement.preview_items_scroll_hint': 'Scroll when the list is long',
|
||||
'settlement.preview_col.result': 'Match result',
|
||||
'settlement.preview_zero_parlay_hint':
|
||||
'Est. payout is 0: {legs} winning leg(s) on this match are in cross-match parlays ({pending} bet(s) awaiting other fixtures).',
|
||||
'Est. payout is 0: {pending} cross-match parlay bet(s) must wait for every fixture before final settlement.',
|
||||
'settlement.preview_zero_lost_hint':
|
||||
'Est. payout is 0: {count} parlay(s) already lost on this match; other bets did not win here either.',
|
||||
'Est. payout is 0: {count} parlay(s) are ready for final settlement and lost; other bets did not win here either.',
|
||||
'settlement.preview_zero_default_hint':
|
||||
'Est. payout is 0: no winning or refundable outcome on this match yet.',
|
||||
'settlement.preview.result.WIN': 'Win',
|
||||
@@ -1833,6 +1850,7 @@ export const adminPagesEn: Record<string, string> = {
|
||||
'content.upload.pick_media_title': 'Select Banner Image',
|
||||
'content.upload.no_media': 'No banner images in library — upload one first',
|
||||
'content.upload.url_placeholder': 'Or paste image URL',
|
||||
'content.upload.recommended_size': 'Recommended size: 860 x 360 px, or any 43:18 image. The player carousel keeps the full image visible and fills extra space.',
|
||||
'content.link.none': 'No link',
|
||||
'content.locale.zh-CN': 'Chinese (Simplified)',
|
||||
'content.locale.en-US': 'English',
|
||||
|
||||
@@ -279,6 +279,7 @@ const adminPages: Record<string, string> = {
|
||||
'match.field.lang_en': 'EN',
|
||||
'match.field.lang_ms': 'MS',
|
||||
'match.field.kickoff': 'Kickoff time',
|
||||
'match.timezone.platform_hint': 'Saved as Malaysia time (UTC+8)',
|
||||
'match.field.home_team': 'Home team',
|
||||
'match.field.away_team': 'Away team',
|
||||
'match.field.home_en': 'Home (EN)',
|
||||
@@ -491,7 +492,8 @@ const adminPages: Record<string, string> = {
|
||||
'matchEditor.field.promo_label': 'Promo label',
|
||||
'matchEditor.field.promo_label_optional': 'Promo label (optional)',
|
||||
'matchEditor.field.line_value': 'Line',
|
||||
'matchEditor.ph.kickoff': 'Select kickoff date & time',
|
||||
'matchEditor.field.player_visible': 'Show on player',
|
||||
'matchEditor.ph.kickoff': 'Select kickoff date & time (Malaysia time)',
|
||||
'matchEditor.group.league': 'League',
|
||||
'matchEditor.hint.league_readonly': 'Edit league name and logo from the tournament list; shown here read-only.',
|
||||
'matchEditor.group.home': 'Home team',
|
||||
@@ -511,7 +513,7 @@ const adminPages: Record<string, string> = {
|
||||
'matchEditor.market.HT_HANDICAP': 'HT handicap',
|
||||
'matchEditor.market.HT_OVER_UNDER': 'HT O/U',
|
||||
'matchEditor.market.FT_CORRECT_SCORE': 'FT correct score',
|
||||
'matchEditor.market.HT_CORRECT_SCORE': 'HT correct score',
|
||||
'matchEditor.market.HT_CORRECT_SCORE': '1H correct score',
|
||||
'matchEditor.market.SH_CORRECT_SCORE': '2H correct score',
|
||||
'matchEditor.period.FT': 'Full time',
|
||||
'matchEditor.period.HT': 'Half time',
|
||||
@@ -571,6 +573,13 @@ const adminPages: Record<string, string> = {
|
||||
'settlement.col.parlay_legs': 'Parlay legs',
|
||||
'settlement.ht_score': 'Half-time score',
|
||||
'settlement.ft_score': 'Full-time score',
|
||||
'settlement.stats_facts': 'Match facts',
|
||||
'settlement.corners': 'Corners',
|
||||
'settlement.cards': 'Cards',
|
||||
'settlement.yellow_cards': 'Yellow cards',
|
||||
'settlement.red_cards': 'Red cards',
|
||||
'settlement.home_stat': 'Home',
|
||||
'settlement.away_stat': 'Away',
|
||||
'settlement.record_score': 'Save score',
|
||||
'settlement.preview_hint': 'Preview moves the match to pending settlement and calculates payouts (scores are saved on confirm; you can reopen betting before confirming)',
|
||||
'settlement.preview_btn': 'Preview settlement',
|
||||
@@ -591,9 +600,9 @@ const adminPages: Record<string, string> = {
|
||||
'settlement.preview_items_scroll_hint': 'Scroll when the list is long',
|
||||
'settlement.preview_col.result': 'Match result',
|
||||
'settlement.preview_zero_parlay_hint':
|
||||
'Est. payout is 0: {legs} winning leg(s) on this match are in cross-match parlays ({pending} bet(s) awaiting other fixtures).',
|
||||
'Est. payout is 0: {pending} cross-match parlay bet(s) must wait for every fixture before final settlement.',
|
||||
'settlement.preview_zero_lost_hint':
|
||||
'Est. payout is 0: {count} parlay(s) already lost on this match; other bets did not win here either.',
|
||||
'Est. payout is 0: {count} parlay(s) are ready for final settlement and lost; other bets did not win here either.',
|
||||
'settlement.preview_zero_default_hint':
|
||||
'Est. payout is 0: no winning or refundable outcome on this match yet.',
|
||||
'settlement.preview.result.WIN': 'Win',
|
||||
@@ -814,6 +823,7 @@ const adminPages: Record<string, string> = {
|
||||
'content.upload.pick_media_title': 'Select Banner Image',
|
||||
'content.upload.no_media': 'No banner images in library — upload one first',
|
||||
'content.upload.url_placeholder': 'Or paste image URL',
|
||||
'content.upload.recommended_size': 'Recommended size: 860 x 360 px, or any 43:18 image. The player carousel keeps the full image visible and fills extra space.',
|
||||
'content.link.none': 'No link',
|
||||
'content.locale.zh-CN': 'Chinese (Simplified)',
|
||||
'content.locale.en-US': 'English',
|
||||
|
||||
@@ -280,6 +280,7 @@ const adminPages: Record<string, string> = {
|
||||
'match.field.lang_en': 'EN',
|
||||
'match.field.lang_ms': 'MS',
|
||||
'match.field.kickoff': '开赛时间',
|
||||
'match.timezone.platform_hint': '按马来西亚时间 (UTC+8) 保存',
|
||||
'match.field.home_team': '主队',
|
||||
'match.field.away_team': '客队',
|
||||
'match.field.home_en': '主队(英)',
|
||||
@@ -489,11 +490,11 @@ const adminPages: Record<string, string> = {
|
||||
'matchEditor.field.stage': '阶段',
|
||||
'matchEditor.field.group': '小组',
|
||||
'matchEditor.field.display_order': '排序',
|
||||
'matchEditor.field.correct_score_enabled': '波胆玩法',
|
||||
'matchEditor.field.promo_label': '促销标签',
|
||||
'matchEditor.field.promo_label_optional': '促销标签(可选)',
|
||||
'matchEditor.field.line_value': '盘口线',
|
||||
'matchEditor.ph.kickoff': '选择开赛日期与时间',
|
||||
'matchEditor.field.player_visible': '玩家端显示',
|
||||
'matchEditor.ph.kickoff': '选择开赛日期与时间(马来西亚时间)',
|
||||
'matchEditor.group.league': '联赛信息',
|
||||
'matchEditor.hint.league_readonly': '联赛名称与 Logo 请在赛事管理列表中点击「编辑」维护,此处仅展示。',
|
||||
'matchEditor.group.home': '主队',
|
||||
@@ -513,7 +514,7 @@ const adminPages: Record<string, string> = {
|
||||
'matchEditor.market.HT_HANDICAP': '半场让球',
|
||||
'matchEditor.market.HT_OVER_UNDER': '半场大小',
|
||||
'matchEditor.market.FT_CORRECT_SCORE': '全场波胆',
|
||||
'matchEditor.market.HT_CORRECT_SCORE': '半场波胆',
|
||||
'matchEditor.market.HT_CORRECT_SCORE': '上半场波胆',
|
||||
'matchEditor.market.SH_CORRECT_SCORE': '下半场波胆',
|
||||
'matchEditor.period.FT': '全场',
|
||||
'matchEditor.period.HT': '半场',
|
||||
@@ -573,6 +574,13 @@ const adminPages: Record<string, string> = {
|
||||
'settlement.col.parlay_legs': '串关腿数',
|
||||
'settlement.ht_score': '半场比分',
|
||||
'settlement.ft_score': '全场比分',
|
||||
'settlement.stats_facts': '统计事实',
|
||||
'settlement.corners': '角球',
|
||||
'settlement.cards': '罚牌',
|
||||
'settlement.yellow_cards': '黄牌',
|
||||
'settlement.red_cards': '红牌',
|
||||
'settlement.home_stat': '主队',
|
||||
'settlement.away_stat': '客队',
|
||||
'settlement.record_score': '录入比分',
|
||||
'settlement.preview_hint': '填写比分后点击生成预览,赛事将进入待结算并计算派彩(正式比分在确认结算后保存;未确认前仍可解除封盘)',
|
||||
'settlement.preview_btn': '生成结算预览',
|
||||
@@ -593,9 +601,9 @@ const adminPages: Record<string, string> = {
|
||||
'settlement.preview_items_scroll_hint': '笔数较多时可滚动查看',
|
||||
'settlement.preview_col.result': '本场结果',
|
||||
'settlement.preview_zero_parlay_hint':
|
||||
'预计派彩为 0:本场虽有 {legs} 条赢腿,但均在跨场串关内({pending} 笔待其他场次),须等其他比赛结算后才派彩。',
|
||||
'预计派彩为 0:{pending} 笔跨场串关注单仍须等其他比赛结算后,才会统一结算。',
|
||||
'settlement.preview_zero_lost_hint':
|
||||
'预计派彩为 0:{count} 笔串关在本场已有输腿,整单作废;其余单关/串关本场亦未中奖。',
|
||||
'预计派彩为 0:{count} 笔串关已满足整单结算条件但结果为输;其余单关/串关本场亦未中奖。',
|
||||
'settlement.preview_zero_default_hint': '预计派彩为 0:本场相关注单均未中奖或暂不满足派彩条件。',
|
||||
'settlement.preview.result.WIN': '赢',
|
||||
'settlement.preview.result.LOSE': '输',
|
||||
@@ -815,6 +823,7 @@ const adminPages: Record<string, string> = {
|
||||
'content.upload.pick_media_title': '选择 Banner 图片',
|
||||
'content.upload.no_media': '媒体库中暂无 Banner 图片,请先上传',
|
||||
'content.upload.url_placeholder': '或手动粘贴图片 URL',
|
||||
'content.upload.recommended_size': '建议尺寸:860 x 360 px,或 43:18 同比例图片;前台会完整显示并自动填充不合比例区域。',
|
||||
'content.link.none': '无跳转',
|
||||
'content.locale.zh-CN': '简体中文',
|
||||
'content.locale.en-US': 'English',
|
||||
|
||||
@@ -541,4 +541,265 @@ watch(() => route.path, () => {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Light management shell */
|
||||
.shell {
|
||||
--sidebar-width: 204px;
|
||||
height: 100dvh;
|
||||
background: var(--bg-body);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background: #ffffff;
|
||||
border-right: 1px solid var(--border);
|
||||
box-shadow: 10px 0 28px rgba(56, 49, 37, 0.04);
|
||||
}
|
||||
|
||||
.brand {
|
||||
height: 64px;
|
||||
min-height: 64px;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid var(--border-soft);
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
max-width: 132px;
|
||||
max-height: 38px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
padding: 12px 10px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
gap: 10px;
|
||||
min-height: 38px;
|
||||
padding: 9px 11px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
color: #67635c;
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.nav-item :deep(.admin-nav-icon) {
|
||||
opacity: 0.86;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
border-color: var(--border-soft);
|
||||
background: var(--accent-hover);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
border-color: var(--primary);
|
||||
background: var(--primary);
|
||||
color: #ffffff;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.nav-item.active :deep(.admin-nav-icon),
|
||||
.nav-item:hover :deep(.admin-nav-icon) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sidebar-foot {
|
||||
padding: 12px 14px;
|
||||
border-top: 1px solid var(--border-soft);
|
||||
color: #aaa49a;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.sidebar-backdrop {
|
||||
background: rgba(31, 35, 32, 0.28);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.main {
|
||||
background: var(--bg-body);
|
||||
}
|
||||
|
||||
.topbar {
|
||||
height: 64px;
|
||||
min-height: 64px;
|
||||
padding: 0 28px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border-bottom: 1px solid var(--border);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.topbar-left {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.btn-menu {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-menu:hover {
|
||||
border-color: #d5cfc3;
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.topbar-title {
|
||||
color: var(--text);
|
||||
font-size: 15px;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.topbar-crumbs {
|
||||
gap: 2px 7px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.crumb-link {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.crumb-link:hover,
|
||||
.crumb-current {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.crumb-sep {
|
||||
color: #b6afa3;
|
||||
}
|
||||
|
||||
.topbar-accent {
|
||||
width: 3px;
|
||||
height: 18px;
|
||||
border-radius: 999px;
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
.topbar-page-actions {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.user-chip {
|
||||
padding: 4px 8px 4px 4px;
|
||||
border: 1px solid var(--border-soft);
|
||||
border-radius: 999px;
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 1px solid #d6ddd4;
|
||||
background: var(--success-bg);
|
||||
color: var(--success-text);
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
color: var(--text);
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.user-role {
|
||||
color: var(--text-muted);
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.portal-tag {
|
||||
padding: 4px 8px;
|
||||
border-color: var(--border);
|
||||
border-radius: 999px;
|
||||
background: #ffffff;
|
||||
color: var(--text-muted);
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.btn-logout {
|
||||
min-height: 32px;
|
||||
padding: 0 14px;
|
||||
border-color: var(--border);
|
||||
border-radius: 7px;
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.btn-logout:hover {
|
||||
border-color: var(--danger-border);
|
||||
background: var(--danger-bg);
|
||||
color: var(--danger-text);
|
||||
}
|
||||
|
||||
.page-main {
|
||||
padding: 22px 28px 30px;
|
||||
background: var(--bg-body);
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
.sidebar.open {
|
||||
box-shadow: 18px 0 46px rgba(56, 49, 37, 0.16);
|
||||
}
|
||||
|
||||
.topbar {
|
||||
height: auto;
|
||||
min-height: 60px;
|
||||
padding: 10px 14px;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.topbar-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.topbar-title {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.topbar-crumbs,
|
||||
.topbar-page-label {
|
||||
max-width: 42vw;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.page-main {
|
||||
padding: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.topbar {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.topbar-right {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.user-chip {
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.btn-logout {
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -60,6 +60,11 @@ const router = createRouter({
|
||||
component: () => import('../views/MatchesOutrights.vue'),
|
||||
meta: { adminOnly: true },
|
||||
},
|
||||
{
|
||||
path: 'matches/market-templates',
|
||||
component: () => import('../views/MarketTemplates.vue'),
|
||||
meta: { adminOnly: true },
|
||||
},
|
||||
{
|
||||
path: 'matches/:matchId/edit',
|
||||
name: 'admin-match-edit',
|
||||
|
||||
@@ -14,6 +14,12 @@ export function resolveAdminBreadcrumb(
|
||||
{ label: t('breadcrumb.settlement') },
|
||||
];
|
||||
}
|
||||
if (path === '/matches/market-templates') {
|
||||
return [
|
||||
{ label: t('nav.matches'), to: '/matches' },
|
||||
{ label: t('nav.matches.market_templates') },
|
||||
];
|
||||
}
|
||||
if (/^\/matches\/[^/]+\/edit/.test(path)) {
|
||||
return [
|
||||
{ label: t('nav.matches'), to: '/matches' },
|
||||
|
||||
@@ -14,13 +14,13 @@ export type ChartI18n = {
|
||||
};
|
||||
|
||||
const tooltipBase = {
|
||||
backgroundColor: '#141414',
|
||||
borderColor: '#2a2a2a',
|
||||
textStyle: { color: '#e0e0e0', fontSize: 12 },
|
||||
backgroundColor: '#ffffff',
|
||||
borderColor: '#e7e2d8',
|
||||
textStyle: { color: '#1f2320', fontSize: 12 },
|
||||
};
|
||||
|
||||
const axisLabel = { color: '#888', fontSize: 11 };
|
||||
const splitLine = { lineStyle: { color: '#252525' } };
|
||||
const axisLabel = { color: '#78746b', fontSize: 11 };
|
||||
const splitLine = { lineStyle: { color: '#eee9df' } };
|
||||
|
||||
export type ChartSeries = { name: string; color: string; values: number[] };
|
||||
export type PieSegment = { label: string; value: number; color: string };
|
||||
@@ -45,14 +45,14 @@ export function buildBarChartOption(
|
||||
bottom: 0,
|
||||
itemWidth: 10,
|
||||
itemHeight: 10,
|
||||
textStyle: { color: '#999', fontSize: 11 },
|
||||
textStyle: { color: '#78746b', fontSize: 11 },
|
||||
},
|
||||
grid: { left: 52, right: 12, top: 20, bottom: 48 },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: labels,
|
||||
axisLabel,
|
||||
axisLine: { lineStyle: { color: '#333' } },
|
||||
axisLine: { lineStyle: { color: '#e7e2d8' } },
|
||||
axisTick: { show: false },
|
||||
},
|
||||
yAxis: {
|
||||
@@ -90,7 +90,7 @@ export function buildMultiLineChartOption(
|
||||
bottom: 0,
|
||||
itemWidth: 10,
|
||||
itemHeight: 10,
|
||||
textStyle: { color: '#999', fontSize: 11 },
|
||||
textStyle: { color: '#78746b', fontSize: 11 },
|
||||
},
|
||||
grid: { left: 52, right: 12, top: 20, bottom: 48 },
|
||||
xAxis: {
|
||||
@@ -98,7 +98,7 @@ export function buildMultiLineChartOption(
|
||||
data: labels,
|
||||
boundaryGap: false,
|
||||
axisLabel,
|
||||
axisLine: { lineStyle: { color: '#333' } },
|
||||
axisLine: { lineStyle: { color: '#e7e2d8' } },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
@@ -145,7 +145,7 @@ export function buildPieChartOption(
|
||||
top: 'middle',
|
||||
itemWidth: 8,
|
||||
itemHeight: 8,
|
||||
textStyle: { color: '#999', fontSize: 11 },
|
||||
textStyle: { color: '#78746b', fontSize: 11 },
|
||||
},
|
||||
series: [
|
||||
{
|
||||
@@ -154,19 +154,19 @@ export function buildPieChartOption(
|
||||
radius: ['42%', '68%'],
|
||||
center: ['36%', '50%'],
|
||||
avoidLabelOverlap: true,
|
||||
itemStyle: { borderRadius: 4, borderColor: '#111', borderWidth: 2 },
|
||||
itemStyle: { borderRadius: 4, borderColor: '#ffffff', borderWidth: 2 },
|
||||
label: {
|
||||
show: segments.length > 0,
|
||||
color: '#bbb',
|
||||
color: '#5f5b53',
|
||||
fontSize: 11,
|
||||
formatter: '{b}\n{d}%',
|
||||
},
|
||||
labelLine: { lineStyle: { color: '#444' } },
|
||||
labelLine: { lineStyle: { color: '#d5cfc3' } },
|
||||
emphasis: {
|
||||
label: { fontSize: 12, fontWeight: 'bold' },
|
||||
scaleSize: 6,
|
||||
},
|
||||
data: data.length ? data : [{ name: noData, value: 1, itemStyle: { color: '#333' } }],
|
||||
data: data.length ? data : [{ name: noData, value: 1, itemStyle: { color: '#e6e1d8' } }],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -189,7 +189,7 @@ export function buildCombinedTrendOption(
|
||||
const countSuffix = i18n.countSuffix;
|
||||
return {
|
||||
backgroundColor: 'transparent',
|
||||
color: [...amountSeries.map((s) => s.color), '#fb923c'],
|
||||
color: [...amountSeries.map((s) => s.color), '#956400'],
|
||||
tooltip: {
|
||||
...tooltipBase,
|
||||
trigger: 'axis',
|
||||
@@ -210,7 +210,7 @@ export function buildCombinedTrendOption(
|
||||
top: 0,
|
||||
itemWidth: 10,
|
||||
itemHeight: 10,
|
||||
textStyle: { color: '#999', fontSize: 11 },
|
||||
textStyle: { color: '#78746b', fontSize: 11 },
|
||||
},
|
||||
grid: { left: 56, right: 48, top: 36, bottom: 28 },
|
||||
xAxis: {
|
||||
@@ -218,20 +218,20 @@ export function buildCombinedTrendOption(
|
||||
data: labels,
|
||||
boundaryGap: true,
|
||||
axisLabel,
|
||||
axisLine: { lineStyle: { color: '#333' } },
|
||||
axisLine: { lineStyle: { color: '#e7e2d8' } },
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: i18n.axisAmount,
|
||||
nameTextStyle: { color: '#666', fontSize: 10 },
|
||||
nameTextStyle: { color: '#8c867c', fontSize: 10 },
|
||||
axisLabel: { ...axisLabel, formatter: (v: number) => formatAmount(v, 2, loc) },
|
||||
splitLine,
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: i18n.axisCount,
|
||||
nameTextStyle: { color: '#666', fontSize: 10 },
|
||||
nameTextStyle: { color: '#8c867c', fontSize: 10 },
|
||||
axisLabel: { ...axisLabel, formatter: (v: number) => fmtCount(v, loc) },
|
||||
splitLine: { show: false },
|
||||
},
|
||||
@@ -254,7 +254,7 @@ export function buildCombinedTrendOption(
|
||||
yAxisIndex: 1,
|
||||
data: betCounts,
|
||||
barMaxWidth: 14,
|
||||
itemStyle: { color: 'rgba(251, 146, 60, 0.45)', borderRadius: [3, 3, 0, 0] },
|
||||
itemStyle: { color: 'rgba(149, 100, 0, 0.34)', borderRadius: [3, 3, 0, 0] },
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -286,7 +286,7 @@ export function buildTriplePieOption(
|
||||
top: '6%',
|
||||
style: {
|
||||
text: b.title,
|
||||
fill: '#aaa',
|
||||
fill: '#5f5b53',
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
textAlign: 'center',
|
||||
@@ -307,7 +307,7 @@ export function buildTriplePieOption(
|
||||
labelLine: { show: false },
|
||||
data: data.length
|
||||
? data
|
||||
: [{ name: pieEmpty, value: 1, itemStyle: { color: '#333' } }],
|
||||
: [{ name: pieEmpty, value: 1, itemStyle: { color: '#e6e1d8' } }],
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -2,11 +2,11 @@ import type { EChartsOption } from 'echarts';
|
||||
import { buildBarChartOption, buildPieChartOption, type PieSegment } from './dashboard-charts';
|
||||
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
PENDING: '#e8a040',
|
||||
WON: '#d4d4d4',
|
||||
LOST: '#ff453a',
|
||||
PUSH: '#8e8e93',
|
||||
VOID: '#555',
|
||||
PENDING: '#956400',
|
||||
WON: '#346538',
|
||||
LOST: '#9f2f2d',
|
||||
PUSH: '#5f5b53',
|
||||
VOID: '#aaa49a',
|
||||
};
|
||||
|
||||
function withCompactHeader(option: EChartsOption, text: string, subtext?: string): EChartsOption {
|
||||
@@ -19,8 +19,8 @@ function withCompactHeader(option: EChartsOption, text: string, subtext?: string
|
||||
subtext,
|
||||
left: 'center',
|
||||
top: 0,
|
||||
textStyle: { color: '#ccc', fontSize: 11, fontWeight: 600 },
|
||||
subtextStyle: { color: '#777', fontSize: 9 },
|
||||
textStyle: { color: '#1f2320', fontSize: 11, fontWeight: 700 },
|
||||
subtextStyle: { color: '#78746b', fontSize: 9 },
|
||||
itemGap: 2,
|
||||
},
|
||||
grid: { ...grid, top: Math.max(top, grid.top ?? 0) },
|
||||
@@ -44,7 +44,7 @@ function compactPie(option: EChartsOption, centerLines?: string[]): EChartsOptio
|
||||
style: {
|
||||
text: line,
|
||||
textAlign: 'center' as const,
|
||||
fill: i === 0 ? '#eee' : '#888',
|
||||
fill: i === 0 ? '#1f2320' : '#78746b',
|
||||
fontSize: i === 0 ? 15 : 9,
|
||||
fontWeight: i === 0 ? 700 : 400,
|
||||
},
|
||||
@@ -57,7 +57,7 @@ function compactPie(option: EChartsOption, centerLines?: string[]): EChartsOptio
|
||||
left: 'center',
|
||||
itemWidth: 8,
|
||||
itemHeight: 8,
|
||||
textStyle: { color: '#999', fontSize: 9 },
|
||||
textStyle: { color: '#78746b', fontSize: 9 },
|
||||
},
|
||||
graphic,
|
||||
};
|
||||
@@ -74,8 +74,8 @@ export function buildBetTypePieOption(input: {
|
||||
centerLabel: string;
|
||||
}): EChartsOption {
|
||||
const segments: PieSegment[] = [
|
||||
{ label: input.labels.single, value: input.singleBets, color: '#d4d4d4' },
|
||||
{ label: input.labels.parlay, value: input.parlayBets, color: '#fb923c' },
|
||||
{ label: input.labels.single, value: input.singleBets, color: '#346538' },
|
||||
{ label: input.labels.parlay, value: input.parlayBets, color: '#956400' },
|
||||
].filter((s) => s.value > 0);
|
||||
|
||||
const base = buildPieChartOption(input.title, segments, { pieTooltip: '{b}:{c}({d}%)' });
|
||||
@@ -94,7 +94,7 @@ export function buildStatusPieOption(input: {
|
||||
.map(([status, value]) => ({
|
||||
label: input.labelFor(status),
|
||||
value,
|
||||
color: STATUS_COLORS[status] ?? '#666',
|
||||
color: STATUS_COLORS[status] ?? '#8c867c',
|
||||
}));
|
||||
|
||||
const total = segments.reduce((sum, s) => sum + s.value, 0);
|
||||
@@ -112,7 +112,7 @@ export function buildSelectionStakeBarOption(input: {
|
||||
}): EChartsOption {
|
||||
const base = buildBarChartOption(
|
||||
input.labels,
|
||||
[{ name: input.seriesName, color: '#d4d4d4', values: input.stakes }],
|
||||
[{ name: input.seriesName, color: '#346538', values: input.stakes }],
|
||||
{ amountAxis: true },
|
||||
);
|
||||
return withCompactHeader(
|
||||
|
||||
@@ -2759,4 +2759,72 @@ function creditTypeLabel(type: string) {
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mgr-top-tabs :deep(.el-tabs__item) {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.list-panel-toolbar {
|
||||
border-bottom-color: var(--border-soft);
|
||||
}
|
||||
|
||||
.expand-panel {
|
||||
background: #fbfaf7;
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.expand-loading,
|
||||
.expand-section-title,
|
||||
.inner-tabs :deep(.el-tabs__item),
|
||||
.field-hint,
|
||||
.create-meta-label,
|
||||
.amount-full-hint,
|
||||
.text-muted,
|
||||
.list-settings-hint,
|
||||
.list-settings-unit,
|
||||
.section-title {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.inner-tabs :deep(.el-tabs__item.is-active) {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.inner-tabs :deep(.el-tabs__active-bar) {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
|
||||
.create-meta-bar {
|
||||
border-color: var(--border);
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.create-meta-value {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.c-green,
|
||||
.create-meta-row .c-green {
|
||||
color: var(--success-text);
|
||||
}
|
||||
|
||||
.invite-code-cell {
|
||||
color: var(--primary-link);
|
||||
}
|
||||
|
||||
.list-settings-block--danger {
|
||||
border-top-color: var(--danger-border);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.list-panel-toolbar,
|
||||
.list-panel-toolbar .list-chrome__grow,
|
||||
.list-panel-toolbar .list-chrome__actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.create-form-pair {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -547,4 +547,21 @@ function creditTypeLabel(type: string) {
|
||||
color: #666;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.field-hint,
|
||||
.section-title,
|
||||
.amount-full-hint {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.detail-block {
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.section-title--row {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -384,4 +384,30 @@ async function openDetail(row: BetListRow) {
|
||||
color: #aaa;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.filter-card,
|
||||
.data-card {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.bet-content-summary,
|
||||
.bet-no,
|
||||
.selections-title {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.bet-content-empty,
|
||||
.detail-league {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.bets-filter-form {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.bets-filter-form :deep(.el-form-item) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -738,4 +738,62 @@ onMounted(loadHistory);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.tool-card,
|
||||
.preview-card,
|
||||
.data-card {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.rules-help-btn {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.rules-help-btn:hover {
|
||||
border-color: #d5cfc3;
|
||||
background: var(--accent-hover);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.rules-list,
|
||||
.detail-summary,
|
||||
.preview-title,
|
||||
.table-title,
|
||||
.pstat-value {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.rules-note,
|
||||
.preview-meta,
|
||||
.detail-meta,
|
||||
.pstat-label {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.pstat {
|
||||
border-color: var(--border);
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.amount-cell,
|
||||
.pstat-green {
|
||||
color: var(--success-text);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.preview-head,
|
||||
.history-head {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.preview-actions,
|
||||
.detail-actions,
|
||||
.pager {
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -670,6 +670,7 @@ void load();
|
||||
:required="form.status === 'ACTIVE'"
|
||||
>
|
||||
<div class="banner-upload-field">
|
||||
<p class="banner-size-hint">{{ t('content.upload.recommended_size') }}</p>
|
||||
<!-- Image preview -->
|
||||
<div v-if="tr.imageUrl" class="banner-preview">
|
||||
<img :src="tr.imageUrl" alt="" class="banner-preview-img" />
|
||||
@@ -822,10 +823,18 @@ void load();
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.banner-size-hint {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.banner-preview {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
aspect-ratio: 43 / 18;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #252525;
|
||||
@@ -834,8 +843,8 @@ void load();
|
||||
|
||||
.banner-preview-img {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -980,4 +989,106 @@ void load();
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.contents-page {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.type-hint,
|
||||
.batch-hint,
|
||||
.banner-size-hint,
|
||||
.schedule-line,
|
||||
.schedule-sep,
|
||||
.media-picker-loading,
|
||||
.media-picker-empty,
|
||||
.media-picker-name,
|
||||
.thumb-empty,
|
||||
.locale-head {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.hidden-tip {
|
||||
color: var(--warning-text);
|
||||
}
|
||||
|
||||
.thumb,
|
||||
.banner-preview,
|
||||
.media-picker-thumb {
|
||||
background: #f4f0e8;
|
||||
}
|
||||
|
||||
.banner-preview,
|
||||
.locale-block {
|
||||
border-color: var(--border);
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.banner-preview-remove {
|
||||
border-color: rgba(255, 255, 255, 0.36);
|
||||
background: rgba(31, 35, 32, 0.78);
|
||||
}
|
||||
|
||||
.banner-preview-remove:hover {
|
||||
background: rgba(159, 47, 45, 0.92);
|
||||
}
|
||||
|
||||
.banner-upload-btn,
|
||||
.banner-pick-btn {
|
||||
border-radius: 7px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.banner-upload-btn {
|
||||
border-color: var(--primary);
|
||||
background: var(--primary);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.banner-upload-btn:hover {
|
||||
background: var(--primary-light);
|
||||
}
|
||||
|
||||
.banner-pick-btn {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.banner-pick-btn:hover {
|
||||
border-color: #d5cfc3;
|
||||
background: var(--accent-hover);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.media-picker-card {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.media-picker-card:hover {
|
||||
border-color: #d5cfc3;
|
||||
box-shadow: 0 8px 22px rgba(56, 49, 37, 0.08);
|
||||
}
|
||||
|
||||
.media-picker-svg {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.filter-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.banner-preview,
|
||||
.banner-url-input {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -87,17 +87,17 @@ const mainTrendOption = computed(() =>
|
||||
[
|
||||
{
|
||||
name: t('dash.chart_stake'),
|
||||
color: '#d4d4d4',
|
||||
color: '#346538',
|
||||
values: s.value?.trend7d?.map((d) => toNum(d.stake)) ?? [],
|
||||
},
|
||||
{
|
||||
name: t('dash.chart_payout'),
|
||||
color: '#60a5fa',
|
||||
color: '#1f6c9f',
|
||||
values: s.value?.trend7d?.map((d) => toNum(d.payout)) ?? [],
|
||||
},
|
||||
{
|
||||
name: t('dash.chart_ggr'),
|
||||
color: '#a78bfa',
|
||||
color: '#956400',
|
||||
values: s.value?.trend7d?.map((d) => toNum(d.ggr)) ?? [],
|
||||
},
|
||||
],
|
||||
@@ -111,20 +111,20 @@ const distributionOption = computed(() => {
|
||||
const u = s.value?.users;
|
||||
const raw = s.value?.bets.todayByStatus ?? {};
|
||||
const betColors: Record<string, string> = {
|
||||
PENDING: '#fb923c',
|
||||
WON: '#d4d4d4',
|
||||
LOST: '#f87171',
|
||||
VOID: '#6b7280',
|
||||
REFUNDED: '#60a5fa',
|
||||
PENDING: '#956400',
|
||||
WON: '#346538',
|
||||
LOST: '#9f2f2d',
|
||||
VOID: '#8c867c',
|
||||
REFUNDED: '#1f6c9f',
|
||||
};
|
||||
|
||||
const matchSegs = m
|
||||
? [
|
||||
{ label: t('dash.match_draft'), value: m.draft, color: '#6b7280' },
|
||||
{ label: t('dash.match_published'), value: m.published, color: '#d4d4d4' },
|
||||
{ label: t('dash.match_closed'), value: m.closed, color: '#60a5fa' },
|
||||
{ label: t('dash.match_pending_settle'), value: m.pendingSettlement, color: '#fb923c' },
|
||||
{ label: t('dash.match_settled'), value: m.settled ?? 0, color: '#5eead4' },
|
||||
{ label: t('dash.match_draft'), value: m.draft, color: '#8c867c' },
|
||||
{ label: t('dash.match_published'), value: m.published, color: '#346538' },
|
||||
{ label: t('dash.match_closed'), value: m.closed, color: '#1f6c9f' },
|
||||
{ label: t('dash.match_pending_settle'), value: m.pendingSettlement, color: '#956400' },
|
||||
{ label: t('dash.match_settled'), value: m.settled ?? 0, color: '#477a4c' },
|
||||
].filter((x) => x.value > 0)
|
||||
: [];
|
||||
|
||||
@@ -133,15 +133,15 @@ const distributionOption = computed(() => {
|
||||
.map((k) => ({
|
||||
label: betStatusLabel(k),
|
||||
value: raw[k].count,
|
||||
color: betColors[k] ?? '#888',
|
||||
color: betColors[k] ?? '#8c867c',
|
||||
}));
|
||||
|
||||
const userSegs = u
|
||||
? [
|
||||
{ label: t('dash.user_active'), value: u.playersActive, color: '#d4d4d4' },
|
||||
{ label: t('dash.user_suspended'), value: u.playersSuspended, color: '#f87171' },
|
||||
{ label: t('dash.user_direct'), value: u.playersDirect, color: '#60a5fa' },
|
||||
{ label: t('dash.user_agents'), value: u.agentsTotal, color: '#a78bfa' },
|
||||
{ label: t('dash.user_active'), value: u.playersActive, color: '#346538' },
|
||||
{ label: t('dash.user_suspended'), value: u.playersSuspended, color: '#9f2f2d' },
|
||||
{ label: t('dash.user_direct'), value: u.playersDirect, color: '#1f6c9f' },
|
||||
{ label: t('dash.user_agents'), value: u.agentsTotal, color: '#956400' },
|
||||
].filter((x) => x.value > 0)
|
||||
: [];
|
||||
|
||||
@@ -428,4 +428,68 @@ const kpiSecondary = computed(() => {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Local light overrides for the legacy dashboard entry */
|
||||
.state-hint {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.overview-board {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.dash-updated {
|
||||
color: #9b9489;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.kpi-cell {
|
||||
border-color: var(--border-soft);
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.kpi-cell--link:hover,
|
||||
.kpi-cell--link:focus-visible {
|
||||
border-color: #d5cfc3;
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.kpi-label {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.kpi-value {
|
||||
color: var(--text);
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.kpi-sub {
|
||||
color: #9b9489;
|
||||
}
|
||||
|
||||
.kpi-delta {
|
||||
color: var(--text-muted);
|
||||
background: #f0ede6;
|
||||
}
|
||||
|
||||
.kpi-delta.up {
|
||||
color: var(--success-text);
|
||||
background: var(--success-bg);
|
||||
}
|
||||
|
||||
.kpi-delta.down {
|
||||
color: var(--danger-text);
|
||||
background: var(--danger-bg);
|
||||
}
|
||||
|
||||
.charts-stack {
|
||||
border-top-color: var(--border-soft);
|
||||
}
|
||||
|
||||
.chart-main-caption {
|
||||
color: #9b9489;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -59,42 +59,41 @@ function switchTab(key: string) {
|
||||
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
margin-bottom: 0;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
margin-bottom: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
position: relative;
|
||||
padding: 10px 20px;
|
||||
background: none;
|
||||
padding: 8px 18px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #888;
|
||||
border-radius: 6px;
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
color: #ccc;
|
||||
background: var(--accent-hover);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
color: #fff;
|
||||
background: var(--primary);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.tab-btn.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: var(--primary, #409eff);
|
||||
border-radius: 2px 2px 0 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-deposit-manage :deep(.page-deposit-orders),
|
||||
@@ -108,4 +107,14 @@ function switchTab(key: string) {
|
||||
.page-deposit-manage :deep(.page-payment-methods .toolbar h2) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.tab-bar {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -421,4 +421,239 @@ onMounted(fetchList);
|
||||
.preview-box { position: relative; max-width: 90vw; max-height: 90vh; }
|
||||
.preview-image { max-width: 90vw; max-height: 85vh; object-fit: contain; border-radius: 8px; }
|
||||
.close-preview { position: absolute; top: -12px; right: -12px; background: #333; color: #fff; border: none; border-radius: 50%; width: 28px; height: 28px; font-size: 14px; cursor: pointer; }
|
||||
|
||||
.page-deposit-orders {
|
||||
padding: 0;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.toolbar h2 {
|
||||
color: var(--text);
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.filters {
|
||||
padding: 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.filters select,
|
||||
.filters input {
|
||||
min-height: 34px;
|
||||
border-color: var(--border);
|
||||
border-radius: 7px;
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.filters select:focus,
|
||||
.filters input:focus {
|
||||
border-color: var(--primary);
|
||||
outline: 3px solid rgba(31, 35, 32, 0.08);
|
||||
}
|
||||
|
||||
.btn-search,
|
||||
.btn-primary {
|
||||
border: 1px solid var(--primary);
|
||||
border-radius: 7px;
|
||||
background: var(--primary);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
border-bottom-color: var(--border-soft);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
background: #fbfaf7;
|
||||
color: var(--text-muted);
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.data-table tbody tr:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.time-cell,
|
||||
.remark-cell,
|
||||
.info-row span,
|
||||
.form-group label {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.screenshot-thumb,
|
||||
.approve-screenshot {
|
||||
border-color: var(--border);
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.badge,
|
||||
.status-pending,
|
||||
.status-approved,
|
||||
.status-rejected {
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.badge-blue {
|
||||
background: var(--info-bg);
|
||||
color: var(--info-text);
|
||||
}
|
||||
|
||||
.badge-green {
|
||||
background: var(--success-bg);
|
||||
color: var(--success-text);
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
color: var(--warning-text);
|
||||
}
|
||||
|
||||
.status-rejected {
|
||||
color: var(--danger-text);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 7px;
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.btn-sm:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.btn-approve {
|
||||
border-color: var(--success-border);
|
||||
background: var(--success-bg);
|
||||
color: var(--success-text);
|
||||
}
|
||||
|
||||
.btn-reject {
|
||||
border-color: var(--danger-border);
|
||||
background: var(--danger-bg);
|
||||
color: var(--danger-text);
|
||||
}
|
||||
|
||||
.btn-reopen {
|
||||
border-color: var(--warning-border);
|
||||
background: var(--warning-bg);
|
||||
color: var(--warning-text);
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.btn-delete:hover {
|
||||
border-color: var(--danger-border);
|
||||
background: var(--danger-bg);
|
||||
color: var(--danger-text);
|
||||
}
|
||||
|
||||
.pagination button {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 7px;
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.pagination button:not(:disabled):hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.dialog-overlay {
|
||||
background: rgba(31, 35, 32, 0.28);
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
.dialog-box {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
box-shadow: 0 24px 64px rgba(56, 49, 37, 0.14);
|
||||
}
|
||||
|
||||
.dialog-box h3 {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.approve-check-hint {
|
||||
border-color: var(--warning-border);
|
||||
background: var(--warning-bg);
|
||||
color: var(--warning-text);
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
border-color: var(--border);
|
||||
border-radius: 7px;
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 7px;
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-danger-full {
|
||||
border: 1px solid var(--danger-text);
|
||||
border-radius: 7px;
|
||||
background: var(--danger-text);
|
||||
}
|
||||
|
||||
.close-preview {
|
||||
background: var(--text);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.toolbar,
|
||||
.filters {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.filters > * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dialog-box {
|
||||
min-width: 0;
|
||||
width: calc(100vw - 32px);
|
||||
max-height: calc(100dvh - 32px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useAdminLocale } from '../composables/useAdminLocale';
|
||||
import api from '../api';
|
||||
import AdminTableEmpty from '../components/AdminTableEmpty.vue';
|
||||
import { formatAmount, formatAmountFull } from '../utils/format-amount';
|
||||
import { walletTxTypeKey } from '../utils/walletTx';
|
||||
const { t, locale, localeTag } = useAdminLocale();
|
||||
const auth = useAuthStore();
|
||||
const route = useRoute();
|
||||
@@ -75,9 +76,8 @@ function creditTypeLabel(type: string) {
|
||||
}
|
||||
|
||||
function transferTypeLabel(type: string) {
|
||||
if (type === 'MANUAL_DEPOSIT') return t('finance.tx.deposit');
|
||||
if (type === 'MANUAL_WITHDRAW') return t('finance.tx.withdraw');
|
||||
return type;
|
||||
const key = walletTxTypeKey(type);
|
||||
return key ? t(key) : type;
|
||||
}
|
||||
|
||||
const TRANSFER_REMARK_KEYS: Record<string, string> = {
|
||||
@@ -289,7 +289,12 @@ watch(
|
||||
<el-form-item :label="t('agent.col.credit_type')">
|
||||
<el-select v-model="transactionType" clearable :placeholder="t('common.all')" style="width: 110px">
|
||||
<el-option :label="t('finance.tx.deposit')" value="MANUAL_DEPOSIT" />
|
||||
<el-option :label="t('finance.tx.admin_deposit')" value="ADMIN_DEPOSIT" />
|
||||
<el-option :label="t('finance.tx.agent_deposit')" value="AGENT_DEPOSIT" />
|
||||
<el-option :label="t('finance.tx.player_deposit')" value="PLAYER_DEPOSIT" />
|
||||
<el-option :label="t('finance.tx.withdraw')" value="MANUAL_WITHDRAW" />
|
||||
<el-option :label="t('finance.tx.admin_withdraw')" value="ADMIN_WITHDRAW" />
|
||||
<el-option :label="t('finance.tx.agent_withdraw')" value="AGENT_WITHDRAW" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
@@ -311,4 +311,187 @@ label {
|
||||
font-weight: 600;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* Light operational login */
|
||||
.login-page {
|
||||
min-height: 100dvh;
|
||||
background-color: #f7f6f3;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.login-page::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
linear-gradient(120deg, rgba(247, 246, 243, 0.94), rgba(247, 246, 243, 0.82)),
|
||||
rgba(247, 246, 243, 0.9);
|
||||
}
|
||||
|
||||
.login-mask {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.54), rgba(240, 237, 230, 0.82));
|
||||
}
|
||||
|
||||
.login-wrap {
|
||||
max-width: 760px;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
padding: 12px 0 0;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
box-shadow: 0 24px 72px rgba(56, 49, 37, 0.14);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.form-lang {
|
||||
padding: 0 18px 12px;
|
||||
}
|
||||
|
||||
.form-body {
|
||||
grid-template-columns: minmax(210px, 270px) 1fr;
|
||||
}
|
||||
|
||||
.form-brand {
|
||||
background: #f4f0e8;
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.logo-large {
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.form-fields {
|
||||
gap: 10px;
|
||||
padding: 24px 26px 26px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0 0 8px;
|
||||
color: var(--text);
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
label {
|
||||
color: #6c675f;
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.field {
|
||||
height: 44px;
|
||||
background: #ffffff;
|
||||
border-color: var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.field::placeholder {
|
||||
color: #aaa49a;
|
||||
}
|
||||
|
||||
.field:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(31, 35, 32, 0.08);
|
||||
}
|
||||
|
||||
.field:-webkit-autofill,
|
||||
.field:-webkit-autofill:focus {
|
||||
-webkit-box-shadow: 0 0 0 1000px #ffffff inset;
|
||||
-webkit-text-fill-color: var(--text);
|
||||
caret-color: var(--text);
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
height: 44px;
|
||||
border-color: var(--primary);
|
||||
border-radius: 8px;
|
||||
background: var(--primary);
|
||||
color: #ffffff;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.btn-login:hover:not(:disabled) {
|
||||
border-color: var(--primary-light);
|
||||
background: var(--primary-light);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-login:disabled {
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.quick-label {
|
||||
margin-top: 8px;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.quick-btns {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.quick-btn {
|
||||
min-height: 54px;
|
||||
border-color: var(--border);
|
||||
border-radius: 8px;
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.quick-btn:hover:not(:disabled) {
|
||||
background: var(--accent-hover);
|
||||
border-color: #d5cfc3;
|
||||
}
|
||||
|
||||
.quick-role {
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.quick-acc {
|
||||
color: var(--text);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.login-page {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.login-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 420px;
|
||||
min-height: 100dvh;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.form-brand {
|
||||
padding: 22px 24px 14px;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.form-fields {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.quick-btns {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
938
apps/admin/src/views/MarketTemplates.vue
Normal file
938
apps/admin/src/views/MarketTemplates.vue
Normal file
@@ -0,0 +1,938 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import api from '../api';
|
||||
import { useAdminLocale } from '../composables/useAdminLocale';
|
||||
import MatchesSubNav from '../components/MatchesSubNav.vue';
|
||||
|
||||
const { t, locale } = useAdminLocale();
|
||||
|
||||
const LOCALES = [
|
||||
{ code: 'zh-CN', label: '中文' },
|
||||
{ code: 'en-US', label: 'English' },
|
||||
{ code: 'ms-MY', label: 'Bahasa Melayu' },
|
||||
] as const;
|
||||
|
||||
const UI_TEXT: Record<string, Record<string, string>> = {
|
||||
add: { 'zh-CN': '添加', 'en-US': 'Add', 'ms-MY': 'Tambah' },
|
||||
addMarket: { 'zh-CN': '增加盘口', 'en-US': 'Add market', 'ms-MY': 'Tambah pasaran' },
|
||||
close: { 'zh-CN': '关闭', 'en-US': 'Close', 'ms-MY': 'Tutup' },
|
||||
copyCreated: { 'zh-CN': '已复制盘口线', 'en-US': 'Market line duplicated', 'ms-MY': 'Garisan pasaran disalin' },
|
||||
copyMarket: { 'zh-CN': '复制盘口', 'en-US': 'Duplicate market', 'ms-MY': 'Salin pasaran' },
|
||||
copyMarketCreated: { 'zh-CN': '已复制盘口', 'en-US': 'Market duplicated', 'ms-MY': 'Pasaran disalin' },
|
||||
copyMarketShort: { 'zh-CN': '复制盘口', 'en-US': 'Duplicate', 'ms-MY': 'Salin pasaran' },
|
||||
copyLine: { 'zh-CN': '复制盘口线', 'en-US': 'Duplicate line', 'ms-MY': 'Salin garisan' },
|
||||
copyLineShort: { 'zh-CN': '复制线', 'en-US': 'Copy', 'ms-MY': 'Salin' },
|
||||
createTemplate: { 'zh-CN': '新建模板', 'en-US': 'New template', 'ms-MY': 'Templat baharu' },
|
||||
defaultTemplate: { 'zh-CN': '默认', 'en-US': 'Default', 'ms-MY': 'Lalai' },
|
||||
duplicateTemplate: { 'zh-CN': '复制模板', 'en-US': 'Duplicate template', 'ms-MY': 'Salin templat' },
|
||||
hidden: { 'zh-CN': '玩家端隐藏', 'en-US': 'Hidden on player', 'ms-MY': 'Disembunyikan pemain' },
|
||||
lineValue: { 'zh-CN': '盘口线', 'en-US': 'Line', 'ms-MY': 'Garisan' },
|
||||
marketDetails: { 'zh-CN': '模板详情', 'en-US': 'Template details', 'ms-MY': 'Butiran templat' },
|
||||
marketName: { 'zh-CN': '盘口名称', 'en-US': 'Market name', 'ms-MY': 'Nama pasaran' },
|
||||
marketToAdd: { 'zh-CN': '选择要添加的盘口', 'en-US': 'Choose a market to add', 'ms-MY': 'Pilih pasaran untuk ditambah' },
|
||||
marketUnavailable: { 'zh-CN': '暂不开放玩家端', 'en-US': 'Not available on player', 'ms-MY': 'Belum tersedia untuk pemain' },
|
||||
moveDown: { 'zh-CN': '下移', 'en-US': 'Move down', 'ms-MY': 'Turun' },
|
||||
moveUp: { 'zh-CN': '上移', 'en-US': 'Move up', 'ms-MY': 'Naik' },
|
||||
odds: { 'zh-CN': '赔率', 'en-US': 'Odds', 'ms-MY': 'Odds' },
|
||||
parlay: { 'zh-CN': '串关', 'en-US': 'Parlay', 'ms-MY': 'Parlay' },
|
||||
playerVisible: { 'zh-CN': '玩家端显示', 'en-US': 'Show on player', 'ms-MY': 'Papar pemain' },
|
||||
promoLabel: { 'zh-CN': '促销标签', 'en-US': 'Promo label', 'ms-MY': 'Label promosi' },
|
||||
remove: { 'zh-CN': '移除', 'en-US': 'Remove', 'ms-MY': 'Buang' },
|
||||
selectMarket: { 'zh-CN': '从盘口池选择', 'en-US': 'Select from market pool', 'ms-MY': 'Pilih daripada kumpulan pasaran' },
|
||||
selectTemplate: { 'zh-CN': '选择模板', 'en-US': 'Select template', 'ms-MY': 'Pilih templat' },
|
||||
setDefault: { 'zh-CN': '设为默认', 'en-US': 'Set default', 'ms-MY': 'Jadikan lalai' },
|
||||
single: { 'zh-CN': '单关', 'en-US': 'Single', 'ms-MY': 'Tunggal' },
|
||||
status: { 'zh-CN': '状态', 'en-US': 'Status', 'ms-MY': 'Status' },
|
||||
statusClosed: { 'zh-CN': '已关闭', 'en-US': 'Closed', 'ms-MY': 'Ditutup' },
|
||||
statusOpen: { 'zh-CN': '可售', 'en-US': 'Open', 'ms-MY': 'Dibuka' },
|
||||
statusSuspended: { 'zh-CN': '暂停', 'en-US': 'Suspended', 'ms-MY': 'Digantung' },
|
||||
templateDefaultSet: { 'zh-CN': '已设为默认模板', 'en-US': 'Default template updated', 'ms-MY': 'Templat lalai dikemas kini' },
|
||||
templateMarkets: { 'zh-CN': '模板盘口', 'en-US': 'Template markets', 'ms-MY': 'Pasaran templat' },
|
||||
templateName: { 'zh-CN': '模板名称', 'en-US': 'Template name', 'ms-MY': 'Nama templat' },
|
||||
};
|
||||
|
||||
type LocalizedText = Record<string, string>;
|
||||
|
||||
interface MarketDefinition {
|
||||
marketType: string;
|
||||
marketKey: string;
|
||||
period: string;
|
||||
sortOrder: number;
|
||||
defaultLineValue: number | null;
|
||||
allowSingle: boolean;
|
||||
allowParlay: boolean;
|
||||
showOnPlayer: boolean;
|
||||
settlementSupported: boolean;
|
||||
usesLineValue: boolean;
|
||||
nameI18n: LocalizedText;
|
||||
selectionTemplate: Array<{ code: string; name: string; nameI18n: LocalizedText; odds: number }>;
|
||||
}
|
||||
|
||||
interface SelectionRow {
|
||||
id?: string;
|
||||
selectionCode: string;
|
||||
selectionName: string;
|
||||
nameI18n: LocalizedText;
|
||||
odds: number;
|
||||
status: string;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
interface MarketRow {
|
||||
id?: string;
|
||||
marketType: string;
|
||||
marketKey: string;
|
||||
period: string;
|
||||
lineValue: number | null;
|
||||
paramsJson: Record<string, unknown> | null;
|
||||
status: string;
|
||||
allowSingle: boolean;
|
||||
allowParlay: boolean;
|
||||
showOnPlayer: boolean;
|
||||
sortOrder: number;
|
||||
promoLabelI18n: LocalizedText;
|
||||
nameI18n: LocalizedText;
|
||||
selections: SelectionRow[];
|
||||
}
|
||||
|
||||
interface MarketTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
nameI18n: LocalizedText;
|
||||
isDefault: boolean;
|
||||
status: string;
|
||||
sortOrder: number;
|
||||
items: MarketRow[];
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const definitions = ref<MarketDefinition[]>([]);
|
||||
const templates = ref<MarketTemplate[]>([]);
|
||||
const selectedId = ref('');
|
||||
const selectedIndex = ref(0);
|
||||
const addMarketVisible = ref(false);
|
||||
const selectedDefinitionType = ref('');
|
||||
const current = computed(() => templates.value.find((tpl) => tpl.id === selectedId.value) ?? null);
|
||||
const selectedItem = computed(() => current.value?.items[selectedIndex.value] ?? null);
|
||||
const definitionMap = computed(() => new Map(definitions.value.map((d) => [d.marketType, d])));
|
||||
const definitionOptions = computed(() => [...definitions.value].sort((a, b) => a.sortOrder - b.sortOrder));
|
||||
const displayLocales = computed(() => {
|
||||
const zhLabels: Record<string, string> = {
|
||||
'zh-CN': '中文',
|
||||
'en-US': '英语',
|
||||
'ms-MY': '马来语',
|
||||
};
|
||||
return LOCALES.map((loc) => ({
|
||||
code: loc.code,
|
||||
label: locale.value === 'zh-CN' ? zhLabels[loc.code] : loc.label,
|
||||
}));
|
||||
});
|
||||
|
||||
function localized(input?: Record<string, string> | null, fallback = ''): LocalizedText {
|
||||
const out: LocalizedText = {};
|
||||
for (const loc of LOCALES) out[loc.code] = input?.[loc.code]?.trim() || '';
|
||||
if (fallback && !out['zh-CN']) out['zh-CN'] = fallback;
|
||||
return out;
|
||||
}
|
||||
|
||||
function resolveText(map?: LocalizedText | null, fallback = '') {
|
||||
const order = Array.from(new Set([locale.value, 'en-US', 'zh-CN', 'ms-MY']));
|
||||
for (const loc of order) {
|
||||
const value = map?.[loc]?.trim();
|
||||
if (value) return value;
|
||||
}
|
||||
return Object.values(map ?? {}).find((value) => value?.trim()) || fallback;
|
||||
}
|
||||
|
||||
function ui(key: string) {
|
||||
return resolveText(UI_TEXT[key], key);
|
||||
}
|
||||
|
||||
function statusLabel(status: string) {
|
||||
if (status === 'OPEN') return ui('statusOpen');
|
||||
if (status === 'SUSPENDED') return ui('statusSuspended');
|
||||
if (status === 'CLOSED') return ui('statusClosed');
|
||||
return status;
|
||||
}
|
||||
|
||||
function selectionTitle(selection: SelectionRow) {
|
||||
const score = /^SCORE_(\d+)_(\d+)$/.exec(selection.selectionCode);
|
||||
if (score) return `${score[1]}:${score[2]}`;
|
||||
return resolveText(selection.nameI18n, selection.selectionName || selection.selectionCode);
|
||||
}
|
||||
|
||||
function isScoreSelection(selection: SelectionRow) {
|
||||
return /^SCORE_\d+_\d+$/.test(selection.selectionCode);
|
||||
}
|
||||
|
||||
function marketTitle(market: MarketRow) {
|
||||
const line = isLineMarket(market) && market.lineValue != null ? ` ${market.lineValue}` : '';
|
||||
return `${resolveText(market.nameI18n, market.marketType)}${line}`;
|
||||
}
|
||||
|
||||
function marketMetaText(market: MarketRow) {
|
||||
const parts = [statusLabel(market.status)];
|
||||
if (isLineMarket(market) && market.lineValue != null) parts.push(`${ui('lineValue')} ${market.lineValue}`);
|
||||
if (!market.showOnPlayer) parts.push(ui('hidden'));
|
||||
return parts.join(' · ');
|
||||
}
|
||||
|
||||
function definitionLabel(def: MarketDefinition) {
|
||||
const label = resolveText(def.nameI18n, def.marketType);
|
||||
return def.settlementSupported ? label : `${label}(${ui('marketUnavailable')})`;
|
||||
}
|
||||
|
||||
function templateLabel(tpl: MarketTemplate) {
|
||||
const label = resolveText(tpl.nameI18n, tpl.name || '');
|
||||
return tpl.isDefault ? `${label}(${ui('defaultTemplate')})` : label;
|
||||
}
|
||||
|
||||
function mapItem(raw: MarketRow, index: number): MarketRow {
|
||||
return {
|
||||
id: raw.id,
|
||||
marketType: raw.marketType,
|
||||
marketKey: raw.marketKey || raw.marketType,
|
||||
period: raw.period,
|
||||
lineValue: raw.lineValue,
|
||||
paramsJson: raw.paramsJson ?? null,
|
||||
status: raw.status || 'OPEN',
|
||||
allowSingle: raw.allowSingle ?? true,
|
||||
allowParlay: raw.allowParlay ?? true,
|
||||
showOnPlayer: raw.showOnPlayer ?? true,
|
||||
sortOrder: raw.sortOrder ?? index,
|
||||
promoLabelI18n: localized(raw.promoLabelI18n),
|
||||
nameI18n: localized(raw.nameI18n, raw.marketType),
|
||||
selections: (raw.selections ?? []).map((s, sIndex) => ({
|
||||
id: s.id,
|
||||
selectionCode: s.selectionCode,
|
||||
selectionName: s.selectionName,
|
||||
nameI18n: localized(s.nameI18n, s.selectionName),
|
||||
odds: Number(s.odds),
|
||||
status: s.status || 'OPEN',
|
||||
sortOrder: s.sortOrder ?? sIndex,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function usesLineValue(marketType: string) {
|
||||
return definitionMap.value.get(marketType)?.usesLineValue ?? false;
|
||||
}
|
||||
|
||||
function isLineMarket(market: MarketRow) {
|
||||
return usesLineValue(market.marketType);
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const [{ data: defRes }, { data: tplRes }] = await Promise.all([
|
||||
api.get('/admin/market-definitions'),
|
||||
api.get('/admin/market-templates'),
|
||||
]);
|
||||
definitions.value = defRes.data ?? [];
|
||||
templates.value = (tplRes.data ?? []).map((tpl: MarketTemplate) => ({
|
||||
...tpl,
|
||||
nameI18n: localized(tpl.nameI18n, tpl.name),
|
||||
items: (tpl.items ?? []).map(mapItem),
|
||||
}));
|
||||
selectedId.value =
|
||||
selectedId.value && templates.value.some((tpl) => tpl.id === selectedId.value)
|
||||
? selectedId.value
|
||||
: templates.value.find((tpl) => tpl.isDefault)?.id || templates.value[0]?.id || '';
|
||||
selectedIndex.value = 0;
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.load_failed'));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(selectedId, () => {
|
||||
selectedIndex.value = 0;
|
||||
});
|
||||
|
||||
function openAddMarketDialog() {
|
||||
selectedDefinitionType.value = definitionOptions.value[0]?.marketType ?? '';
|
||||
addMarketVisible.value = true;
|
||||
}
|
||||
|
||||
function addSelectedMarket() {
|
||||
const def = definitionMap.value.get(selectedDefinitionType.value);
|
||||
if (!def) return;
|
||||
addItem(def);
|
||||
addMarketVisible.value = false;
|
||||
selectedDefinitionType.value = '';
|
||||
}
|
||||
|
||||
function addItem(def: MarketDefinition) {
|
||||
if (!current.value) return;
|
||||
current.value.items.push({
|
||||
marketType: def.marketType,
|
||||
marketKey: def.marketKey,
|
||||
period: def.period,
|
||||
lineValue: def.defaultLineValue,
|
||||
paramsJson: null,
|
||||
status: 'OPEN',
|
||||
allowSingle: def.allowSingle,
|
||||
allowParlay: def.allowParlay,
|
||||
showOnPlayer: def.showOnPlayer && def.settlementSupported,
|
||||
sortOrder: current.value.items.length,
|
||||
promoLabelI18n: localized(),
|
||||
nameI18n: localized(def.nameI18n, def.marketType),
|
||||
selections: def.selectionTemplate.map((s, index) => ({
|
||||
selectionCode: s.code,
|
||||
selectionName: s.name,
|
||||
nameI18n: localized(s.nameI18n, s.name),
|
||||
odds: Number(s.odds),
|
||||
status: 'OPEN',
|
||||
sortOrder: index,
|
||||
})),
|
||||
});
|
||||
selectedIndex.value = current.value.items.length - 1;
|
||||
}
|
||||
|
||||
function copyLocalizedName(map: LocalizedText) {
|
||||
const suffix: Record<string, string> = {
|
||||
'zh-CN': ' 副本',
|
||||
'en-US': ' Copy',
|
||||
'ms-MY': ' Salinan',
|
||||
};
|
||||
const next = localized(map, resolveText(map));
|
||||
for (const loc of LOCALES) {
|
||||
const base = next[loc.code] || resolveText(next);
|
||||
next[loc.code] = `${base}${suffix[loc.code]}`;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function clonePlain<T>(value: T): T {
|
||||
return value == null ? value : JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
|
||||
function nextCopiedLineValue(source: MarketRow) {
|
||||
const tpl = current.value;
|
||||
const used = new Set(
|
||||
(tpl?.items ?? [])
|
||||
.filter((item) => item.marketType === source.marketType && item.lineValue != null)
|
||||
.map((item) => Number(item.lineValue).toFixed(2)),
|
||||
);
|
||||
let candidate = Number(((source.lineValue ?? 0) + 0.25).toFixed(2));
|
||||
while (used.has(candidate.toFixed(2))) {
|
||||
candidate = Number((candidate + 0.25).toFixed(2));
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
function duplicateItem(index: number) {
|
||||
const tpl = current.value;
|
||||
const source = tpl?.items[index];
|
||||
if (!tpl || !source) return;
|
||||
const shouldCopyLine = isLineMarket(source);
|
||||
const sourceCopy = clonePlain(source);
|
||||
const paramsCopy = clonePlain(source.paramsJson);
|
||||
const copy: MarketRow = {
|
||||
...sourceCopy,
|
||||
id: undefined,
|
||||
lineValue: shouldCopyLine ? nextCopiedLineValue(source) : null,
|
||||
paramsJson: shouldCopyLine
|
||||
? paramsCopy
|
||||
: { ...(paramsCopy ?? {}), copyId: `copy-${Date.now()}-${index}` },
|
||||
nameI18n: copyLocalizedName(source.nameI18n),
|
||||
sortOrder: tpl.items.length,
|
||||
selections: source.selections.map((selection) => ({ ...clonePlain(selection), id: undefined })),
|
||||
};
|
||||
tpl.items.splice(index + 1, 0, copy);
|
||||
tpl.items.forEach((item, i) => {
|
||||
item.sortOrder = i;
|
||||
});
|
||||
selectedIndex.value = index + 1;
|
||||
ElMessage.success(ui(shouldCopyLine ? 'copyCreated' : 'copyMarketCreated'));
|
||||
}
|
||||
|
||||
function moveItem(index: number, direction: -1 | 1) {
|
||||
const tpl = current.value;
|
||||
if (!tpl) return;
|
||||
const target = index + direction;
|
||||
if (target < 0 || target >= tpl.items.length) return;
|
||||
const [row] = tpl.items.splice(index, 1);
|
||||
tpl.items.splice(target, 0, row);
|
||||
tpl.items.forEach((item, i) => {
|
||||
item.sortOrder = i;
|
||||
});
|
||||
selectedIndex.value = target;
|
||||
}
|
||||
|
||||
function removeItem(index: number) {
|
||||
const tpl = current.value;
|
||||
if (!tpl) return;
|
||||
tpl.items.splice(index, 1);
|
||||
tpl.items.forEach((item, i) => {
|
||||
item.sortOrder = i;
|
||||
});
|
||||
selectedIndex.value = Math.min(selectedIndex.value, Math.max(0, tpl.items.length - 1));
|
||||
}
|
||||
|
||||
function payload(tpl: MarketTemplate) {
|
||||
return {
|
||||
name: tpl.name,
|
||||
nameI18n: localized(tpl.nameI18n, tpl.name),
|
||||
isDefault: tpl.isDefault,
|
||||
status: tpl.status,
|
||||
sortOrder: tpl.sortOrder,
|
||||
items: tpl.items.map((m, index) => ({
|
||||
id: m.id,
|
||||
marketType: m.marketType,
|
||||
marketKey: m.marketKey,
|
||||
lineKey: null,
|
||||
period: m.period,
|
||||
lineValue: usesLineValue(m.marketType) ? m.lineValue : null,
|
||||
paramsJson: m.paramsJson,
|
||||
status: m.status,
|
||||
allowSingle: m.allowSingle,
|
||||
allowParlay: m.allowParlay,
|
||||
showOnPlayer: m.showOnPlayer,
|
||||
sortOrder: index,
|
||||
promoLabelI18n: localized(m.promoLabelI18n),
|
||||
nameI18n: localized(m.nameI18n, m.marketType),
|
||||
selections: m.selections.map((s, sIndex) => ({
|
||||
id: s.id,
|
||||
selectionCode: s.selectionCode,
|
||||
selectionName: resolveText(s.nameI18n, s.selectionName),
|
||||
nameI18n: localized(s.nameI18n, s.selectionName),
|
||||
odds: Number(s.odds),
|
||||
status: s.status,
|
||||
sortOrder: sIndex,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async function createTemplate() {
|
||||
saving.value = true;
|
||||
try {
|
||||
const { data } = await api.post('/admin/market-templates', {
|
||||
name: '新盘口模板',
|
||||
nameI18n: { 'zh-CN': '新盘口模板', 'en-US': 'New Market Template', 'ms-MY': 'Templat Pasaran Baharu' },
|
||||
items: [],
|
||||
});
|
||||
await load();
|
||||
selectedId.value = data.data.id;
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function duplicateTemplate() {
|
||||
if (!current.value) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
const { data } = await api.post(`/admin/market-templates/${current.value.id}/duplicate`);
|
||||
await load();
|
||||
selectedId.value = data.data.id;
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function setDefault() {
|
||||
if (!current.value) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
await api.post(`/admin/market-templates/${current.value.id}/set-default`);
|
||||
ElMessage.success(ui('templateDefaultSet'));
|
||||
await load();
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveTemplate() {
|
||||
if (!current.value) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
await api.put(`/admin/market-templates/${current.value.id}`, payload(current.value));
|
||||
ElMessage.success(t('msg.saved'));
|
||||
await load();
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-loading="loading" class="template-page">
|
||||
<MatchesSubNav />
|
||||
<div class="template-toolbar">
|
||||
<el-select v-model="selectedId" class="template-select" :placeholder="ui('selectTemplate')">
|
||||
<el-option
|
||||
v-for="tpl in templates"
|
||||
:key="tpl.id"
|
||||
:label="templateLabel(tpl)"
|
||||
:value="tpl.id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-button size="small" @click="createTemplate">{{ ui('createTemplate') }}</el-button>
|
||||
<el-button size="small" :disabled="!current" @click="duplicateTemplate">{{ ui('duplicateTemplate') }}</el-button>
|
||||
<el-button size="small" :disabled="!current?.id || current?.isDefault" @click="setDefault">{{ ui('setDefault') }}</el-button>
|
||||
<el-button size="small" :disabled="!current" @click="openAddMarketDialog">{{ ui('addMarket') }}</el-button>
|
||||
<el-button size="small" type="primary" :loading="saving" :disabled="!current" @click="saveTemplate">
|
||||
{{ t('common.save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div v-if="current" class="template-grid">
|
||||
<section class="items">
|
||||
<div class="section-title">{{ ui('templateMarkets') }}</div>
|
||||
<div class="template-name">
|
||||
<label v-for="loc in displayLocales" :key="loc.code">
|
||||
<span>{{ loc.label }}</span>
|
||||
<el-input v-model="current.nameI18n[loc.code]" size="small" />
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
v-for="(item, index) in current.items"
|
||||
:key="item.id || `${item.marketType}-${index}`"
|
||||
class="item-card"
|
||||
:class="{ active: selectedIndex === index }"
|
||||
@click="selectedIndex = index"
|
||||
>
|
||||
<div class="item-main">
|
||||
<strong>{{ marketTitle(item) }}</strong>
|
||||
<span>{{ marketMetaText(item) }}</span>
|
||||
</div>
|
||||
<div class="item-actions" @click.stop>
|
||||
<el-button class="item-action-btn" size="small" text :title="ui('moveUp')" :disabled="index === 0" @click="moveItem(index, -1)">↑</el-button>
|
||||
<el-button class="item-action-btn" size="small" text :title="ui('moveDown')" :disabled="index === current.items.length - 1" @click="moveItem(index, 1)">↓</el-button>
|
||||
<el-button
|
||||
class="item-action-btn copy-btn"
|
||||
size="small"
|
||||
text
|
||||
:title="ui(isLineMarket(item) ? 'copyLine' : 'copyMarket')"
|
||||
@click="duplicateItem(index)"
|
||||
>
|
||||
{{ ui(isLineMarket(item) ? 'copyLineShort' : 'copyMarketShort') }}
|
||||
</el-button>
|
||||
<el-button class="item-action-btn" size="small" text type="danger" :title="ui('remove')" @click="removeItem(index)">{{ ui('remove') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="selectedItem" class="detail">
|
||||
<div class="detail-head">
|
||||
<div>
|
||||
<div class="section-title">{{ ui('marketDetails') }}</div>
|
||||
<strong>{{ marketTitle(selectedItem) }}</strong>
|
||||
</div>
|
||||
<el-switch v-model="selectedItem.showOnPlayer" :active-text="ui('playerVisible')" />
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<label>
|
||||
<span>{{ ui('status') }}</span>
|
||||
<el-select v-model="selectedItem.status" size="small">
|
||||
<el-option :label="ui('statusOpen')" value="OPEN" />
|
||||
<el-option :label="ui('statusSuspended')" value="SUSPENDED" />
|
||||
<el-option :label="ui('statusClosed')" value="CLOSED" />
|
||||
</el-select>
|
||||
</label>
|
||||
<label v-if="isLineMarket(selectedItem)">
|
||||
<span>{{ ui('lineValue') }}</span>
|
||||
<el-input-number v-model="selectedItem.lineValue" size="small" :step="0.25" controls-position="right" />
|
||||
</label>
|
||||
<label>
|
||||
<span>{{ ui('single') }}</span>
|
||||
<el-switch v-model="selectedItem.allowSingle" />
|
||||
</label>
|
||||
<label>
|
||||
<span>{{ ui('parlay') }}</span>
|
||||
<el-switch v-model="selectedItem.allowParlay" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="locale-block">
|
||||
<div class="block-title">{{ ui('marketName') }}</div>
|
||||
<div class="locale-grid">
|
||||
<label v-for="loc in displayLocales" :key="loc.code">
|
||||
<span>{{ loc.label }}</span>
|
||||
<el-input v-model="selectedItem.nameI18n[loc.code]" size="small" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="locale-block">
|
||||
<div class="block-title">{{ ui('promoLabel') }}</div>
|
||||
<div class="locale-grid">
|
||||
<label v-for="loc in displayLocales" :key="`promo-${loc.code}`">
|
||||
<span>{{ loc.label }}</span>
|
||||
<el-input v-model="selectedItem.promoLabelI18n[loc.code]" size="small" clearable />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="selection-list">
|
||||
<div
|
||||
v-for="sel in selectedItem.selections"
|
||||
:key="sel.selectionCode"
|
||||
class="selection-row"
|
||||
:class="{ 'selection-row--score': isScoreSelection(sel) }"
|
||||
>
|
||||
<div class="selection-meta">
|
||||
<strong :title="sel.selectionCode">{{ selectionTitle(sel) }}</strong>
|
||||
<label class="selection-field selection-field--status">
|
||||
<span>{{ ui('status') }}</span>
|
||||
<el-select v-model="sel.status" size="small">
|
||||
<el-option :label="ui('statusOpen')" value="OPEN" />
|
||||
<el-option :label="ui('statusSuspended')" value="SUSPENDED" />
|
||||
<el-option :label="ui('statusClosed')" value="CLOSED" />
|
||||
</el-select>
|
||||
</label>
|
||||
<label class="selection-field selection-field--odds">
|
||||
<span>{{ ui('odds') }}</span>
|
||||
<el-input-number v-model="sel.odds" size="small" :min="1.01" :step="0.01" controls-position="right" />
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="!isScoreSelection(sel)" class="locale-grid compact">
|
||||
<label v-for="loc in displayLocales" :key="loc.code">
|
||||
<span>{{ loc.label }}</span>
|
||||
<el-input v-model="sel.nameI18n[loc.code]" size="small" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="addMarketVisible" class="add-market-dialog" :title="ui('addMarket')" width="460px" destroy-on-close>
|
||||
<label class="add-market-field">
|
||||
<span>{{ ui('marketToAdd') }}</span>
|
||||
<el-select
|
||||
v-model="selectedDefinitionType"
|
||||
class="full-select"
|
||||
filterable
|
||||
:placeholder="ui('selectMarket')"
|
||||
>
|
||||
<el-option
|
||||
v-for="def in definitionOptions"
|
||||
:key="def.marketType"
|
||||
:label="definitionLabel(def)"
|
||||
:value="def.marketType"
|
||||
/>
|
||||
</el-select>
|
||||
</label>
|
||||
<template #footer>
|
||||
<el-button @click="addMarketVisible = false">{{ t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" :disabled="!selectedDefinitionType" @click="addSelectedMarket">
|
||||
{{ ui('add') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.template-page {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding: 0 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.template-toolbar {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
padding: 10px 0 12px;
|
||||
}
|
||||
|
||||
.template-select {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.template-grid {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(320px, 420px) minmax(480px, 1fr);
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.items,
|
||||
.detail {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
border: 1px solid #262626;
|
||||
border-radius: 8px;
|
||||
background: #111;
|
||||
padding: 10px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.section-title,
|
||||
.block-title {
|
||||
color: #bbb;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.item-card {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
border: 1px solid #282828;
|
||||
border-radius: 6px;
|
||||
background: #151515;
|
||||
color: #ddd;
|
||||
padding: 8px;
|
||||
margin-bottom: 6px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.item-card span {
|
||||
display: block;
|
||||
margin-top: 3px;
|
||||
color: #777;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.item-card.active {
|
||||
border-color: #9f853d;
|
||||
background: #1a1710;
|
||||
}
|
||||
|
||||
.item-main {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item-main strong,
|
||||
.detail-head strong {
|
||||
display: block;
|
||||
color: #f0f0f0;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
flex: 0 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: 30px 30px 46px 40px;
|
||||
gap: 2px;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.item-action-btn {
|
||||
width: 100%;
|
||||
height: 26px;
|
||||
padding: 0;
|
||||
margin-left: 0 !important;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.template-name,
|
||||
.form-grid,
|
||||
.locale-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.locale-grid.compact {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.locale-block {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
color: #aaa;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.detail-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.selection-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.selection-row {
|
||||
padding: 8px;
|
||||
border: 1px solid #262626;
|
||||
border-radius: 6px;
|
||||
background: #141414;
|
||||
}
|
||||
|
||||
.selection-meta {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(90px, 1fr) 120px 132px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.selection-row--score .selection-meta {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.selection-meta strong {
|
||||
color: #ddd;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.selection-field {
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.selection-field > span {
|
||||
color: #f3c969;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.selection-field--status > span {
|
||||
color: #7dd3fc;
|
||||
}
|
||||
|
||||
.selection-field--odds :deep(.el-input-number),
|
||||
.selection-field--odds :deep(.el-input) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.add-market-field {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.full-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.template-toolbar,
|
||||
.template-grid,
|
||||
.template-name,
|
||||
.form-grid,
|
||||
.locale-grid,
|
||||
.selection-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.template-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.items,
|
||||
.detail {
|
||||
min-height: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
.template-toolbar {
|
||||
padding: 12px 0 14px;
|
||||
}
|
||||
|
||||
.items,
|
||||
.detail,
|
||||
.item-card,
|
||||
.selection-row {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.items,
|
||||
.detail {
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.section-title,
|
||||
.block-title,
|
||||
.item-main strong,
|
||||
.detail-head strong,
|
||||
.selection-meta strong {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.item-card span,
|
||||
label,
|
||||
.selection-field > span {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.item-card:hover {
|
||||
border-color: #d5cfc3;
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.item-card.active {
|
||||
border-color: var(--primary);
|
||||
background: var(--accent-subtle);
|
||||
}
|
||||
|
||||
.selection-row {
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.selection-field > span {
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.selection-field--status > span {
|
||||
color: var(--info-text);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.item-actions {
|
||||
grid-template-columns: repeat(4, minmax(30px, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -408,6 +408,11 @@ function onLeagueArchived() {
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-chrome__actions">
|
||||
<el-button type="primary" @click.stop="openCreateLeague">
|
||||
{{ t('match.create_btn') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="filterStatus" class="list-hint">{{ t('match.filter.status_hint') }}</p>
|
||||
</div>
|
||||
@@ -489,9 +494,6 @@ function onLeagueArchived() {
|
||||
<template #header>
|
||||
<div class="actions-col-header">
|
||||
<span class="actions-col-header__label">{{ t('common.actions') }}</span>
|
||||
<el-button type="primary" size="small" @click.stop="openCreateLeague">
|
||||
{{ t('match.create_btn') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
@@ -596,6 +598,7 @@ function onLeagueArchived() {
|
||||
:placeholder="t('matchEditor.ph.kickoff')"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<p class="field-hint schedule-timezone-hint">{{ t('match.timezone.platform_hint') }}</p>
|
||||
</el-form-item>
|
||||
<div class="teams-row">
|
||||
<!-- Home Team Column -->
|
||||
@@ -686,19 +689,24 @@ function onLeagueArchived() {
|
||||
|
||||
.team-col-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #bbb;
|
||||
font-weight: 750;
|
||||
color: var(--text);
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
border-bottom: 1px solid var(--border-soft);
|
||||
}
|
||||
|
||||
.field-hint {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.schedule-timezone-hint {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.edit-hint {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@@ -714,7 +722,7 @@ function onLeagueArchived() {
|
||||
|
||||
.matches-page :deep(.el-table__expanded-cell) {
|
||||
padding: 0 !important;
|
||||
background: #0a0a0a;
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.list-panel :deep(.row-expandable) {
|
||||
@@ -734,7 +742,7 @@ function onLeagueArchived() {
|
||||
font-weight: 600;
|
||||
}
|
||||
.bet-stat-zero {
|
||||
color: #555;
|
||||
color: #aaa49a;
|
||||
}
|
||||
|
||||
.league-cell {
|
||||
@@ -751,13 +759,13 @@ function onLeagueArchived() {
|
||||
}
|
||||
|
||||
.league-en {
|
||||
color: #aaa;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.league-readonly {
|
||||
color: var(--green-text);
|
||||
font-weight: 500;
|
||||
color: var(--success-text);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.actions-col-header {
|
||||
@@ -773,8 +781,8 @@ function onLeagueArchived() {
|
||||
|
||||
.actions-col-header__label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #aaa;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@@ -811,8 +819,8 @@ function onLeagueArchived() {
|
||||
gap: 4px;
|
||||
padding: 2px 4px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
background: #fbfaf7;
|
||||
border: 1px solid var(--border-soft);
|
||||
}
|
||||
|
||||
.league-row-actions :deep(.el-button) {
|
||||
@@ -836,4 +844,50 @@ function onLeagueArchived() {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.list-chrome__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.team-col {
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border-soft);
|
||||
border-radius: 8px;
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.matchup-link,
|
||||
.bet-stat-active {
|
||||
color: var(--primary-link);
|
||||
}
|
||||
|
||||
.league-row-actions :deep(.el-button) {
|
||||
min-height: 26px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.list-chrome__actions {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.teams-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.actions-col-header {
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.league-row-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -247,13 +247,13 @@ function isLeagueExpanded(id: string) {
|
||||
}
|
||||
.matches-page :deep(.el-table__expanded-cell) {
|
||||
padding: 0 !important;
|
||||
background: #0a0a0a;
|
||||
background: #fbfaf7;
|
||||
}
|
||||
.list-panel :deep(.row-expandable) {
|
||||
cursor: pointer;
|
||||
}
|
||||
.matchup-link {
|
||||
color: var(--green-text);
|
||||
color: var(--primary-link);
|
||||
}
|
||||
.league-cell {
|
||||
display: flex;
|
||||
@@ -267,7 +267,7 @@ function isLeagueExpanded(id: string) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.league-en {
|
||||
color: #aaa;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -649,4 +649,223 @@ async function doUpload() {
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #1e1e1e;
|
||||
}
|
||||
|
||||
.media-page {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
padding: 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.unused-badge {
|
||||
border-color: var(--warning-border);
|
||||
background: var(--warning-bg);
|
||||
color: var(--warning-text);
|
||||
}
|
||||
|
||||
.tab-btn,
|
||||
.btn {
|
||||
border-radius: 7px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
border-color: #d5cfc3;
|
||||
background: var(--accent-hover);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
border-color: var(--primary);
|
||||
background: var(--primary);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-ghost:hover:not(:disabled) {
|
||||
border-color: #d5cfc3;
|
||||
background: var(--accent-hover);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
border-color: var(--primary);
|
||||
background: var(--primary);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
border-color: var(--primary-light);
|
||||
background: var(--primary-light);
|
||||
}
|
||||
|
||||
.file-grid {
|
||||
padding: 2px 2px 8px;
|
||||
}
|
||||
|
||||
.file-card {
|
||||
border-color: var(--border);
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.file-card:hover {
|
||||
border-color: #d5cfc3;
|
||||
box-shadow: 0 10px 26px rgba(56, 49, 37, 0.08);
|
||||
}
|
||||
|
||||
.card-thumb,
|
||||
.media-picker-thumb {
|
||||
background: #f4f0e8;
|
||||
}
|
||||
|
||||
.svg-badge,
|
||||
.media-picker-svg {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.badge-used {
|
||||
border-color: var(--warning-border);
|
||||
background: var(--warning-bg);
|
||||
color: var(--warning-text);
|
||||
}
|
||||
|
||||
.badge-unused {
|
||||
border-color: var(--border);
|
||||
background: #f0ede6;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.card-filename {
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.card-meta,
|
||||
.card-date,
|
||||
.page-info,
|
||||
.state-center,
|
||||
.muted,
|
||||
.drop-hint,
|
||||
.sel-size,
|
||||
.upload-hint-text,
|
||||
.form-row label {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.cat-tag {
|
||||
border-color: var(--border-soft);
|
||||
background: #f0ede6;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
border-top-color: var(--border-soft);
|
||||
}
|
||||
|
||||
.act-btn {
|
||||
border-right-color: var(--border-soft);
|
||||
color: var(--text-muted);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.act-btn:hover {
|
||||
background: var(--accent-hover);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.act-delete:hover {
|
||||
color: var(--danger-text);
|
||||
}
|
||||
|
||||
.file-grid::-webkit-scrollbar-thumb {
|
||||
background: #d5cfc3;
|
||||
}
|
||||
|
||||
.file-grid::-webkit-scrollbar-thumb:hover {
|
||||
background: #bdb6a9;
|
||||
}
|
||||
|
||||
.dialog-overlay {
|
||||
background: rgba(31, 35, 32, 0.28);
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
.dialog {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
box-shadow: 0 24px 64px rgba(56, 49, 37, 0.14);
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
border-bottom-color: var(--border-soft);
|
||||
color: var(--text);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.dialog-close {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.dialog-close:hover {
|
||||
color: var(--danger-text);
|
||||
}
|
||||
|
||||
.drop-zone {
|
||||
border-color: #d5cfc3;
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.drop-zone:hover,
|
||||
.drop-zone.drop-active {
|
||||
border-color: var(--primary);
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.sel-name {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
border-top-color: var(--border-soft);
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.toolbar,
|
||||
.toolbar-right,
|
||||
.filter-tabs {
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tab-btn,
|
||||
.toolbar-right .btn {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.file-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
|
||||
}
|
||||
|
||||
.dialog {
|
||||
max-height: calc(100dvh - 32px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -309,7 +309,11 @@ onMounted(fetchList);
|
||||
<input v-model.number="form.sortOrder" type="number" />
|
||||
</div>
|
||||
<div class="form-group row-checks">
|
||||
<label><input type="checkbox" v-model="form.isActive" /> {{ t('deposit.active') }}</label>
|
||||
<label class="check-option">
|
||||
<input type="checkbox" v-model="form.isActive" />
|
||||
<span class="check-option__box" aria-hidden="true"></span>
|
||||
<span>{{ t('deposit.active') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<button class="btn-cancel" @click="dialogVisible = false">{{ t('common.cancel') }}</button>
|
||||
@@ -365,4 +369,262 @@ onMounted(fetchList);
|
||||
.lang-row:last-child { margin-bottom: 0; }
|
||||
.lang-tag { flex-shrink: 0; font-size: 10px; font-weight: 700; color: #aaa; background: #252525; padding: 2px 6px; border-radius: 3px; min-width: 48px; text-align: center; }
|
||||
.lang-row input { flex: 1; padding: 5px 8px; border: 1px solid #333; border-radius: 3px; background: #0e0e0e; color: #ddd; font-size: 12px; box-sizing: border-box; }
|
||||
|
||||
.page-payment-methods {
|
||||
padding: 0;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.toolbar h2 {
|
||||
color: var(--text);
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-select,
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.lang-row input {
|
||||
min-height: 34px;
|
||||
border-color: var(--border);
|
||||
border-radius: 7px;
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
border: 1px solid var(--primary);
|
||||
border-radius: 7px;
|
||||
background: var(--primary);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--primary-light);
|
||||
}
|
||||
|
||||
.data-table {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
border-bottom-color: var(--border-soft);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
background: #fbfaf7;
|
||||
color: var(--text-muted);
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.data-table tbody tr:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.details-cell .sub,
|
||||
.form-group label,
|
||||
.lang-hint {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.qr-thumb,
|
||||
.qr-preview {
|
||||
border: 1px solid var(--border);
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.badge {
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.badge-blue {
|
||||
background: var(--info-bg);
|
||||
color: var(--info-text);
|
||||
}
|
||||
|
||||
.badge-green {
|
||||
background: var(--success-bg);
|
||||
color: var(--success-text);
|
||||
}
|
||||
|
||||
.status-on {
|
||||
color: var(--success-text);
|
||||
}
|
||||
|
||||
.status-off {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 7px;
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.btn-sm:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.btn-toggle {
|
||||
border-color: var(--info-border);
|
||||
background: var(--info-bg);
|
||||
color: var(--info-text);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
border-color: var(--danger-border);
|
||||
background: var(--danger-bg);
|
||||
color: var(--danger-text);
|
||||
}
|
||||
|
||||
.dialog-overlay {
|
||||
background: rgba(31, 35, 32, 0.28);
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
.dialog-box {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
box-shadow: 0 24px 64px rgba(56, 49, 37, 0.14);
|
||||
}
|
||||
|
||||
.dialog-box h3 {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.row-checks label {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.row-checks .check-option {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
min-height: 34px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
padding: 7px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: #fbfaf7;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
.row-checks .check-option:hover {
|
||||
border-color: #d5cfc3;
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.row-checks .check-option input[type='checkbox'] {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.check-option__box {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex: 0 0 16px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #cfc7b9;
|
||||
border-radius: 5px;
|
||||
background: #ffffff;
|
||||
transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.check-option__box::after {
|
||||
content: '';
|
||||
width: 5px;
|
||||
height: 9px;
|
||||
border: solid #ffffff;
|
||||
border-width: 0 2px 2px 0;
|
||||
opacity: 0;
|
||||
transform: rotate(45deg) translateY(-1px);
|
||||
transition: opacity 0.12s ease;
|
||||
}
|
||||
|
||||
.row-checks .check-option input[type='checkbox']:checked + .check-option__box {
|
||||
border-color: var(--primary);
|
||||
background: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(31, 35, 32, 0.08);
|
||||
}
|
||||
|
||||
.row-checks .check-option input[type='checkbox']:checked + .check-option__box::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.row-checks .check-option input[type='checkbox']:focus-visible + .check-option__box {
|
||||
box-shadow: 0 0 0 3px rgba(31, 35, 32, 0.14);
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 7px;
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.lang-section {
|
||||
border-color: var(--border-soft);
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.lang-tag {
|
||||
background: #f0ede6;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.toolbar {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.actions,
|
||||
.filter-select,
|
||||
.actions .btn-primary {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dialog-box {
|
||||
min-width: 0;
|
||||
width: calc(100vw - 32px);
|
||||
max-height: calc(100dvh - 32px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, defineAsyncComponent } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { formatApiErrorMessage, isApiErrorCode } from '@thebet365/shared';
|
||||
import api from '../api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { useAdminLocale } from '../composables/useAdminLocale';
|
||||
@@ -70,11 +71,82 @@ interface SettlementBetStats {
|
||||
const { t, locale, localeTag } = useAdminLocale();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const STAT_FACT_LABELS = {
|
||||
homeCorners: {
|
||||
'zh-CN': '主队角球',
|
||||
'en-US': 'Home corners',
|
||||
'ms-MY': 'Sepakan sudut tuan rumah',
|
||||
},
|
||||
awayCorners: {
|
||||
'zh-CN': '客队角球',
|
||||
'en-US': 'Away corners',
|
||||
'ms-MY': 'Sepakan sudut pelawat',
|
||||
},
|
||||
homeCards: {
|
||||
'zh-CN': '主队黄牌/红牌',
|
||||
'en-US': 'Home yellow/red cards',
|
||||
'ms-MY': 'Kad kuning/merah tuan rumah',
|
||||
},
|
||||
awayCards: {
|
||||
'zh-CN': '客队黄牌/红牌',
|
||||
'en-US': 'Away yellow/red cards',
|
||||
'ms-MY': 'Kad kuning/merah pelawat',
|
||||
},
|
||||
} as const;
|
||||
|
||||
const loading = ref(false);
|
||||
const previewing = ref(false);
|
||||
const match = ref<AdminMatchDetail | null>(null);
|
||||
const score = ref({ htHome: 0, htAway: 0, ftHome: 0, ftAway: 0 });
|
||||
const matchStats = ref<{
|
||||
homeCorners: number | null;
|
||||
awayCorners: number | null;
|
||||
homeYellowCards: number | null;
|
||||
awayYellowCards: number | null;
|
||||
homeRedCards: number | null;
|
||||
awayRedCards: number | null;
|
||||
homeCards: number | null;
|
||||
awayCards: number | null;
|
||||
}>(emptyMatchStats());
|
||||
|
||||
function emptyMatchStats() {
|
||||
return {
|
||||
homeCorners: null,
|
||||
awayCorners: null,
|
||||
homeYellowCards: null,
|
||||
awayYellowCards: null,
|
||||
homeRedCards: null,
|
||||
awayRedCards: null,
|
||||
homeCards: null,
|
||||
awayCards: null,
|
||||
};
|
||||
}
|
||||
|
||||
function cardTotalFromBreakdown(
|
||||
yellow: number | null,
|
||||
red: number | null,
|
||||
fallback: number | null,
|
||||
) {
|
||||
const hasBreakdown = yellow != null || red != null;
|
||||
if (!hasBreakdown) return fallback ?? null;
|
||||
if (yellow == null || red == null) return null;
|
||||
return yellow + red;
|
||||
}
|
||||
|
||||
function settlementStatsPayload() {
|
||||
const s = matchStats.value;
|
||||
return {
|
||||
...s,
|
||||
homeCards: cardTotalFromBreakdown(s.homeYellowCards, s.homeRedCards, s.homeCards),
|
||||
awayCards: cardTotalFromBreakdown(s.awayYellowCards, s.awayRedCards, s.awayCards),
|
||||
};
|
||||
}
|
||||
|
||||
function resetSettlementInputs() {
|
||||
score.value = { htHome: 0, htAway: 0, ftHome: 0, ftAway: 0 };
|
||||
matchStats.value = emptyMatchStats();
|
||||
}
|
||||
|
||||
const winnerTeamId = ref('');
|
||||
const outrightSelections = ref<
|
||||
Array<{ teamId: string; teamCode: string; teamZh: string; teamEn: string }>
|
||||
@@ -133,7 +205,7 @@ function buildSettlementPayload(): Record<string, unknown> | null {
|
||||
}
|
||||
return { winnerTeamId: Number(winnerTeamId.value) };
|
||||
}
|
||||
return { ...score.value };
|
||||
return { ...score.value, ...settlementStatsPayload() };
|
||||
}
|
||||
type PreviewItem = {
|
||||
betNo: string;
|
||||
@@ -162,7 +234,7 @@ const previewZeroHint = computed(() => {
|
||||
if (payout > 0 || refund > 0) return '';
|
||||
const pending = Number(p.pendingOtherMatches ?? 0);
|
||||
const wonLegs = Number(p.wonLegsOnMatch ?? 0);
|
||||
if (pending > 0 && wonLegs > 0) {
|
||||
if (pending > 0) {
|
||||
return t('settlement.preview_zero_parlay_hint', { pending: String(pending), legs: String(wonLegs) });
|
||||
}
|
||||
if (Number(p.lostOnThisMatch ?? 0) > 0) {
|
||||
@@ -380,8 +452,19 @@ async function loadMatch() {
|
||||
ftHome: detail.score.ftHome,
|
||||
ftAway: detail.score.ftAway,
|
||||
};
|
||||
matchStats.value = {
|
||||
homeCorners: detail.score.homeCorners ?? null,
|
||||
awayCorners: detail.score.awayCorners ?? null,
|
||||
homeYellowCards: detail.score.homeYellowCards ?? null,
|
||||
awayYellowCards: detail.score.awayYellowCards ?? null,
|
||||
homeRedCards: detail.score.homeRedCards ?? null,
|
||||
awayRedCards: detail.score.awayRedCards ?? null,
|
||||
homeCards: detail.score.homeCards ?? null,
|
||||
awayCards: detail.score.awayCards ?? null,
|
||||
};
|
||||
winnerTeamId.value = detail.score.winnerTeamId ?? '';
|
||||
} else {
|
||||
resetSettlementInputs();
|
||||
winnerTeamId.value = '';
|
||||
}
|
||||
if (detail.isOutright) {
|
||||
@@ -437,7 +520,21 @@ async function confirmResettle() {
|
||||
}
|
||||
|
||||
function settlementApiError(e: unknown, fallback: string) {
|
||||
const err = e as { response?: { data?: { error?: string; message?: string } } };
|
||||
const err = e as { response?: { data?: { code?: unknown; params?: Record<string, string | number>; error?: string; message?: string } } };
|
||||
const code = err.response?.data?.code;
|
||||
if (isApiErrorCode(code)) {
|
||||
const params = { ...(err.response?.data?.params ?? {}) };
|
||||
if (code === 'SETTLEMENT_FACTS_REQUIRED' && typeof params.fields === 'string') {
|
||||
params.fields = params.fields
|
||||
.split(',')
|
||||
.map((field) => {
|
||||
const labels = STAT_FACT_LABELS[field as keyof typeof STAT_FACT_LABELS];
|
||||
return labels?.[locale.value as keyof typeof labels] ?? field;
|
||||
})
|
||||
.join(locale.value === 'zh-CN' ? '、' : ', ');
|
||||
}
|
||||
return formatApiErrorMessage(code, locale.value, params);
|
||||
}
|
||||
const raw = err.response?.data?.error ?? err.response?.data?.message ?? fallback;
|
||||
if (raw === 'Score not recorded' || raw === 'Score not found') {
|
||||
return t('settlement.err_score_not_recorded');
|
||||
@@ -595,6 +692,69 @@ onMounted(() => {
|
||||
<el-input-number v-model="score.ftAway" :min="0" controls-position="right" style="width: 88px" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="score-block compact stat-block">
|
||||
<span class="score-title">{{ t('settlement.corners') }}</span>
|
||||
<div class="stat-inputs">
|
||||
<span class="stat-side">{{ t('settlement.home_stat') }}</span>
|
||||
<el-input-number
|
||||
v-model="matchStats.homeCorners"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
controls-position="right"
|
||||
style="width: 80px"
|
||||
/>
|
||||
<span class="stat-side">{{ t('settlement.away_stat') }}</span>
|
||||
<el-input-number
|
||||
v-model="matchStats.awayCorners"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
controls-position="right"
|
||||
style="width: 80px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="score-block compact stat-block">
|
||||
<span class="score-title">{{ t('settlement.yellow_cards') }}</span>
|
||||
<div class="stat-inputs">
|
||||
<span class="stat-side">{{ t('settlement.home_stat') }}</span>
|
||||
<el-input-number
|
||||
v-model="matchStats.homeYellowCards"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
controls-position="right"
|
||||
style="width: 80px"
|
||||
/>
|
||||
<span class="stat-side">{{ t('settlement.away_stat') }}</span>
|
||||
<el-input-number
|
||||
v-model="matchStats.awayYellowCards"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
controls-position="right"
|
||||
style="width: 80px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="score-block compact stat-block">
|
||||
<span class="score-title">{{ t('settlement.red_cards') }}</span>
|
||||
<div class="stat-inputs">
|
||||
<span class="stat-side">{{ t('settlement.home_stat') }}</span>
|
||||
<el-input-number
|
||||
v-model="matchStats.homeRedCards"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
controls-position="right"
|
||||
style="width: 80px"
|
||||
/>
|
||||
<span class="stat-side">{{ t('settlement.away_stat') }}</span>
|
||||
<el-input-number
|
||||
v-model="matchStats.awayRedCards"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
controls-position="right"
|
||||
style="width: 80px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settle-actions">
|
||||
@@ -873,7 +1033,7 @@ onMounted(() => {
|
||||
flex-wrap: wrap;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #2a2a2a;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.outright-settle-head {
|
||||
@@ -887,7 +1047,7 @@ onMounted(() => {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--green-text);
|
||||
color: var(--success-text);
|
||||
}
|
||||
|
||||
.outright-winner-row {
|
||||
@@ -938,19 +1098,19 @@ onMounted(() => {
|
||||
|
||||
.team-name-inline {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #eee;
|
||||
font-weight: 750;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.kickoff-inline {
|
||||
font-size: 13px;
|
||||
color: #aaa;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.kickoff-inline .meta-k {
|
||||
color: #777;
|
||||
color: #aaa49a;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
@@ -971,6 +1131,22 @@ onMounted(() => {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.stat-block {
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
.stat-inputs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-side {
|
||||
font-size: 11px;
|
||||
color: var(--warning-text);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
@@ -997,7 +1173,7 @@ onMounted(() => {
|
||||
.leg-badge {
|
||||
margin-left: 4px;
|
||||
font-size: 10px;
|
||||
color: var(--gold-text, #dcc078);
|
||||
color: var(--warning-text);
|
||||
}
|
||||
|
||||
.bet-content-cell {
|
||||
@@ -1083,7 +1259,7 @@ onMounted(() => {
|
||||
.vs {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--gold-text, #dcc078);
|
||||
color: var(--warning-text);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -1256,7 +1432,70 @@ onMounted(() => {
|
||||
|
||||
}
|
||||
|
||||
/* 智能比分弹窗样式(功能已关闭,保留便于恢复)
|
||||
.smart-hint { ... }
|
||||
*/
|
||||
.section-title,
|
||||
.preview-title,
|
||||
.preview-bar-title,
|
||||
.pstat-value {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.subsection-title,
|
||||
.bet-content-cell,
|
||||
.match-league,
|
||||
.score-title,
|
||||
.preview-metric-label,
|
||||
.preview-items-title,
|
||||
.period-tag,
|
||||
.empty-hint,
|
||||
.subsection-hint {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.stat-side,
|
||||
.leg-badge,
|
||||
.vs,
|
||||
.preview-metric-orange,
|
||||
.pstat-orange {
|
||||
color: var(--warning-text);
|
||||
}
|
||||
|
||||
.preview-metric-green,
|
||||
.pstat-green {
|
||||
color: var(--success-text);
|
||||
}
|
||||
|
||||
.mini-chart,
|
||||
.pstat {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.preview-zero-hint {
|
||||
border-color: var(--warning-border);
|
||||
background: var(--warning-bg);
|
||||
color: var(--warning-text);
|
||||
}
|
||||
|
||||
.score-sep {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.stats-table-block--bets .table-wrap {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.score-row,
|
||||
.preview-bar,
|
||||
.preview-metrics {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.preview-metric {
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -411,4 +411,37 @@ onMounted(async () => {
|
||||
color: #888;
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.intro-text,
|
||||
.intro-list,
|
||||
.summary-meta,
|
||||
.empty-card,
|
||||
.case-details-empty {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.summary-title,
|
||||
.table-title {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.case-details {
|
||||
border: 1px solid var(--border-soft);
|
||||
border-radius: 8px;
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.case-details pre {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.summary-actions,
|
||||
.summary-stats,
|
||||
.table-head,
|
||||
.log-head {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -890,4 +890,22 @@ function statusLabel(s: string) {
|
||||
.reset-db-alert {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.field-hint,
|
||||
.amount-full-hint,
|
||||
.text-muted,
|
||||
.list-settings-hint {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.list-settings-block--danger {
|
||||
border-top-color: var(--danger-border);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.settings-form :deep(.el-form-item) {
|
||||
margin-right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -104,5 +104,5 @@ function onSizeChange(size: number) {
|
||||
.page-title { font-size: 20px; font-weight: 700; color: #e0e0e0; }
|
||||
.page-desc { font-size: 13px; color: #3a3a3a; }
|
||||
.data-card { border-radius: 12px; }
|
||||
.bet-no { font-size: 12px; color: #ccc; font-family: ui-monospace, monospace; word-break: break-all; }
|
||||
.bet-no { font-size: 12px; color: var(--text); font-family: ui-monospace, monospace; word-break: break-all; }
|
||||
</style>
|
||||
|
||||
@@ -86,17 +86,17 @@ const mainTrendOption = computed(() =>
|
||||
[
|
||||
{
|
||||
name: t('dash.chart_stake'),
|
||||
color: '#d4d4d4',
|
||||
color: '#346538',
|
||||
values: s.value?.trend7d?.map((d) => toNum(d.stake)) ?? [],
|
||||
},
|
||||
{
|
||||
name: t('dash.chart_payout'),
|
||||
color: '#60a5fa',
|
||||
color: '#1f6c9f',
|
||||
values: s.value?.trend7d?.map((d) => toNum(d.payout)) ?? [],
|
||||
},
|
||||
{
|
||||
name: t('dash.chart_ggr'),
|
||||
color: '#a78bfa',
|
||||
color: '#956400',
|
||||
values: s.value?.trend7d?.map((d) => toNum(d.ggr)) ?? [],
|
||||
},
|
||||
],
|
||||
@@ -110,11 +110,11 @@ const distributionOption = computed(() => {
|
||||
const p = s.value?.players;
|
||||
const raw = s.value?.bets.todayByStatus ?? {};
|
||||
const betColors: Record<string, string> = {
|
||||
PENDING: '#fb923c',
|
||||
WON: '#d4d4d4',
|
||||
LOST: '#f87171',
|
||||
VOID: '#6b7280',
|
||||
REFUNDED: '#60a5fa',
|
||||
PENDING: '#956400',
|
||||
WON: '#346538',
|
||||
LOST: '#9f2f2d',
|
||||
VOID: '#8c867c',
|
||||
REFUNDED: '#1f6c9f',
|
||||
};
|
||||
|
||||
const creditSegs = c
|
||||
@@ -122,12 +122,12 @@ const distributionOption = computed(() => {
|
||||
{
|
||||
label: t('agent_dash.credit_available'),
|
||||
value: toNum(c.availableCredit),
|
||||
color: '#d4d4d4',
|
||||
color: '#346538',
|
||||
},
|
||||
{
|
||||
label: t('agent_dash.credit_used'),
|
||||
value: toNum(c.usedCredit),
|
||||
color: '#fb923c',
|
||||
color: '#956400',
|
||||
},
|
||||
].filter((x) => x.value > 0)
|
||||
: [];
|
||||
@@ -137,13 +137,13 @@ const distributionOption = computed(() => {
|
||||
.map((k) => ({
|
||||
label: betStatusLabel(k),
|
||||
value: raw[k].count,
|
||||
color: betColors[k] ?? '#888',
|
||||
color: betColors[k] ?? '#8c867c',
|
||||
}));
|
||||
|
||||
const playerSegs = p
|
||||
? [
|
||||
{ label: t('dash.user_active'), value: p.active, color: '#d4d4d4' },
|
||||
{ label: t('dash.user_suspended'), value: p.suspended, color: '#f87171' },
|
||||
{ label: t('dash.user_active'), value: p.active, color: '#346538' },
|
||||
{ label: t('dash.user_suspended'), value: p.suspended, color: '#9f2f2d' },
|
||||
].filter((x) => x.value > 0)
|
||||
: [];
|
||||
|
||||
@@ -449,4 +449,78 @@ const kpiSecondary = computed(() => {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Agent dashboard light overrides */
|
||||
.state-card {
|
||||
border-color: var(--danger-border);
|
||||
background: var(--danger-bg);
|
||||
}
|
||||
|
||||
.state-title {
|
||||
color: var(--danger-text);
|
||||
}
|
||||
|
||||
.state-hint,
|
||||
.board-hint {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.overview-board {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.dash-updated {
|
||||
color: #9b9489;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.kpi-cell {
|
||||
border-color: var(--border-soft);
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.kpi-cell--link:hover,
|
||||
.kpi-cell--link:focus-visible {
|
||||
border-color: #d5cfc3;
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.kpi-label {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.kpi-value {
|
||||
color: var(--text);
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.kpi-sub {
|
||||
color: #9b9489;
|
||||
}
|
||||
|
||||
.kpi-delta {
|
||||
color: var(--text-muted);
|
||||
background: #f0ede6;
|
||||
}
|
||||
|
||||
.kpi-delta.up {
|
||||
color: var(--success-text);
|
||||
background: var(--success-bg);
|
||||
}
|
||||
|
||||
.kpi-delta.down {
|
||||
color: var(--danger-text);
|
||||
background: var(--danger-bg);
|
||||
}
|
||||
|
||||
.charts-stack {
|
||||
border-top-color: var(--border-soft);
|
||||
}
|
||||
|
||||
.chart-main-caption {
|
||||
color: #9b9489;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1601,4 +1601,62 @@ function statusTagType(s: string) {
|
||||
.empty-hint { padding: 32px 0; color: #666; font-size: 13px; }
|
||||
.create-alert { margin-bottom: 16px; }
|
||||
.create-form { margin-top: 4px; }
|
||||
|
||||
.credit-strip {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.credit-label,
|
||||
.expand-loading,
|
||||
.expand-section-title,
|
||||
.expand-readonly-hint,
|
||||
.field-hint,
|
||||
.empty-hint {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.credit-value {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.credit-value.c-green,
|
||||
.invite-code-cell {
|
||||
color: var(--success-text);
|
||||
}
|
||||
|
||||
.credit-divider {
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
.invite-prominent-btn {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.expand-panel {
|
||||
margin-top: 6px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.credit-strip {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.credit-divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.inner-toolbar,
|
||||
.expand-pager {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -434,4 +434,48 @@ function statusLabel(status: string) {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.page-desc,
|
||||
.credit-label,
|
||||
.field-hint,
|
||||
.empty-hint {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.credit-strip,
|
||||
.data-card {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.credit-value {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.credit-value.c-green {
|
||||
color: var(--success-text);
|
||||
}
|
||||
|
||||
.credit-divider {
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.page-header,
|
||||
.credit-strip {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.credit-divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -38,17 +38,17 @@ const mainTrendOption = computed(() =>
|
||||
[
|
||||
{
|
||||
name: t('dash.chart_stake'),
|
||||
color: '#d4d4d4',
|
||||
color: '#346538',
|
||||
values: s.value?.trend7d?.map((d) => toNum(d.stake)) ?? [],
|
||||
},
|
||||
{
|
||||
name: t('dash.chart_payout'),
|
||||
color: '#60a5fa',
|
||||
color: '#1f6c9f',
|
||||
values: s.value?.trend7d?.map((d) => toNum(d.payout)) ?? [],
|
||||
},
|
||||
{
|
||||
name: t('dash.chart_ggr'),
|
||||
color: '#a78bfa',
|
||||
color: '#956400',
|
||||
values: s.value?.trend7d?.map((d) => toNum(d.ggr)) ?? [],
|
||||
},
|
||||
],
|
||||
@@ -61,20 +61,20 @@ const distributionOption = computed(() => {
|
||||
const m = s.value?.matches;
|
||||
const raw = s.value?.bets.todayByStatus ?? {};
|
||||
const betColors: Record<string, string> = {
|
||||
PENDING: '#fb923c',
|
||||
WON: '#d4d4d4',
|
||||
LOST: '#f87171',
|
||||
VOID: '#6b7280',
|
||||
REFUNDED: '#60a5fa',
|
||||
PENDING: '#956400',
|
||||
WON: '#346538',
|
||||
LOST: '#9f2f2d',
|
||||
VOID: '#8c867c',
|
||||
REFUNDED: '#1f6c9f',
|
||||
};
|
||||
|
||||
const matchSegs = m
|
||||
? [
|
||||
{ label: t('dash.match_draft'), value: m.draft, color: '#6b7280' },
|
||||
{ label: t('dash.match_published'), value: m.published, color: '#d4d4d4' },
|
||||
{ label: t('dash.match_closed'), value: m.closed, color: '#60a5fa' },
|
||||
{ label: t('dash.match_pending_settle'), value: m.pendingSettlement, color: '#fb923c' },
|
||||
{ label: t('dash.match_settled'), value: m.settled ?? 0, color: '#5eead4' },
|
||||
{ label: t('dash.match_draft'), value: m.draft, color: '#8c867c' },
|
||||
{ label: t('dash.match_published'), value: m.published, color: '#346538' },
|
||||
{ label: t('dash.match_closed'), value: m.closed, color: '#1f6c9f' },
|
||||
{ label: t('dash.match_pending_settle'), value: m.pendingSettlement, color: '#956400' },
|
||||
{ label: t('dash.match_settled'), value: m.settled ?? 0, color: '#477a4c' },
|
||||
].filter((x) => x.value > 0)
|
||||
: [];
|
||||
|
||||
@@ -83,7 +83,7 @@ const distributionOption = computed(() => {
|
||||
.map((k) => ({
|
||||
label: betStatusLabel(k),
|
||||
value: raw[k].count,
|
||||
color: betColors[k] ?? '#888',
|
||||
color: betColors[k] ?? '#8c867c',
|
||||
}));
|
||||
|
||||
return buildTriplePieOption(
|
||||
|
||||
@@ -54,10 +54,10 @@ const userDistributionOption = computed(() => {
|
||||
const u = s.value?.users;
|
||||
const userSegs = u
|
||||
? [
|
||||
{ label: t('dash.user_active'), value: u.playersActive, color: '#d4d4d4' },
|
||||
{ label: t('dash.user_suspended'), value: u.playersSuspended, color: '#f87171' },
|
||||
{ label: t('dash.user_direct'), value: u.playersDirect, color: '#60a5fa' },
|
||||
{ label: t('dash.user_agents'), value: u.agentsTotal, color: '#a78bfa' },
|
||||
{ label: t('dash.user_active'), value: u.playersActive, color: '#346538' },
|
||||
{ label: t('dash.user_suspended'), value: u.playersSuspended, color: '#9f2f2d' },
|
||||
{ label: t('dash.user_direct'), value: u.playersDirect, color: '#1f6c9f' },
|
||||
{ label: t('dash.user_agents'), value: u.agentsTotal, color: '#956400' },
|
||||
].filter((x) => x.value > 0)
|
||||
: [];
|
||||
|
||||
@@ -167,9 +167,10 @@ function formatBetTime(v: string) {
|
||||
}
|
||||
|
||||
.recent-card {
|
||||
border-radius: 14px;
|
||||
border: 1px solid #1e1e1e;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.recent-card :deep(.el-card__body) {
|
||||
@@ -180,7 +181,7 @@ function formatBetTime(v: string) {
|
||||
margin: 0 0 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #ccc;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
|
||||
@@ -4,30 +4,31 @@
|
||||
|
||||
.state-card {
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(255, 69, 58, 0.04);
|
||||
border: 1px solid var(--danger-border);
|
||||
background: var(--danger-bg);
|
||||
text-align: center;
|
||||
padding: 8px 0 4px;
|
||||
}
|
||||
|
||||
.state-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #fca5a5;
|
||||
font-weight: 750;
|
||||
color: var(--danger-text);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.state-hint {
|
||||
font-size: 13px;
|
||||
color: #737373;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.overview-board {
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: #111111;
|
||||
border: 1px solid var(--border);
|
||||
background: #ffffff;
|
||||
margin-bottom: 28px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.overview-board :deep(.el-card__body) {
|
||||
@@ -45,12 +46,12 @@
|
||||
|
||||
.board-hint {
|
||||
font-size: 12px;
|
||||
color: #737373;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.dash-updated {
|
||||
font-size: 11px;
|
||||
color: #525252;
|
||||
color: #9b9489;
|
||||
}
|
||||
|
||||
.kpi-grid {
|
||||
@@ -71,8 +72,8 @@
|
||||
.kpi-cell {
|
||||
padding: 12px 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid var(--border-soft);
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.kpi-cell.compact {
|
||||
@@ -86,25 +87,25 @@
|
||||
|
||||
.kpi-cell--link:hover,
|
||||
.kpi-cell--link:focus-visible {
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-color: #d5cfc3;
|
||||
background: var(--accent-hover);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.kpi-label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #737373;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.kpi-value {
|
||||
display: block;
|
||||
font-size: 22px;
|
||||
font-weight: 500;
|
||||
color: #f5f5f5;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
line-height: 1.15;
|
||||
letter-spacing: -0.02em;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.kpi-value.sm {
|
||||
@@ -114,7 +115,7 @@
|
||||
.kpi-sub {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: #525252;
|
||||
color: #9b9489;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
@@ -122,29 +123,31 @@
|
||||
display: inline-block;
|
||||
margin-top: 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: #737373;
|
||||
font-weight: 750;
|
||||
color: var(--text-muted);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
background: #f0ede6;
|
||||
}
|
||||
|
||||
.kpi-delta.up {
|
||||
color: #a3a3a3;
|
||||
color: var(--success-text);
|
||||
background: var(--success-bg);
|
||||
}
|
||||
|
||||
.kpi-delta.down {
|
||||
color: #f87171;
|
||||
color: var(--danger-text);
|
||||
background: var(--danger-bg);
|
||||
}
|
||||
|
||||
.charts-stack {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-top: 1px solid var(--border-soft);
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.chart-main-caption {
|
||||
font-size: 11px;
|
||||
color: #525252;
|
||||
color: #9b9489;
|
||||
text-align: center;
|
||||
margin: -8px 0 8px;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@ import {
|
||||
type BuiltinCountry,
|
||||
} from '../data/builtinCountries';
|
||||
import { FormValidationError } from '../i18n/form-validation';
|
||||
import {
|
||||
isoToPlatformPickerDateTime,
|
||||
platformPickerDateTimeToIso,
|
||||
} from '@thebet365/shared';
|
||||
|
||||
export interface MatchCreateForm {
|
||||
leagueId: string;
|
||||
@@ -23,7 +27,6 @@ export interface MatchCreateForm {
|
||||
awayTeamEn: string;
|
||||
awayTeamMs: string;
|
||||
isHot: boolean;
|
||||
correctScoreEnabled: boolean;
|
||||
displayOrder: number;
|
||||
matchName: string;
|
||||
stage: string;
|
||||
@@ -49,7 +52,6 @@ export function emptyMatchForm(): MatchCreateForm {
|
||||
awayTeamEn: '',
|
||||
awayTeamMs: '',
|
||||
isHot: false,
|
||||
correctScoreEnabled: true,
|
||||
displayOrder: 0,
|
||||
matchName: '',
|
||||
stage: '',
|
||||
@@ -64,17 +66,26 @@ export interface AdminMarketSelection {
|
||||
id: string;
|
||||
selectionCode: string;
|
||||
selectionName: string;
|
||||
nameI18n?: Record<string, string>;
|
||||
odds: number;
|
||||
status: string;
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
export interface AdminMarket {
|
||||
id: string;
|
||||
marketType: string;
|
||||
marketKey?: string;
|
||||
lineKey?: string | null;
|
||||
period: string;
|
||||
lineValue: number | null;
|
||||
paramsJson?: Record<string, unknown> | null;
|
||||
status: string;
|
||||
showOnPlayer: boolean;
|
||||
promoLabel: string;
|
||||
promoLabelI18n?: Record<string, string>;
|
||||
nameI18n?: Record<string, string>;
|
||||
sortOrder?: number;
|
||||
selections: AdminMarketSelection[];
|
||||
}
|
||||
|
||||
@@ -83,7 +94,6 @@ export type AdminMatchDetail = {
|
||||
status: string;
|
||||
isOutright: boolean;
|
||||
isHot: boolean;
|
||||
correctScoreEnabled: boolean;
|
||||
displayOrder: number;
|
||||
startTime: string;
|
||||
leagueId?: string;
|
||||
@@ -110,25 +120,25 @@ export type AdminMatchDetail = {
|
||||
htAway: number;
|
||||
ftHome: number;
|
||||
ftAway: number;
|
||||
homeCorners?: number | null;
|
||||
awayCorners?: number | null;
|
||||
homeYellowCards?: number | null;
|
||||
awayYellowCards?: number | null;
|
||||
homeRedCards?: number | null;
|
||||
awayRedCards?: number | null;
|
||||
homeCards?: number | null;
|
||||
awayCards?: number | null;
|
||||
winnerTeamId?: string | null;
|
||||
} | null;
|
||||
markets?: AdminMarket[];
|
||||
};
|
||||
|
||||
export function normalizeStartTimeForPicker(iso?: string): string {
|
||||
if (!iso?.trim()) return '';
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return iso.slice(0, 19);
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
||||
return isoToPlatformPickerDateTime(iso);
|
||||
}
|
||||
|
||||
export function normalizeStartTimeForApi(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return '';
|
||||
const d = new Date(trimmed);
|
||||
if (Number.isNaN(d.getTime())) return trimmed;
|
||||
return d.toISOString();
|
||||
return platformPickerDateTimeToIso(value);
|
||||
}
|
||||
|
||||
export function formFromDetail(d: AdminMatchDetail): MatchCreateForm {
|
||||
@@ -147,7 +157,6 @@ export function formFromDetail(d: AdminMatchDetail): MatchCreateForm {
|
||||
awayTeamEn: d.awayTeamEn,
|
||||
awayTeamMs: d.awayTeamMs ?? '',
|
||||
isHot: d.isHot,
|
||||
correctScoreEnabled: d.correctScoreEnabled ?? true,
|
||||
displayOrder: d.displayOrder ?? 0,
|
||||
matchName: d.matchName ?? '',
|
||||
stage: d.stage ?? '',
|
||||
@@ -244,7 +253,6 @@ export function buildPlatformPayload(form: MatchCreateForm) {
|
||||
awayTeamMs: form.awayTeamMs.trim() || undefined,
|
||||
startTime: normalizeStartTimeForApi(form.startTime),
|
||||
isHot: form.isHot,
|
||||
correctScoreEnabled: form.correctScoreEnabled,
|
||||
displayOrder: form.displayOrder,
|
||||
matchName: form.matchName.trim() || undefined,
|
||||
stage: form.stage.trim() || undefined,
|
||||
@@ -290,7 +298,6 @@ export function buildMatchUpdatePayload(form: MatchCreateForm) {
|
||||
awayTeamMs: form.awayTeamMs.trim() || undefined,
|
||||
startTime: normalizeStartTimeForApi(form.startTime),
|
||||
isHot: form.isHot,
|
||||
correctScoreEnabled: form.correctScoreEnabled,
|
||||
displayOrder: form.displayOrder,
|
||||
matchName: form.matchName.trim() || undefined,
|
||||
stage: form.stage.trim() || undefined,
|
||||
|
||||
@@ -7,6 +7,10 @@ import api from '../../api';
|
||||
import MatchArchiveDialog from '../../components/MatchArchiveDialog.vue';
|
||||
import { ensureLeagueExpanded } from '../../utils/matchesListState';
|
||||
import { formatAmount } from '../../utils/format-amount';
|
||||
import {
|
||||
formatPlatformMatchDateTime,
|
||||
platformPickerDateTimeToIso,
|
||||
} from '@thebet365/shared';
|
||||
const props = defineProps<{
|
||||
leagueId: string;
|
||||
filterStatus: string;
|
||||
@@ -180,7 +184,7 @@ function matchId(row: unknown) {
|
||||
return String(rowOf(row).id ?? '');
|
||||
}
|
||||
function matchTime(row: unknown) {
|
||||
return new Date(String(rowOf(row).startTime)).toLocaleString();
|
||||
return formatPlatformMatchDateTime(String(rowOf(row).startTime), locale.value);
|
||||
}
|
||||
function betCount(row: unknown) {
|
||||
return Number(rowOf(row).betCount ?? 0);
|
||||
@@ -260,6 +264,13 @@ async function promptReopenKickoff(): Promise<string | null> {
|
||||
},
|
||||
t('match.reopen_kickoff_hint'),
|
||||
),
|
||||
h(
|
||||
'p',
|
||||
{
|
||||
style: 'margin: 0 0 12px; font-size: 12px; color: var(--el-text-color-secondary)',
|
||||
},
|
||||
t('match.timezone.platform_hint'),
|
||||
),
|
||||
h(ElDatePicker, {
|
||||
modelValue: kickoff.value,
|
||||
'onUpdate:modelValue': (v: string) => {
|
||||
@@ -275,7 +286,8 @@ async function promptReopenKickoff(): Promise<string | null> {
|
||||
cancelButtonText: t('common.cancel'),
|
||||
beforeClose: (action, _instance, done) => {
|
||||
if (action === 'confirm') {
|
||||
if (!kickoff.value || new Date(kickoff.value) <= new Date()) {
|
||||
const iso = kickoff.value ? platformPickerDateTimeToIso(kickoff.value) : '';
|
||||
if (!iso || Number.isNaN(new Date(iso).getTime()) || new Date(iso) <= new Date()) {
|
||||
ElMessage.warning(t('match.reopen_kickoff_invalid'));
|
||||
return;
|
||||
}
|
||||
@@ -283,7 +295,7 @@ async function promptReopenKickoff(): Promise<string | null> {
|
||||
done();
|
||||
},
|
||||
});
|
||||
return kickoff.value || null;
|
||||
return kickoff.value ? platformPickerDateTimeToIso(kickoff.value) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -316,6 +328,11 @@ defineExpose({ reload: load });
|
||||
|
||||
<template>
|
||||
<div class="league-matches-panel">
|
||||
<div class="nested-panel-toolbar">
|
||||
<el-button type="primary" size="small" @click.stop="emit('add-match')">
|
||||
{{ t('match.create_fixture_btn') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="matches" stripe row-key="id" class="nested-match-table">
|
||||
<el-table-column prop="id" label="ID" width="64" />
|
||||
<el-table-column :label="t('match.col.matchup')" min-width="180">
|
||||
@@ -328,7 +345,7 @@ defineExpose({ reload: load });
|
||||
<el-tag :type="matchStatusType(row)" size="small">{{ matchStatusLabel(row) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('match.col.kickoff')" min-width="150">
|
||||
<el-table-column :label="t('match.col.kickoff')" min-width="190">
|
||||
<template #default="{ row }">{{ matchTime(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('match.col.bet_count')" width="72" align="center">
|
||||
@@ -347,18 +364,15 @@ defineExpose({ reload: load });
|
||||
<span v-else class="bet-stat-zero">0</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column width="460" align="center">
|
||||
<el-table-column width="256" align="center">
|
||||
<template #header>
|
||||
<div class="actions-col-header">
|
||||
<span class="actions-col-header__label">{{ t('common.actions') }}</span>
|
||||
<el-button type="primary" size="small" @click.stop="emit('add-match')">
|
||||
{{ t('match.create_fixture_btn') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<div class="action-btns">
|
||||
<div class="action-group">
|
||||
<div class="action-row action-row--primary">
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
@@ -371,13 +385,12 @@ defineExpose({ reload: load });
|
||||
size="small"
|
||||
type="primary"
|
||||
plain
|
||||
class="action-btn--markets"
|
||||
:disabled="!canManage(row)"
|
||||
@click="openMarkets(matchId(row))"
|
||||
>
|
||||
{{ t('match.btn.markets') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="action-group">
|
||||
<el-button
|
||||
v-if="canPublishRow(row)"
|
||||
size="small"
|
||||
@@ -394,6 +407,8 @@ defineExpose({ reload: load });
|
||||
>
|
||||
{{ t('match.btn.unpublish') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="action-row action-row--workflow">
|
||||
<el-button
|
||||
size="small"
|
||||
type="warning"
|
||||
@@ -419,15 +434,15 @@ defineExpose({ reload: load });
|
||||
>
|
||||
{{ settleButtonLabel(row) }}
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
plain
|
||||
@click="confirmDelete(row)"
|
||||
>
|
||||
{{ t('common.delete') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
plain
|
||||
@click="confirmDelete(row)"
|
||||
>
|
||||
{{ t('common.delete') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -456,8 +471,17 @@ defineExpose({ reload: load });
|
||||
|
||||
<style scoped>
|
||||
.league-matches-panel {
|
||||
width: min(100%, calc(100vw - 272px));
|
||||
max-width: calc(100vw - 272px);
|
||||
padding: 10px 12px 12px;
|
||||
background: #0a0a0a;
|
||||
overflow: hidden;
|
||||
background: #fbfaf7;
|
||||
}
|
||||
.nested-panel-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.nested-pager {
|
||||
display: flex;
|
||||
@@ -477,8 +501,8 @@ defineExpose({ reload: load });
|
||||
|
||||
.actions-col-header__label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #aaa;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@@ -502,43 +526,70 @@ defineExpose({ reload: load });
|
||||
}
|
||||
.empty-hint {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
color: var(--text-muted);
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
.matchup-link {
|
||||
color: var(--green-text);
|
||||
color: var(--primary-link);
|
||||
}
|
||||
.bet-stat-active {
|
||||
color: var(--green-text);
|
||||
color: var(--success-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
.bet-stat-zero {
|
||||
color: #555;
|
||||
color: #aaa49a;
|
||||
}
|
||||
.action-btns {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
display: grid;
|
||||
grid-template-rows: repeat(2, 26px);
|
||||
align-items: center;
|
||||
gap: 6px 8px;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
.action-group {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
.action-row {
|
||||
display: inline-grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: max-content;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
padding: 2px 4px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--border-soft);
|
||||
}
|
||||
.action-row--primary {
|
||||
background: #fbfaf7;
|
||||
}
|
||||
.action-row--workflow {
|
||||
background: #ffffff;
|
||||
}
|
||||
.action-btns :deep(.el-button) {
|
||||
margin: 0 !important;
|
||||
min-width: 52px;
|
||||
padding: 4px 10px !important;
|
||||
min-width: 48px;
|
||||
height: 22px !important;
|
||||
min-height: 22px !important;
|
||||
padding: 0 9px !important;
|
||||
font-size: 12px !important;
|
||||
line-height: 1 !important;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.action-btns :deep(.action-btn--markets:not(.is-disabled):not(:disabled)) {
|
||||
background: var(--info-bg) !important;
|
||||
border-color: var(--info-border) !important;
|
||||
color: var(--info-text) !important;
|
||||
}
|
||||
|
||||
.action-btns :deep(.action-btn--markets:not(.is-disabled):not(:disabled):hover),
|
||||
.action-btns :deep(.action-btn--markets:not(.is-disabled):not(:disabled):focus) {
|
||||
background: #d5ecfb !important;
|
||||
border-color: #9fd0ed !important;
|
||||
color: #155b86 !important;
|
||||
}
|
||||
|
||||
.action-btns :deep(.el-button:not(.is-disabled):not(:disabled)) {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -547,4 +598,21 @@ defineExpose({ reload: load });
|
||||
.action-btns :deep(.el-button:disabled) {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.league-matches-panel {
|
||||
width: calc(100vw - 32px);
|
||||
max-width: calc(100vw - 32px);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.nested-panel-toolbar,
|
||||
.nested-pager {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.action-btns {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -901,7 +901,8 @@ watch(
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px 16px 16px;
|
||||
border-bottom: 1px solid #1a1a1a;
|
||||
border-bottom: 1px solid var(--border-soft);
|
||||
background: #fbfaf7;
|
||||
}
|
||||
.outright-odds-panel__head {
|
||||
display: flex;
|
||||
@@ -1027,8 +1028,8 @@ watch(
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
padding: 8px 10px 8px 8px;
|
||||
background: #111;
|
||||
border: 1px solid #262626;
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
transition:
|
||||
border-color 0.15s ease,
|
||||
@@ -1036,7 +1037,8 @@ watch(
|
||||
}
|
||||
|
||||
.team-row-wrap:hover .team-row {
|
||||
border-color: #3a3a3a;
|
||||
border-color: #d5cfc3;
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.team-row-wrap--batch-selected .team-row {
|
||||
@@ -1160,12 +1162,13 @@ watch(
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.team-row__odds {
|
||||
width: 72px;
|
||||
.team-row__odds,
|
||||
.team-row__odds.el-input-number {
|
||||
width: 86px;
|
||||
}
|
||||
|
||||
.team-row__odds :deep(.el-input-number) {
|
||||
width: 72px;
|
||||
.team-row__odds :deep(.el-input__wrapper) {
|
||||
padding-right: 28px !important;
|
||||
}
|
||||
|
||||
.add-teams-dialog__toolbar {
|
||||
@@ -1306,4 +1309,93 @@ watch(
|
||||
letter-spacing: 0.05em;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.outright-odds-panel__hint,
|
||||
.outright-odds-panel__batch-count,
|
||||
.outright-odds-panel__sort-label,
|
||||
.outright-odds-panel__empty,
|
||||
.team-row__rank,
|
||||
.team-row__meta,
|
||||
.team-row__odds-label,
|
||||
.add-teams-dialog__count,
|
||||
.add-teams-dialog__odds,
|
||||
.add-teams-dialog__empty,
|
||||
.add-teams-dialog__custom-hint,
|
||||
.add-team-pick__code {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.outright-odds-panel__workflow-hint {
|
||||
color: var(--warning-text);
|
||||
}
|
||||
|
||||
.outright-odds-panel__batch,
|
||||
.team-row,
|
||||
.add-team-pick {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.team-row-wrap:hover .team-row,
|
||||
.add-team-pick:hover {
|
||||
border-color: #d5cfc3;
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.team-row-wrap--batch-selected .team-row,
|
||||
.add-team-pick--selected {
|
||||
border-color: var(--primary);
|
||||
background: var(--accent-subtle);
|
||||
}
|
||||
|
||||
.team-list-scroll {
|
||||
scrollbar-color: #d5cfc3 transparent;
|
||||
}
|
||||
|
||||
.team-list-scroll::-webkit-scrollbar-thumb {
|
||||
background: #d5cfc3;
|
||||
}
|
||||
|
||||
.team-row__trash {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.team-row__trash:hover {
|
||||
background: var(--danger-bg);
|
||||
color: var(--danger-text);
|
||||
}
|
||||
|
||||
.team-row__flag {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.team-row__name,
|
||||
.add-team-pick__name {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.add-teams-dialog__actions {
|
||||
border-bottom-color: var(--border-soft);
|
||||
}
|
||||
|
||||
.add-teams-dialog__custom :deep(.el-form-item__label) {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.team-list {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.team-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.outright-odds-panel__batch,
|
||||
.outright-odds-panel__sort {
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -203,8 +203,8 @@ function onOutrightArchived() {
|
||||
margin-bottom: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: rgba(212, 175, 55, 0.04);
|
||||
border: 1px solid rgba(212, 175, 55, 0.14);
|
||||
background: var(--warning-bg);
|
||||
border: 1px solid var(--warning-border);
|
||||
}
|
||||
.panel-head {
|
||||
display: flex;
|
||||
@@ -222,11 +222,11 @@ function onOutrightArchived() {
|
||||
.panel-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--green-text);
|
||||
color: var(--warning-text);
|
||||
}
|
||||
.panel-hint {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.panel-actions {
|
||||
@@ -241,18 +241,18 @@ function onOutrightArchived() {
|
||||
.meta-line {
|
||||
margin: 0 0 4px;
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
color: var(--text);
|
||||
}
|
||||
.meta-warn {
|
||||
margin: 0 0 8px;
|
||||
font-size: 12px;
|
||||
color: #e6a23c;
|
||||
color: var(--warning-text);
|
||||
line-height: 1.45;
|
||||
}
|
||||
.meta-empty {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.preview-table {
|
||||
margin-top: 6px;
|
||||
|
||||
@@ -194,9 +194,9 @@ async function saveMeta() {
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="form-section form-section--schedule">
|
||||
<div class="section-label">{{ t('matchEditor.group.schedule') }}</div>
|
||||
<el-row :gutter="12">
|
||||
<el-row :gutter="12" class="schedule-fields">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="t('match.field.kickoff')" required>
|
||||
<el-date-picker
|
||||
@@ -207,6 +207,7 @@ async function saveMeta() {
|
||||
:placeholder="t('matchEditor.ph.kickoff')"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<p class="field-hint schedule-timezone-hint">{{ t('match.timezone.platform_hint') }}</p>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
@@ -234,11 +235,6 @@ async function saveMeta() {
|
||||
<el-switch v-model="form.isHot" size="small" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="6">
|
||||
<el-form-item :label="t('matchEditor.field.correct_score_enabled')">
|
||||
<el-switch v-model="form.correctScoreEnabled" size="small" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-form>
|
||||
@@ -258,13 +254,15 @@ async function saveMeta() {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding-bottom: 24px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: #111;
|
||||
border: 1px solid #252525;
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
@@ -274,14 +272,14 @@ async function saveMeta() {
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #222;
|
||||
border-bottom: 1px solid var(--border-soft);
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #ccc;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text);
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.panel-foot {
|
||||
@@ -289,7 +287,7 @@ async function saveMeta() {
|
||||
justify-content: flex-end;
|
||||
margin-top: 8px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #222;
|
||||
border-top: 1px solid var(--border-soft);
|
||||
}
|
||||
|
||||
.form-section {
|
||||
@@ -303,9 +301,9 @@ async function saveMeta() {
|
||||
.section-label {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--green-text);
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--success-text);
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
@@ -313,6 +311,44 @@ async function saveMeta() {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.form-section--schedule {
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.schedule-fields {
|
||||
row-gap: 8px;
|
||||
}
|
||||
|
||||
.schedule-fields :deep(.el-form-item) {
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.schedule-fields :deep(.el-form-item__label) {
|
||||
flex: 0 0 auto;
|
||||
width: auto !important;
|
||||
min-width: 64px;
|
||||
padding-right: 10px;
|
||||
white-space: nowrap;
|
||||
word-break: keep-all;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.schedule-fields :deep(.el-form-item__content) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.schedule-fields :deep(.el-date-editor),
|
||||
.schedule-fields :deep(.el-input),
|
||||
.schedule-fields :deep(.el-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.schedule-fields :deep(.el-switch) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logo-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -339,7 +375,7 @@ async function saveMeta() {
|
||||
|
||||
.meta-form :deep(.el-input__inner),
|
||||
.meta-form :deep(.el-input-number .el-input__inner) {
|
||||
color: #fff !important;
|
||||
color: var(--text) !important;
|
||||
}
|
||||
|
||||
.field-hint {
|
||||
@@ -349,6 +385,10 @@ async function saveMeta() {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.schedule-timezone-hint {
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
||||
.league-readonly-grid {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@@ -360,7 +400,7 @@ async function saveMeta() {
|
||||
height: 40px;
|
||||
object-fit: contain;
|
||||
border-radius: 6px;
|
||||
background: #1a1a1a;
|
||||
background: #f4f0e8;
|
||||
}
|
||||
|
||||
.league-readonly-names {
|
||||
@@ -374,7 +414,7 @@ async function saveMeta() {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: #ddd;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.league-readonly-lang {
|
||||
@@ -382,4 +422,58 @@ async function saveMeta() {
|
||||
color: #8e8e93;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.panel-head,
|
||||
.panel-foot {
|
||||
border-color: var(--border-soft);
|
||||
}
|
||||
|
||||
.panel-title,
|
||||
.preview-title,
|
||||
.preview-bar-title,
|
||||
.league-readonly-line {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.section-label {
|
||||
color: var(--success-text);
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.logo-inline-label,
|
||||
.meta-form :deep(.el-form-item__label),
|
||||
.field-hint,
|
||||
.league-readonly-lang {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
.meta-form :deep(.el-input__inner),
|
||||
.meta-form :deep(.el-input-number .el-input__inner) {
|
||||
color: var(--text) !important;
|
||||
}
|
||||
|
||||
.league-readonly-logo img {
|
||||
background: #f4f0e8;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.panel-head,
|
||||
.panel-foot,
|
||||
.league-readonly-grid {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.schedule-fields :deep(.el-form-item) {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.schedule-fields :deep(.el-form-item__label) {
|
||||
width: auto !important;
|
||||
min-width: 0;
|
||||
padding-right: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -17,6 +17,12 @@ const loading = ref(false);
|
||||
const status = ref('DRAFT');
|
||||
const matchLabel = ref('');
|
||||
|
||||
function matchup(home: string, away: string) {
|
||||
const h = home.trim();
|
||||
const a = away.trim();
|
||||
return h && a ? `${h} vs ${a}` : '';
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!matchId.value) return;
|
||||
loading.value = true;
|
||||
@@ -29,11 +35,15 @@ async function load() {
|
||||
return;
|
||||
}
|
||||
status.value = detail.status;
|
||||
const home = detail.homeTeamZh || detail.homeTeamEn || detail.homeTeamCode || '';
|
||||
const away = detail.awayTeamZh || detail.awayTeamEn || detail.awayTeamCode || '';
|
||||
matchLabel.value =
|
||||
detail.matchName?.trim() ||
|
||||
(home && away ? `${home} vs ${away}` : `#${matchId.value}`);
|
||||
const zh = matchup(
|
||||
detail.homeTeamZh || detail.homeTeamCode || detail.homeTeamEn || '',
|
||||
detail.awayTeamZh || detail.awayTeamCode || detail.awayTeamEn || '',
|
||||
);
|
||||
const en = matchup(
|
||||
detail.homeTeamEn || detail.homeTeamCode || detail.homeTeamZh || '',
|
||||
detail.awayTeamEn || detail.awayTeamCode || detail.awayTeamZh || '',
|
||||
);
|
||||
matchLabel.value = zh && en && zh !== en ? `${zh} / ${en}` : zh || en || `#${matchId.value}`;
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.load_failed'));
|
||||
@@ -46,7 +56,7 @@ watch(matchId, load, { immediate: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-loading="loading" class="match-markets-page page-scroll">
|
||||
<div v-loading="loading" class="match-markets-page">
|
||||
<AdminSubNav
|
||||
:title="t('matchEditor.section_markets')"
|
||||
:subtitle="matchLabel"
|
||||
@@ -64,13 +74,21 @@ watch(matchId, load, { immediate: true });
|
||||
|
||||
<style scoped>
|
||||
.match-markets-page {
|
||||
padding: 0 4px 24px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding: 0 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: #111;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 12px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,81 +1,249 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { useAdminLocale } from '../../composables/useAdminLocale';
|
||||
import api from '../../api';
|
||||
import type { AdminMatchDetail } from '../match-form';
|
||||
import { defaultSelectionName } from '../../utils/selectionDefaults';
|
||||
import { adminSelectionLabel } from '../../utils/adminSelectionLabel';
|
||||
|
||||
const props = defineProps<{
|
||||
matchId: string;
|
||||
}>();
|
||||
|
||||
const { t } = useAdminLocale();
|
||||
const { t, locale } = useAdminLocale();
|
||||
|
||||
const LOCALES = [
|
||||
{ code: 'zh-CN', label: '中文' },
|
||||
{ code: 'en-US', label: 'English' },
|
||||
{ code: 'ms-MY', label: 'Bahasa Melayu' },
|
||||
] as const;
|
||||
|
||||
const UI_TEXT: Record<string, Record<string, string>> = {
|
||||
add: { 'zh-CN': '添加', 'en-US': 'Add', 'ms-MY': 'Tambah' },
|
||||
addMarket: { 'zh-CN': '增加盘口', 'en-US': 'Add market', 'ms-MY': 'Tambah pasaran' },
|
||||
addScore: { 'zh-CN': '新增比分', 'en-US': 'Add score', 'ms-MY': 'Tambah skor' },
|
||||
cancel: { 'zh-CN': '取消', 'en-US': 'Cancel', 'ms-MY': 'Batal' },
|
||||
close: { 'zh-CN': '关闭', 'en-US': 'Close', 'ms-MY': 'Tutup' },
|
||||
copyCreated: { 'zh-CN': '已复制盘口线', 'en-US': 'Market line duplicated', 'ms-MY': 'Garisan pasaran disalin' },
|
||||
copyMarket: { 'zh-CN': '复制盘口', 'en-US': 'Duplicate market', 'ms-MY': 'Salin pasaran' },
|
||||
copyMarketCreated: { 'zh-CN': '已复制盘口', 'en-US': 'Market duplicated', 'ms-MY': 'Pasaran disalin' },
|
||||
copyMarketShort: { 'zh-CN': '复制盘口', 'en-US': 'Duplicate', 'ms-MY': 'Salin pasaran' },
|
||||
copyLine: { 'zh-CN': '复制盘口线', 'en-US': 'Duplicate line', 'ms-MY': 'Salin garisan' },
|
||||
copyLineShort: { 'zh-CN': '复制线', 'en-US': 'Copy', 'ms-MY': 'Salin' },
|
||||
defaultTemplate: { 'zh-CN': '默认', 'en-US': 'Default', 'ms-MY': 'Lalai' },
|
||||
fillScoreRange: { 'zh-CN': '补齐 0-4 比分', 'en-US': 'Fill 0-4 scores', 'ms-MY': 'Lengkapkan skor 0-4' },
|
||||
hidden: { 'zh-CN': '玩家端隐藏', 'en-US': 'Hidden on player', 'ms-MY': 'Disembunyikan pemain' },
|
||||
lineValue: { 'zh-CN': '盘口线', 'en-US': 'Line', 'ms-MY': 'Garisan' },
|
||||
loadTemplate: { 'zh-CN': '载入模板', 'en-US': 'Load template', 'ms-MY': 'Muat templat' },
|
||||
marketDetails: { 'zh-CN': '赛事详情', 'en-US': 'Match details', 'ms-MY': 'Butiran perlawanan' },
|
||||
marketName: { 'zh-CN': '盘口名称', 'en-US': 'Market name', 'ms-MY': 'Nama pasaran' },
|
||||
marketToAdd: { 'zh-CN': '选择要添加的盘口', 'en-US': 'Choose a market to add', 'ms-MY': 'Pilih pasaran untuk ditambah' },
|
||||
marketUnavailable: { 'zh-CN': '暂不开放玩家端', 'en-US': 'Not available on player', 'ms-MY': 'Belum tersedia untuk pemain' },
|
||||
matchMarkets: { 'zh-CN': '赛事盘口', 'en-US': 'Match markets', 'ms-MY': 'Pasaran perlawanan' },
|
||||
moveDown: { 'zh-CN': '下移', 'en-US': 'Move down', 'ms-MY': 'Turun' },
|
||||
moveUp: { 'zh-CN': '上移', 'en-US': 'Move up', 'ms-MY': 'Naik' },
|
||||
odds: { 'zh-CN': '赔率', 'en-US': 'Odds', 'ms-MY': 'Odds' },
|
||||
parlay: { 'zh-CN': '串关', 'en-US': 'Parlay', 'ms-MY': 'Parlay' },
|
||||
playerVisible: { 'zh-CN': '玩家端显示', 'en-US': 'Show on player', 'ms-MY': 'Papar pemain' },
|
||||
promoLabel: { 'zh-CN': '促销标签', 'en-US': 'Promo label', 'ms-MY': 'Label promosi' },
|
||||
saveAsTemplate: { 'zh-CN': '保存为模板', 'en-US': 'Save as template', 'ms-MY': 'Simpan templat' },
|
||||
saveTemplateTitle: { 'zh-CN': '保存为盘口模板', 'en-US': 'Save as market template', 'ms-MY': 'Simpan sebagai templat pasaran' },
|
||||
selectMarket: { 'zh-CN': '从盘口池选择', 'en-US': 'Select from market pool', 'ms-MY': 'Pilih daripada kumpulan pasaran' },
|
||||
selectTemplate: { 'zh-CN': '选择模板', 'en-US': 'Select template', 'ms-MY': 'Pilih templat' },
|
||||
single: { 'zh-CN': '单关', 'en-US': 'Single', 'ms-MY': 'Tunggal' },
|
||||
status: { 'zh-CN': '状态', 'en-US': 'Status', 'ms-MY': 'Status' },
|
||||
statusClosed: { 'zh-CN': '已关闭', 'en-US': 'Closed', 'ms-MY': 'Ditutup' },
|
||||
statusOpen: { 'zh-CN': '可售', 'en-US': 'Open', 'ms-MY': 'Dibuka' },
|
||||
statusSuspended: { 'zh-CN': '暂停', 'en-US': 'Suspended', 'ms-MY': 'Digantung' },
|
||||
templateLoaded: { 'zh-CN': '模板已载入', 'en-US': 'Template loaded', 'ms-MY': 'Templat dimuatkan' },
|
||||
templateNameInput: { 'zh-CN': '请输入模板名称', 'en-US': 'Enter template name', 'ms-MY': 'Masukkan nama templat' },
|
||||
templateSaved: { 'zh-CN': '模板已保存', 'en-US': 'Template saved', 'ms-MY': 'Templat disimpan' },
|
||||
unsaved: { 'zh-CN': '未保存', 'en-US': 'Unsaved', 'ms-MY': 'Belum disimpan' },
|
||||
};
|
||||
|
||||
type LocalizedText = Record<string, string>;
|
||||
|
||||
interface MarketDefinition {
|
||||
marketType: string;
|
||||
marketKey: string;
|
||||
period: string;
|
||||
sortOrder: number;
|
||||
defaultLineValue: number | null;
|
||||
allowSingle: boolean;
|
||||
allowParlay: boolean;
|
||||
showOnPlayer: boolean;
|
||||
settlementSupported: boolean;
|
||||
usesLineValue: boolean;
|
||||
renderType: 'standard' | 'correctScore' | 'unsupported';
|
||||
nameI18n: LocalizedText;
|
||||
selectionTemplate: Array<{
|
||||
code: string;
|
||||
name: string;
|
||||
nameI18n: LocalizedText;
|
||||
odds: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface SelectionRow {
|
||||
id: string;
|
||||
id?: string;
|
||||
selectionCode: string;
|
||||
selectionName: string;
|
||||
nameI18n: LocalizedText;
|
||||
odds: number;
|
||||
status: string;
|
||||
editOdds: number;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
interface MarketRow {
|
||||
id: string;
|
||||
id?: string;
|
||||
marketType: string;
|
||||
marketKey: string;
|
||||
period: string;
|
||||
lineValue: number | null;
|
||||
paramsJson: Record<string, unknown> | null;
|
||||
status: string;
|
||||
allowSingle: boolean;
|
||||
allowParlay: boolean;
|
||||
showOnPlayer: boolean;
|
||||
sortOrder: number;
|
||||
promoLabel: string;
|
||||
editPromoLabel: string;
|
||||
editLineValue: number | null;
|
||||
promoLabelI18n: LocalizedText;
|
||||
nameI18n: LocalizedText;
|
||||
selections: SelectionRow[];
|
||||
}
|
||||
|
||||
const DEFAULT_MARKET_TYPES = [
|
||||
'FT_1X2',
|
||||
'FT_HANDICAP',
|
||||
'FT_OVER_UNDER',
|
||||
'FT_ODD_EVEN',
|
||||
'HT_1X2',
|
||||
'HT_HANDICAP',
|
||||
'HT_OVER_UNDER',
|
||||
'FT_CORRECT_SCORE',
|
||||
'HT_CORRECT_SCORE',
|
||||
'SH_CORRECT_SCORE',
|
||||
];
|
||||
interface MarketTemplateSummary {
|
||||
id: string;
|
||||
name: string;
|
||||
nameI18n: LocalizedText;
|
||||
isDefault: boolean;
|
||||
status: string;
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
const savingMarketId = ref<string | null>(null);
|
||||
const saving = ref(false);
|
||||
const applying = ref(false);
|
||||
const definitions = ref<MarketDefinition[]>([]);
|
||||
const templates = ref<MarketTemplateSummary[]>([]);
|
||||
const selectedTemplateId = ref('');
|
||||
const markets = ref<MarketRow[]>([]);
|
||||
const selectedMarketIndex = ref(0);
|
||||
const scoreHome = ref(0);
|
||||
const scoreAway = ref(0);
|
||||
const addMarketVisible = ref(false);
|
||||
const selectedDefinitionType = ref('');
|
||||
|
||||
const definitionMap = computed(() => new Map(definitions.value.map((d) => [d.marketType, d])));
|
||||
const definitionOptions = computed(() => [...definitions.value].sort((a, b) => a.sortOrder - b.sortOrder));
|
||||
const selectedMarket = computed(() => markets.value[selectedMarketIndex.value] ?? markets.value[0] ?? null);
|
||||
const displayLocales = computed(() => {
|
||||
const zhLabels: Record<string, string> = {
|
||||
'zh-CN': '中文',
|
||||
'en-US': '英语',
|
||||
'ms-MY': '马来语',
|
||||
};
|
||||
return LOCALES.map((loc) => ({
|
||||
code: loc.code,
|
||||
label: locale.value === 'zh-CN' ? zhLabels[loc.code] : loc.label,
|
||||
}));
|
||||
});
|
||||
const dirty = ref(false);
|
||||
|
||||
function localized(input?: Record<string, string> | null, fallback = ''): LocalizedText {
|
||||
const out: LocalizedText = {};
|
||||
for (const loc of LOCALES) {
|
||||
const value = input?.[loc.code]?.trim();
|
||||
if (value) out[loc.code] = value;
|
||||
}
|
||||
if (!Object.keys(out).length && fallback) out['zh-CN'] = fallback;
|
||||
return out;
|
||||
}
|
||||
|
||||
function resolveText(map?: LocalizedText | null, fallback = '') {
|
||||
const order = Array.from(new Set([locale.value, 'en-US', 'zh-CN', 'ms-MY']));
|
||||
for (const loc of order) {
|
||||
const value = map?.[loc]?.trim();
|
||||
if (value) return value;
|
||||
}
|
||||
return Object.values(map ?? {}).find((value) => value?.trim()) || fallback;
|
||||
}
|
||||
|
||||
function ui(key: string) {
|
||||
return resolveText(UI_TEXT[key], key);
|
||||
}
|
||||
|
||||
function statusLabel(status: string) {
|
||||
if (status === 'OPEN') return ui('statusOpen');
|
||||
if (status === 'SUSPENDED') return ui('statusSuspended');
|
||||
if (status === 'CLOSED') return ui('statusClosed');
|
||||
return status;
|
||||
}
|
||||
|
||||
function ensureLocaleKeys(map: LocalizedText, fallback = '') {
|
||||
for (const loc of LOCALES) {
|
||||
if (map[loc.code] == null) map[loc.code] = loc.code === 'zh-CN' ? fallback : '';
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function mapMarkets(detail: AdminMatchDetail) {
|
||||
markets.value = (detail.markets ?? []).map((m) => ({
|
||||
id: m.id,
|
||||
marketType: m.marketType,
|
||||
period: m.period,
|
||||
lineValue: m.lineValue,
|
||||
status: m.status,
|
||||
promoLabel: m.promoLabel,
|
||||
editPromoLabel: m.promoLabel,
|
||||
editLineValue: m.lineValue,
|
||||
selections: m.selections.map((s) => ({
|
||||
id: s.id,
|
||||
selectionCode: s.selectionCode,
|
||||
selectionName: s.selectionName,
|
||||
odds: s.odds,
|
||||
status: s.status,
|
||||
editOdds: s.odds,
|
||||
})),
|
||||
}));
|
||||
markets.value = (detail.markets ?? []).map((m, index) => {
|
||||
const def = definitionMap.value.get(m.marketType);
|
||||
const nameI18n = ensureLocaleKeys(localized(m.nameI18n, def ? resolveText(def.nameI18n, m.marketType) : m.marketType), m.marketType);
|
||||
const promoLabelI18n = ensureLocaleKeys(localized(m.promoLabelI18n, m.promoLabel || ''), m.promoLabel || '');
|
||||
return {
|
||||
id: m.id,
|
||||
marketType: m.marketType,
|
||||
marketKey: m.marketKey || m.marketType,
|
||||
period: m.period,
|
||||
lineValue: m.lineValue,
|
||||
paramsJson: m.paramsJson ?? null,
|
||||
status: m.status,
|
||||
allowSingle: true,
|
||||
allowParlay: true,
|
||||
showOnPlayer: m.showOnPlayer ?? true,
|
||||
sortOrder: m.sortOrder ?? index,
|
||||
promoLabel: m.promoLabel ?? '',
|
||||
promoLabelI18n,
|
||||
nameI18n,
|
||||
selections: m.selections.map((s, sIndex) => ({
|
||||
id: s.id,
|
||||
selectionCode: s.selectionCode,
|
||||
selectionName: s.selectionName,
|
||||
nameI18n: ensureLocaleKeys(localized(s.nameI18n, s.selectionName), s.selectionName),
|
||||
odds: s.odds,
|
||||
status: s.status,
|
||||
sortOrder: s.sortOrder ?? sIndex,
|
||||
})),
|
||||
};
|
||||
});
|
||||
selectedMarketIndex.value = Math.min(selectedMarketIndex.value, Math.max(0, markets.value.length - 1));
|
||||
dirty.value = false;
|
||||
}
|
||||
|
||||
function usesLineValue(marketType: string) {
|
||||
return definitionMap.value.get(marketType)?.usesLineValue ?? false;
|
||||
}
|
||||
|
||||
function isLineMarket(market: MarketRow) {
|
||||
return usesLineValue(market.marketType);
|
||||
}
|
||||
|
||||
async function loadDefinitionsAndTemplates() {
|
||||
const [{ data: defRes }, { data: tplRes }] = await Promise.all([
|
||||
api.get('/admin/market-definitions'),
|
||||
api.get('/admin/market-templates'),
|
||||
]);
|
||||
definitions.value = defRes.data ?? [];
|
||||
templates.value = tplRes.data ?? [];
|
||||
selectedTemplateId.value =
|
||||
templates.value.find((tpl) => tpl.isDefault)?.id || templates.value[0]?.id || '';
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!props.matchId) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
await loadDefinitionsAndTemplates();
|
||||
const { data } = await api.get(`/admin/matches/${props.matchId}`);
|
||||
mapMarkets(data.data as AdminMatchDetail);
|
||||
} catch (e: unknown) {
|
||||
@@ -94,387 +262,846 @@ watch(
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function marketLabel(type: string) {
|
||||
const key = `matchEditor.market.${type}`;
|
||||
const v = t(key);
|
||||
return v !== key ? v : type;
|
||||
function markDirty() {
|
||||
dirty.value = true;
|
||||
}
|
||||
|
||||
function selectionCodeLabel(code: string, market?: MarketRow) {
|
||||
return adminSelectionLabel(t, code, {
|
||||
lineValue: market?.editLineValue ?? market?.lineValue,
|
||||
period: market?.period,
|
||||
function isCorrectScoreMarket(market: MarketRow) {
|
||||
return ['FT_CORRECT_SCORE', 'HT_CORRECT_SCORE', 'SH_CORRECT_SCORE'].includes(market.marketType);
|
||||
}
|
||||
|
||||
function marketTitle(market: MarketRow) {
|
||||
const line = isLineMarket(market) && market.lineValue != null ? ` ${market.lineValue}` : '';
|
||||
return `${resolveText(market.nameI18n, market.marketType)}${line}`;
|
||||
}
|
||||
|
||||
function definitionLabel(def: MarketDefinition) {
|
||||
const label = resolveText(def.nameI18n, def.marketType);
|
||||
return def.settlementSupported ? label : `${label}(${ui('marketUnavailable')})`;
|
||||
}
|
||||
|
||||
function templateLabel(tpl: MarketTemplateSummary) {
|
||||
const label = resolveText(tpl.nameI18n, tpl.name || '');
|
||||
return tpl.isDefault ? `${label}(${ui('defaultTemplate')})` : label;
|
||||
}
|
||||
|
||||
function marketMetaText(market: MarketRow) {
|
||||
const parts = [statusLabel(market.status)];
|
||||
if (isLineMarket(market) && market.lineValue != null) parts.push(`${ui('lineValue')} ${market.lineValue}`);
|
||||
if (!market.showOnPlayer) parts.push(ui('hidden'));
|
||||
return parts.join(' · ');
|
||||
}
|
||||
|
||||
function selectionTitle(selection: SelectionRow) {
|
||||
const score = /^SCORE_(\d+)_(\d+)$/.exec(selection.selectionCode);
|
||||
if (score) return `${score[1]}:${score[2]}`;
|
||||
return resolveText(selection.nameI18n, selection.selectionName || selection.selectionCode);
|
||||
}
|
||||
|
||||
function isScoreSelection(selection: SelectionRow) {
|
||||
return /^SCORE_\d+_\d+$/.test(selection.selectionCode);
|
||||
}
|
||||
|
||||
function openAddMarketDialog() {
|
||||
selectedDefinitionType.value = definitionOptions.value[0]?.marketType ?? '';
|
||||
addMarketVisible.value = true;
|
||||
}
|
||||
|
||||
function addSelectedMarket() {
|
||||
const def = definitionMap.value.get(selectedDefinitionType.value);
|
||||
if (!def) return;
|
||||
addMarket(def);
|
||||
addMarketVisible.value = false;
|
||||
selectedDefinitionType.value = '';
|
||||
}
|
||||
|
||||
function addMarket(def: MarketDefinition) {
|
||||
const row: MarketRow = {
|
||||
marketType: def.marketType,
|
||||
marketKey: def.marketKey,
|
||||
period: def.period,
|
||||
lineValue: def.defaultLineValue,
|
||||
paramsJson: null,
|
||||
status: 'OPEN',
|
||||
allowSingle: def.allowSingle,
|
||||
allowParlay: def.allowParlay,
|
||||
showOnPlayer: def.showOnPlayer && def.settlementSupported,
|
||||
sortOrder: markets.value.length,
|
||||
promoLabel: '',
|
||||
promoLabelI18n: ensureLocaleKeys({}),
|
||||
nameI18n: ensureLocaleKeys(localized(def.nameI18n, def.marketType), def.marketType),
|
||||
selections: def.selectionTemplate.map((s, index) => ({
|
||||
selectionCode: s.code,
|
||||
selectionName: s.name,
|
||||
nameI18n: ensureLocaleKeys(localized(s.nameI18n, s.name), s.name),
|
||||
odds: Number(s.odds),
|
||||
status: 'OPEN',
|
||||
sortOrder: index,
|
||||
})),
|
||||
};
|
||||
markets.value.push(row);
|
||||
selectedMarketIndex.value = markets.value.length - 1;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
function copyLocalizedName(map: LocalizedText) {
|
||||
const suffix: Record<string, string> = {
|
||||
'zh-CN': ' 副本',
|
||||
'en-US': ' Copy',
|
||||
'ms-MY': ' Salinan',
|
||||
};
|
||||
const next = ensureLocaleKeys(localized(map, resolveText(map)));
|
||||
for (const loc of LOCALES) {
|
||||
const base = next[loc.code] || resolveText(next);
|
||||
next[loc.code] = `${base}${suffix[loc.code]}`;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function clonePlain<T>(value: T): T {
|
||||
return value == null ? value : JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
|
||||
function nextCopiedLineValue(source: MarketRow) {
|
||||
const used = new Set(
|
||||
markets.value
|
||||
.filter((market) => market.marketType === source.marketType && market.lineValue != null)
|
||||
.map((market) => Number(market.lineValue).toFixed(2)),
|
||||
);
|
||||
let candidate = Number(((source.lineValue ?? 0) + 0.25).toFixed(2));
|
||||
while (used.has(candidate.toFixed(2))) {
|
||||
candidate = Number((candidate + 0.25).toFixed(2));
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
function duplicateMarket(index: number) {
|
||||
const source = markets.value[index];
|
||||
if (!source) return;
|
||||
const shouldCopyLine = isLineMarket(source);
|
||||
const sourceCopy = clonePlain(source);
|
||||
const paramsCopy = clonePlain(source.paramsJson);
|
||||
const copy: MarketRow = {
|
||||
...sourceCopy,
|
||||
id: undefined,
|
||||
lineValue: shouldCopyLine ? nextCopiedLineValue(source) : null,
|
||||
paramsJson: shouldCopyLine
|
||||
? paramsCopy
|
||||
: { ...(paramsCopy ?? {}), copyId: `copy-${Date.now()}-${index}` },
|
||||
nameI18n: copyLocalizedName(source.nameI18n),
|
||||
sortOrder: markets.value.length,
|
||||
selections: source.selections.map((s) => ({ ...clonePlain(s), id: undefined })),
|
||||
};
|
||||
markets.value.splice(index + 1, 0, copy);
|
||||
selectedMarketIndex.value = index + 1;
|
||||
reorder();
|
||||
markDirty();
|
||||
ElMessage.success(ui(shouldCopyLine ? 'copyCreated' : 'copyMarketCreated'));
|
||||
}
|
||||
|
||||
function removeMarket(index: number) {
|
||||
markets.value.splice(index, 1);
|
||||
selectedMarketIndex.value = Math.min(selectedMarketIndex.value, Math.max(0, markets.value.length - 1));
|
||||
reorder();
|
||||
markDirty();
|
||||
}
|
||||
|
||||
function moveMarket(index: number, direction: -1 | 1) {
|
||||
const target = index + direction;
|
||||
if (target < 0 || target >= markets.value.length) return;
|
||||
const [row] = markets.value.splice(index, 1);
|
||||
markets.value.splice(target, 0, row);
|
||||
selectedMarketIndex.value = target;
|
||||
reorder();
|
||||
markDirty();
|
||||
}
|
||||
|
||||
function reorder() {
|
||||
markets.value.forEach((m, index) => {
|
||||
m.sortOrder = index;
|
||||
});
|
||||
}
|
||||
|
||||
function hasLine(market: MarketRow) {
|
||||
return market.lineValue != null || market.editLineValue != null;
|
||||
function addScoreRange(market: MarketRow) {
|
||||
const existing = new Set(market.selections.map((s) => s.selectionCode));
|
||||
for (let home = 0; home <= 4; home += 1) {
|
||||
for (let away = 0; away <= 4; away += 1) {
|
||||
const code = `SCORE_${home}_${away}`;
|
||||
if (existing.has(code)) continue;
|
||||
market.selections.push({
|
||||
selectionCode: code,
|
||||
selectionName: `${home}-${away}`,
|
||||
nameI18n: ensureLocaleKeys({ 'zh-CN': `${home}-${away}`, 'en-US': `${home}-${away}`, 'ms-MY': `${home}-${away}` }),
|
||||
odds: 8,
|
||||
status: 'OPEN',
|
||||
sortOrder: market.selections.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
markDirty();
|
||||
}
|
||||
|
||||
const CORRECT_SCORE_TYPES = new Set([
|
||||
'FT_CORRECT_SCORE',
|
||||
'HT_CORRECT_SCORE',
|
||||
'SH_CORRECT_SCORE',
|
||||
]);
|
||||
|
||||
function isMultiRowMarket(market: MarketRow) {
|
||||
return CORRECT_SCORE_TYPES.has(market.marketType) || market.selections.length > 6;
|
||||
function addScore(market: MarketRow) {
|
||||
const home = Math.max(0, Number(scoreHome.value) || 0);
|
||||
const away = Math.max(0, Number(scoreAway.value) || 0);
|
||||
const code = `SCORE_${home}_${away}`;
|
||||
if (market.selections.some((s) => s.selectionCode === code)) return;
|
||||
market.selections.push({
|
||||
selectionCode: code,
|
||||
selectionName: `${home}-${away}`,
|
||||
nameI18n: ensureLocaleKeys({ 'zh-CN': `${home}-${away}`, 'en-US': `${home}-${away}`, 'ms-MY': `${home}-${away}` }),
|
||||
odds: 8,
|
||||
status: 'OPEN',
|
||||
sortOrder: market.selections.length,
|
||||
});
|
||||
markDirty();
|
||||
}
|
||||
|
||||
function normPromo(value: string | null | undefined) {
|
||||
return (value ?? '').trim() || null;
|
||||
function removeSelection(market: MarketRow, index: number) {
|
||||
market.selections.splice(index, 1);
|
||||
market.selections.forEach((s, i) => {
|
||||
s.sortOrder = i;
|
||||
});
|
||||
markDirty();
|
||||
}
|
||||
|
||||
function normLine(value: number | null | undefined) {
|
||||
return value == null ? null : Number(value);
|
||||
function payloadMarkets() {
|
||||
return markets.value.map((m, index) => ({
|
||||
id: m.id,
|
||||
marketType: m.marketType,
|
||||
marketKey: m.marketKey,
|
||||
lineKey: null,
|
||||
period: m.period,
|
||||
lineValue: usesLineValue(m.marketType) ? m.lineValue : null,
|
||||
paramsJson: m.paramsJson,
|
||||
status: m.status,
|
||||
allowSingle: m.allowSingle,
|
||||
allowParlay: m.allowParlay,
|
||||
showOnPlayer: m.showOnPlayer,
|
||||
sortOrder: index,
|
||||
promoLabel: resolveText(m.promoLabelI18n, m.promoLabel),
|
||||
promoLabelI18n: localized(m.promoLabelI18n),
|
||||
nameI18n: localized(m.nameI18n, m.marketType),
|
||||
selections: m.selections.map((s, sIndex) => ({
|
||||
id: s.id,
|
||||
selectionCode: s.selectionCode,
|
||||
selectionName: resolveText(s.nameI18n, s.selectionName),
|
||||
nameI18n: localized(s.nameI18n, s.selectionName),
|
||||
odds: Number(s.odds),
|
||||
status: s.status,
|
||||
sortOrder: sIndex,
|
||||
})),
|
||||
}));
|
||||
}
|
||||
|
||||
function isMarketDirty(market: MarketRow) {
|
||||
if (normPromo(market.editPromoLabel) !== normPromo(market.promoLabel)) return true;
|
||||
if (normLine(market.editLineValue) !== normLine(market.lineValue)) return true;
|
||||
return market.selections.some((s) => Number(s.editOdds) !== Number(s.odds));
|
||||
}
|
||||
|
||||
async function generateTemplates() {
|
||||
loading.value = true;
|
||||
async function applyTemplate() {
|
||||
if (!selectedTemplateId.value) return;
|
||||
applying.value = true;
|
||||
try {
|
||||
await api.post(`/admin/matches/${props.matchId}/markets/templates`, {
|
||||
marketTypes: DEFAULT_MARKET_TYPES,
|
||||
await api.post(`/admin/matches/${props.matchId}/markets/apply-template`, {
|
||||
templateId: selectedTemplateId.value,
|
||||
});
|
||||
ElMessage.success(t('matchEditor.templates_generated'));
|
||||
ElMessage.success(ui('templateLoaded'));
|
||||
await load();
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
applying.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveMarket(market: MarketRow) {
|
||||
const invalid = market.selections.find((s) => !s.editOdds || s.editOdds <= 1);
|
||||
async function saveAll() {
|
||||
const invalid = markets.value.flatMap((m) => m.selections).find((s) => !s.odds || s.odds <= 1);
|
||||
if (invalid) {
|
||||
ElMessage.warning(t('outright.err_odds_min'));
|
||||
return;
|
||||
}
|
||||
|
||||
savingMarketId.value = market.id;
|
||||
saving.value = true;
|
||||
try {
|
||||
await api.patch(`/admin/markets/${market.id}`, {
|
||||
promoLabel: market.editPromoLabel.trim() || null,
|
||||
lineValue: market.editLineValue,
|
||||
await api.put(`/admin/matches/${props.matchId}/markets/bulk`, {
|
||||
markets: payloadMarkets(),
|
||||
});
|
||||
await api.put(`/admin/matches/${props.matchId}/odds`, {
|
||||
updates: market.selections.map((s) => ({
|
||||
selectionId: s.id,
|
||||
odds: s.editOdds,
|
||||
})),
|
||||
});
|
||||
for (const s of market.selections) {
|
||||
const name = defaultSelectionName(s.selectionCode, {
|
||||
lineValue: market.editLineValue,
|
||||
period: market.period,
|
||||
});
|
||||
if (name !== s.selectionName) {
|
||||
await api.patch(`/admin/selections/${s.id}`, {
|
||||
selectionName: name,
|
||||
});
|
||||
}
|
||||
}
|
||||
ElMessage.success(t('msg.saved'));
|
||||
await load();
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
||||
} finally {
|
||||
savingMarketId.value = null;
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAsTemplate() {
|
||||
let value = '';
|
||||
try {
|
||||
const result = await ElMessageBox.prompt(ui('templateNameInput'), ui('saveTemplateTitle'), {
|
||||
confirmButtonText: t('common.save'),
|
||||
cancelButtonText: t('common.cancel'),
|
||||
inputValue: `${resolveText({ 'zh-CN': '自定义足球盘口模板', 'en-US': 'Custom football market template', 'ms-MY': 'Templat pasaran bola sepak tersuai' })}`,
|
||||
});
|
||||
value = result.value?.trim() || '';
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (!value) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
await api.post('/admin/market-templates', {
|
||||
name: value,
|
||||
nameI18n: { 'zh-CN': value },
|
||||
items: payloadMarkets(),
|
||||
});
|
||||
ElMessage.success(ui('templateSaved'));
|
||||
await loadDefinitionsAndTemplates();
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-loading="loading" class="match-markets-panel">
|
||||
<div class="panel-head">
|
||||
<span class="panel-title">{{ t('matchEditor.section_markets') }}</span>
|
||||
<el-button size="small" type="primary" plain @click="generateTemplates">
|
||||
{{ t('matchEditor.generate_templates') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<p v-if="!markets.length" class="empty-hint">{{ t('matchEditor.no_markets') }}</p>
|
||||
|
||||
<div v-else class="market-lines">
|
||||
<div
|
||||
v-for="market in markets"
|
||||
:key="market.id"
|
||||
class="market-line"
|
||||
:class="{ 'market-line--wrap': isMultiRowMarket(market) }"
|
||||
>
|
||||
<label class="field-promo-wrap">
|
||||
<span class="promo-label">{{ t('matchEditor.field.promo_label_optional') }}</span>
|
||||
<el-input
|
||||
v-model="market.editPromoLabel"
|
||||
size="small"
|
||||
class="field-promo"
|
||||
clearable
|
||||
<div v-loading="loading" class="market-editor">
|
||||
<div class="market-toolbar">
|
||||
<div class="template-tools">
|
||||
<el-select v-model="selectedTemplateId" size="small" class="template-select" :placeholder="ui('selectTemplate')">
|
||||
<el-option
|
||||
v-for="tpl in templates"
|
||||
:key="tpl.id"
|
||||
:label="templateLabel(tpl)"
|
||||
:value="tpl.id"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div class="market-line-head">
|
||||
<span class="market-label" :title="market.marketType">{{ marketLabel(market.marketType) }}</span>
|
||||
|
||||
<el-input-number
|
||||
v-if="hasLine(market)"
|
||||
v-model="market.editLineValue"
|
||||
size="small"
|
||||
class="field-line"
|
||||
:step="0.25"
|
||||
controls-position="right"
|
||||
/>
|
||||
|
||||
<el-button
|
||||
v-if="isMultiRowMarket(market)"
|
||||
size="small"
|
||||
:type="isMarketDirty(market) ? 'primary' : 'default'"
|
||||
class="btn-save"
|
||||
:disabled="!isMarketDirty(market)"
|
||||
:loading="savingMarketId === market.id"
|
||||
@click="saveMarket(market)"
|
||||
>
|
||||
{{ t('common.save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="selections-wrap"
|
||||
:class="isMultiRowMarket(market) ? 'selections-grid' : 'selections-inline'"
|
||||
>
|
||||
<div v-for="sel in market.selections" :key="sel.id" class="sel-inline">
|
||||
<span class="sel-label" :title="sel.selectionCode">{{
|
||||
selectionCodeLabel(sel.selectionCode, market)
|
||||
}}</span>
|
||||
<el-input-number
|
||||
v-model="sel.editOdds"
|
||||
size="small"
|
||||
class="sel-odds"
|
||||
:min="1.01"
|
||||
:step="0.01"
|
||||
controls-position="right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-button
|
||||
v-if="!isMultiRowMarket(market)"
|
||||
size="small"
|
||||
:type="isMarketDirty(market) ? 'primary' : 'default'"
|
||||
class="btn-save"
|
||||
:disabled="!isMarketDirty(market)"
|
||||
:loading="savingMarketId === market.id"
|
||||
@click="saveMarket(market)"
|
||||
>
|
||||
</el-select>
|
||||
<el-button size="small" :loading="applying" @click="applyTemplate">{{ ui('loadTemplate') }}</el-button>
|
||||
<el-button size="small" @click="openAddMarketDialog">{{ ui('addMarket') }}</el-button>
|
||||
<el-button size="small" @click="saveAsTemplate">{{ ui('saveAsTemplate') }}</el-button>
|
||||
</div>
|
||||
<div class="save-tools">
|
||||
<el-tag v-if="dirty" size="small" type="warning">{{ ui('unsaved') }}</el-tag>
|
||||
<el-button size="small" type="primary" :loading="saving" @click="saveAll">
|
||||
{{ t('common.save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-grid">
|
||||
<section class="market-list">
|
||||
<div class="section-title">{{ ui('matchMarkets') }}</div>
|
||||
<p v-if="!markets.length" class="empty-hint">{{ t('matchEditor.no_markets') }}</p>
|
||||
<div
|
||||
v-for="(market, index) in markets"
|
||||
:key="market.id || `${market.marketType}-${index}`"
|
||||
class="market-card"
|
||||
:class="{ active: selectedMarketIndex === index, hidden: !market.showOnPlayer }"
|
||||
@click="selectedMarketIndex = index"
|
||||
>
|
||||
<div class="card-main">
|
||||
<strong>{{ marketTitle(market) }}</strong>
|
||||
<span>{{ marketMetaText(market) }}</span>
|
||||
</div>
|
||||
<div class="card-actions" @click.stop>
|
||||
<el-button
|
||||
class="card-action-btn"
|
||||
size="small"
|
||||
text
|
||||
:title="ui('moveUp')"
|
||||
:disabled="index === 0"
|
||||
@click="moveMarket(index, -1)"
|
||||
>
|
||||
↑
|
||||
</el-button>
|
||||
<el-button
|
||||
class="card-action-btn"
|
||||
size="small"
|
||||
text
|
||||
:title="ui('moveDown')"
|
||||
:disabled="index === markets.length - 1"
|
||||
@click="moveMarket(index, 1)"
|
||||
>
|
||||
↓
|
||||
</el-button>
|
||||
<el-button
|
||||
class="card-action-btn copy-btn"
|
||||
size="small"
|
||||
text
|
||||
:title="ui(isLineMarket(market) ? 'copyLine' : 'copyMarket')"
|
||||
@click="duplicateMarket(index)"
|
||||
>
|
||||
{{ ui(isLineMarket(market) ? 'copyLineShort' : 'copyMarketShort') }}
|
||||
</el-button>
|
||||
<el-button class="card-action-btn" size="small" text type="danger" :title="ui('close')" @click="removeMarket(index)">
|
||||
{{ ui('close') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="selectedMarket" class="detail-panel">
|
||||
<div class="detail-head">
|
||||
<div>
|
||||
<div class="section-title">{{ ui('marketDetails') }}</div>
|
||||
<strong>{{ marketTitle(selectedMarket) }}</strong>
|
||||
</div>
|
||||
<el-switch
|
||||
v-model="selectedMarket.showOnPlayer"
|
||||
:active-text="ui('playerVisible')"
|
||||
@change="markDirty"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-grid">
|
||||
<label>
|
||||
<span>{{ ui('status') }}</span>
|
||||
<el-select v-model="selectedMarket.status" size="small" @change="markDirty">
|
||||
<el-option :label="ui('statusOpen')" value="OPEN" />
|
||||
<el-option :label="ui('statusSuspended')" value="SUSPENDED" />
|
||||
<el-option :label="ui('statusClosed')" value="CLOSED" />
|
||||
</el-select>
|
||||
</label>
|
||||
<label v-if="isLineMarket(selectedMarket)">
|
||||
<span>{{ ui('lineValue') }}</span>
|
||||
<el-input-number
|
||||
v-model="selectedMarket.lineValue"
|
||||
size="small"
|
||||
:step="0.25"
|
||||
controls-position="right"
|
||||
@change="markDirty"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>{{ ui('single') }}</span>
|
||||
<el-switch v-model="selectedMarket.allowSingle" @change="markDirty" />
|
||||
</label>
|
||||
<label>
|
||||
<span>{{ ui('parlay') }}</span>
|
||||
<el-switch v-model="selectedMarket.allowParlay" @change="markDirty" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="locale-block">
|
||||
<div class="block-title">{{ ui('marketName') }}</div>
|
||||
<div class="locale-grid">
|
||||
<label v-for="loc in displayLocales" :key="loc.code">
|
||||
<span>{{ loc.label }}</span>
|
||||
<el-input v-model="selectedMarket.nameI18n[loc.code]" size="small" @input="markDirty" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="locale-block">
|
||||
<div class="block-title">{{ ui('promoLabel') }}</div>
|
||||
<div class="locale-grid">
|
||||
<label v-for="loc in displayLocales" :key="loc.code">
|
||||
<span>{{ loc.label }}</span>
|
||||
<el-input v-model="selectedMarket.promoLabelI18n[loc.code]" size="small" clearable @input="markDirty" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isCorrectScoreMarket(selectedMarket)" class="score-tools">
|
||||
<el-button size="small" @click="addScoreRange(selectedMarket)">{{ ui('fillScoreRange') }}</el-button>
|
||||
<el-input-number v-model="scoreHome" size="small" :min="0" controls-position="right" />
|
||||
<span>-</span>
|
||||
<el-input-number v-model="scoreAway" size="small" :min="0" controls-position="right" />
|
||||
<el-button size="small" @click="addScore(selectedMarket)">{{ ui('addScore') }}</el-button>
|
||||
</div>
|
||||
|
||||
<div class="selection-list">
|
||||
<div
|
||||
v-for="(sel, index) in selectedMarket.selections"
|
||||
:key="sel.id || `${sel.selectionCode}-${index}`"
|
||||
class="selection-row"
|
||||
:class="{ 'selection-row--score': isScoreSelection(sel) }"
|
||||
>
|
||||
<div class="selection-meta">
|
||||
<strong :title="sel.selectionCode">{{ selectionTitle(sel) }}</strong>
|
||||
<label class="selection-field selection-field--status">
|
||||
<span>{{ ui('status') }}</span>
|
||||
<el-select v-model="sel.status" size="small" @change="markDirty">
|
||||
<el-option :label="ui('statusOpen')" value="OPEN" />
|
||||
<el-option :label="ui('statusSuspended')" value="SUSPENDED" />
|
||||
<el-option :label="ui('statusClosed')" value="CLOSED" />
|
||||
</el-select>
|
||||
</label>
|
||||
<label class="selection-field selection-field--odds">
|
||||
<span>{{ ui('odds') }}</span>
|
||||
<el-input-number
|
||||
v-model="sel.odds"
|
||||
size="small"
|
||||
:min="1.01"
|
||||
:step="0.01"
|
||||
controls-position="right"
|
||||
@change="markDirty"
|
||||
/>
|
||||
</label>
|
||||
<el-button size="small" text type="danger" @click="removeSelection(selectedMarket, index)">{{ ui('close') }}</el-button>
|
||||
</div>
|
||||
<div v-if="!isScoreSelection(sel)" class="locale-grid compact">
|
||||
<label v-for="loc in displayLocales" :key="loc.code">
|
||||
<span>{{ loc.label }}</span>
|
||||
<el-input v-model="sel.nameI18n[loc.code]" size="small" @input="markDirty" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="addMarketVisible" class="add-market-dialog" :title="ui('addMarket')" width="460px" destroy-on-close>
|
||||
<label class="add-market-field">
|
||||
<span>{{ ui('marketToAdd') }}</span>
|
||||
<el-select
|
||||
v-model="selectedDefinitionType"
|
||||
class="full-select"
|
||||
filterable
|
||||
:placeholder="ui('selectMarket')"
|
||||
>
|
||||
<el-option
|
||||
v-for="def in definitionOptions"
|
||||
:key="def.marketType"
|
||||
:label="definitionLabel(def)"
|
||||
:value="def.marketType"
|
||||
/>
|
||||
</el-select>
|
||||
</label>
|
||||
<template #footer>
|
||||
<el-button @click="addMarketVisible = false">{{ ui('cancel') }}</el-button>
|
||||
<el-button type="primary" :disabled="!selectedDefinitionType" @click="addSelectedMarket">
|
||||
{{ ui('add') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.match-markets-panel {
|
||||
padding: 10px 12px 12px 16px;
|
||||
.market-editor {
|
||||
background: #0a0a0a;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
.market-toolbar {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #ccc;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.empty-hint {
|
||||
color: #888;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.market-lines {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.market-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #252525;
|
||||
border-radius: 6px;
|
||||
background: #111;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.market-line--wrap {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.market-line--wrap .field-promo-wrap {
|
||||
width: 100%;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.field-promo-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.promo-label {
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #8e8e93;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.field-promo {
|
||||
flex: 0 0 88px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.market-line--wrap .field-promo {
|
||||
flex: 1;
|
||||
max-width: 160px;
|
||||
}
|
||||
|
||||
.market-line-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.market-line:not(.market-line--wrap) .market-line-head {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.market-line--wrap .market-line-head {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.market-line--wrap .market-line-head .btn-save {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.market-label {
|
||||
flex: 0 0 76px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #e8e8e8;
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.field-line {
|
||||
flex: 0 0 88px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.field-line :deep(.el-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selections-inline {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.selections-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(112px, 1fr));
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selections-grid .sel-inline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.selections-grid .sel-label {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.selections-grid .sel-odds {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selections-grid .sel-odds :deep(.el-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sel-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid #242424;
|
||||
background: #0d0d0d;
|
||||
}
|
||||
|
||||
.sel-label {
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
.template-tools,
|
||||
.save-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.template-select {
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.editor-grid {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(320px, 420px) minmax(480px, 1fr);
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
padding: 10px 12px 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.market-list,
|
||||
.detail-panel {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
border: 1px solid #262626;
|
||||
border-radius: 8px;
|
||||
background: #111;
|
||||
padding: 10px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.section-title,
|
||||
.block-title {
|
||||
color: #b8b8b8;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--green-text);
|
||||
min-width: 14px;
|
||||
text-align: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.empty-hint {
|
||||
margin: 0;
|
||||
color: #777;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.market-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
margin-bottom: 6px;
|
||||
border: 1px solid #282828;
|
||||
border-radius: 6px;
|
||||
background: #151515;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.market-card.active {
|
||||
border-color: #9f853d;
|
||||
background: #1a1710;
|
||||
}
|
||||
|
||||
.market-card.hidden {
|
||||
opacity: 0.66;
|
||||
}
|
||||
|
||||
.card-main {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-main strong,
|
||||
.detail-head strong {
|
||||
color: #f0f0f0;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.sel-odds {
|
||||
width: 88px;
|
||||
.card-main span {
|
||||
color: #777;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.sel-odds :deep(.el-input-number) {
|
||||
.card-actions {
|
||||
flex: 0 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: 30px 30px 46px 40px;
|
||||
gap: 2px;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.card-action-btn {
|
||||
width: 100%;
|
||||
height: 26px;
|
||||
padding: 0;
|
||||
margin-left: 0 !important;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.detail-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.form-grid,
|
||||
.locale-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.locale-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.locale-grid.compact {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
color: #aaa;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.locale-block {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.score-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.selection-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.selection-row {
|
||||
padding: 8px;
|
||||
border: 1px solid #262626;
|
||||
border-radius: 6px;
|
||||
background: #141414;
|
||||
}
|
||||
|
||||
.selection-meta {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(90px, 1fr) 120px 132px 70px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.selection-row--score .selection-meta {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.selection-meta strong {
|
||||
color: #ddd;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.selection-field {
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.selection-field > span {
|
||||
color: #f3c969;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.selection-field--status > span {
|
||||
color: #7dd3fc;
|
||||
}
|
||||
|
||||
.selection-field--odds :deep(.el-input-number),
|
||||
.selection-field--odds :deep(.el-input) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
flex-shrink: 0;
|
||||
min-width: 52px;
|
||||
.add-market-field {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.btn-save.is-disabled,
|
||||
.btn-save:disabled {
|
||||
opacity: 0.45;
|
||||
.full-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.market-toolbar,
|
||||
.editor-grid,
|
||||
.form-grid,
|
||||
.locale-grid,
|
||||
.selection-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.template-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.market-list,
|
||||
.detail-panel {
|
||||
min-height: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
.market-editor {
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.market-toolbar {
|
||||
border-bottom-color: var(--border);
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.market-list,
|
||||
.detail-panel,
|
||||
.market-card,
|
||||
.selection-row {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.market-list,
|
||||
.detail-panel {
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.section-title,
|
||||
.block-title,
|
||||
.card-main strong,
|
||||
.detail-head strong,
|
||||
.selection-meta strong {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.empty-hint,
|
||||
.card-main span,
|
||||
label,
|
||||
.selection-field > span {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.market-card:hover {
|
||||
border-color: #d5cfc3;
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.market-card.active {
|
||||
border-color: var(--primary);
|
||||
background: var(--accent-subtle);
|
||||
}
|
||||
|
||||
.selection-row {
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.selection-field > span {
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.selection-field--status > span {
|
||||
color: var(--info-text);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.market-toolbar {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.template-tools,
|
||||
.save-tools {
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -603,4 +603,33 @@ async function saveRow(row: SelectionRow) {
|
||||
font-size: 13px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.panel {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.league-meta,
|
||||
.league-meta-k,
|
||||
.readonly-field {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.panel-foot.compact {
|
||||
border-top-color: var(--border-soft);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.settings-top,
|
||||
.panel-head.compact,
|
||||
.panel-foot.compact {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -356,26 +356,26 @@ onMounted(() => {
|
||||
.expand-line {
|
||||
margin: 0 0 4px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.expand-k {
|
||||
display: inline-block;
|
||||
min-width: 72px;
|
||||
color: #666;
|
||||
color: var(--text-muted);
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.expand-warn {
|
||||
margin: 0 0 8px;
|
||||
font-size: 12px;
|
||||
color: #c9a227;
|
||||
color: var(--warning-text);
|
||||
}
|
||||
|
||||
.expand-empty {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.preview-table {
|
||||
|
||||
@@ -1 +1,14 @@
|
||||
{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "skipLibCheck": true }, "include": ["src/**/*.ts", "src/**/*.vue"] }
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@thebet365/shared": ["../../packages/shared/src/index.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.vue"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user