修改河南项目的管理后台ui

This commit is contained in:
2026-09-27 19:12:59 +08:00
parent 34c7fa9d72
commit 3248ee4b83
31 changed files with 1605 additions and 558 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
# 页面标题
VUE_APP_TITLE = 河南项目
VUE_APP_TITLE = 凯科佰娱
# 开发环境配置
ENV = 'development'
+1 -1
View File
@@ -1,5 +1,5 @@
# 页面标题
VUE_APP_TITLE = 河南项目
VUE_APP_TITLE = 凯科佰娱
# 生产环境配置
ENV = 'production'
+1 -1
View File
@@ -1,5 +1,5 @@
# 页面标题
VUE_APP_TITLE = 河南项目
VUE_APP_TITLE = 凯科佰娱
NODE_ENV = production
+8 -2
View File
@@ -190,9 +190,15 @@ $line: $cc-border-3;
box-shadow: 0 0 0 3px rgba(154, 92, 53, .12);
}
/* 搜索条不再显示 label,用 placeholder 表达(与设计图一致) */
/* 搜索条恢复显示 label(2026-09-27 用户反馈「搜索框旁边的提示词不显示了」)。
此前这里把 inline 表单的 label 整片 display:none、只用 placeholder 表达,实测丢语境:
①「最小值 / 最大值」两个框没有前面的「价格区间」就不知道在填什么(同一处还有「请选择」×3 看不出是外观/类型/是否上架);
② 只写了 label、没写 placeholder 的框会变成没有任何说明的空白框;
③ 清空/选中后 placeholder 消失,条件就再也认不出来了。
⇒ 交回 Element 默认的 inline label(float:none + inline-block),只把字号/颜色对齐 size="small" 的控件。 */
.el-form--inline .el-form-item__label {
display: none;
font-size: 13px;
color: $ink-3;
}
.el-form--inline .el-form-item {
@@ -255,7 +255,7 @@ export default {
poolStatsList: [],
multiplierTable: [
{ color: 'gold', A: 1, B: 1, C: 1, D: 1 },
{ color: '#a4485a', A: 1, B: 1, C: 1, D: 1 },
{ color: 'red', A: 1, B: 1, C: 1, D: 1 },
{ color: 'purple', A: 1, B: 1, C: 1, D: 1 }
],
// 奖池调节相关
@@ -353,7 +353,9 @@ export default {
},
syncMultiplierTable() {
const multipliers = this.config.multipliers || [];
const colors = ['gold', '#a4485a', 'purple'];
// 这里的 color 必须与后端契约一致(gold/red/purple),也是 cellCounts() 的 key:
// 写成颜色值(如 #a4485a)会同时导致标签显示成色号、红色那一行算不到盘面格数(整行倍率被跳过)
const colors = ['gold', 'red', 'purple'];
const symbols = ['A', 'B', 'C', 'D'];
this.multiplierTable = colors.map(color => {
const row = { color };
+355 -180
View File
@@ -16,87 +16,133 @@
>刷新数据</el-button>
</div>
<!-- 统计卡片网格 -->
<div class="stat-grid">
<div
v-for="card in statCards"
:key="card.key"
class="stat-card"
:class="{ clickable: !!card.route }"
@click="card.route && handto(card.route)"
>
<div class="stat-icon" :class="'theme-' + card.theme">
<i :class="card.icon"></i>
</div>
<div class="stat-info">
<div class="stat-value">{{ card.value }}</div>
<div class="stat-label">
{{ card.label }}
<el-tooltip v-if="card.note" :content="card.note" placement="top">
<i class="el-icon-info stat-note-icon"></i>
</el-tooltip>
</div>
</div>
<i v-if="card.route" class="el-icon-arrow-right stat-arrow"></i>
</div>
</div>
<!-- 主体:左窄轨 + 右侧主区 -->
<div class="home-body">
<!-- 左窄轨:核心指标 + 待办 -->
<div class="rail">
<div class="rail-title">核心指标</div>
<!-- 实时 RTP -->
<div class="section-card">
<div class="section-header">
<div class="section-title">
<span class="title-bar"></span>
<span>实时 RTP</span>
</div>
<span class="section-sub">每 30 秒自动刷新</span>
</div>
<div class="rtp-grid">
<div
v-for="(item, idx) in visibleRtpList"
:key="item.name || idx"
class="rtp-card"
:class="'rtp-theme-' + (idx % 5)"
>
<div class="rtp-name">
<span class="rtp-dot"></span>
{{ item.name }}
</div>
<div class="rtp-value">
{{ item.rtp !== null && item.rtp !== undefined ? item.rtp.toFixed(2) + '%' : '--' }}
</div>
<div class="rtp-progress">
<div class="rtp-progress-inner" :style="{ width: Math.min(Number(item.rtp) || 0, 100) + '%' }"></div>
</div>
<div class="rtp-rows">
<div class="rtp-row">
<span>投注</span>
<span>¥{{ item.bet !== null && item.bet !== undefined ? Number(item.bet).toFixed(2) : '--' }}</span>
<div class="rail-metrics">
<div
v-for="m in railMetrics"
:key="m.key"
class="rail-metric"
:class="{ clickable: !!m.route }"
@click="m.route && handto(m.route)"
>
<div class="rail-metric-label">
<span>{{ m.label }}</span>
<el-tooltip v-if="m.note" :content="m.note" placement="top">
<i class="el-icon-info rail-note"></i>
</el-tooltip>
</div>
<div class="rtp-row">
<span>奖励</span>
<span>¥{{ item.reward !== null && item.reward !== undefined ? Number(item.reward).toFixed(2) : '--' }}</span>
<div class="rail-metric-value">{{ m.value }}</div>
<i v-if="m.route" class="el-icon-arrow-right rail-arrow"></i>
</div>
</div>
<div class="rail-todo">
<div
class="rail-todo-item clickable"
:class="{ alert: totaldeliver > 0 }"
@click="handto(3)"
>
<span class="rail-todo-label">待处理提货申请</span>
<span class="rail-todo-value">{{ totaldeliver === null ? '--' : totaldeliver }}</span>
</div>
<div class="rail-todo-item">
<span class="rail-todo-label">提货余额</span>
<span class="rail-todo-value">¥{{ balance }}</span>
</div>
</div>
</div>
<!-- 右侧主区 -->
<div class="main-col">
<!-- 实时 RTP -->
<div class="section-card">
<div class="section-header">
<div class="section-title">
<span class="title-bar"></span>
<span>实时 RTP</span>
</div>
<span class="section-sub">每 30 秒自动刷新</span>
</div>
<div class="rtp-grid">
<div
v-for="(item, idx) in visibleRtpList"
:key="item.name || idx"
class="rtp-card"
:class="'rtp-theme-' + (idx % 5)"
>
<div class="rtp-name">
<span class="rtp-dot"></span>
{{ item.name }}
</div>
<div class="rtp-value">
{{ item.rtp !== null && item.rtp !== undefined ? item.rtp.toFixed(2) + '%' : '--' }}
</div>
<div class="rtp-progress">
<div class="rtp-progress-inner" :style="{ width: Math.min(Number(item.rtp) || 0, 100) + '%' }"></div>
</div>
<div class="rtp-rows">
<div class="rtp-row">
<span>投注</span>
<span>¥{{ item.bet !== null && item.bet !== undefined ? Number(item.bet).toFixed(2) : '--' }}</span>
</div>
<div class="rtp-row">
<span>奖励</span>
<span>¥{{ item.reward !== null && item.reward !== undefined ? Number(item.reward).toFixed(2) : '--' }}</span>
</div>
</div>
</div>
<div v-if="!visibleRtpList.length" class="rtp-empty">暂无 RTP 数据</div>
</div>
</div>
<!-- 经营数据(次级指标带)—— 放在图表上方:图表最高,放最后滚到底正好 -->
<div class="section-card">
<div class="section-header">
<div class="section-title">
<span class="title-bar"></span>
<span>经营数据</span>
</div>
</div>
<div class="mini-stats">
<div v-for="s in miniStats" :key="s.key" class="mini-stat">
<div class="mini-icon" :class="'theme-' + s.theme">
<i :class="s.icon"></i>
</div>
<div class="mini-info">
<div class="mini-label">
<span>{{ s.label }}</span>
<el-tooltip v-if="s.note" :content="s.note" placement="top">
<i class="el-icon-info mini-note"></i>
</el-tooltip>
</div>
<div class="mini-value">{{ s.value }}</div>
</div>
</div>
</div>
</div>
<div v-if="!visibleRtpList.length" class="rtp-empty">暂无 RTP 数据</div>
</div>
</div>
<!-- 流水统计图表 -->
<div class="section-card">
<div class="section-header">
<div class="section-title">
<span class="title-bar"></span>
<span>流水统计</span>
<span class="section-sub chart-sub">{{ chartTypeLabel }}</span>
</div>
<div class="chart-tabs">
<span :class="['tab-item', chartType === 'hour' ? 'active' : '']" @click="switchChartType('hour')">小时</span>
<span :class="['tab-item', chartType === 'day' ? 'active' : '']" @click="switchChartType('day')">天</span>
<span :class="['tab-item', chartType === 'week' ? 'active' : '']" @click="switchChartType('week')">周</span>
<!-- 流水统计图表 -->
<div class="section-card">
<div class="section-header">
<div class="section-title">
<span class="title-bar"></span>
<span>流水统计</span>
<span class="section-sub chart-sub">{{ chartTypeLabel }}</span>
</div>
<div class="chart-tabs">
<span :class="['tab-item', chartType === 'hour' ? 'active' : '']" @click="switchChartType('hour')">小时</span>
<span :class="['tab-item', chartType === 'day' ? 'active' : '']" @click="switchChartType('day')">天</span>
<span :class="['tab-item', chartType === 'week' ? 'active' : '']" @click="switchChartType('week')">周</span>
</div>
</div>
<div id="lineChart" style="width: 100%; height: 400px;"></div>
</div>
</div>
<div id="lineChart" style="width: 100%; height: 400px;"></div>
</div>
</div>
</template>
@@ -155,17 +201,39 @@ export default {
chartTypeLabel() {
return this.chartType === "hour" ? "今日(按小时)" : this.chartType === "week" ? "近 4 周" : "近 7 天";
},
statCards() {
/* 左窄轨:只放「平台体量」三项,给足字号做层级;
其余指标下沉到右侧主区底部的「经营数据」带。 */
railMetrics() {
return [
{
key: "online",
label: "在线用户",
note: "当前在线的用户数,点击可查看在线用户列表",
value: this.total === null ? "--" : this.total,
route: 1
},
{
key: "totalRecharge",
label: "总充值",
note: "历史累计充值总额(排除无效用户)",
value: "¥" + this.totalRecord,
route: 2
},
{
key: "totalFlow",
label: "总流水",
note: "历史累计全部游戏投注消费总额(已排除无效用户)",
value: "¥" + Number(this.totalFlow).toFixed(2)
}
];
},
/* 右侧主区底部:其余 6 项,紧凑成一条指标带 */
miniStats() {
return [
{ key: "online", label: "在线用户", note: "当前在线的用户数,点击可查看在线用户列表", value: this.total === null ? "--" : this.total, icon: "el-icon-user-solid", theme: "blue", route: 1 },
{ key: "totalRecharge", label: "总充值", note: "历史累计充值总额(排除无效用户)", value: "¥" + this.totalRecord, icon: "el-icon-s-order", theme: "orange", route: 2 },
{ key: "deliver", label: "待处理提货申请", note: "待审核处理的提货申请数量,点击可前往处理", value: this.totaldeliver === null ? "--" : this.totaldeliver, icon: "el-icon-shopping-cart-full", theme: "green", route: 3 },
{ key: "balance", label: "提货余额", note: "第三方发货平台账户余额,用于采购发货", value: "$" + this.balance, icon: "el-icon-wallet", theme: "red" },
{ key: "todayRecharge", label: "今日总充值", note: "今日0点至今的充值总额(已排除无效用户)", value: "¥" + this.todayRecharge, icon: "el-icon-money", theme: "purple" },
{ key: "todayBet", label: "今日总投注", note: "今日全部游戏的投注消费总额(已排除无效用户)", value: "¥" + this.todayBet, icon: "el-icon-coin", theme: "cyan" },
{ key: "todayReward", label: "今日总返奖", note: "今日全游戏产出饰品总价值(按背包产出统计,含已分解)", value: "¥" + this.todayReward, icon: "el-icon-present", theme: "pink" },
{ key: "deliveryAmount", label: "总发货金额", note: "已完成发货订单的采购价合计(已排除无效用户)", value: "¥" + Number(this.totalDeliveryAmount).toFixed(2), icon: "el-icon-box", theme: "amber" },
{ key: "totalFlow", label: "总流水", note: "历史累计全部游戏投注消费总额(已排除无效用户)", value: "¥" + Number(this.totalFlow).toFixed(2), icon: "el-icon-data-line", theme: "indigo" },
{ key: "historyReward", label: "历史总返奖", note: "全游戏产出饰品总价值(按背包产出统计,含已分解)", value: "¥" + Number(this.historyReward).toFixed(2), icon: "el-icon-trophy", theme: "teal" },
{ key: "rollRoom", label: "Roll 房今日返奖", note: "不含下注(仅饰品产出)", value: "¥" + Number(this.rollRoomReward || 0).toFixed(2), icon: "el-icon-house", theme: "sky" }
];
@@ -480,107 +548,95 @@ export default {
}
}
/* ========== 统计卡片网格 ========== */
.stat-grid {
/* ========== 主体两栏:左窄轨 + 右主区 ========== */
.home-body {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 20px;
grid-template-columns: 300px minmax(0, 1fr);
gap: 20px;
/* start:左轨按内容高度,不跟着右列拉长(否则底部空一大截) */
align-items: start;
@media (max-width: 1400px) {
grid-template-columns: repeat(3, 1fr);
}
@media (max-width: 1000px) {
grid-template-columns: repeat(2, 1fr);
}
@media (max-width: 600px) {
grid-template-columns: 1fr;
@media (max-width: 1200px) {
grid-template-columns: minmax(0, 1fr);
}
}
.stat-card {
position: relative;
display: flex;
align-items: center;
/* ========== 左窄轨 ========== */
.rail {
/* 右列很长,左轨跟着滚,随时能看到核心数字 */
position: sticky;
top: 16px;
padding: 20px;
background: #fffdfa;
border-radius: 16px;
padding: 20px;
box-shadow: 0 4px 14px rgba(139, 115, 85, 0.10);
border: 1px solid rgba(212, 160, 160, 0.55);
transition:
transform 0.5s ease-in-out,
box-shadow 0.5s ease-in-out,
border-color 0.5s ease-in-out;
overflow: hidden;
&:hover {
/* 规范:悬停用「小幅放大 + ≤1deg 微旋」,避免机械式直上直下 */
transform: scale(1.015) rotate(-0.3deg);
border-color: #c08a8a;
box-shadow: 0 10px 26px rgba(139, 115, 85, 0.16);
@media (max-width: 1200px) {
position: static;
}
}
.stat-icon {
transform: rotate(-1deg) scale(1.04);
}
.stat-arrow {
opacity: 1;
transform: translateX(0);
}
.rail-title {
font-size: 13px;
font-weight: 600;
color: #7a664e;
letter-spacing: 0.5px;
margin-bottom: 2px;
}
.rail-metric {
position: relative;
padding: 16px 0;
border-top: 1px solid #f3ece0;
&:first-child {
border-top: none;
padding-top: 10px;
}
&.clickable {
cursor: pointer;
}
}
.stat-icon {
flex-shrink: 0;
width: 54px;
height: 54px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
/* 底色与图标色由 .theme-* 提供:浅底 + 同色深图标(水彩感),
不再是一格一色的高饱和实心块 */
background: rgba(154, 92, 53, 0.22);
transition: transform 0.5s ease-in-out;
i {
font-size: 26px;
color: #9a5c35;
}
}
.stat-info {
min-width: 0;
.stat-value {
font-size: 24px;
font-weight: 700;
color: #4f4130;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.stat-label {
margin-top: 4px;
font-size: 13px;
color: #7a664e;
.stat-note-icon {
margin-left: 4px;
color: #7a664e;
cursor: help;
&.clickable:hover {
.rail-metric-value {
color: #9a5c35;
}
.rail-arrow {
opacity: 1;
transform: translateX(0);
}
}
}
.stat-arrow {
.rail-metric-label {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
color: #7a664e;
.rail-note {
cursor: help;
}
}
.rail-metric-value {
margin-top: 6px;
font-size: 28px;
font-weight: 700;
line-height: 1.15;
letter-spacing: -0.4px;
color: #4f4130;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.rail-arrow {
position: absolute;
right: 16px;
right: 0;
top: 50%;
margin-top: -7px;
font-size: 14px;
@@ -590,11 +646,74 @@ export default {
transition: all 0.25s ease;
}
/* 统计卡图标配色
11 个卡片只用 5 个田园色(陶土 / 草绿 / 苔绿 / 雏菊金 / 玫瑰粉),循环使用。
浅底 + 同色深图标。
tint 从 .14~.18 提到 .22~.26:原来太淡,小图标几乎"看不见"(用户反馈对比度不够)。
类名(blue/orange/...)只是 card.theme 的旧键名,沿用不改,免得动模板。 */
/* ========== 左轨待办区 ========== */
.rail-todo {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 12px;
padding-top: 16px;
border-top: 1px solid #f3ece0;
}
.rail-todo-item {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
padding: 10px 12px;
border-radius: 10px;
background: #f6efe3;
border: 1px solid rgba(212, 160, 160, 0.40);
transition:
background-color 0.25s ease,
border-color 0.25s ease;
&.clickable {
cursor: pointer;
}
&.clickable:hover {
background: #fbf5ea;
border-color: #c08a8a;
}
/* 有积压时用雏菊金描边 + 数值变色提醒。
刻意不给 background:留空才不会和 hover 的底色打架(同权重,靠顺序定胜负太脆) */
&.alert {
border-color: rgba(168, 121, 31, 0.32);
.rail-todo-value {
color: #7d5512;
}
}
}
.rail-todo-label {
font-size: 12px;
color: #7a664e;
}
.rail-todo-value {
font-size: 18px;
font-weight: 700;
line-height: 1.2;
color: #4f4130;
}
/* ========== 右侧主区 ========== */
.main-col {
display: flex;
flex-direction: column;
gap: 20px;
min-width: 0;
}
/* ========== 统计卡图标配色 ==========
浅底 + 同色深图标(水彩感)。五个田园色循环使用:
陶土 / 草绿 / 苔绿 / 雏菊金 / 玫瑰粉。
tint 用 .22~.26:太淡小图标会"看不见"。
类名(blue/orange/...)沿用旧键名不改,免得动逻辑。 */
.theme-blue,
.theme-indigo {
background: rgba(154, 92, 53, 0.22) !important;
@@ -627,14 +746,13 @@ export default {
i { color: #8f3f50 !important; }
}
/* ========== 区块卡片(RTP / 图表) ========== */
/* ========== 区块卡片(RTP / 图表 / 经营数据) ========== */
.section-card {
background: #faf6f0;
border-radius: 16px;
padding: 20px 24px;
box-shadow: 0 6px 18px rgba(139, 115, 85, 0.11);
border: 1px solid rgba(212, 160, 160, 0.55);
margin-bottom: 20px;
}
.section-header {
@@ -675,21 +793,13 @@ export default {
}
}
/* ========== RTP 卡片 ========== */
/* ========== RTP 卡片 ==========
右侧主区宽度随屏幕浮动(有左轨占位),用 auto-fit 自适应
—— 宽屏一行 5 张,窄屏自动折行,不再写死 5 列。 */
.rtp-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 16px;
@media (max-width: 1400px) {
grid-template-columns: repeat(3, 1fr);
}
@media (max-width: 900px) {
grid-template-columns: repeat(2, 1fr);
}
@media (max-width: 600px) {
grid-template-columns: 1fr;
}
}
.rtp-card {
@@ -774,9 +884,7 @@ export default {
/* RTP 主题色(只给圆点 + 进度条上色)
★ 与上方折线图的颜色一一对应:同一游戏在哪儿都是同一个色,
这样「颜色」本身就带信息,不用每次重新认。
(原来是品红 #ec4899 / 青 #14b8a6 / 紫 #8b5cf6 / 翠绿 #10b981 / 琥珀 #f59e0b,
属于规范禁止的高饱和荧光色,已换掉。) */
这样「颜色」本身就带信息,不用每次重新认。 */
.rtp-theme-0 { /* 疯狂转盘 */
.rtp-dot { background: #7a6a9c; }
.rtp-progress-inner { background: linear-gradient(90deg, #a89cc0, #7a6a9c); }
@@ -798,6 +906,73 @@ export default {
.rtp-progress-inner { background: linear-gradient(90deg, #c69bb8, #a35790); }
}
/* ========== 经营数据(次级指标带) ========== */
.mini-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.mini-stat {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
border-radius: 12px;
background: #f6efe3;
border: 1px solid rgba(212, 160, 160, 0.40);
transition:
background-color 0.5s ease-in-out,
border-color 0.5s ease-in-out;
&:hover {
background: #fbf5ea;
border-color: #c08a8a;
}
}
.mini-icon {
flex-shrink: 0;
width: 34px;
height: 34px;
border-radius: 11px;
display: flex;
align-items: center;
justify-content: center;
i {
font-size: 17px;
}
}
.mini-info {
min-width: 0;
}
.mini-label {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #7a664e;
white-space: nowrap;
.mini-note {
cursor: help;
}
}
.mini-value {
margin-top: 2px;
font-size: 17px;
font-weight: 700;
line-height: 1.25;
color: #4f4130;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ========== 图表切换标签 ========== */
.chart-tabs {
display: flex;
+1 -1
View File
@@ -2,7 +2,7 @@
<div class="login">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
<h3 class="title">河南项目</h3>
<h3 class="title">凯科佰娱</h3>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
+1 -1
View File
@@ -1,7 +1,7 @@
<template>
<div class="register">
<el-form ref="registerForm" :model="registerForm" :rules="registerRules" class="register-form">
<h3 class="title">河南项目</h3>
<h3 class="title">凯科佰娱</h3>
<el-form-item prop="username">
<el-input v-model="registerForm.username" type="text" auto-complete="off" placeholder="账号">
<svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" />
+1 -1
View File
@@ -7,7 +7,7 @@ function resolve(dir) {
const CompressionPlugin = require('compression-webpack-plugin')
const name = process.env.VUE_APP_TITLE || '河南项目' // 网页标题
const name = process.env.VUE_APP_TITLE || '凯科佰娱' // 网页标题
const port = process.env.port || process.env.npm_config_port || 80 // 端口