pxb 2 viikkoa sitten
vanhempi
sitoutus
ba2a6fbf9a

+ 5 - 1
src/components/Table/index.vue

@@ -372,7 +372,7 @@
       <el-table-column
         v-if="tableRowsActions && tableRowsActions.length"
         label="操作"
-        fixed="right"
+        :fixed="actionsFixed ? 'right' : false"
         align="center"
         :width="tableRowsActions.length>=3?actionsWidth||'200': actionsWidth?actionsWidth: tableRowsActions.length*80"
       >
@@ -539,6 +539,10 @@ export default {
       type: Number,
       default: null
     },
+    actionsFixed: {
+      type: Boolean,
+      default: true
+    },
     // 字段列表
     columns: {
       type: Array,

+ 6 - 0
src/router/index.js

@@ -81,6 +81,12 @@ export const constantRoutes = [
         name: "userList",
         meta: { title: "会员列表" },
       },
+      {
+        path: "demoList",
+        component: () => import("@/views/user/demoList/index"),
+        name: "demoList",
+        meta: { title: "试玩列表" },
+      },
       {
         path: "userLevel",
         component: () => import("@/views/user/userLevel/index"),

+ 1298 - 0
src/views/user/demoList/index.vue

@@ -0,0 +1,1298 @@
+<template>
+  <div class="new-app-container layout-vertical">
+    <YMTableSearch :loading="loading" :search-form="searchForm" @onValueChange="onValueChange" @submit="onSubmit">
+      <template #dateRange="{ form }">
+        <div style="margin-right: 20px">
+          <el-date-picker v-model="form.dateRange" type="daterange" align="right" unlink-panels range-separator="至"
+            start-placeholder="开始日期" end-placeholder="结束日期" value-format="yyyy-MM-dd HH:mm:ss"
+            :default-time="['00:00:00', '23:59:59']" :picker-options="pickerOptions" style="width: 260px">
+          </el-date-picker>
+        </div>
+      </template>
+    </YMTableSearch>
+
+    <div style="margin-bottom: 12px; padding-left: 16px">
+      <el-button type="primary" :disabled="selectedMemberIds.length === 0"
+        @click="handleBatchSetChannel">批量设置通道</el-button>
+    </div>
+
+    <YMTable ref="table" :table-key="1" :index="true" :selection="true" @selection-change="handleSelectionChange"
+      :table-options="tableOptions" :edit-form="editForm" :edit-config="editConfig" :columns="columns"
+      :rows-actions="rowsActions" :show-summary="false" :apis="apis" :actionsWidth="100" @headerClick="headerClick"
+      :custom-style="{ height: 'calc(100vh - 280px)' }">
+      <template #agent="scoped">
+        <div v-if="scoped.row.agent">
+          <div>
+            {{ scoped.row.agent.first_name || "--" }}
+          </div>
+          <div>
+            ID: {{ scoped.row.agent.member_id }}
+          </div>
+          <div>
+            邀请码: {{ scoped.row.agent.user_code }}
+          </div>
+        </div>
+        <span v-else style="color: #c0c4cc">--</span>
+      </template>
+
+      <!-- 注册来源 -->
+      <template #from="{ row }">
+        <span :style="{
+          color: Number(row.from) === 1 ? '#05A58F' : Number(row.from) === 2 ? '#F3061F' : '#606266'
+        }">
+          {{
+            Number(row.from) === 1 ? '真实用户' :
+              Number(row.from) === 2 ? '虚拟用户' :
+                Number(row.from) === -1 ? 'tg用户' :
+                  Number(row.from) === 0 ? '后台客服账号' : '--'
+          }}
+        </span>
+      </template>
+
+      <!-- 冻结金额 -->
+      <template #wallet="{ row }">
+        <span :style="{
+          color: (row.wallet && Number(row.wallet.frozen_balance) > 0) ? '#F3061F' : '#606266'
+        }">
+          {{ row.wallet ? Number(row.wallet.frozen_balance) : '--' }}
+        </span>
+      </template>
+
+      <!-- 余额宝金额 -->
+      <template #yuebao="{ row }">
+        <span :style="{
+          color: (row.wallet && Number(row.wallet.yuebao) > 0) ? '#05A58F' : '#606266'
+        }">
+          {{ row.wallet ? Number(row.wallet.yuebao) : '--' }}
+        </span>
+      </template>
+
+      <!-- 打码量 -->
+      <template #total_consume="{ row }">
+        <span :style="{
+          color: Number(row.total_consume) > 0 ? '#18C80A' : '#606266'
+        }">
+          {{ row.total_consume != null ? Number(row.total_consume) : '--' }}
+        </span>
+      </template>
+
+      <!-- 是否封号 -->
+      <template #is_banned="{ row }">
+        <span :style="{
+          color: Number(row.is_banned) === 0 ? '#18C80A' : '#F3061F',
+          fontWeight: 'bold'
+        }">
+          {{ Number(row.is_banned) === 0 ? '正常' : '封号' }}
+        </span>
+      </template>
+
+      <!-- 是否封号 -->
+      <template #is_banned="{ row }">
+        <span :style="{
+          color: Number(row.is_banned) === 0 ? '#18C80A' : '#F3061F'
+        }">
+          {{ Number(row.is_banned) === 0 ? '正常' : '封号' }}
+        </span>
+      </template>
+
+    </YMTable>
+
+    <!-- 用户详情弹窗 -->
+    <Dialog ref="dialogDetail" :dialog-actions="{
+      width: '800px',
+      title: '用户详情',
+      confirmButtom: false,
+    }">
+      <template slot="content">
+        <!-- 原有详情展示区 -->
+        <div style="display: flex; flex-wrap: wrap; gap: 12px; padding: 10px 0; max-height: 400px; overflow-y: auto;">
+          <div v-for="item in detailItems" :key="item.label"
+            style="flex: 0 0 calc(33.33% - 12px); background: #f5f7fa; border-radius: 6px; padding: 12px 16px; box-sizing: border-box; border: 1px solid #e4e7ed;">
+            <div style="font-size: 13px; color: #909399; margin-bottom: 4px;">{{ item.label }}</div>
+            <!-- 三方余额:显示数值 + 一键转出按钮 -->
+            <div v-if="item.label === '三方余额'" style="display: flex; align-items: center; gap: 8px;">
+              <span :style="{ color: item.color || '#303133', fontSize: '15px' }">{{ item.value }}</span>
+              <el-button type="primary" size="mini"
+                @click="sfBalanceTransferClick(detailData.member_id)">一键转出</el-button>
+            </div>
+            <!-- 其他字段 -->
+            <div v-else :style="{ color: item.color || '#303133', fontSize: '15px', wordBreak: 'break-all' }">
+              {{ item.value }}
+            </div>
+          </div>
+        </div>
+
+        <!-- ========== 新增:操作按钮区域 ========== -->
+        <div style="margin-top: 16px; text-align: center; border-top: 1px solid #e4e7ed; padding-top: 16px;">
+          <!-- 充值 -->
+          <el-button type="primary" size="small"
+            @click="rowsActions.find(a => a.key === 'upWallet').method(detailData)">
+            充值
+          </el-button>
+          <!-- 扣款 -->
+          <el-button type="danger" size="small" @click="rowsActions.find(a => a.key === 'unWallet').method(detailData)">
+            扣款
+          </el-button>
+          <!-- 提现地址 -->
+          <el-button type="info" size="small" @click="rowsActions.find(a => a.key === 'address').method(detailData)">
+            提现地址
+          </el-button>
+          <!-- 设置通道 -->
+          <el-button type="warning" size="small"
+            @click="rowsActions.find(a => a.key === 'setChannel').method(detailData)">
+            设置通道
+          </el-button>
+          <!-- 修改密码 -->
+          <el-button type="primary" size="small"
+            @click="rowsActions.find(a => a.key === 'modifyPassword').method(detailData)">
+            修改密码
+          </el-button>
+          <!-- 合并用户 -->
+          <el-button type="danger" size="small"
+            @click="rowsActions.find(a => a.key === 'secret_key').method(detailData)">
+            合并用户
+          </el-button>
+          <!-- 备注 -->
+          <el-button type="info" size="small" @click="rowsActions.find(a => a.key === 'note').method(detailData)">
+            备注
+          </el-button>
+          <!-- 用户日志 -->
+          <el-button type="primary" size="small" @click="rowsActions.find(a => a.key === 'log').method(detailData)">
+            用户日志
+          </el-button>
+          <!-- 封号(仅当未封号) -->
+          <el-button v-if="detailData.is_banned === 0" type="danger" size="small"
+            @click="rowsActions.find(a => a.key === 'banned').method(detailData)">
+            封号
+          </el-button>
+          <!-- 解封(仅当已封号) -->
+          <el-button v-if="detailData.is_banned === 1" type="success" size="small"
+            @click="rowsActions.find(a => a.key === 'jiefeng').method(detailData)">
+            解封
+          </el-button>
+        </div>
+      </template>
+    </Dialog>
+
+    <Dialog ref="sfLog" :dialog-actions="{
+      width: '800px',
+      title: '三方余额',
+      confirmButtom: false,
+    }">
+      <template slot="content">
+        <div>
+          <span style="padding-right: 10px">换算后总余额:{{ sfBalanceData.total_balance }}</span>
+          <span style="padding-right: 10px">原始游戏币总余额:{{ sfBalanceData.total_game_balance }}</span>
+          <span style="padding-right: 10px">当前换算比例:{{ sfBalanceData.rate }}</span>
+          <el-button type="primary" @click="sfBalanceTransferClick(sfBalanceData.member_id)">一键转出</el-button>
+          <!-- sfBalanceTransfer -->
+        </div>
+        <el-table :data="sfBalanceData.list" border stripe height="400px" v-loading="logLoading">
+          <el-table-column prop="platform" label="平台名称" align="center" />
+          <el-table-column prop="balance" label="平台余额" align="center" />
+        </el-table>
+      </template>
+    </Dialog>
+
+    <Dialog ref="dialog" :dialog-actions="{ width: '480px', title: '扣款金额' }" :loading="loading" @confirm="confirm">
+      <template slot="content">
+        <el-form ref="formDataref" :rules="rulesData" :model="formData" label-width="80px">
+          <el-form-item label="扣款金额 " prop="amount">
+            <el-input type="number" v-model="formData.amount" style="width: 300px" />
+          </el-form-item>
+          <el-form-item label="说明" prop="remark">
+            <el-input v-model="formData.remark" style="width: 300px" />
+          </el-form-item>
+        </el-form>
+      </template>
+    </Dialog>
+
+    <Dialog ref="dialog1" :dialog-actions="{ width: '480px', title: '充值金额' }" :loading="loading" @confirm="confirm">
+      <template slot="content">
+        <el-form ref="formDataref" :rules="rulesData" :model="formData" label-width="80px">
+          <el-form-item label="充值类型" prop="change_type">
+            <el-select v-model="formData.change_type" placeholder="请选择" style="width: 300px">
+              <el-option v-for="item in listTypes" :key="item" :label="item" :value="item">
+              </el-option>
+            </el-select>
+          </el-form-item>
+          <el-form-item label="充值金额" prop="amount">
+            <el-input type="number" v-model="formData.amount" style="width: 300px" />
+          </el-form-item>
+          <el-form-item label="说明" prop="remark">
+            <el-input v-model="formData.remark" style="width: 300px" />
+          </el-form-item>
+        </el-form>
+      </template>
+    </Dialog>
+
+    <Dialog ref="dialogAddress" :dialog-actions="{
+      width: '720px',
+      title: '提现地址',
+      confirmButtom: false,
+    }">
+      <template slot="content">
+        <div style="padding: 10px 0">
+          <div style="margin-bottom: 24px">
+            <h3 style="margin: 0 0 12px 0; color: #303133">
+              银行卡 / 支付账户
+            </h3>
+            <el-table :data="listDataBank" border stripe height="260" empty-text="暂无银行卡/支付账户">
+              <el-table-column prop="bank_name" label="银行/渠道" width="140" />
+              <el-table-column prop="account" label="户名" width="120" />
+              <el-table-column prop="card_no" label="卡号/账号" />
+              <el-table-column prop="alias" label="别名" />
+              <el-table-column prop="channel" label="渠道码" width="100" />
+            </el-table>
+          </div>
+
+          <div>
+            <h3 style="margin: 0 0 12px 0; color: #303133">数字货币地址</h3>
+            <el-table :data="listDataAddress" border stripe height="260" empty-text="暂无数字货币地址">
+              <el-table-column prop="address" label="提现地址" />
+              <el-table-column prop="alias" label="别名" width="180" />
+            </el-table>
+          </div>
+        </div>
+      </template>
+    </Dialog>
+
+    <Dialog ref="dialogLog" :dialog-actions="{
+      width: '800px',
+      title: '用户日志',
+      confirmButtom: false,
+    }">
+      <template slot="content">
+        <el-form :model="logFormData" inline label-width="80px">
+          <el-form-item label="日期范围">
+            <el-date-picker v-model="logFormData.dateRange" type="daterange" value-format="yyyy-MM-dd"
+              range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="getLogList">查询</el-button>
+          </el-form-item>
+        </el-form>
+        <el-table :data="logTableData" border stripe height="400px" v-loading="logLoading">
+          <el-table-column prop="login_ip" label="IP" align="center" />
+          <el-table-column prop="created_at" label="时间" align="center" />
+        </el-table>
+      </template>
+    </Dialog>
+
+    <Dialog ref="dialogChannel" :dialog-actions="{ width: '480px', title: '设置通道组合' }" :loading="loading"
+      @confirm="confirmChannel">
+      <template slot="content">
+        <el-form ref="channelFormRef" :rules="channelRules" :model="channelFormData" label-width="120px">
+          <el-form-item label="通道组合" prop="recharge_channel_group_id">
+            <el-select v-model="channelFormData.recharge_channel_group_id" placeholder="请选择通道组合" style="width: 280px">
+              <el-option v-for="item in channelGroupList" :key="item.id" :label="item.name"
+                :value="item.id"></el-option>
+            </el-select>
+          </el-form-item>
+        </el-form>
+      </template>
+    </Dialog>
+
+    <Dialog ref="dialogPassword" :dialog-actions="{ width: '480px', title: '修改密码' }" :loading="loading"
+      @confirm="confirmPassword">
+      <template slot="content">
+        <el-form ref="pwdFormRef" :rules="pwdRules" :model="pwdFormData" label-width="100px">
+          <el-form-item label="密码类型">
+            <el-radio-group v-model="pwdFormData.pwdType">
+              <el-radio label="login">登录密码</el-radio>
+              <el-radio label="payment">资金密码</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="新密码" prop="newPassword">
+            <el-input v-model="pwdFormData.newPassword" placeholder="请输入新密码" style="width: 280px" />
+          </el-form-item>
+        </el-form>
+      </template>
+    </Dialog>
+  </div>
+</template>
+
+<script>
+import Dialog from "@/components/Dialog/index.vue";
+import {
+  getChangeTypes,
+  menucheck,
+  sfBalance,
+  sfBalanceTransfer,
+  useraddress,
+  userbanned,
+  usermerge,
+  usersetNote,
+  walletdebiting,
+  withdraw,
+  userLoginLog,
+  walletAddress,
+  walletBankList,
+  setUserRechargeChannelGroup,
+  getRechargeChannelGroupList,
+  setUserPassword,
+} from "@/api/data";
+
+export default {
+  components: {
+    Dialog,
+  },
+  computed: {
+    searchForm() {
+      return [
+        {
+          label: "用户名称",
+          type: "input",
+          prop: "username",
+          clearable: false,
+        },
+        {
+          label: "用户昵称",
+          type: "input",
+          prop: "like_first_name",
+          clearable: false,
+        },
+        {
+          label: "会员ID",
+          type: "input",
+          prop: "member_id",
+          clearable: false,
+        },
+        {
+          label: "用户等级",
+          type: "input",
+          prop: "level",
+          clearable: false,
+        },
+        {
+          label: "用户邀请码",
+          labelWidth: "110px",
+          type: "input",
+          prop: "user_code",
+          clearable: false,
+        },
+        {
+          label: "邀请人邀请码",
+          labelWidth: "110px",
+          type: "input",
+          prop: "agent_user_code",
+          clearable: false,
+        },
+        {
+          label: "充值通道",
+          type: "select",
+          prop: "recharge_channel_group_id",
+          options: this.channelGroupList.map((group) => ({
+            label: group.name,
+            value: group.id,
+          })),
+        },
+        {
+          label: "注册来源",
+          type: "select",
+          prop: "from",
+          default: "2",
+          isHide: true,
+          options: [
+            { label: "tg用户", value: "-1" },
+            { label: "后台客服账号", value: "0" },
+            { label: "真实用户", value: "1" },
+            { label: "虚拟用户", value: "2" },
+          ],
+        },
+        {
+          label: "新增用户",
+          type: "slot",
+          prop: "dateRange",
+          slotName: "dateRange",
+        },
+        {
+          label: "开始时间",
+          prop: "start_time",
+          isHide: true,
+        },
+        {
+          label: "结束时间",
+          prop: "end_time",
+          isHide: true,
+        },
+        {
+          label: "IP",
+          type: "input",
+          prop: "register_ip",
+          clearable: false,
+        },
+      ];
+    },
+    detailItems() {
+      if (!this.detailData || !Object.keys(this.detailData).length) return [];
+      const row = this.detailData;
+      return [
+        { label: '会员ID', value: row.member_id },
+        { label: '邀请码', value: row.user_code },
+        { label: '会员昵称', value: row.first_name || '--' },
+        {
+          label: '注册来源',
+          value: this.getFromText(row.from),
+          color: Number(row.from) === 1 ? '#05A58F' : Number(row.from) === 2 ? '#F3061F' : '#606266'
+        },
+        { label: '飞机账号', value: row.username || '--' },
+        {
+          label: '上级邀请人',
+          value: row.agent ? `${row.agent.first_name} (ID:${row.agent.member_id}, 邀请码:${row.agent.user_code})` : '--'
+        },
+        { label: '会员等级', value: row.level?.level_name || '--' },
+        {
+          label: '钱包余额',
+          value: row.wallet ? Number(row.wallet.available_balance).toFixed(2) : '--',
+          color: '#409EFF'   // 蓝色
+        },
+        {
+          label: '三方余额',
+          value: row.third_game_total_balance != null ? Number(row.third_game_total_balance).toFixed(2) : '--',
+          color: '#409EFF'   // 蓝色
+        },
+        {
+          label: '冻结金额',
+          value: row.wallet ? Number(row.wallet.frozen_balance) : '--',
+          color: (row.wallet && Number(row.wallet.frozen_balance) > 0) ? '#F3061F' : '#606266'
+        },
+        {
+          label: '余额宝金额',
+          value: row.wallet ? row.wallet.yuebao : '--',
+          color: (row.wallet && Number(row.wallet.yuebao) > 0) ? '#05A58F' : '#606266'
+        },
+        {
+          label: '打码量',
+          value: row.total_consume != null ? Number(row.total_consume) : '--',
+          color: Number(row.total_consume) > 0 ? '#18C80A' : '#606266'
+        },
+        { label: '充值通道', value: this.getChannelName(row.recharge_channel_group_id) },
+        { label: '注册时间', value: row.created_at || '--' },
+        { label: '新增用户', value: this.getNewUserType(row.created_at) },
+        { label: '活跃时间', value: row.last_active_time || '--' },
+        {
+          label: '是否封号',
+          value: row.is_banned === 0 ? '正常' : '封号',
+          color: Number(row.is_banned) === 0 ? '#18C80A' : '#F3061F'
+        },
+        { label: '秘钥', value: row.secret_key || '--' },
+        { label: '访客ID', value: row.visitor_id || '--' },
+        { label: '备注', value: row.admin_note || '--' },
+        { label: '手机号', value: row.phone || '--' },
+        { label: 'IP', value: row.register_ip || '--' },
+      ];
+    }
+  },
+  data() {
+    return {
+      pwdFormData: {
+        member_id: "",
+        pwdType: "login",
+        newPassword: "",
+      },
+      sfBalanceData: [],
+      pwdRules: {
+        newPassword: [
+          { required: true, message: "请输入新密码", trigger: "blur" },
+        ],
+      },
+      pickerOptions: {
+        shortcuts: [
+          {
+            text: "今天",
+            onClick(picker) {
+              const start = new Date();
+              start.setHours(0, 0, 0, 0);
+              const end = new Date();
+              end.setHours(23, 59, 59, 999);
+              picker.$emit("pick", [start, end]);
+            },
+          },
+          {
+            text: "昨天",
+            onClick(picker) {
+              const start = new Date();
+              start.setTime(start.getTime() - 3600 * 1000 * 24 * 1);
+              start.setHours(0, 0, 0, 0);
+              const end = new Date();
+              end.setTime(end.getTime() - 3600 * 1000 * 24 * 1);
+              end.setHours(23, 59, 59, 999);
+              picker.$emit("pick", [start, end]);
+            },
+          },
+          {
+            text: "7天内",
+            onClick(picker) {
+              const start = new Date();
+              start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
+              start.setHours(0, 0, 0, 0);
+              const end = new Date();
+              end.setHours(23, 59, 59, 999);
+              picker.$emit("pick", [start, end]);
+            },
+          },
+        ],
+      },
+      selectedMemberIds: [],
+      channelGroupList: [],
+      channelFormData: {
+        member_ids: [],
+        recharge_channel_group_id: "",
+      },
+      channelRules: {
+        recharge_channel_group_id: [
+          { required: true, message: "请选择充值通道组合", trigger: "change" },
+        ],
+      },
+
+      listDataBank: [],
+      listDataAddress: [],
+      listTypes: [],
+      positive: true,
+      SHOW: false,
+      loading: false,
+      queryData: {
+        order: "",
+        by: "",
+      },
+      visibleBasicDrawer: false,
+      renderContent: "",
+      rowsActions: [
+        {
+          key: "view",
+          label: "查看",
+          method: (row) => this.showDetail(row)
+        },
+        {
+          key: "upWallet",
+          label: "充值",
+          // 隐藏该按钮(列表不显示,保留数组项供详情弹窗调用)
+          vIF: () => false,
+          method: (row) => {
+            this.positive = true;
+            this.formData.member_id = row.member_id;
+            this.$refs.dialog1.open();
+            getChangeTypes().then((res) => {
+              this.listTypes = res.data;
+            });
+          },
+        },
+        {
+          key: "unWallet",
+          label: "扣款",
+          // 隐藏该按钮(列表不显示,保留数组项供详情弹窗调用)
+          vIF: () => false,
+          method: (row) => {
+            this.positive = false;
+            this.formData.member_id = row.member_id;
+            this.$refs.dialog.open();
+          },
+        },
+        {
+          key: "address",
+          label: "提现地址",
+          // 隐藏该按钮(列表不显示,保留数组项供详情弹窗调用)
+          vIF: () => false,
+          method: (row) => this.showWithdrawalAddress(row),
+        },
+        {
+          key: "setChannel",
+          label: "设置通道",
+          // 隐藏该按钮(列表不显示,保留数组项供详情弹窗调用)
+          vIF: () => false,
+          method: (row) =>
+            this.openChannelDialog(
+              [row.member_id],
+              row.recharge_channel_group_id
+            ),
+        },
+        {
+          key: "modifyPassword",
+          label: "修改密码",
+          // 隐藏该按钮(列表不显示,保留数组项供详情弹窗调用)
+          vIF: () => false,
+          method: (row) => {
+            this.pwdFormData.member_id = row.member_id;
+            this.pwdFormData.pwdType = "login";
+            this.pwdFormData.newPassword = "";
+            this.$refs.dialogPassword.open();
+            this.$nextTick(() => {
+              this.$refs.pwdFormRef && this.$refs.pwdFormRef.clearValidate();
+            });
+          },
+        },
+        {
+          key: "secret_key",
+          label: "合并用户",
+          // 隐藏该按钮(列表不显示,保留数组项供详情弹窗调用)
+          vIF: () => false,
+          method: (row) => {
+            this.$prompt("请输入被合并的用户的秘钥", "提示", {
+              confirmButtonText: "确定",
+              cancelButtonText: "取消",
+              inputPattern: /^\S+$/,
+              inputErrorMessage: "请输入被合并的用户的秘钥",
+            }).then(({ value }) => {
+              usermerge({
+                secret_key: value,
+                member_id: row.member_id,
+              }).then((res) => {
+                this.$message({
+                  type: "success",
+                  message: "合并成功!",
+                });
+                this.onSubmit(this.queryData);
+              });
+            });
+          },
+        },
+        {
+          key: "note",
+          label: "备注",
+          // 隐藏该按钮(列表不显示,保留数组项供详情弹窗调用)
+          vIF: () => false,
+          method: (row) => {
+            this.$prompt("请输入备注", "备注", {
+              confirmButtonText: "确定",
+              cancelButtonText: "取消",
+              inputPattern: /^\S+$/,
+              inputValue: row.admin_note,
+              inputErrorMessage: "请输入备注",
+            }).then(({ value }) => {
+              usersetNote({
+                member_id: row.member_id,
+                admin_note: value,
+              }).then(() => {
+                this.onSubmit(this.queryData);
+                this.$message({
+                  type: "success",
+                  message: "操作成功",
+                });
+              });
+            });
+          },
+        },
+        {
+          key: "log",
+          label: "用户日志",
+          // 隐藏该按钮(列表不显示,保留数组项供详情弹窗调用)
+          vIF: () => false,
+          method: (row) => {
+            this.logFormData.user_id = row.member_id;
+            this.logFormData.dateRange = [];
+            this.logTableData = [];
+            this.$refs.dialogLog.open();
+            this.getLogList();
+          },
+        },
+        {
+          key: "banned",
+          label: "封号",
+          // 隐藏该按钮(列表不显示,保留数组项供详情弹窗调用)
+          vIF: () => false,
+          method: (row) => {
+            this.$confirm(
+              "此操作将修改用户是否封号,保存封号修改?",
+              "是否封号",
+              {
+                distinguishCancelAndClose: true,
+                confirmButtonText: "封号",
+                cancelButtonText: "取消",
+              }
+            ).then(() => {
+              userbanned({
+                member_id: row.member_id,
+                is_banned: 1,
+              }).then((res) => {
+                this.onSubmit(this.queryData);
+                this.$message({
+                  type: "warning",
+                  message: "保存修改为封号!",
+                });
+              });
+            });
+          },
+        },
+        {
+          key: "jiefeng",
+          label: "解封",
+          // 隐藏该按钮(列表不显示,保留数组项供详情弹窗调用)
+          vIF: () => false,
+          method: (row) => {
+            this.$confirm(
+              "此操作将修改用户是否解封,保存解封修改?",
+              "是否解封",
+              {
+                distinguishCancelAndClose: true,
+                confirmButtonText: "正常",
+                cancelButtonText: "取消",
+              }
+            ).then(() => {
+              userbanned({
+                member_id: row.member_id,
+                is_banned: 0,
+              }).then((res) => {
+                this.onSubmit(this.queryData);
+                this.$message({
+                  type: "success",
+                  message: "保存修改为正常!",
+                });
+              });
+            });
+          },
+        },
+      ],
+      apis: {
+        list: {
+          url: "user",
+          dataParam: "data.data",
+          resultParam: "data",
+          init: false,
+        },
+      },
+      tableOptions: {
+        add: {
+          show: false,
+          initData: () => {
+            return { test: "test" };
+          },
+        },
+      },
+      editForm: [
+        {
+          body: [],
+        },
+      ],
+      editConfig: {},
+      formData: {
+        amount: "",
+        member_id: "",
+        remark: "",
+        change_type: "",
+      },
+      rulesData: {
+        change_type: [{ required: true, message: "请输入", trigger: "blur" }],
+        amount: [{ required: true, message: "请输入", trigger: "blur" }],
+        remark: [{ required: true, message: "请输入", trigger: "blur" }],
+      },
+      logFormData: {
+        user_id: "",
+        dateRange: [],
+      },
+      logTableData: [],
+      logLoading: false,
+      columns: [
+        {
+          label: "会员ID",
+          prop: "member_id",
+        },
+        {
+          label: "邀请码",
+          prop: "user_code",
+        },
+        {
+          label: "会员昵称",
+          popover: true,
+          width: 140,
+          props: ["first_name"],
+          tagType: (row) => (row?.status === 1 ? "danger" : ""),
+          options: [
+            {
+              label: "会员ID",
+              value: ["member_id"],
+            },
+            {
+              label: "用户名称",
+              value: ["username"],
+            },
+            {
+              label: "备注",
+              vShow: (row) => (row?.admin_note ? true : false),
+              value: ["admin_note"],
+            },
+          ],
+        },
+        {
+          label: "注册来源",
+          prop: "from",
+          method: (row) => {
+            const map = {
+              "-1": "tg用户",
+              0: "后台客服账号",
+              1: "真实用户",
+              2: "虚拟用户",
+            };
+            return map[row.from] || "--";
+          },
+        },
+        {
+          label: "飞机账号",
+          prop: "username",
+        },
+        {
+          label: "上级邀请人",
+          prop: "agent",
+          width: 240,
+        },
+        {
+          label: "会员等级",
+          prop: "level",
+          method: (row) => row.level?.level_name || "--",
+        },
+        {
+          label: "钱包余额",
+          link: true,
+          // sortable: "custom",
+          method: (row) =>
+            row.wallet
+              ? `${Number(row.wallet.available_balance).toFixed(2)}`
+              : "--",
+          click: (row) => {
+            this.$router.push({
+              name: "balance",
+              query: {
+                member_id: row.member_id,
+              },
+            });
+          }
+        },
+        {
+          label: "三方余额",
+          link: true,
+          // sortable: "custom",
+          method: (row) =>
+            row.wallet
+              ? `${Number(row.third_game_total_balance).toFixed(2)}`
+              : "--",
+          click: (row) => {
+            this.$refs.sfLog.open();
+            this.getSfList(row.member_id);
+          },
+        },
+        {
+          label: "冻结金额",
+          prop: "wallet",
+          method: (row) =>
+            row.wallet ? Number(row.wallet.frozen_balance) : "--",
+        },
+        {
+          label: "余额宝金额",
+          prop: "yuebao",
+          method: (row) => (row.wallet ? row.wallet.yuebao : "--"),
+        },
+        {
+          label: "打码量",
+          prop: "total_consume",
+        },
+        {
+          label: "充值通道",
+          prop: "recharge_channel",
+          method: (row) => {
+            return (
+              this.channelGroupList.find(
+                (group) => group.id === row.recharge_channel_group_id
+              )?.name || "--"
+            );
+          },
+        },
+        {
+          label: "注册时间",
+          prop: "created_at",
+          // sortable: "custom",
+        },
+        {
+          label: "新增用户",
+          prop: "created_at",
+          method: (row) => {
+            if (!row.created_at) return "--";
+            const targetDate = new Date(row.created_at.replace(/-/g, "/"));
+            const targetTime = targetDate.getTime();
+
+            const now = new Date();
+            const todayStart = new Date(
+              now.getFullYear(),
+              now.getMonth(),
+              now.getDate()
+            ).getTime();
+            const yesterdayStart = todayStart - 24 * 60 * 60 * 1000;
+            const sevenDaysAgoStart = todayStart - 7 * 24 * 60 * 60 * 1000;
+
+            if (targetTime >= todayStart) {
+              return "今天";
+            } else if (
+              targetTime >= yesterdayStart &&
+              targetTime < todayStart
+            ) {
+              return "昨天";
+            } else if (
+              targetTime >= sevenDaysAgoStart &&
+              targetTime < yesterdayStart
+            ) {
+              return "7天内";
+            } else {
+              return "7天前";
+            }
+          },
+        },
+        {
+          label: "活跃时间",
+          // sortable: "custom",
+          prop: "last_active_time",
+        },
+        {
+          label: "是否封号",
+          prop: "is_banned",
+          method: (row) => (row.is_banned === 0 ? "正常" : "封号"),
+        },
+
+        {
+          label: "秘钥",
+          popover: true,
+          default: "秘钥",
+          width: 70,
+          options: [
+            {
+              label: "找回账号的秘钥",
+              value: ["secret_key"],
+            },
+          ],
+        },
+        {
+          label: "访客ID",
+          default: "访客ID",
+          width: 80,
+          popover: true,
+          options: [
+            {
+              label: "访客ID",
+              value: ["visitor_id"],
+            },
+          ],
+        },
+        {
+          label: "备注",
+          prop: "admin_note",
+        },
+        {
+          label: "手机号",
+          prop: "phone",
+        },
+        {
+          label: "IP",
+          prop: "register_ip",
+        },
+      ],
+      detailData: {},
+    };
+  },
+  mounted() {
+    this.onSubmit();
+    menucheck("admin/wallet/topUp").then(({ code }) => {
+      this.rowsActions[0].vIF = () => code === 0;
+    });
+
+    getRechargeChannelGroupList({ page: 1, limit: 1000 })
+      .then((res) => {
+        this.channelGroupList = res.data?.data || [];
+      })
+      .catch((err) => console.log("获取通道组合失败", err));
+  },
+  methods: {
+    getSfList(member_id) {
+      this.logLoading = true;
+      const params = {
+        member_id: member_id,
+      };
+      sfBalance(params)
+        .then((res) => {
+          this.sfBalanceData = res.data;
+          console.log(this.sfBalanceData);
+          this.logLoading = false;
+        })
+        .catch(() => {
+          this.logLoading = false;
+        });
+    },
+
+    // 转换注册来源文字
+    getFromText(from) {
+      const map = {
+        '-1': 'tg用户',
+        '0': '后台客服账号',
+        '1': '真实用户',
+        '2': '虚拟用户'
+      };
+      return map[from] || '--';
+    },
+
+    // 根据通道组合ID获取名称
+    getChannelName(id) {
+      const group = this.channelGroupList.find(g => g.id === id);
+      return group ? group.name : '--';
+    },
+
+    // 计算新增用户分类(与列表逻辑一致)
+    getNewUserType(created_at) {
+      if (!created_at) return '--';
+      const targetDate = new Date(created_at.replace(/-/g, '/'));
+      const targetTime = targetDate.getTime();
+      const now = new Date();
+      const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
+      const yesterdayStart = todayStart - 24 * 60 * 60 * 1000;
+      const sevenDaysAgoStart = todayStart - 7 * 24 * 60 * 60 * 1000;
+      if (targetTime >= todayStart) return '今天';
+      else if (targetTime >= yesterdayStart && targetTime < todayStart) return '昨天';
+      else if (targetTime >= sevenDaysAgoStart && targetTime < yesterdayStart) return '7天内';
+      else return '7天前';
+    },
+
+    // 打开详情弹窗
+    showDetail(row) {
+      this.detailData = { ...row };
+      this.$refs.dialogDetail.open();
+    },
+
+    sfBalanceTransferClick(member_id) {
+      this.logLoading = true;
+      const params = {
+        member_id: member_id,
+      };
+      sfBalanceTransfer(params)
+        .then((res) => {
+          this.logLoading = false;
+          this.$notify({
+            title: "成功",
+            message: "操作成功",
+            type: "success",
+          });
+          this.getSfList(member_id);
+          this.onSubmit(this.queryData);
+        })
+        .catch(() => {
+          this.logLoading = false;
+        });
+    },
+    confirmPassword() {
+      this.$refs.pwdFormRef.validate((valid) => {
+        if (valid) {
+          this.loading = true;
+          const payload = {
+            member_id: this.pwdFormData.member_id,
+          };
+          if (this.pwdFormData.pwdType === "login") {
+            payload.password = this.pwdFormData.newPassword;
+          } else {
+            payload.payment_password = this.pwdFormData.newPassword;
+          }
+
+          setUserPassword(payload)
+            .then((res) => {
+              this.loading = false;
+              if (res.code === 0) {
+                this.$notify({
+                  title: "成功",
+                  message: "修改成功",
+                  type: "success",
+                });
+                this.$refs.dialogPassword.close();
+              } else {
+                this.$message.error(res.msg || "修改失败");
+              }
+            })
+            .catch(() => {
+              this.loading = false;
+            });
+        }
+      });
+    },
+
+    handleSelectionChange(val) {
+      this.selectedMemberIds = val.map((item) => item.member_id);
+    },
+
+    handleBatchSetChannel() {
+      this.openChannelDialog(this.selectedMemberIds, "");
+    },
+
+    openChannelDialog(ids, groupId) {
+      this.channelFormData.member_ids = ids;
+      this.channelFormData.recharge_channel_group_id = groupId || "";
+      this.$refs.dialogChannel.open();
+
+      if (this.channelGroupList.length === 0) {
+        getRechargeChannelGroupList({ page: 1, limit: 1000 })
+          .then((res) => {
+            this.channelGroupList = res.data?.data || [];
+          })
+          .catch((err) => console.log("获取通道组合失败", err));
+      }
+    },
+
+    confirmChannel() {
+      this.$refs.channelFormRef.validate((valid) => {
+        if (valid) {
+          this.loading = true;
+          setUserRechargeChannelGroup({
+            member_id: this.channelFormData.member_ids,
+            recharge_channel_group_id:
+              this.channelFormData.recharge_channel_group_id,
+          })
+            .then((res) => {
+              this.loading = false;
+              if (res.code === 0) {
+                this.$notify({
+                  title: "成功",
+                  message: "设置成功",
+                  type: "success",
+                });
+                this.$refs.dialogChannel.close();
+                this.onSubmit(this.queryData);
+              } else {
+                this.$message.error(res.msg || "设置失败");
+              }
+            })
+            .catch(() => {
+              this.loading = false;
+            });
+        }
+      });
+    },
+
+    showWithdrawalAddress(row) {
+      this.listDataBank = [];
+      this.listDataAddress = [];
+
+      Promise.all([
+        walletBankList({ member_id: row.member_id }),
+        walletAddress({ member_id: row.member_id }),
+      ])
+        .then(([bankRes, addrRes]) => {
+          this.listDataBank = bankRes?.data?.list || [];
+          this.listDataAddress = addrRes?.data?.list || [];
+          this.$refs.dialogAddress.open();
+        })
+        .catch(() => {
+          this.$message.error("获取提现地址失败");
+        });
+    },
+
+    headerClick(column) {
+      const order =
+        column.order === "ascending"
+          ? "asc"
+          : column.order === "descending"
+            ? "desc"
+            : "";
+      let byValue = "";
+      if (column.label === "钱包余额") {
+        byValue = "available_balance";
+      }
+      if (column.label === "注册时间") {
+        byValue = "created_at";
+      }
+      if (column.label === "活跃时间") {
+        byValue = "last_active_time";
+      }
+      if (order) {
+        this.$set(this.queryData, "by", byValue);
+        this.$set(this.queryData, "order", order);
+        this.onSubmit(this.queryData);
+      } else {
+        this.$delete(this.queryData, "by");
+        this.$delete(this.queryData, "order");
+        this.onSubmit(this.queryData);
+      }
+    },
+
+    confirm() {
+      console.log("提交数据:", this.formData);
+      this.$refs.formDataref.validate((valid) => {
+        let fn = withdraw;
+        if (valid) {
+          this.loading = true;
+          if (!this.positive) {
+            fn = walletdebiting;
+          }
+          fn({
+            member_id: this.formData.member_id,
+            remark: this.formData.remark,
+            amount: this.formData.amount,
+            change_type: this.formData.change_type,
+          })
+            .then((res) => {
+              this.loading = false;
+              if (res.code === 0) {
+                this.$notify({
+                  title: "成功",
+                  message: "操作成功",
+                  type: "success",
+                });
+                this.$refs.dialog.close();
+                this.$refs.dialog1.close();
+                this.onSubmit(this.queryData);
+                this.$refs.formDataref.resetFields();
+              }
+            })
+            .catch((err) => {
+              this.loading = false;
+            });
+        } else {
+          console.log("error submit!!");
+          return false;
+        }
+      });
+    },
+
+    onValueChange(val) { },
+
+    onSubmit(params = {}) {
+      this.queryData = params;
+
+      const reqData = { ...params };
+
+      // 注册来源默认虚拟用户:仅当未通过表单指定(undefined)时兜底
+      if (reqData.from === undefined) {
+        reqData.from = "2";
+      }
+
+      if (reqData.dateRange && reqData.dateRange.length === 2) {
+        reqData.start_time = reqData.dateRange[0].split(" ")[0];
+        reqData.end_time = reqData.dateRange[1].split(" ")[0];
+      } else if (
+        reqData.dateRange === null ||
+        reqData.dateRange === undefined ||
+        reqData.dateRange.length === 0
+      ) {
+        reqData.start_time = "";
+        reqData.end_time = "";
+      }
+
+      delete reqData.dateRange;
+
+      this.$refs.table.getList(reqData);
+    },
+
+    getLogList() {
+      this.logLoading = true;
+      const params = {
+        user_id: this.logFormData.user_id,
+        start_time: this.logFormData.dateRange
+          ? this.logFormData.dateRange[0]
+          : "",
+        end_time: this.logFormData.dateRange
+          ? this.logFormData.dateRange[1]
+          : "",
+      };
+      userLoginLog(params)
+        .then((res) => {
+          this.logTableData = res.data?.data || [];
+          this.logLoading = false;
+        })
+        .catch(() => {
+          this.logLoading = false;
+        });
+    },
+  },
+};
+</script>

