Files
Portal/pages/index.vue
T

959 lines
27 KiB
Vue

<template>
<view class="page-container">
<!-- Top Header Area -->
<view class="top-header">
<image
class="header-bg"
src="/static/images/login_bg.png"
mode="aspectFill"
></image>
<view class="header-overlay"></view>
<view class="status-bar"></view>
<view class="header-content">
<!-- User Info Row -->
<view class="user-bar">
<view class="brand-side">
<view class="brand-logo-shell">
<image
class="brand-logo"
src="/static/logo.png"
mode="aspectFit"
></image>
</view>
</view>
<view class="header-right">
<view class="user-summary" @click="goPage('/pages/user/index')">
<text class="username">{{ userInfo.username || $lang.guest }}</text>
<view class="balance-pill">
<text class="currency-symbol"></text>
<text class="balance-text">{{
$fk(userInfo.money) || "0.00"
}}</text>
</view>
</view>
<view class="right-actions">
<view class="action-btn" @click="goService">
<u-icon name="server-fill" color="#f6e7bf" size="20"></u-icon>
</view>
<view class="action-btn" @click="goPage('/pages/user/index')">
<u-icon name="grid-fill" color="#f6e7bf" size="20"></u-icon>
</view>
</view>
</view>
</view>
<!-- Banner Area -->
<view class="banner-area">
<u-swiper
:list="bannerList"
height="150"
radius="14"
indicator
indicatorMode="line"
circular
></u-swiper>
</view>
</view>
</view>
<!-- Main Content Layout -->
<view class="main-body">
<!-- Left Sidebar -->
<scroll-view scroll-y class="sidebar">
<view
class="sidebar-item"
v-for="(cat, index) in categories"
:key="index"
:class="{ active: currentCategory === cat.id }"
@click="switchCategory(cat.id)"
>
<view class="item-marker"></view>
<view class="icon-box">
<image :src="cat.icon" mode="widthFix" style="width: 22px; height: 22px;" v-if="cat.icon" />
<u-icon
:name="cat.uIcon"
:color="currentCategory === cat.id ? '#f1d392' : '#d1c6aa'"
size="24"
v-else
></u-icon>
</view>
<view class="cat-texts">
<text class="cat-name">{{ $lang[cat.nameKey] }}</text>
<text class="cat-count">{{ getCategoryCount(cat.id) }}</text>
</view>
</view>
<!-- Online Count Bottom -->
<view class="online-count-box">
<text class="label">{{ $lang.onlineCount }}</text>
<text class="count">{{ onlineNum }}</text>
</view>
</scroll-view>
<!-- Right Content Area -->
<scroll-view scroll-y class="content-area">
<view class="table-list">
<view class="loading-state" v-if="loading">
<u-loading-icon color="#f4c46f" vertical :text="$lang.loading"></u-loading-icon>
</view>
<view
class="game-card"
v-for="(item, index) in filteredTables"
:key="index"
@click="enterGame(item)"
>
<!-- Card Header -->
<view class="card-header">
<view class="header-left">
<text class="table-name">{{ item.table_name }}</text>
</view>
<view class="header-right">
<text class="limit-info">{{ $lang.maxLimit }}:{{ $fk((item.game_id == 4 || item.game_id == 5) && userInfo.limit_high_nn > 0 ? userInfo.limit_high_nn : (userInfo.limit_high || 0)) }} {{ $lang.minLimit }}:{{ (item.game_id == 4 || item.game_id == 5) && userInfo.limit_low_nn > 0 ? userInfo.limit_low_nn : (userInfo.limit_low || 0) }}</text>
<view class="status-live">
<view class="status-dot" :class="{ 'is-live': item.status===1 }"></view>
</view>
</view>
</view>
<!-- Roadmap Visualization (Body) -->
<view class="card-body">
<view class="card-body-overlay"></view>
<!-- 百家乐/龙虎: 珠盘路圆圈 -->
<view class="roadmap-grid" v-if="item.game_id !== 4 && item.game_id !== 5">
<view
v-for="(cell, cIdx) in item.beadRoad"
:key="cIdx"
class="road-cell"
:style="{
left: ((cell.show_x - 1) * cellWidth) + 'px',
top: ((cell.show_y - 1) * cellHeight) + 'px'
}"
:class="getCellClass(cell.result)"
>
<text class="cell-text">{{ getResultText(cell.result, item.game_id) }}</text>
</view>
</view>
<!-- 牛牛/三卡: 表格路单 -->
<view class="nn-roadmap" v-else>
<view class="nn-row" v-for="row in 4" :key="row">
<view class="nn-label" :class="row === 1 ? 'nn-label-banker' : 'nn-label-player'">
{{ ['', '庄', '闲1', '闲2', '闲3'][row] }}
</view>
<scroll-view scroll-x class="nn-cells">
<view class="nn-cells-inner">
<view
v-for="(cell, cIdx) in getNNRowData(item.beadRoad, row)"
:key="cIdx"
class="nn-cell"
:class="{ 'nn-win': cell.is_win == 1, 'nn-win-banker': cell.is_win == 1 && row === 1 }"
>
<text class="nn-text" :class="cell.type == 1 ? 'nn-text-red' : 'nn-text-blue'">{{ getNNResultText(cell.result) }}</text>
<text class="nn-win-tag" v-if="cell.is_win == 1">WIN</text>
</view>
</view>
</scroll-view>
</view>
</view>
<!-- Center Overlay Status -->
<view class="center-overlay" v-if="true">
<view class="status-badge-lg" :class="getStatusClass(item.bet_status)">
<u-icon name="hourglass" size="20" color="#fff" v-if="item.bet_status == 1"></u-icon>
<u-icon name="lock" size="20" color="#fff" v-else-if="item.bet_status == 2"></u-icon>
<u-icon name="play-circle" size="20" color="#fff" v-else></u-icon>
<text class="status-text-lg">
{{ getStatusText(item.bet_status) }}
</text>
<text class="countdown-text" v-if="item.countdown > 0">{{ item.countdown }}s</text>
</view>
</view>
</view>
</view>
<view style="height: 60px;"></view>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
import { mapState } from "vuex";
export default {
data() {
return {
userInfo: {},
bannerList: [
'/static/images/new_banner_1.jpg',
'/static/images/new_banner_2.jpg',
],
currentCategory: 0, // 0 = All or default category
categories: [
// Mapping based on common IDs: 1=Baccarat, 2=DT, 4=Bull, 5=ThreeCard
{ id: 1, nameKey: 'baccarat', uIcon: 'chrome-circle-fill', gameIds: [1] },
{ id: 2, nameKey: 'longhu', uIcon: 'heart-fill', gameIds: [2] },
{ id: 4, nameKey: 'niuniu', uIcon: 'thumb-up-fill', gameIds: [4] },
],
allTables: [],
loading: false,
timer: null,
onlineNum: 0,
// Roadmap config
cellWidth: 16.5, // slightly adjusted for mobile fit
cellHeight: 17,
};
},
computed: {
...mapState({
storeUserInfo: (state) => state.app.userInfo,
language: (state) => state.app.language,
$lang: (state) => state.app.lang[state.app.language],
}),
filteredTables() {
if (!this.allTables.length) return [];
const currentCatParams = this.categories.find(c => c.id === this.currentCategory);
if (!currentCatParams) return this.allTables;
return this.allTables.filter(t => currentCatParams.gameIds.includes(t.game_id));
}
},
onShow() {
// Sync userInfo
this.userInfo = this.storeUserInfo || {};
this.startPolling();
},
onHide() {
this.stopPolling();
},
methods: {
goPage(url) {
uni.navigateTo({ url });
},
goService() {
// 跳转到客服页面
uni.navigateTo({
url: '/pages/user/customservice'
});
},
switchCategory(id) {
this.currentCategory = id;
},
getCategoryCount(id) {
if (!this.allTables) return 0;
const cat = this.categories.find(c => c.id === id);
if (!cat) return 0;
return this.allTables.filter(t => cat.gameIds.includes(t.game_id)).length;
},
startPolling() {
this.loadData();
this.timer = setInterval(() => {
this.loadData();
}, 3000);
},
stopPolling() {
if(this.timer) clearInterval(this.timer);
},
async loadData() {
if(!this.userInfo.id) return;
try {
const res = await this.$api.getAllTable({
user_id: this.userInfo.id,
api_token: this.userInfo.api_token
});
if (res.Success && res.Data) {
this.processData(res.Data);
this.updateOnlineNum();
}
} catch(e) {
console.error(e);
}
},
updateOnlineNum() {
const hour = new Date().getHours();
const base = 800;
const extra = (hour >= 0 && hour < 6) ? 200 : 0;
this.onlineNum = base + extra + Math.floor(Math.random() * 100);
},
processData(data) {
// Transform data for UI
this.allTables = data.map(item => {
// Extract Bead Road (showRoad usually)
// Note: In helper.php, waybill() returns { showRoad, bigRoad ... }
let beadRoad = [];
if (item.game_id == 4 || item.game_id == 5) {
// 牛牛/三卡: ludan.waybill 直接就是数组,只取最后7列
if (item.ludan && Array.isArray(item.ludan.waybill)) {
let all = item.ludan.waybill;
let maxX = Math.max(...all.map(c => c.show_x), 0);
let keepCols = 7;
let cutX = Math.max(maxX - keepCols, 0);
beadRoad = all.filter(c => c.show_x > cutX).map(c => ({...c, show_x: c.show_x - cutX}));
}
} else if (item.ludan && item.ludan.waybill && item.ludan.waybill.showRoad) {
beadRoad = item.ludan.waybill.showRoad;
}
// Adjust roadmap to keep latest on screen (simple truncation logic for demo)
// Real logic might need scrolling or windowing
// For card display, we usually show last X columns
return {
...item,
beadRoad: beadRoad,
// Mock countdown if not provided
countdown: item.countdown || 15
};
});
},
// Visual Helpers
getCellClass(result) {
// 1=Banker(Red), 2=Player(Blue), 3=Tie(Green)
if (result === 1) return 'cell-red';
if (result === 2) return 'cell-blue';
if (result === 3) return 'cell-green';
return '';
},
getResultText(result, gameId) {
if (gameId == 2) {
if (result === 1) return '龙';
if (result === 2) return '虎';
if (result === 3) return '和';
}
if (result === 1) return '庄';
if (result === 2) return '闲';
if (result === 3) return '和';
return '';
},
getNNRowData(beadRoad, row) {
return beadRoad.filter(cell => cell.show_y === row);
},
getNNResultText(result) {
if (result == 0) return '无牛';
if (result >= 1 && result <= 9) return '牛' + result;
if (result == 10) return '牛牛';
if (result == 11) return '五公';
if (result == 12) return '豹子';
if (result == 13) return '同花顺';
return '牛' + result;
},
getStatusClass(status) {
// 1: betting, 2: dealing/disabled
if (status === 1) return 'status-betting';
return 'status-dealing';
},
getStatusText(status) {
if (status === 1) return this.$lang.betting;
if (status === 2) return this.$lang.dealing;
return this.$lang.preparing;
},
enterGame(item) {
// 跳转到同域名下的 /game 子目录
const portalUrl = window.location.origin + '/#/pages/index';
const url = `/game/?token=${this.userInfo.api_token}&language=${this.language}&from=portal&returnUrl=${encodeURIComponent(portalUrl)}#/play?id=${item.id}`;
window.location.href = url;
}
}
};
</script>
<style lang="scss" scoped>
/* Global Layout */
.page-container {
display: flex;
flex-direction: column;
height: 100vh;
background: linear-gradient(180deg, #060505 0%, #0d0b09 100%);
color: #fff6e1;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
/* Header */
.top-header {
flex-shrink: 0;
position: relative;
padding-bottom: 14px;
overflow: hidden;
.header-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
opacity: 0.42;
}
.header-overlay {
position: absolute;
inset: 0;
z-index: 0;
background: linear-gradient(
180deg,
rgba(5, 4, 4, 0.86) 0%,
rgba(11, 9, 8, 0.66) 54%,
rgba(13, 11, 9, 0.96) 100%
);
}
.status-bar {
height: var(--status-bar-height);
width: 100%;
position: relative;
z-index: 1;
}
.header-content {
position: relative;
z-index: 1;
padding: 8px 12px 0;
}
.user-bar {
display: flex;
align-items: center;
gap: 14px;
padding: 6px 4px 14px;
.brand-side {
flex-shrink: 0;
}
.brand-logo-shell {
width: 58px;
height: 58px;
display: flex;
align-items: center;
justify-content: center;
}
.brand-logo {
width: 44px;
height: 44px;
display: block;
}
.header-right {
min-width: 0;
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.user-summary {
min-width: 0;
flex: 1;
display: flex;
flex-direction: column;
gap: 7px;
}
.username {
max-width: 100%;
font-size: 16px;
font-weight: 700;
color: #fbf3e0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.balance-pill {
width: fit-content;
max-width: 100%;
min-height: 30px;
padding: 0 12px;
display: inline-flex;
align-items: center;
gap: 6px;
border-radius: 999px;
background: rgba(10, 9, 8, 0.72);
border: 1px solid rgba(223, 189, 120, 0.24);
box-shadow: inset 0 1px 0 rgba(255, 243, 219, 0.04);
.currency-symbol {
color: #f1cc84;
font-size: 13px;
font-weight: 700;
}
.balance-text {
color: #f6e1b2;
font-size: 12px;
font-weight: 700;
}
}
.right-actions {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
.action-btn {
width: 42px;
height: 42px;
border-radius: 15px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(12, 11, 10, 0.72);
border: 1px solid rgba(223, 189, 120, 0.16);
box-shadow: inset 0 1px 0 rgba(255, 244, 220, 0.04);
}
}
}
.banner-area {
position: relative;
z-index: 1;
padding: 0 2px;
::v-deep .u-swiper {
border-radius: 18px !important;
overflow: hidden;
border: 1px solid rgba(223, 189, 120, 0.14);
box-shadow: 0 16px 34px rgba(0, 0, 0, 0.24);
}
::v-deep .u-swiper__wrapper__item__wrapper {
border-radius: 18px !important;
}
::v-deep .u-swiper-indicator {
bottom: 10px !important;
}
::v-deep .u-swiper-indicator__wrapper__item {
background-color: rgba(255, 244, 220, 0.28) !important;
}
::v-deep .u-swiper-indicator__wrapper__item--active {
background-color: #dfbd78 !important;
}
}
}
/* Body Layout */
.main-body {
flex: 1;
display: flex;
overflow: hidden;
gap: 10px;
padding: 0 12px 12px;
/* Sidebar styles */
.sidebar {
width: 86px;
background: linear-gradient(
180deg,
rgba(18, 15, 13, 0.98) 0%,
rgba(10, 9, 8, 0.98) 100%
);
border: 1px solid rgba(223, 189, 120, 0.14);
border-radius: 22px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.18);
.sidebar-item {
position: relative;
margin: 6px;
padding: 14px 6px 12px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 18px;
transition: all 0.25s ease;
.item-marker {
position: absolute;
left: 50%;
top: 0;
width: 20px;
height: 3px;
border-radius: 999px;
transform: translateX(-50%);
background: transparent;
}
&.active {
background: linear-gradient(
180deg,
rgba(59, 43, 19, 0.24) 0%,
rgba(27, 20, 12, 0.08) 100%
);
box-shadow: inset 0 1px 0 rgba(255, 239, 203, 0.06);
.item-marker {
background: linear-gradient(90deg, #f2ddb3 0%, #dbaf57 100%);
}
.icon-box {
background: rgba(223, 189, 120, 0.08);
border-color: rgba(223, 189, 120, 0.18);
}
.cat-name,
.cat-count {
color: #f1d392;
}
}
.icon-box {
width: 40px;
height: 40px;
margin-bottom: 8px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(14, 13, 12, 0.8);
border: 1px solid rgba(255, 255, 255, 0.03);
}
.cat-texts {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
.cat-name {
font-size: 11px;
color: #cdc2a3;
text-align: center;
word-break: break-word;
line-height: 1.25;
max-width: 70px;
}
.cat-count {
min-width: 32px;
height: 22px;
padding: 0 9px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 700;
line-height: 1;
color: #e3c27a;
font-variant-numeric: tabular-nums;
letter-spacing: 0.2px;
background: linear-gradient(
180deg,
rgba(29, 24, 19, 0.96) 0%,
rgba(16, 13, 11, 0.96) 100%
);
border: 1px solid rgba(223, 189, 120, 0.12);
box-shadow: inset 0 1px 0 rgba(255, 245, 223, 0.04);
border-radius: 999px;
}
}
.online-count-box {
margin: auto 6px 6px;
padding: 14px 6px calc(14px + env(safe-area-inset-bottom));
border-top: 1px solid rgba(223, 189, 120, 0.1);
text-align: center;
.label {
font-size: 10px;
color: #8d7a55;
display: block;
margin-bottom: 4px;
}
.count {
margin-top: 6px;
font-size: 15px;
color: #f1cc84;
font-weight: 700;
font-variant-numeric: tabular-nums;
letter-spacing: 0.3px;
}
}
}
/* Content styles */
.content-area {
flex: 1;
background: rgba(8, 7, 6, 0.52);
border: 1px solid rgba(223, 189, 120, 0.1);
border-radius: 22px;
padding: 12px;
box-shadow: inset 0 1px 0 rgba(255, 244, 220, 0.03);
.table-list {
min-height: 100%;
}
.loading-state {
min-height: 180px;
display: flex;
align-items: center;
justify-content: center;
}
.game-card {
margin-bottom: 14px;
background: linear-gradient(
180deg,
rgba(21, 18, 15, 0.98) 0%,
rgba(14, 12, 10, 0.98) 100%
);
border: 1px solid rgba(223, 189, 120, 0.14);
border-radius: 18px;
overflow: hidden;
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.16);
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
min-height: 44px;
background: linear-gradient(
180deg,
rgba(30, 25, 20, 0.88) 0%,
rgba(19, 17, 14, 0.88) 100%
);
border-bottom: 1px solid rgba(223, 189, 120, 0.1);
.table-name {
font-size: 14px;
color: #f9f1de;
font-weight: 700;
}
.header-right {
display: flex;
align-items: center;
gap: 8px;
}
.limit-info {
font-size: 10px;
color: #bca987;
text-align: right;
line-height: 1.35;
}
.status-live {
width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 999px;
background: rgba(255, 255, 255, 0.03);
}
.status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background-color: #6f6658;
&.is-live {
background-color: #53c57b;
box-shadow: 0 0 10px rgba(83, 197, 123, 0.28);
}
}
}
.card-body {
position: relative;
height: 114px;
background: linear-gradient(180deg, #fbf9f3 0%, #f0ece3 100%);
}
.card-body-overlay {
position: absolute;
inset: 0;
pointer-events: none;
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.02) 0%,
rgba(0, 0, 0, 0.02) 100%
);
}
.roadmap-grid {
width: 100%;
height: 100%;
position: relative;
background-size: 16.5px 17px;
background-image: linear-gradient(to right, #dad3c7 1px, transparent 1px),
linear-gradient(to bottom, #dad3c7 1px, transparent 1px);
.road-cell {
position: absolute;
width: 14px;
height: 14px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid rgba(0, 0, 0, 0.08);
background-color: #fff;
&.cell-red {
border: 2px solid #cf4444;
color: #cf4444;
}
&.cell-blue {
border: 2px solid #467ed7;
color: #467ed7;
}
&.cell-green {
border: 2px solid #4ba96f;
color: #4ba96f;
}
.cell-text {
font-size: 9px;
font-weight: 700;
}
}
}
.nn-roadmap {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background: linear-gradient(180deg, #fbf9f3 0%, #f0ece3 100%);
.nn-row {
display: flex;
flex: 1;
border-bottom: 1px solid #ddd4c8;
&:last-child {
border-bottom: none;
}
}
.nn-label {
width: 30px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 700;
color: #fff;
flex-shrink: 0;
&.nn-label-banker {
background: #a84237;
}
&.nn-label-player {
background: #3c6399;
}
}
.nn-cells {
flex: 1;
}
.nn-cells-inner {
display: flex;
height: 100%;
}
.nn-cell {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-right: 1px solid #ddd4c8;
padding: 1px 0;
}
.nn-text {
font-size: 10px;
font-weight: 700;
&.nn-text-red {
color: #a84237;
}
&.nn-text-blue {
color: #3c6399;
}
}
.nn-win-tag {
font-size: 7px;
color: #fff;
background: #3c6399;
padding: 0 3px;
border-radius: 2px;
line-height: 1.4;
margin-top: 1px;
}
.nn-cell.nn-win-banker .nn-win-tag {
background: #a84237;
}
}
.center-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
.status-badge-lg {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 110px;
padding: 10px 18px;
border-radius: 16px;
color: #fff;
background: linear-gradient(
180deg,
rgba(47, 39, 28, 0.92) 0%,
rgba(17, 14, 11, 0.92) 100%
);
border: 1px solid rgba(223, 189, 120, 0.18);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.18);
&.status-betting {
background: linear-gradient(
180deg,
rgba(88, 67, 33, 0.92) 0%,
rgba(24, 18, 11, 0.94) 100%
);
}
.status-text-lg {
font-size: 13px;
font-weight: 700;
margin-top: 4px;
}
.countdown-text {
font-size: 15px;
color: #fff5dc;
margin-top: 2px;
font-family: monospace;
font-weight: 700;
}
}
}
}
}
}
</style>