+ 66 - 14
src/views/user/userDiary/index.vue

@@ -1,20 +1,47 @@
-<template>
+<template>
   <div class="new-app-container layout-vertical">
     <YMTableSearch :loading="loading" :search-form="searchForm" @submit="onSubmit" />
     <YMTable ref="table" :table-key="5" :index="false" :selection="false" :columns="columns"
-      :apis="apis" :custom-style="{ height: 'calc(100vh - 200px)' }">
+      :rows-actions="rowsActions" :apis="apis" :custom-style="{ height: 'calc(100vh - 200px)' }"
+      :actions-width="100"
+      :actions-fixed="false">
     </YMTable>
+
+    <!-- 查看详情弹窗 -->
+    <Dialog ref="viewDialog" :dialog-actions="{
+      width: '820px',
+      title: '登录日志详情',
+      confirmButtom: false,
+    }">
+      <template slot="content">
+        <div style="display: flex; flex-wrap: wrap; gap: 12px; padding: 10px 0; max-height: 500px; overflow-y: auto;">
+          <div v-for="field in viewFields" :key="field.prop"
+            style="flex: 0 0 calc(33.33% - 12px); background: #f5f7fa; border-radius: 6px; padding: 12px 16px; box-sizing: border-box; border: 1px solid #e4e7ed;">
+            <div style="font-size: 13px; color: #909399; margin-bottom: 4px;">{{ field.label }}</div>
+            <div v-if="field.type === 'status'"
+              :style="{ color: Number(field.rawValue) === 1 ? '#18C80A' : '#909399', fontSize: '15px' }">
+              {{ field.value }}
+            </div>
+            <div v-else style="color: #303133; font-size: 15px; word-break: break-all;">{{ field.value }}</div>
+          </div>
+        </div>
+      </template>
+    </Dialog>
   </div>
 </template>
 
 <script>
-import { userLoginLog } from '@/api/data'
+import Dialog from '@/components/Dialog/index.vue'
 
 export default {
   name: 'UserDiary',
+  components: {
+    Dialog
+  },
   data() {
     return {
       loading: false,
+      viewRow: null,
       searchForm: [
         { label: '会员ID', type: 'input', prop: 'member_id', clearable: true },
         { label: '会员昵称', type: 'input', prop: 'first_name', clearable: true },
@@ -41,31 +68,56 @@ export default {
           method: (row) => row.login_ip || '--' },
         { label: '国家/地区', prop: 'country', width: 120, align: 'center',
           method: (row) => row.country || '--' },
-        // { label: '登入网站/包名称', prop: 'website', minWidth: 200, align: 'center',
-        //   method: (row) => row.website || '--' },
-        // { label: '平台', prop: 'platform', width: 100, align: 'center',
-        //   method: (row) => row.platform || '--' },
         { label: '浏览器核心', prop: 'browser', width: 120, align: 'center',
           method: (row) => row.browser || '--' },
         { label: '操作系统', prop: 'os', width: 120, align: 'center',
           method: (row) => row.os || '--' },
         { label: '状态', prop: 'status', width: 80, align: 'center',
-          method: (row) => row.status ? (Number(row.status) === 1 ? '在线' : '离线') : '--' },
+          html: true,
+          method: (row) => {
+            if (row.status == null) return '--'
+            const isOnline = Number(row.status) === 1
+            const color = isOnline ? '#18C80A' : '#909399'
+            const text = isOnline ? '在线' : '离线'
+            return `<span style="color: ${color}">${text}</span>`
+          } },
         { label: '在线时长(分)', prop: 'online_duration', width: 120, align: 'center',
           method: (row) => row.online_duration != null ? row.online_duration : '--' },
         { label: '登入时间', prop: 'created_at', width: 175, align: 'center' },
         { label: '登出时间', prop: 'logout_time', width: 175, align: 'center',
           method: (row) => row.logout_time ? row.logout_time : '--' },
       ]
+    },
+    rowsActions() {
+      return [
+        {
+          key: 'view',
+          label: '查看',
+          method: (row) => {
+            this.viewRow = { ...row }
+            this.$refs.viewDialog.open()
+          }
+        }
+      ]
+    },
+    viewFields() {
+      if (!this.viewRow) return []
+      const row = this.viewRow
+      return [
+        { label: '会员ID', prop: 'member_id', value: row.member_id ?? '--' },
+        { label: '会员昵称', prop: 'first_name', value: row.first_name || '--' },
+        { label: 'IP地址', prop: 'login_ip', value: row.login_ip || '--' },
+        { label: '国家/地区', prop: 'country', value: row.country || '--' },
+        { label: '浏览器核心', prop: 'browser', value: row.browser || '--' },
+        { label: '操作系统', prop: 'os', value: row.os || '--' },
+        { label: '状态', prop: 'status', value: row.status != null ? (Number(row.status) === 1 ? '在线' : '离线') : '--', type: 'status', rawValue: row.status },
+        { label: '在线时长(分)', prop: 'online_duration', value: row.online_duration != null ? row.online_duration : '--' },
+        { label: '登入时间', prop: 'created_at', value: row.created_at || '--' },
+        { label: '登出时间', prop: 'logout_time', value: row.logout_time || '--' }
+      ]
     }
   },
   methods: {
-    formatTime(ts) {
-      if (!ts) return '--'
-      const d = new Date(ts * 1000)
-      const pad = n => String(n).padStart(2, '0')
-      return d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds())
-    },
     onSubmit(params = {}) {
       const query = { ...params }
       if (params.dateRange && params.dateRange.length === 2) {

+ 28 - 3
src/views/user/userList/index.vue

@@ -18,7 +18,7 @@
 
     <YMTable ref="table" :table-key="1" :index="true" :selection="true" @selection-change="handleSelectionChange"
       :table-options="tableOptions" :edit-form="editForm" :edit-config="editConfig" :columns="columns"
-      :rows-actions="rowsActions" :show-summary="false" :apis="apis" :actionsWidth="300" @headerClick="headerClick"
+      :rows-actions="rowsActions" :show-summary="false" :apis="apis" :actionsWidth="100" @headerClick="headerClick"
       :custom-style="{ height: 'calc(100vh - 280px)' }">
       <template #agent="scoped">
         <div v-if="scoped.row.agent">
@@ -391,6 +391,7 @@ export default {
           label: "注册来源",
           type: "select",
           prop: "from",
+          default: "1",
           options: [
             { label: "tg用户", value: "-1" },
             { label: "后台客服账号", value: "0" },
@@ -565,6 +566,8 @@ export default {
         {
           key: "upWallet",
           label: "充值",
+          // 隐藏该按钮(列表不显示,保留数组项供详情弹窗调用)
+          vIF: () => false,
           method: (row) => {
             this.positive = true;
             this.formData.member_id = row.member_id;
@@ -577,6 +580,8 @@ export default {
         {
           key: "unWallet",
           label: "扣款",
+          // 隐藏该按钮(列表不显示,保留数组项供详情弹窗调用)
+          vIF: () => false,
           method: (row) => {
             this.positive = false;
             this.formData.member_id = row.member_id;
@@ -586,11 +591,15 @@ export default {
         {
           key: "address",
           label: "提现地址",
+          // 隐藏该按钮(列表不显示,保留数组项供详情弹窗调用)
+          vIF: () => false,
           method: (row) => this.showWithdrawalAddress(row),
         },
         {
           key: "setChannel",
           label: "设置通道",
+          // 隐藏该按钮(列表不显示,保留数组项供详情弹窗调用)
+          vIF: () => false,
           method: (row) =>
             this.openChannelDialog(
               [row.member_id],
@@ -600,6 +609,8 @@ export default {
         {
           key: "modifyPassword",
           label: "修改密码",
+          // 隐藏该按钮(列表不显示,保留数组项供详情弹窗调用)
+          vIF: () => false,
           method: (row) => {
             this.pwdFormData.member_id = row.member_id;
             this.pwdFormData.pwdType = "login";
@@ -613,6 +624,8 @@ export default {
         {
           key: "secret_key",
           label: "合并用户",
+          // 隐藏该按钮(列表不显示,保留数组项供详情弹窗调用)
+          vIF: () => false,
           method: (row) => {
             this.$prompt("请输入被合并的用户的秘钥", "提示", {
               confirmButtonText: "确定",
@@ -636,6 +649,8 @@ export default {
         {
           key: "note",
           label: "备注",
+          // 隐藏该按钮(列表不显示,保留数组项供详情弹窗调用)
+          vIF: () => false,
           method: (row) => {
             this.$prompt("请输入备注", "备注", {
               confirmButtonText: "确定",
@@ -660,6 +675,8 @@ export default {
         {
           key: "log",
           label: "用户日志",
+          // 隐藏该按钮(列表不显示,保留数组项供详情弹窗调用)
+          vIF: () => false,
           method: (row) => {
             this.logFormData.user_id = row.member_id;
             this.logFormData.dateRange = [];
@@ -671,7 +688,8 @@ export default {
         {
           key: "banned",
           label: "封号",
-          vIF: (row) => row.is_banned === 0,
+          // 隐藏该按钮(列表不显示,保留数组项供详情弹窗调用)
+          vIF: () => false,
           method: (row) => {
             this.$confirm(
               "此操作将修改用户是否封号,保存封号修改?",
@@ -698,7 +716,8 @@ export default {
         {
           key: "jiefeng",
           label: "解封",
-          vIF: (row) => row.is_banned === 1,
+          // 隐藏该按钮(列表不显示,保留数组项供详情弹窗调用)
+          vIF: () => false,
           method: (row) => {
             this.$confirm(
               "此操作将修改用户是否解封,保存解封修改?",
@@ -1231,6 +1250,12 @@ export default {
 
       const reqData = { ...params };
 
+      // 注册来源默认真实用户:仅当未通过表单指定(undefined)时兜底,
+      // 用户手动选择/清空后则尊重所选值
+      if (reqData.from === undefined) {
+        reqData.from = "1";
+      }
+
       if (reqData.dateRange && reqData.dateRange.length === 2) {
         reqData.start_time = reqData.dateRange[0].split(" ")[0];
         reqData.end_time = reqData.dateRange[1].split(" ")[0];

+ 212 - 16
src/views/user/userOnlineSummary/index.vue

@@ -10,12 +10,16 @@
       <template v-if="activeTab === 'online'">
         <YMTableSearch :loading="loading" :search-form="onlineSearchForm" @submit="onSubmit" />
         <div class="table-section">
-          <el-table v-loading="loading" :data="onlineFilteredData" border stripe
-            height="calc(100vh - 340px)" style="width: 100%; border-radius: 8px;"
+          <el-table v-loading="loading" :data="onlinePagedData"
+            height="calc(100vh - 340px)" style="width: 100%; border-radius: 12px; overflow: hidden;"
             empty-text="暂无在线用户数据">
             <el-table-column prop="member_id" label="会员ID" width="100" align="center" />
             <el-table-column prop="first_name" label="会员昵称" width="130" align="center" />
-            <el-table-column prop="balance" label="账户余额" width="120" align="center" />
+            <el-table-column prop="balance" label="账户余额" width="120" align="center">
+              <template slot-scope="{ row }">
+                <span style="color: #F56C6C; font-weight: bold;">{{ row.balance != null ? Number(row.balance).toFixed(2) : '--' }}</span>
+              </template>
+            </el-table-column>
             <el-table-column prop="ip" label="IP地址" width="150" align="center" />
             <el-table-column prop="location" label="位置" width="130" align="center" />
             <el-table-column prop="website" label="登录网站" min-width="180" align="center" />
@@ -23,7 +27,17 @@
             <el-table-column prop="browser" label="浏览器" width="120" align="center" />
             <el-table-column prop="os" label="操作系统" width="120" align="center" />
             <el-table-column prop="login_time" label="登入时间" width="175" align="center" :formatter="formatTime" />
+            <el-table-column label="操作" width="90" align="center">
+              <template slot-scope="{ row }">
+                <el-button type="text" class="ym-view-btn" @click="handleOnlineView(row)">查看</el-button>
+              </template>
+            </el-table-column>
           </el-table>
+          <el-pagination v-if="onlineFilteredData.length" background
+            layout="total, prev, pager, next, jumper" :page-size="onlinePageSize"
+            :page-sizes="[15, 30, 45, 60]" :current-page="onlinePage"
+            :total="onlineFilteredData.length"
+            @current-change="onlinePage = $event" @size-change="handleOnlineSizeChange" />
         </div>
       </template>
 
@@ -35,27 +49,114 @@
           <div class="info-line">2. 有效会员:下注金额&gt;=¥50的会员数</div>
         </div>
         <div class="table-section">
-          <el-table v-loading="loading" :data="newUserFilteredData" border stripe
-            height="calc(100vh - 440px)" style="width: 100%; border-radius: 8px;"
+          <el-table v-loading="loading" :data="newUserPagedData"
+            height="calc(100vh - 440px)" style="width: 100%; border-radius: 12px; overflow: hidden;"
             empty-text="暂无新用户汇总数据">
             <el-table-column prop="date" label="时间" width="150" align="center" />
-            <el-table-column prop="h5_reg" label="H5注册" width="100" align="center" />
-            <el-table-column prop="app_reg" label="APP注册" width="100" align="center" />
-            <el-table-column prop="h5_first_deposit" label="H5首存次数" width="110" align="center" />
-            <el-table-column prop="app_first_deposit" label="APP首存次数" width="110" align="center" />
-            <el-table-column prop="active_members" label="活跃会员" width="100" align="center" />
-            <el-table-column prop="total_deposit" label="存款总额" width="120" align="center" />
-            <el-table-column prop="total_bet" label="下注总数" width="120" align="center" />
-            <el-table-column prop="company_profit" label="公司盈利" width="120" align="center" />
-            <el-table-column prop="valid_members" label="有效会员" width="100" align="center" />
+            <el-table-column prop="h5_reg" label="H5注册" width="100" align="center">
+              <template slot-scope="{ row }">
+                <span style="color: #1677FF; font-weight: bold;">{{ row.h5_reg != null ? Number(row.h5_reg) : '--' }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="app_reg" label="APP注册" width="100" align="center">
+              <template slot-scope="{ row }">
+                <span style="color: #1677FF; font-weight: bold;">{{ row.app_reg != null ? Number(row.app_reg) : '--' }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="h5_first_deposit" label="H5首存次数" width="110" align="center">
+              <template slot-scope="{ row }">
+                <span style="color: #F5A623; font-weight: bold;">{{ row.h5_first_deposit != null ? Number(row.h5_first_deposit) : '--' }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="app_first_deposit" label="APP首存次数" width="110" align="center">
+              <template slot-scope="{ row }">
+                <span style="color: #F5A623; font-weight: bold;">{{ row.app_first_deposit != null ? Number(row.app_first_deposit) : '--' }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="active_members" label="活跃会员" width="100" align="center">
+              <template slot-scope="{ row }">
+                <span style="color: #18C80A; font-weight: bold;">{{ row.active_members != null ? Number(row.active_members) : '--' }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="valid_members" label="有效会员" width="100" align="center">
+              <template slot-scope="{ row }">
+                <span style="color: #18C80A; font-weight: bold;">{{ row.valid_members != null ? Number(row.valid_members) : '--' }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="total_deposit" label="存款总额" width="150" align="center">
+              <template slot-scope="{ row }">
+                <span style="color: #F56C6C; font-weight: bold;">{{ row.total_deposit != null ? Number(row.total_deposit).toLocaleString() : '--' }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="total_bet" label="下注总数" width="150" align="center">
+              <template slot-scope="{ row }">
+                <span style="color: #F56C6C; font-weight: bold;">{{ row.total_bet != null ? Number(row.total_bet).toLocaleString() : '--' }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="company_profit" label="公司盈利" min-width="150" align="center">
+              <template slot-scope="{ row }">
+                <span style="color: #F56C6C; font-weight: bold;">{{ row.company_profit != null ? Number(row.company_profit).toLocaleString() : '--' }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="操作" width="90" align="center">
+              <template slot-scope="{ row }">
+                <el-button type="text" size="small" class="ym-view-btn" @click="handleNewUserView(row)">查看</el-button>
+              </template>
+            </el-table-column>
           </el-table>
+          <el-pagination v-if="newUserFilteredData.length" background
+            layout="total, prev, pager, next, jumper" :page-size="newUserPageSize"
+            :page-sizes="[15, 30, 45, 60]" :current-page="newUserPage"
+            :total="newUserFilteredData.length"
+            @current-change="newUserPage = $event" @size-change="handleNewUserSizeChange" />
         </div>
       </template>
+
+      <!-- 在线用户查看详情弹窗 -->
+      <Dialog ref="onlineViewDialog" :dialog-actions="{
+        width: '820px',
+        title: '在线用户详情',
+        confirmButtom: false,
+      }">
+        <template slot="content">
+          <div style="display: flex; flex-wrap: wrap; gap: 12px; padding: 10px 0; max-height: 500px; overflow-y: auto;">
+            <div v-for="field in onlineViewFields" :key="field.prop"
+              style="flex: 0 0 calc(33.33% - 12px); background: #f5f7fa; border-radius: 6px; padding: 12px 16px; box-sizing: border-box; border: 1px solid #e4e7ed;">
+              <div style="font-size: 13px; color: #909399; margin-bottom: 4px;">{{ field.label }}</div>
+              <div v-if="field.type === 'balance'" style="color: #F56C6C; font-weight: bold; font-size: 15px;">{{ field.value }}</div>
+              <div v-else style="color: #303133; font-size: 15px; word-break: break-all;">{{ field.value }}</div>
+            </div>
+          </div>
+        </template>
+      </Dialog>
+
+      <!-- 新用户汇总查看详情弹窗 -->
+      <Dialog ref="newUserViewDialog" :dialog-actions="{
+        width: '820px',
+        title: '新用户汇总详情',
+        confirmButtom: false,
+      }">
+        <template slot="content">
+          <div style="display: flex; flex-wrap: wrap; gap: 12px; padding: 10px 0; max-height: 500px; overflow-y: auto;">
+            <div v-for="field in newUserViewFields" :key="field.prop"
+              style="flex: 0 0 calc(33.33% - 12px); background: #f5f7fa; border-radius: 6px; padding: 12px 16px; box-sizing: border-box; border: 1px solid #e4e7ed;">
+              <div style="font-size: 13px; color: #909399; margin-bottom: 4px;">{{ field.label }}</div>
+              <div v-if="field.type === 'amount'" style="color: #F56C6C; font-weight: bold; font-size: 15px;">{{ field.value }}</div>
+              <div v-else-if="field.type === 'blue'" style="color: #1677FF; font-weight: bold; font-size: 15px;">{{ field.value }}</div>
+              <div v-else-if="field.type === 'yellow'" style="color: #F5A623; font-weight: bold; font-size: 15px;">{{ field.value }}</div>
+              <div v-else-if="field.type === 'green'" style="color: #18C80A; font-weight: bold; font-size: 15px;">{{ field.value }}</div>
+              <div v-else style="color: #303133; font-size: 15px; word-break: break-all;">{{ field.value }}</div>
+            </div>
+          </div>
+        </template>
+      </Dialog>
     </div>
   </div>
 </template>
 
 <script>
+import Dialog from '@/components/Dialog/index.vue'
+
 const ONLINE_MOCK = [
   { member_id: 10001, first_name: "test01", balance: 1250.50, ip: "192.168.1.100", location: "上海", website: "www.example.com", platform: "PC", browser: "Chrome 120", os: "Windows 10", login_time: 1722499200 },
   { member_id: 10002, first_name: "test02", balance: 3200.00, ip: "192.168.1.101", location: "北京", website: "com.example.app", platform: "Android", browser: "WebView", os: "Android 14", login_time: 1722502800 },
@@ -80,10 +181,13 @@ const NEWUSER_MOCK = [
 ]
 
 export default {
-  name: "UserOnlineSummary",
+  name: 'UserOnlineSummary',
+  components: {
+    Dialog
+  },
   data() {
     return {
-      activeTab: "online",
+      activeTab: 'online',
       loading: false,
       onlineSearchForm: [
         { label: "会员ID", type: "input", prop: "member_id", clearable: true },
@@ -106,9 +210,73 @@ export default {
       newUserFilteredData: [],
       allOnlineData: [...ONLINE_MOCK],
       allNewUserData: [...NEWUSER_MOCK],
+      onlineViewRow: null,
+      newUserViewRow: null,
+      onlinePage: 1,
+      onlinePageSize: 15,
+      newUserPage: 1,
+      newUserPageSize: 15,
+    }
+  },
+  computed: {
+    onlinePagedData() {
+      const start = (this.onlinePage - 1) * this.onlinePageSize
+      return this.onlineFilteredData.slice(start, start + this.onlinePageSize)
+    },
+    newUserPagedData() {
+      const start = (this.newUserPage - 1) * this.newUserPageSize
+      return this.newUserFilteredData.slice(start, start + this.newUserPageSize)
+    },
+    onlineViewFields() {
+      if (!this.onlineViewRow) return []
+      const row = this.onlineViewRow
+      return [
+        { label: '会员ID', prop: 'member_id', value: row.member_id ?? '--' },
+        { label: '会员昵称', prop: 'first_name', value: row.first_name || '--' },
+        { label: '账户余额', prop: 'balance', value: row.balance != null ? Number(row.balance).toFixed(2) : '--', type: 'balance' },
+        { label: 'IP地址', prop: 'ip', value: row.ip || '--' },
+        { label: '位置', prop: 'location', value: row.location || '--' },
+        { label: '登录网站', prop: 'website', value: row.website || '--' },
+        { label: '平台', prop: 'platform', value: row.platform || '--' },
+        { label: '浏览器', prop: 'browser', value: row.browser || '--' },
+        { label: '操作系统', prop: 'os', value: row.os || '--' },
+        { label: '登入时间', prop: 'login_time', value: this.formatTime(row) }
+      ]
+    },
+    newUserViewFields() {
+      if (!this.newUserViewRow) return []
+      const row = this.newUserViewRow
+      return [
+        { label: '时间', prop: 'date', value: row.date || '--' },
+        { label: 'H5注册', prop: 'h5_reg', value: row.h5_reg != null ? Number(row.h5_reg) : '--', type: 'blue' },
+        { label: 'APP注册', prop: 'app_reg', value: row.app_reg != null ? Number(row.app_reg) : '--', type: 'blue' },
+        { label: 'H5首存次数', prop: 'h5_first_deposit', value: row.h5_first_deposit != null ? Number(row.h5_first_deposit) : '--', type: 'yellow' },
+        { label: 'APP首存次数', prop: 'app_first_deposit', value: row.app_first_deposit != null ? Number(row.app_first_deposit) : '--', type: 'yellow' },
+        { label: '活跃会员', prop: 'active_members', value: row.active_members != null ? Number(row.active_members) : '--', type: 'green' },
+        { label: '有效会员', prop: 'valid_members', value: row.valid_members != null ? Number(row.valid_members) : '--', type: 'green' },
+        { label: '存款总额', prop: 'total_deposit', value: row.total_deposit != null ? Number(row.total_deposit).toLocaleString() : '--', type: 'amount' },
+        { label: '下注总数', prop: 'total_bet', value: row.total_bet != null ? Number(row.total_bet).toLocaleString() : '--', type: 'amount' },
+        { label: '公司盈利', prop: 'company_profit', value: row.company_profit != null ? Number(row.company_profit).toLocaleString() : '--', type: 'amount' }
+      ]
     }
   },
   methods: {
+    handleOnlineSizeChange(val) {
+      this.onlinePageSize = val
+      this.onlinePage = 1
+    },
+    handleNewUserSizeChange(val) {
+      this.newUserPageSize = val
+      this.newUserPage = 1
+    },
+    handleOnlineView(row) {
+      this.onlineViewRow = { ...row }
+      this.$refs.onlineViewDialog.open()
+    },
+    handleNewUserView(row) {
+      this.newUserViewRow = { ...row }
+      this.$refs.newUserViewDialog.open()
+    },
     formatTime(row) {
       if (!row.login_time) return "--"
       const d = new Date(row.login_time * 1000)
@@ -136,6 +304,7 @@ export default {
       if (params.ip) filtered = filtered.filter(r => r.ip.includes(params.ip))
       if (params.platform) filtered = filtered.filter(r => r.platform === params.platform)
       this.onlineFilteredData = filtered
+      this.onlinePage = 1
     },
     filterNewUser(params) {
       let filtered = [...this.allNewUserData]
@@ -146,6 +315,7 @@ export default {
         filtered = filtered.filter(r => r.date <= params.end_date)
       }
       this.newUserFilteredData = filtered
+      this.newUserPage = 1
     }
   },
   mounted() {
@@ -188,6 +358,32 @@ export default {
 
 .table-section {
   padding: 18px 20px 20px;
+
+  /* 与会员登录日志 YMTable 列表样式一致 */
+  ::v-deep .el-table th {
+    background: #fff;
+  }
+
+  ::v-deep .el-table th .cell {
+    color: #606266;
+    font-weight: 500;
+  }
+
+  ::v-deep .el-table td .cell {
+    color: #606266;
+    font-size: 14px;
+  }
+
+  .el-pagination {
+    margin-top: 14px;
+    text-align: right;
+  }
+}
+
+/* 与会员列表 YMTable 操作列文字按钮样式一致 */
+.ym-view-btn {
+  padding: 8px 0;
+  font-size: 14px;
 }
 
 .info-lines {