feat(user): 重构后台会员列表与详情为全屏选项卡并补齐二开权益资产中心

这个提交包含在:
chen
2026-09-17 16:55:19 +08:00
父节点 c3f5b3d4b5
当前提交 289adf5c36
共有 7 个文件被更改,包括 1453 次插入 和 330 次删除

查看文件

@@ -3,12 +3,175 @@
namespace addons\shopro\filter\user;
use addons\shopro\filter\BaseFilter;
use addons\shopro\service\partner\PartnerConfig;
use think\db\Query;
use app\admin\model\shopro\commission\Agent as AgentModel;
use app\admin\model\shopro\user\User as UserModel;
/**
* 用户管理
* 用户管理 Filter 过滤器
*/
class UserFilter extends BaseFilter
{
protected $keywordFields = ['id', 'username', 'nickname', 'mobile', 'email'];
/**
* 身份筛选:品酒师 / 创客 / 酒道馆 / 区县代理 / 合伙股东
*/
public function identity($query, $data)
{
$value = $this->plainValue($data);
if (!$value) {
return $query;
}
$current = $query->getTable();
$agentTable = (new AgentModel())->getQuery()->getTable();
if ($value === 'taster') {
return $query->where(function (Query $q) use ($current, $agentTable) {
$q->whereExists(function ($sub) use ($current, $agentTable) {
$sub->table($agentTable)->where($agentTable . '.user_id=' . $current . '.id')->where('level', 1);
})->whereOrNotExists(function ($sub) use ($current, $agentTable) {
$sub->table($agentTable)->where($agentTable . '.user_id=' . $current . '.id');
});
});
}
if ($value === 'maker') {
return $query->whereExists(function ($sub) use ($current, $agentTable) {
$sub->table($agentTable)->where($agentTable . '.user_id=' . $current . '.id')->where('level', '>=', 2);
});
}
if ($value === 'venue') {
return $query->whereExists(function ($sub) use ($current, $agentTable) {
$sub->table($agentTable)->where($agentTable . '.user_id=' . $current . '.id')->where('is_venue', 1);
});
}
if ($value === 'county') {
return $query->whereExists(function ($sub) use ($current, $agentTable) {
$sub->table($agentTable)->where($agentTable . '.user_id=' . $current . '.id')->where('is_county', 1);
});
}
if ($value === 'partner') {
return $query->whereExists(function ($sub) use ($current, $agentTable) {
$sub->table($agentTable)->where($agentTable . '.user_id=' . $current . '.id')->where('is_partner', 1);
});
}
return $query;
}
/**
* 创客等级筛选:V1 ~ V6 (对应 level = 2 ~ 7)
*/
public function makerLevel($query, $data)
{
$value = $this->plainValue($data);
if ($value === '' || $value === null) {
return $query;
}
$level = intval($value);
if ($level < 1 || $level > 7) {
return $query;
}
// 如果传入 1~6(即 V1~V6),映射为系统 level = 2~7;若传入 2~7,直接使用
$targetLevel = ($level <= 6) ? ($level + 1) : $level;
$current = $query->getTable();
$agentTable = (new AgentModel())->getQuery()->getTable();
return $query->whereExists(function ($sub) use ($current, $agentTable, $targetLevel) {
$sub->table($agentTable)->where($agentTable . '.user_id=' . $current . '.id')->where('level', $targetLevel);
});
}
/**
* 30天自购活跃状态筛选
*/
public function active30d($query, $data)
{
$value = $this->plainValue($data);
if ($value === '' || $value === null) {
return $query;
}
$days = PartnerConfig::activeDays();
$since = time() - $days * 86400;
$current = $query->getTable();
$agentTable = (new AgentModel())->getQuery()->getTable();
if ($value === '1' || $value === 1 || $value === 'yes') {
return $query->whereExists(function ($sub) use ($current, $agentTable, $since) {
$sub->table($agentTable)->where($agentTable . '.user_id=' . $current . '.id')
->where('last_self_order_time', '>=', $since);
});
} else {
return $query->where(function (Query $q) use ($current, $agentTable, $since) {
$q->whereNotExists(function ($sub) use ($current, $agentTable) {
$sub->table($agentTable)->where($agentTable . '.user_id=' . $current . '.id');
})->whereOrExists(function ($sub) use ($current, $agentTable, $since) {
$sub->table($agentTable)->where($agentTable . '.user_id=' . $current . '.id')
->where(function (Query $sq) use ($since) {
$sq->whereNull('last_self_order_time')->whereOr('last_self_order_time', '<', $since);
});
});
});
}
}
/**
* 是否具备分销商/收益档案
*/
public function hasAgent($query, $data)
{
$value = $this->plainValue($data);
if ($value === '' || $value === null) {
return $query;
}
$current = $query->getTable();
$agentTable = (new AgentModel())->getQuery()->getTable();
if ($value === '1' || $value === 1 || $value === 'yes') {
return $query->whereExists(function ($sub) use ($current, $agentTable) {
$sub->table($agentTable)->where($agentTable . '.user_id=' . $current . '.id');
});
} else {
return $query->whereNotExists(function ($sub) use ($current, $agentTable) {
$sub->table($agentTable)->where($agentTable . '.user_id=' . $current . '.id');
});
}
}
/**
* 推荐人检索
*/
public function parentUser($query, $data)
{
$value = $this->plainValue($data);
if ($value === '' || $value === null) {
return $query;
}
if (is_numeric($value)) {
return $query->where('parent_user_id', intval($value));
}
$current = $query->getTable();
$userTable = (new UserModel())->getQuery()->getTable();
return $query->whereExists(function ($sub) use ($current, $userTable, $value) {
$sub->table($userTable)->where($userTable . '.id=' . $current . '.parent_user_id')
->where(function (Query $q) use ($value) {
$q->where('nickname', 'like', "%{$value}%")
->whereOr('mobile', 'like', "%{$value}%")
->whereOr('username', 'like', "%{$value}%");
});
});
}
protected function plainValue($data)
{
if (is_array($data)) {
return $data[0] ?? '';
}
return $data;
}
}

查看文件

@@ -12,7 +12,7 @@ class User extends Common
{
protected $model = null;
protected $noNeedRight = ['select'];
protected $noNeedRight = ['select', 'rewards', 'voucherPlans', 'team'];
public function _initialize()
{
@@ -29,7 +29,60 @@ class User extends Common
return $this->view->fetch();
}
$data = $this->model->sheepFilter()->paginate($this->request->param('list_rows', 10));
$paginator = $this->model->sheepFilter()
->with(['parent_user', 'agent.level_info'])
->paginate($this->request->param('list_rows', 10));
$rate = (new \addons\shopro\service\partner\RateService())->currentRate();
$days = \addons\shopro\service\partner\PartnerConfig::activeDays();
$now = time();
$list = $paginator->items();
foreach ($list as &$user) {
$agent = $user->agent;
$partner = \addons\shopro\service\partner\IdentityService::attachProfile($agent, $rate);
foreach ($partner as $k => $v) {
$user->$k = $v;
}
$user->has_agent = (bool) $agent;
$user->madow_value = $partner['madow_value'] ?? '0.00';
if ($agent) {
$user->is_venue = intval($agent->is_venue ?? 0);
$user->is_county = intval($agent->is_county ?? 0);
$user->is_partner = intval($agent->is_partner ?? 0);
$user->direct_order_seq = intval($agent->direct_order_seq ?? 0);
$user->voucher_total = \addons\shopro\service\partner\IdentityService::formatMoney($agent->voucher_total ?? 0);
$user->voucher_released = \addons\shopro\service\partner\IdentityService::formatMoney($agent->voucher_released ?? 0);
$user->voucher_used = \addons\shopro\service\partner\IdentityService::formatMoney($agent->voucher_used ?? 0);
$user->voucher_available = \addons\shopro\service\partner\IdentityService::formatMoney(max(0, ($agent->voucher_released ?? 0) - ($agent->voucher_used ?? 0)));
$user->last_self_order_time = intval($agent->last_self_order_time ?? 0);
$user->last_self_order_text = $user->last_self_order_time ? date('Y-m-d H:i:s', $user->last_self_order_time) : '从未下单';
} else {
$user->is_venue = 0;
$user->is_county = 0;
$user->is_partner = 0;
$user->direct_order_seq = 0;
$user->voucher_total = '0.00';
$user->voucher_released = '0.00';
$user->voucher_used = '0.00';
$user->voucher_available = '0.00';
$user->last_self_order_time = 0;
$user->last_self_order_text = '从未下单';
}
}
$stats = [
'total_users' => UserModel::count(),
'maker_count' => Db::name('shopro_commission_agent')->where('level', '>=', 2)->count(),
'venue_count' => Db::name('shopro_commission_agent')->where('is_venue', 1)->count(),
'county_count' => Db::name('shopro_commission_agent')->where('is_county', 1)->count(),
'partner_count' => Db::name('shopro_commission_agent')->where('is_partner', 1)->count(),
'active_count' => Db::name('shopro_commission_agent')->where('last_self_order_time', '>=', $now - $days * 86400)->count(),
];
$data = $paginator->toArray();
$data['stats'] = $stats;
$this->success('获取成功', null, $data);
}
@@ -54,9 +107,141 @@ class User extends Common
$data = array_merge($user->toArray(), $partner);
$data['has_agent'] = (bool) $agent;
// 推三返一循环信息
$push3Amounts = \addons\shopro\service\partner\PartnerConfig::push3Amounts();
$seq = intval($agent->direct_order_seq ?? 0);
$cycleCount = count($push3Amounts) > 0 ? count($push3Amounts) : 3;
$nextAmount = $push3Amounts[$seq % $cycleCount] ?? 398;
$data['direct_order_seq'] = $seq;
$data['push3_cycle_index'] = ($seq % $cycleCount) + 1;
$data['next_push3_amount'] = number_format((float) $nextAmount, 2, '.', '');
$data['next_push3_text'] = '第 ' . ($seq + 1) . ' 单将返 ¥' . $data['next_push3_amount'];
// 30天自购活跃状态与倒计时
$lastOrder = intval($agent->last_self_order_time ?? 0);
$activeDays = \addons\shopro\service\partner\PartnerConfig::activeDays();
$ago = $lastOrder ? intval(floor((time() - $lastOrder) / 86400)) : null;
$isAdvanced = !empty($data['is_advanced']);
$data['last_self_order_time'] = $lastOrder;
$data['last_self_order_text'] = $lastOrder ? date('Y-m-d H:i:s', $lastOrder) : '从未下单';
$data['active_days_ago'] = $ago;
$data['active_days_limit'] = $activeDays;
if (!$isAdvanced) {
$data['active_status_text'] = '免自购考核 (基础会员/品酒师)';
} else {
$data['active_status_text'] = !empty($data['active_30d'])
? '正常领取(近30天有自购)'
: ('已暂停领取' . ($ago !== null ? '(距今 ' . $ago . ' 天)' : '(从未下单)'));
}
// 累计现金收益统计
$totalCash = \app\admin\model\shopro\partner\Reward::where('user_id', $id)
->where('status', 1)
->where('amount', '>', 0)
->sum('amount');
$push3Cash = \app\admin\model\shopro\partner\Reward::where('user_id', $id)
->where('status', 1)
->where('amount', '>', 0)
->where('reward_type', 'push3')
->sum('amount');
$partnerExclusiveCash = \app\admin\model\shopro\partner\Reward::where('user_id', $id)
->where('status', 1)
->where('amount', '>', 0)
->where('reward_type', '<>', 'push3')
->sum('amount');
$data['income_total_cash'] = number_format((float) $totalCash, 2, '.', '');
$data['push3_cash_income'] = number_format((float) $push3Cash, 2, '.', '');
$data['partner_exclusive_cash'] = number_format((float) $partnerExclusiveCash, 2, '.', '');
// 兑换券计划资产统计
$voucherTotal = floatval($agent->voucher_total ?? 0);
$voucherReleased = floatval($agent->voucher_released ?? 0);
$voucherUsed = floatval($agent->voucher_used ?? 0);
$data['voucher_total'] = number_format($voucherTotal, 2, '.', '');
$data['voucher_released'] = number_format($voucherReleased, 2, '.', '');
$data['voucher_used'] = number_format($voucherUsed, 2, '.', '');
$data['voucher_available'] = number_format(max(0, $voucherReleased - $voucherUsed), 2, '.', '');
$data['voucher_plans_count'] = \app\admin\model\shopro\partner\VoucherPlan::where('user_id', $id)->count();
// 团队统计
$data['direct_users_count'] = UserModel::where('parent_user_id', $id)->count();
if ($agent) {
$data['agent_level'] = $agent->level;
$data['agent_level_info'] = $agent->level_info;
$data['is_venue'] = intval($agent->is_venue ?? 0);
$data['is_county'] = intval($agent->is_county ?? 0);
$data['is_partner'] = intval($agent->is_partner ?? 0);
$data['county_code'] = (string) ($agent->county_code ?? '');
$data['upgrade_lock'] = intval($agent->upgrade_lock ?? 0);
}
$this->success('获取成功', null, $data);
}
/**
* 详情动态:收益中心流水
*/
public function rewards($id)
{
$showSkip = $this->request->param('show_skip', 0);
$query = \app\admin\model\shopro\partner\Reward::sheepFilter()
->with(['buyer', 'order'])
->where('user_id', $id);
if (!$showSkip) {
$query->where(function ($q) {
$q->where('amount', '>', 0)->whereOr('madow', '>', 0);
});
}
$list = $query->order('id', 'desc')->paginate($this->request->param('list_rows', 10));
$this->success('获取成功', null, $list);
}
/**
* 详情动态:兑换券释放计划
*/
public function voucherPlans($id)
{
$list = \app\admin\model\shopro\partner\VoucherPlan::sheepFilter()
->with(['order'])
->where('user_id', $id)
->order('id', 'desc')
->paginate($this->request->param('list_rows', 10));
$agent = \app\admin\model\shopro\commission\Agent::where('user_id', $id)->field('voucher_used,voucher_released')->find();
$used = (float) ($agent['voucher_used'] ?? 0);
$released = (float) ($agent['voucher_released'] ?? 0);
$available = number_format(max(0, $released - $used), 2, '.', '');
foreach ($list as $row) {
$row->voucher_used = number_format($used, 2, '.', '');
$row->voucher_available = $available;
}
$this->success('获取成功', null, $list);
}
/**
* 详情动态:团队成员列表
*/
public function team($id)
{
$list = UserModel::with(['agent.level_info'])
->where('parent_user_id', $id)
->order('id', 'desc')
->paginate($this->request->param('list_rows', 10));
$rate = (new \addons\shopro\service\partner\RateService())->currentRate();
foreach ($list as $sub) {
$agent = $sub->agent;
$partner = \addons\shopro\service\partner\IdentityService::attachProfile($agent, $rate);
foreach ($partner as $k => $v) {
$sub->$k = $v;
}
$sub->has_agent = (bool) $agent;
}
$this->success('获取成功', null, $list);
}
/**
* 更新用户
*

查看文件

@@ -1,48 +1,125 @@
{include file="/shopro/common/script" /}
<style>
.user-detail .title {
height: 32px;
line-height: 32px;
font-size: 16px;
color: var(--sa-subtitle);
margin-bottom: 8px;
.user-detail {
background: #fff;
height: 100vh;
overflow: hidden;
}
.user-detail .bottom {
.user-detail .panel-block {
height: 100%;
}
.user-detail .detail-header-bar {
padding: 12px 16px;
margin-bottom: 12px;
background: #fff;
border-radius: 6px;
border: 1px solid var(--sa-border);
}
.user-detail .detail-title-text {
font-size: 16px;
font-weight: 600;
margin-left: 12px;
color: var(--sa-title);
}
.user-detail .user-detail-scrollbar>.el-scrollbar__wrap {
overflow-x: hidden;
}
.user-detail .user-detail-scrollbar>.el-scrollbar__bar.is-horizontal {
display: none;
}
.user-detail .card-title {
height: 32px;
line-height: 32px;
font-size: 15px;
font-weight: 600;
color: var(--sa-subtitle);
margin-bottom: 8px;
display: flex;
align-items: center;
justify-content: space-between;
}
.user-detail .card-section {
padding: 16px;
background: var(--sa-table-header-bg);
border-radius: 8px;
margin-bottom: 20px;
margin-bottom: 16px;
}
.user-detail .left {
height: 12px;
line-height: 12px;
height: 14px;
line-height: 14px;
font-size: 12px;
font-weight: 500;
color: var(--sa-subfont);
margin-bottom: 8px;
margin-bottom: 6px;
}
.user-detail .right {
line-height: 32px;
line-height: 28px;
font-size: 14px;
font-weight: 400;
color: var(--sa-subtitle);
}
.user-detail .info {
padding: 16px 16px 0;
padding: 16px 16px 4px;
background: var(--sa-background-assist);
border-radius: 4px;
border-radius: 6px;
}
.user-detail .info .el-col {
margin-bottom: 16px;
margin-bottom: 14px;
}
.user-detail .log .bottom {
.user-detail .asset-card-box {
background: #fff;
border: 1px solid var(--sa-border);
border-radius: 6px;
padding: 12px 14px;
margin-bottom: 12px;
}
.user-detail .asset-card-num {
font-size: 18px;
font-weight: 600;
color: var(--sa-title);
margin-top: 4px;
}
.user-detail .asset-card-label {
font-size: 12px;
color: var(--sa-subfont);
}
.user-detail .asset-card-desc {
font-size: 11px;
color: var(--sa-subfont);
margin-top: 3px;
}
.user-detail .active-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
}
.user-detail .active-dot.is-on {
background: #52c41a;
}
.user-detail .active-dot.is-off {
background: #c0c4cc;
}
.user-detail .log .card-section {
padding: 0;
background: var(--sa-background-assist);
}
@@ -58,266 +135,492 @@
}
.user-detail .third-oauth .provider-item {
margin-bottom: 20px;
margin-bottom: 16px;
}
.user-detail .third-oauth .provider-item:last-of-type {
margin-bottom: 0;
}
.user-detail .formula-box {
background: #f8fafc;
border: 1px solid var(--sa-border);
border-radius: 6px;
padding: 12px;
font-family: monospace;
font-size: 12px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
}
</style>
<div id="detail" class="user-detail" v-cloak>
<el-container class="panel-block">
<el-main>
<el-row :gutter="24">
<el-col class="sa-col-24" :xs="24" :sm="24" :md="6" :lg="6" :xl="6">
<div class="title">基本信息</div>
<div class="bottom">
<div class="sa-flex sa-row-between mb-3">
<div class="sa-flex sa-flex-1">
<sa-image :url="state.detail.avatar" size="64" radius="32"></sa-image>
<div class="ml-4 sa-flex-1">
<div class="nickname mb-1 sa-table-line-1">
{{ state.detail.nickname }}
<el-main style="padding: 12px 16px;">
<!-- 顶部导航条(用于选项卡或全屏页面操作) -->
<div class="detail-header-bar sa-flex sa-row-between">
<div class="sa-flex sa-col-center">
<el-button icon="Back" @click="onBack">返回</el-button>
<span class="detail-title-text">
会员详情 - {{ state.detail.nickname || state.detail.username || '会员' }} (#{{ state.detail.id }})
</span>
<el-tag :type="state.detail.status == 'normal' ? 'success' : 'danger'" size="small" class="ml-2">
{{ state.detail.status == 'normal' ? '正常' : '已禁用' }}
</el-tag>
<el-tag v-if="state.detail.identity_text" type="primary" size="small" class="ml-2">
{{ state.detail.identity_text }}
</el-tag>
</div>
<div class="sa-flex">
<el-button v-if="state.detail.has_agent" type="success" @click="onOpenAgentDetail(state.detail.id)">
收益中心分销商档案
</el-button>
{if $auth->check('shopro/user/user/recharge')}
<el-button type="warning" @click="onRecharge('money')">账户充值</el-button>
{/if}
<el-button class="sa-button-refresh ml-2" icon="RefreshRight" @click="getDetail"></el-button>
</div>
</div>
<!-- 可滚动核心区域 -->
<el-scrollbar class="user-detail-scrollbar" style="height: calc(100vh - 76px);">
<el-row :gutter="16">
<!-- 左列:基本信息与账号安全 -->
<el-col class="sa-col-24" :xs="24" :sm="24" :md="7" :lg="7" :xl="7">
<div class="card-title">基本信息</div>
<div class="card-section">
<div class="sa-flex sa-row-between mb-3">
<div class="sa-flex sa-flex-1">
<sa-image :url="state.detail.avatar" size="60" radius="30"></sa-image>
<div class="ml-3 sa-flex-1">
<div class="nickname mb-1 sa-table-line-1" style="font-weight: 600;">
{{ state.detail.nickname }}
</div>
<div class="id" style="color: var(--sa-subfont); font-size: 12px;">#{{ state.detail.id }}</div>
</div>
<div class="id">#{{ state.detail.id }}</div>
</div>
{if $auth->check('shopro/user/user/edit')}
<el-button type="primary" link @click="onSelectAvatar">更换头像</el-button>
{/if}
</div>
{if $auth->check('shopro/user/user/edit')}
<el-button type="primary" @click="onSelectAvatar">更换头像</el-button>
{/if}
</div>
<div class="info">
<el-row :gutter="24">
<el-col :xs="24" :sm="12" :md="24" :lg="24" :xl="24">
<div class="left">昵称</div>
<div class="right">
<el-input v-model="state.detail.nickname" />
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="24" :lg="24" :xl="24">
<div class="left">性别</div>
<div class="right">
<el-radio-group v-model="state.detail.gender">
<el-radio :label="0">女</el-radio>
<el-radio :label="1">男</el-radio>
</el-radio-group>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="24" :lg="24" :xl="24">
<div class="left">用户名</div>
<div class="right">
<el-input v-model="state.detail.username" placeholder="首位需为字母且不少五位">
<template #suffix>
<span v-html="statusStyle('username')"></span>
</template>
</el-input>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="24" :lg="24" :xl="24">
<div class="left">密码</div>
<div class="right">
<el-input v-model="state.detail.password" placeholder="不修改则留空">
<template #suffix>
<span v-html="statusStyle('password')"></span>
</template>
</el-input>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="24" :lg="24" :xl="24">
<div class="left">手机号</div>
<div class="right">
<el-input v-model="state.detail.mobile" type="number">
<template #suffix>
<span v-html="statusStyle('mobile')"></span>
</template>
</el-input>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="24" :lg="24" :xl="24">
<div class="left">电子邮箱</div>
<div class="right">
<el-input v-model="state.detail.email">
<template #suffix>
<span v-html="statusStyle('email')"></span>
</template>
</el-input>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="24" :lg="24" :xl="24">
<div class="left">状态</div>
<div class="right">
<el-radio-group v-model="state.detail.status">
<el-radio label="normal">正常</el-radio>
<el-radio label="disabled">禁用</el-radio>
</el-radio-group>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="24" :lg="24" :xl="24">
<div class="label"></div>
<div class="right sa-flex sa-row-right">
<el-button @click="getDetail">重置</el-button>
{if $auth->check('shopro/user/user/edit')}
<el-button type="primary" @click="onSave">保存</el-button>
{/if}
</div>
</el-col>
</el-row>
</div>
</div>
</el-col>
<el-col class="sa-col-24" :xs="24" :sm="24" :md="18" :lg="18" :xl="18">
<el-row :gutter="24">
<el-col class="sa-col-24" :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<div class="title">账户信息</div>
<div class="bottom">
<div class="info">
<el-row>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<div class="left">推荐人</div>
<div class="right sa-flex">
<sa-user-profile type="agent" :user="state.detail.parent_user"
:id="state.detail.parent_user_id"></sa-user-profile>
{if $is_pro}
<el-button class="ml-2" type="primary" link @click="onChangeParentUser">
更换</el-button>
{/if}
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<div class="left">佣金</div>
<div class="right">{{ state.detail.commission }}</div>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<div class="left">身份</div>
<div class="right">{{ state.detail.identity_text || '尚未生成分销身份' }}</div>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<div class="left">马豆</div>
<div class="right">{{ state.detail.madow || '0' }}</div>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<div class="left">可用兑换券</div>
<div class="right sa-flex">
{{ state.detail.voucher_available || '0.00' }}
<el-button v-if="state.detail.has_agent" class="ml-2" type="primary" link
@click="onOpenAgentDetail(state.detail.id)">
分销商详情
</el-button>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<div class="left">余额</div>
<div class="right sa-flex">
{{ state.detail.money }}
{if $auth->check('shopro/user/user/recharge')}
<el-button class="ml-2" type="primary" link
@click="onRecharge('money')">充值</el-button>
{/if}
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<div class="left">上次登陆时间</div>
<div class="right">{{ state.detail.logintime }}</div>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<div class="left">积分</div>
<div class="right sa-flex">
{{ state.detail.score }}
{if $auth->check('shopro/user/user/recharge')}
<el-button class="ml-2" type="primary" link
@click="onRecharge('score')">充值</el-button>
{/if}
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<div class="left">登录IP</div>
<div class="right">{{ state.detail.loginip }}</div>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<div class="left">总计消费</div>
<div class="right">
{{ state.detail.total_consume }}
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<div class="left">注册时间</div>
<div class="right">{{ state.detail.createtime }}</div>
</el-col>
</el-row>
</div>
</div>
</el-col>
<el-col v-if="state.detail.third_oauth?.length>0" class="third-oauth sa-col-24" :xs="24"
:sm="24" :md="12" :lg="12" :xl="12">
<div class="title">第三方账号</div>
<div class="bottom">
<el-row class="info">
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<div class="provider-item sa-flex sa-row-between"
v-for="item in state.detail.third_oauth" :key="item.id">
<div class="sa-flex">
<img class="provider-platform"
:src="`/assets/addons/shopro/img/user/${item.provider}/${item.platform}.png`" />
<span class="ml-2">
{{ platform[item.provider][item.platform] }}
</span>
</div>
<el-popover popper-class="sa-popper" placement="top-start" trigger="hover">
<div>
<div class="item sa-flex">
<div class="left">登录次数:</div>
<div class="right">{{ item.login_num }}</div>
</div>
<div class="item sa-flex">
<div class="left">Openid:</div>
<div class="right">{{ item.openid }}</div>
</div>
<div v-if="item.unionid" class="item sa-flex">
<div class="left">Unionid:</div>
<div class="right">{{ item.unionid }}</div>
</div>
<div class="item sa-flex">
<div class="left">更新时间:</div>
<div class="right">{{ item.updatetime }}</div>
</div>
</div>
<template #reference>
<div class="sa-flex">
<sa-image :url="item.avatar" size="32"></sa-image>
<span class="name ml-2">
{{ item.nickname }}
</span>
</div>
<div class="info">
<el-row :gutter="16">
<el-col :span="24">
<div class="left">昵称</div>
<div class="right">
<el-input v-model="state.detail.nickname" />
</div>
</el-col>
<el-col :span="24">
<div class="left">性别</div>
<div class="right">
<el-radio-group v-model="state.detail.gender">
<el-radio :label="0">女</el-radio>
<el-radio :label="1">男</el-radio>
</el-radio-group>
</div>
</el-col>
<el-col :span="24">
<div class="left">用户名</div>
<div class="right">
<el-input v-model="state.detail.username" placeholder="首位需为字母且不少五位">
<template #suffix>
<span v-html="statusStyle('username')"></span>
</template>
</el-popover>
</el-input>
</div>
</el-col>
<el-col :span="24">
<div class="left">密码</div>
<div class="right">
<el-input v-model="state.detail.password" placeholder="不修改则留空">
<template #suffix>
<span v-html="statusStyle('password')"></span>
</template>
</el-input>
</div>
</el-col>
<el-col :span="24">
<div class="left">手机号</div>
<div class="right">
<el-input v-model="state.detail.mobile" type="number">
<template #suffix>
<span v-html="statusStyle('mobile')"></span>
</template>
</el-input>
</div>
</el-col>
<el-col :span="24">
<div class="left">电子邮箱</div>
<div class="right">
<el-input v-model="state.detail.email">
<template #suffix>
<span v-html="statusStyle('email')"></span>
</template>
</el-input>
</div>
</el-col>
<el-col :span="24">
<div class="left">状态</div>
<div class="right">
<el-radio-group v-model="state.detail.status">
<el-radio label="normal">正常</el-radio>
<el-radio label="disabled">禁用</el-radio>
</el-radio-group>
</div>
</el-col>
<el-col :span="24">
<div class="right sa-flex sa-row-right">
<el-button @click="getDetail">重置</el-button>
{if $auth->check('shopro/user/user/edit')}
<el-button type="primary" @click="onSave">保存修改</el-button>
{/if}
</div>
</el-col>
</el-row>
</div>
</el-col>
<el-col class="log sa-col-24" :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<div class="title sa-flex"> 用户动态 </div>
</div>
<!-- 第三方账号绑定 -->
<div v-if="state.detail.third_oauth?.length > 0">
<div class="card-title">第三方账号绑定</div>
<div class="card-section third-oauth">
<div class="info">
<div class="provider-item sa-flex sa-row-between"
v-for="item in state.detail.third_oauth" :key="item.id">
<div class="sa-flex">
<img class="provider-platform"
:src="`/assets/addons/shopro/img/user/${item.provider}/${item.platform}.png`" />
<span class="ml-2">
{{ platform[item.provider]?.[item.platform] || item.platform }}
</span>
</div>
<el-popover popper-class="sa-popper" placement="top-start" trigger="hover">
<div>
<div>登录次数:{{ item.login_num }}</div>
<div>Openid:{{ item.openid }}</div>
<div v-if="item.unionid">Unionid:{{ item.unionid }}</div>
<div>更新时间:{{ item.updatetime }}</div>
</div>
<template #reference>
<div class="sa-flex">
<sa-image :url="item.avatar" size="28"></sa-image>
<span class="name ml-2">{{ item.nickname }}</span>
</div>
</template>
</el-popover>
</div>
</div>
</div>
</div>
</el-col>
<!-- 右列:二开身份权益、四大资产看板、全能动态流水 -->
<el-col class="sa-col-24" :xs="24" :sm="24" :md="17" :lg="17" :xl="17">
<!-- 看板 1:业务身份与二开权益体系 -->
<div class="card-title">
<span>业务身份与二开权益体系</span>
<span v-if="state.detail.has_agent" style="font-size: 12px; font-weight: normal; color: var(--sa-subfont);">
直推有效单数: {{ state.detail.direct_order_seq || 0 }} 单
</span>
</div>
<div class="card-section">
<div class="info">
<el-row :gutter="16">
<el-col :xs="24" :sm="12" :md="12">
<div class="left">推荐人 / 直属上级</div>
<div class="right sa-flex">
<sa-user-profile type="agent" :user="state.detail.parent_user"
:id="state.detail.parent_user_id"></sa-user-profile>
<el-button v-if="state.detail.parent_user_id" class="ml-2" type="primary" link
@click="onOpenUserDetail(state.detail.parent_user_id)">
查看上级
</el-button>
{if $is_pro}
<el-button class="ml-2" type="warning" link @click="onChangeParentUser">
更换
</el-button>
{/if}
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="12">
<div class="left">主业务身份</div>
<div class="right">
<b style="font-size: 15px;">{{ state.detail.identity_text || '普通注册会员' }}</b>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="12">
<div class="left">30天自购活跃窗口(风控硬性门槛)</div>
<div class="right sa-flex sa-col-center">
<i class="active-dot" :class="state.detail.active_30d ? 'is-on' : 'is-off'"></i>
<span>{{ state.detail.active_status_text }}</span>
</div>
<div class="asset-card-desc">
最近自购: {{ state.detail.last_self_order_text }}
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="12">
<div class="left">直推「推三返一」循环进度</div>
<div class="right">
<span style="color: #e6a23c; font-weight: 600;">
第 {{ state.detail.push3_cycle_index || 1 }} 档 (398/400/600循环)
</span>
<span class="ml-2" style="font-size: 12px; color: var(--sa-subfont);">
{{ state.detail.next_push3_text }}
</span>
</div>
<div class="asset-card-desc">
累计获得推三返一: ¥{{ state.detail.push3_cash_income || '0.00' }}
</div>
</el-col>
<el-col :span="24">
<div class="left">身份权益徽章清单</div>
<div class="right sa-flex sa-flex-wrap" style="gap: 8px;">
<el-tag v-if="state.detail.level == 1" type="info">
品酒师 (基础会员·享推三返一/GMV分红/1.5倍券)
</el-tag>
<el-tag v-if="state.detail.level >= 2" type="primary">
创客 V{{ state.detail.level - 1 }} (合伙骨干·享极差/平级10%/超越5%)
</el-tag>
<el-tag v-if="state.detail.is_venue" type="success">
酒道馆 (线下实体枢纽·享50元/单团队奖+100元均分池)
</el-tag>
<el-tag v-if="state.detail.is_county" type="warning">
区县代理 (区域领头人·享团队拆分奖+50元均分池)
</el-tag>
<el-tag v-if="state.detail.is_partner" type="danger">
合伙股东 (独立高阶合伙身份·享顶层红利)
</el-tag>
</div>
</el-col>
</el-row>
</div>
</div>
<!-- 看板 2:二开收益中心四大资产核心中枢 -->
<div class="card-title">二开核心资产中枢</div>
<div class="card-section">
<el-row :gutter="12">
<!-- 资产 1: 收益钱包 -->
<el-col :xs="24" :sm="12" :md="6">
<div class="asset-card-box">
<div class="asset-card-label">可提现收益 (钱包现金)</div>
<div class="asset-card-num" style="color: #409eff;">
¥{{ state.detail.commission || '0.00' }}
</div>
<div class="asset-card-desc">
累计总现金: ¥{{ state.detail.income_total_cash || '0.00' }}
</div>
<div class="asset-card-desc">
进阶加权奖: ¥{{ state.detail.partner_exclusive_cash || '0.00' }}
</div>
</div>
</el-col>
<!-- 资产 2: 分红马豆 -->
<el-col :xs="24" :sm="12" :md="6">
<div class="asset-card-box">
<div class="asset-card-label">分红马豆 (GMV 1:1 分红)</div>
<div class="asset-card-num" style="color: #67c23a;">
{{ state.detail.madow || 0 }} <span style="font-size: 13px;">颗</span>
</div>
<div class="asset-card-desc">
当前估值: ¥{{ state.detail.madow_value || '0.00' }}
</div>
<div class="asset-card-desc">
实时汇率: {{ state.detail.madow_rate || '0.00000075' }}
</div>
</div>
</el-col>
<!-- 资产 3: 商城兑换券 -->
<el-col :xs="24" :sm="12" :md="6">
<div class="asset-card-box">
<div class="asset-card-label">商城可用兑换券</div>
<div class="asset-card-num" style="color: #e6a23c;">
¥{{ state.detail.voucher_available || '0.00' }}
</div>
<div class="asset-card-desc">
计划总额: ¥{{ state.detail.voucher_total || '0.00' }}
</div>
<div class="asset-card-desc">
已释放: ¥{{ state.detail.voucher_released || '0.00' }} / 已用: ¥{{ state.detail.voucher_used || '0.00' }}
</div>
</div>
</el-col>
<!-- 资产 4: 基础钱包与消费 -->
<el-col :xs="24" :sm="12" :md="6">
<div class="asset-card-box">
<div class="asset-card-label">商城余额 / 积分</div>
<div class="asset-card-num" style="color: #303133;">
¥{{ state.detail.money || '0.00' }}
</div>
<div class="asset-card-desc">
商城积分: {{ state.detail.score || 0 }}
</div>
<div class="asset-card-desc">
累计消费: ¥{{ state.detail.total_consume || '0.00' }}
</div>
</div>
</el-col>
</el-row>
</div>
<!-- 看板 3:全能用户动态与流水明细 -->
<div class="card-title sa-flex sa-row-between">
<span>全能动态与业务流水</span>
<div v-if="log.tabActive == 'partner_reward'" class="sa-flex sa-col-center">
<el-checkbox v-model="log.showSkip" @change="onChangeTab">
显示未发放/跳过原因记录
</el-checkbox>
</div>
</div>
<div class="card-section log">
<div class="bottom">
<el-tabs v-model="log.tabActive" @tab-change="onChangeTab">
<el-tab-pane label="收益中心流水" name="partner_reward"></el-tab-pane>
<el-tab-pane label="兑换券释放计划" name="voucher_plan"></el-tab-pane>
<el-tab-pane label="直推团队成员" name="team"></el-tab-pane>
<el-tab-pane label="佣金记录" name="commission"></el-tab-pane>
<el-tab-pane label="余额记录" name="money"></el-tab-pane>
<el-tab-pane label="积分记录" name="score"></el-tab-pane>
<el-tab-pane label="佣金记录" name="commission"></el-tab-pane>
<el-tab-pane label="订单记录" name="order"></el-tab-pane>
<el-tab-pane label="商城订单" name="order"></el-tab-pane>
<el-tab-pane label="分享记录" name="share"></el-tab-pane>
<el-tab-pane label="优惠券明细" name="coupon"></el-tab-pane>
</el-tabs>
<!-- Tab 1: 收益中心流水 (二开核心) -->
<el-table v-if="log.tabActive == 'partner_reward'" height="460" class="sa-table" :data="log.data" stripe>
<el-table-column prop="createtime" label="入账时间" width="165"></el-table-column>
<el-table-column prop="reward_type_text" label="奖项类型" min-width="120">
<template #default="scope">
<el-tag size="small">{{ scope.row.reward_type_text || scope.row.reward_type }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="role_text" label="受益角色" min-width="100"></el-table-column>
<el-table-column prop="amount" label="结算金额" min-width="100">
<template #default="scope">
<b :style="{ color: scope.row.amount > 0 ? '#67c23a' : '#909399' }">
¥{{ scope.row.amount }}
</b>
</template>
</el-table-column>
<el-table-column prop="madow" label="结算马豆" min-width="90"></el-table-column>
<el-table-column label="买家" min-width="140">
<template #default="scope">
<sa-user-profile v-if="scope.row.buyer" :user="scope.row.buyer" :id="scope.row.buyer_id"></sa-user-profile>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="关联订单" min-width="150">
<template #default="scope">
<span>{{ scope.row.order?.order_sn || scope.row.order_id || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="status_text" label="状态" min-width="90">
<template #default="scope">
<el-tag size="small" :type="scope.row.status == 1 ? 'success' : (scope.row.status == -1 ? 'info' : 'danger')">
{{ scope.row.status_text || (scope.row.status == 1 ? '已入账' : (scope.row.status == -1 ? '未发放' : '其他')) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="测算摘要 / 跳过原因" min-width="180" show-overflow-tooltip>
<template #default="scope">
<span>{{ scope.row.formula || scope.row.ext?.formula || scope.row.ext?.skip_reason || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="80" fixed="right">
<template #default="scope">
<el-button type="primary" link @click="onShowFormula(scope.row)">明细</el-button>
</template>
</el-table-column>
</el-table>
<!-- Tab 2: 兑换券释放计划 (二开核心) -->
<el-table v-if="log.tabActive == 'voucher_plan'" height="460" class="sa-table" :data="log.data" stripe>
<el-table-column prop="id" label="计划ID" width="80"></el-table-column>
<el-table-column label="订单号" min-width="170">
<template #default="scope">
{{ scope.row.order?.order_sn || scope.row.order_id || '-' }}
</template>
</el-table-column>
<el-table-column prop="total_fee" label="计划总额" min-width="110">
<template #default="scope">
<b>¥{{ scope.row.total_fee }}</b>
</template>
</el-table-column>
<el-table-column prop="day_amount" label="每日释放" min-width="100">
<template #default="scope">
¥{{ scope.row.day_amount }}
</template>
</el-table-column>
<el-table-column label="释放进度" min-width="140">
<template #default="scope">
<el-progress :percentage="Math.min(100, Math.round(((scope.row.days_released || 0) / (scope.row.total_days || 365)) * 100))"></el-progress>
<div style="font-size: 11px; color: var(--sa-subfont); margin-top: 2px;">
第 {{ scope.row.days_released || 0 }} / {{ scope.row.total_days || 365 }} 天
</div>
</template>
</el-table-column>
<el-table-column prop="released_amount" label="已释放金额" min-width="110"></el-table-column>
<el-table-column prop="voucher_available" label="当前可用额" min-width="110">
<template #default="scope">
<span style="color: #67c23a; font-weight: 600;">¥{{ scope.row.voucher_available }}</span>
</template>
</el-table-column>
<el-table-column prop="start_date" label="开始日期" width="120"></el-table-column>
<el-table-column prop="status_text" label="状态" width="100">
<template #default="scope">
<el-tag size="small" :type="scope.row.status == 'running' ? 'success' : (scope.row.status == 'finished' ? 'info' : 'danger')">
{{ scope.row.status_text || scope.row.status }}
</el-tag>
</template>
</el-table-column>
</el-table>
<!-- Tab 3: 直推团队成员 (二开核心) -->
<el-table v-if="log.tabActive == 'team'" height="460" class="sa-table" :data="log.data" stripe>
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column label="下级成员" min-width="160">
<template #default="scope">
<sa-user-profile :user="scope.row" :id="scope.row.id"></sa-user-profile>
</template>
</el-table-column>
<el-table-column prop="mobile" label="手机号" min-width="120"></el-table-column>
<el-table-column label="业务身份" min-width="160">
<template #default="scope">
<div>{{ scope.row.identity_text || '普通会员' }}</div>
</template>
</el-table-column>
<el-table-column prop="total_consume" label="累计消费" min-width="110">
<template #default="scope">
¥{{ scope.row.total_consume }}
</template>
</el-table-column>
<el-table-column prop="createtime" label="注册时间" width="165"></el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template #default="scope">
<el-button type="primary" link @click="onOpenUserDetail(scope.row.id)">
查看详情
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 原生 Tab 4/5/6: 佣金/余额/积分 -->
<el-table
v-if="log.tabActive=='money' || log.tabActive=='score' || log.tabActive=='commission'"
height="520" class="sa-table" :data="log.data" stripe>
<el-table-column prop="createtime" label="交易时间" width="172">
</el-table-column>
<el-table-column prop="amount" label="变动余额" min-width="120">
</el-table-column>
<el-table-column prop="before" label="变更前" min-width="120">
</el-table-column>
<el-table-column prop="after" label="剩余余额" min-width="120">
</el-table-column>
height="460" class="sa-table" :data="log.data" stripe>
<el-table-column prop="createtime" label="交易时间" width="172"></el-table-column>
<el-table-column prop="amount" label="变动金额/积分" min-width="120"></el-table-column>
<el-table-column prop="before" label="变动前" min-width="120"></el-table-column>
<el-table-column prop="after" label="变动后" min-width="120"></el-table-column>
<el-table-column label="操作人" min-width="160">
<template #default="scope">
<sa-user-profile type="oper" :user="scope.row.oper" :id="scope.row.oper_id">
@@ -330,66 +633,86 @@
</template>
</el-table-column>
</el-table>
<el-table v-if="log.tabActive=='order'" height="520" class="sa-table" :data="log.data"
stripe>
<el-table-column prop="createtime" label="下单时间" min-width="172"></el-table-column>
<el-table-column prop="order_sn" label="订单号" min-width="280"></el-table-column>
<el-table-column prop="platform_text" label="订单来源" min-width="120">
</el-table-column>
<!-- 原生 Tab 7: 商城订单 -->
<el-table v-if="log.tabActive=='order'" height="460" class="sa-table" :data="log.data" stripe>
<el-table-column prop="createtime" label="下单时间" min-width="165"></el-table-column>
<el-table-column prop="order_sn" label="订单号" min-width="200"></el-table-column>
<el-table-column prop="platform_text" label="订单来源" min-width="100"></el-table-column>
<el-table-column prop="type_text" label="订单类型" min-width="100"></el-table-column>
<el-table-column prop="order_amount" label="订单总金额" min-width="140">
<el-table-column prop="order_amount" label="总金额" min-width="110"></el-table-column>
<el-table-column prop="total_discount_fee" label="优惠" min-width="90"></el-table-column>
<el-table-column prop="pay_fee" label="实付金额" min-width="110">
<template #default="scope">
<b style="color: #67c23a;">¥{{ scope.row.pay_fee }}</b>
</template>
</el-table-column>
<el-table-column prop="total_discount_fee" label="优惠减免" min-width="140">
</el-table-column>
<el-table-column prop="pay_fee" label="实付金额" min-width="140"></el-table-column>
<el-table-column prop="status_text" label="订单状态" min-width="100"></el-table-column>
</el-table>
<el-table v-if="log.tabActive=='share'" height="520" class="sa-table" :data="log.data"
stripe>
<el-table-column prop="createtime" label="分享时间" width="172"></el-table-column>
<el-table-column label="被分享用户" min-width="180">
<!-- 原生 Tab 8: 分享记录 -->
<el-table v-if="log.tabActive=='share'" height="460" class="sa-table" :data="log.data" stripe>
<el-table-column prop="createtime" label="分享时间" width="165"></el-table-column>
<el-table-column label="被分享用户" min-width="160">
<template #default="scope">
<sa-user-profile :user="scope.row.user" :id="scope.user_id">
</sa-user-profile>
<sa-user-profile :user="scope.row.user" :id="scope.row.user_id"></sa-user-profile>
</template>
</el-table-column>
<el-table-column prop="from_text" label="分享类型" min-width="120"></el-table-column>
<el-table-column label="分享信息" min-width="180" show-overflow-tooltip>
<template #default="scope">
<div v-if="scope.row.ext" class="sa-flex">
<sa-image :url="scope.row.ext.image" size="32"></sa-image>
<div class="sa-table-line-1 ml-2">{{ scope.row.ext.memo}}</div>
</div>
<div v-else>-</div>
</template>
</el-table-column>
<el-table-column prop="platform_text" label="平台" min-width="130"></el-table-column>
<el-table-column prop="platform_text" label="平台" min-width="110"></el-table-column>
</el-table>
<el-table v-if="log.tabActive=='coupon'" height="520" class="sa-table" :data="log.data"
stripe>
<!-- 原生 Tab 9: 优惠券明细 -->
<el-table v-if="log.tabActive=='coupon'" height="460" class="sa-table" :data="log.data" stripe>
<el-table-column label="优惠券名称" min-width="160">
<template #default="scope">
<div class="sa-line-1">{{ scope.row.coupon?.name || '-' }}</div>
</template>
</el-table-column>
<el-table-column label="优惠券面额" min-width="120">
<el-table-column label="面额" min-width="100">
<template #default="scope">
<div class="sa-line-1">{{ scope.row.coupon?.amount || '-' }}</div>
<div>¥{{ scope.row.coupon?.amount || '-' }}</div>
</template>
</el-table-column>
<el-table-column prop="status_text" label="优惠券状态" min-width="120"></el-table-column>
<el-table-column prop="createtime" label="领取时间" width="172"></el-table-column>
<el-table-column prop="use_time" label="使用时间" width="172"></el-table-column>
<el-table-column prop="status_text" label="状态" min-width="100"></el-table-column>
<el-table-column prop="createtime" label="领取时间" width="165"></el-table-column>
<el-table-column prop="use_time" label="使用时间" width="165"></el-table-column>
</el-table>
<el-footer class="sa-flex sa-row-right">
<sa-pagination v-model="pagination" @pagination-change="getLog">
</sa-pagination>
<el-footer class="sa-flex sa-row-right mt-2">
<sa-pagination v-model="pagination" @pagination-change="getLog"></sa-pagination>
</el-footer>
</div>
</el-col>
</el-row>
</el-col>
</el-row>
</div>
</el-col>
</el-row>
</el-scrollbar>
</el-main>
</el-container>
<!-- 奖励测算公式与跳过原因详情弹窗 -->
<el-dialog v-model="formulaDialog.visible" title="奖励测算公式与跳过记录详情" width="550px">
<div v-if="formulaDialog.row">
<div class="mb-3">
<span style="font-weight: 600;">奖项类型:</span>
<el-tag size="small">{{ formulaDialog.row.reward_type_text || formulaDialog.row.reward_type }}</el-tag>
</div>
<div class="mb-3">
<span style="font-weight: 600;">结算金额/马豆:</span>
<b>¥{{ formulaDialog.row.amount }}</b>
<span v-if="formulaDialog.row.madow > 0" class="ml-2">({{ formulaDialog.row.madow }} 马豆)</span>
</div>
<div class="mb-3" v-if="formulaDialog.row.ext?.skip_reason">
<span style="font-weight: 600; color: #e6a23c;">跳过/未发放原因:</span>
<el-tag type="warning" size="small">{{ formulaDialog.row.ext.skip_reason }}</el-tag>
</div>
<div class="mb-2" style="font-weight: 600;">公式摘要与上下文(ext):</div>
<div class="formula-box">
{{ JSON.stringify(formulaDialog.row.ext || formulaDialog.row.formula || {}, null, 2) }}
</div>
</div>
<template #footer>
<el-button type="primary" @click="formulaDialog.visible = false">关闭</el-button>
</template>
</el-dialog>
</div>

查看文件

@@ -1,11 +1,60 @@
{include file="/shopro/common/script" /}
<style>
.user-index .stats-row {
margin-bottom: 12px;
}
.user-index .stat-card {
background: #f8fafc;
border: 1px solid var(--sa-border);
border-radius: 6px;
padding: 10px 14px;
display: flex;
flex-direction: column;
justify-content: center;
}
.user-index .stat-title {
font-size: 12px;
color: var(--sa-subfont);
margin-bottom: 4px;
}
.user-index .stat-num {
font-size: 18px;
font-weight: 600;
color: var(--sa-title);
}
.user-index .active-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
}
.user-index .active-dot.is-on {
background: #52c41a;
}
.user-index .active-dot.is-off {
background: #c0c4cc;
}
.user-index .asset-sub {
font-size: 11px;
color: var(--sa-subfont);
margin-top: 2px;
}
.user-index .badge-group {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 4px;
}
</style>
<div id="index" class="user-index panel panel-default panel-intro" v-cloak>
<el-container class="panel-block">
<el-header class="sa-header">
<el-header class="sa-header" style="height: auto; padding-bottom: 12px;">
<div class="sa-title sa-flex sa-row-between">
<div class="sa-title-left">
<div class="left-name">用户管理</div>
<div class="left-name">会员管理</div>
<sa-filter-condition v-model="state.filter" @filter-delete="onChangeFilter">
</sa-filter-condition>
</div>
@@ -14,30 +63,125 @@
<el-button class="sa-button-refresh" icon="Search" @click="onOpenFilter"></el-button>
</div>
</div>
<!-- 数据看板概览 -->
<el-row :gutter="12" class="stats-row">
<el-col :xs="12" :sm="8" :md="4">
<div class="stat-card">
<div class="stat-title">全平台会员数</div>
<div class="stat-num">{{ state.stats.total_users || 0 }}</div>
</div>
</el-col>
<el-col :xs="12" :sm="8" :md="4">
<div class="stat-card">
<div class="stat-title">创客团队骨干</div>
<div class="stat-num" style="color: #409eff;">{{ state.stats.maker_count || 0 }}</div>
</div>
</el-col>
<el-col :xs="12" :sm="8" :md="4">
<div class="stat-card">
<div class="stat-title">线下酒道馆</div>
<div class="stat-num" style="color: #67c23a;">{{ state.stats.venue_count || 0 }}</div>
</div>
</el-col>
<el-col :xs="12" :sm="8" :md="4">
<div class="stat-card">
<div class="stat-title">区县代理总数</div>
<div class="stat-num" style="color: #e6a23c;">{{ state.stats.county_count || 0 }}</div>
</div>
</el-col>
<el-col :xs="12" :sm="8" :md="4">
<div class="stat-card">
<div class="stat-title">合伙股东总数</div>
<div class="stat-num" style="color: #f56c6c;">{{ state.stats.partner_count || 0 }}</div>
</div>
</el-col>
<el-col :xs="12" :sm="8" :md="4">
<div class="stat-card">
<div class="stat-title">30日自购活跃</div>
<div class="stat-num" style="color: #52c41a;">{{ state.stats.active_count || 0 }}</div>
</div>
</el-col>
</el-row>
</el-header>
<el-main class="sa-main">
<el-table height="100%" class="sa-table" :data="state.data" stripe @sort-change="onChangeSort">
<el-table-column prop="id" label="ID" min-width="90" sortable="custom"> </el-table-column>
<el-table-column label="用户信息" min-width="150">
<el-table-column prop="id" label="ID" min-width="80" sortable="custom"> </el-table-column>
<el-table-column label="用户信息" min-width="160">
<template #default="scope">
<sa-user-profile :user="scope.row" :id="scope.row.id"></sa-user-profile>
<div class="asset-sub">{{ scope.row.mobile || scope.row.username || '' }}</div>
</template>
</el-table-column>
<el-table-column prop="username" label="用户名" min-width="120"></el-table-column>
<el-table-column prop="mobile" label="手机号" min-width="120"></el-table-column>
<el-table-column sortable="custom" prop="commission" label="佣金" min-width="110"></el-table-column>
<el-table-column sortable="custom" prop="total_consume" label="总消费" min-width="110"></el-table-column>
<el-table-column sortable="custom" prop="score" label="积分" min-width="110"></el-table-column>
<el-table-column sortable="custom" prop="money" label="余额" min-width="110"></el-table-column>
<el-table-column sortable="custom" prop="createtime" label="注册时间" min-width="172"></el-table-column>
<el-table-column sortable="custom" prop="logintime" label="上次登录" min-width="172"></el-table-column>
<el-table-column fixed="right" label="操作" min-width="120">
<el-table-column label="推荐人" min-width="140">
<template #default="scope">
<sa-user-profile type="agent" :user="scope.row.parent_user" :id="scope.row.parent_user_id"></sa-user-profile>
</template>
</el-table-column>
<el-table-column label="业务身份" min-width="190">
<template #default="scope">
<div style="font-weight: 500;">{{ scope.row.identity_text || '普通会员' }}</div>
<div class="badge-group">
<el-tag v-if="scope.row.level == 1" size="small" type="info">品酒师</el-tag>
<el-tag v-if="scope.row.level >= 2" size="small" type="primary">创客 V{{ scope.row.level - 1 }}</el-tag>
<el-tag v-if="scope.row.is_venue" size="small" type="success">酒道馆</el-tag>
<el-tag v-if="scope.row.is_county" size="small" type="warning">区县代理</el-tag>
<el-tag v-if="scope.row.is_partner" size="small" type="danger">合伙股东</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="30日自购活跃" min-width="110">
<template #default="scope">
<template v-if="scope.row.is_advanced">
<span class="sa-flex sa-col-center">
<i class="active-dot" :class="scope.row.active_30d ? 'is-on' : 'is-off'"></i>
{{ scope.row.active_30d ? '正常领奖' : '已暂停' }}
</span>
</template>
<template v-else>
<el-tag size="small" type="info" effect="plain">免考核</el-tag>
</template>
</template>
</el-table-column>
<el-table-column label="收益中心资产" min-width="190">
<template #default="scope">
<div><span class="asset-sub">可提现收益:</span> <b>¥{{ scope.row.commission || '0.00' }}</b></div>
<div class="asset-sub">马豆: {{ scope.row.madow || 0 }} (约 ¥{{ scope.row.madow_value || '0.00' }})</div>
<div class="asset-sub">可用券: ¥{{ scope.row.voucher_available || '0.00' }}</div>
</template>
</el-table-column>
<el-table-column label="商城钱包与消费" min-width="160">
<template #default="scope">
<div><span class="asset-sub">余额:</span> ¥{{ scope.row.money || '0.00' }}</div>
<div class="asset-sub">积分: {{ scope.row.score || 0 }}</div>
<div class="asset-sub">消费: ¥{{ scope.row.total_consume || '0.00' }}</div>
</template>
</el-table-column>
<el-table-column sortable="custom" prop="createtime" label="注册时间" min-width="165"></el-table-column>
<el-table-column fixed="right" label="操作" min-width="180">
<template #default="scope">
{if $auth->check('shopro/user/user/detail')}
<el-button type="primary" link @click="onDetail(scope.row.id)">查看</el-button>
<el-button type="primary" link @click="onDetail(scope.row.id, scope.row)">详情</el-button>
{/if}
<el-button v-if="scope.row.has_agent" type="success" link @click="onOpenAgentDetail(scope.row.id)">
收益档案
</el-button>
{if $auth->check('shopro/user/user/recharge')}
<el-button type="warning" link @click="onRecharge('money', scope.row.id)">
充值
</el-button>
{/if}
<el-popconfirm width="fit-content" confirm-button-text="确认" cancel-button-text="取消"
title="确认删除这条记录?" @confirm="onDelete(scope.row.id)">
title="确认删除该会员记录?" @confirm="onDelete(scope.row.id)">
<template #reference>
{if $auth->check('shopro/user/user/delete')}
<el-button type="danger" link>删除</el-button>

查看文件

@@ -125,7 +125,18 @@ const SaUserProfile = {
});
function onOpenUserDetail(id) {
Fast.api.open(`shopro/user/user/detail?id=${id}`, "会员详情")
const url = `shopro/user/user/detail?id=${id}`;
const name = (userData.value && (userData.value.nickname || userData.value.username)) ? userData.value.nickname || userData.value.username : '';
const title = name ? `会员详情 - ${name} (#${id})` : `会员详情 #${id}`;
if (typeof Backend !== 'undefined' && Backend.api && typeof Backend.api.addtabs === 'function') {
Backend.api.addtabs(url, title);
} else if (top && top.Backend && top.Backend.api && typeof top.Backend.api.addtabs === 'function') {
top.Backend.api.addtabs(url, title);
} else if (Fast && Fast.api && typeof Fast.api.addtabs === 'function') {
Fast.api.addtabs(url, title);
} else {
Fast.api.open(url, title);
}
}
return {
Fast,

查看文件

@@ -9,10 +9,23 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
data: [],
order: '',
sort: '',
stats: {
total_users: 0,
maker_count: 0,
venue_count: 0,
county_count: 0,
partner_count: 0,
active_count: 0,
},
filter: {
drawer: false,
data: {
user: { field: 'id', value: '' },
identity: '',
maker_level: '',
active_30d: '',
has_agent: '',
parent_user: '',
createtime: [],
logintime: [],
},
@@ -48,6 +61,63 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
}],
}
},
identity: {
type: 'tselect',
label: '业务身份',
value: '',
options: {
data: [
{ value: 'taster', label: '品酒师' },
{ value: 'maker', label: '创客 (V1~V6)' },
{ value: 'venue', label: '酒道馆' },
{ value: 'county', label: '区县代理' },
{ value: 'partner', label: '合伙股东' },
]
}
},
maker_level: {
type: 'tselect',
label: '创客等级',
value: '',
options: {
data: [
{ value: '1', label: '创客 V1' },
{ value: '2', label: '创客 V2' },
{ value: '3', label: '创客 V3' },
{ value: '4', label: '创客 V4' },
{ value: '5', label: '创客 V5' },
{ value: '6', label: '创客 V6' },
]
}
},
active_30d: {
type: 'tselect',
label: '30日活跃',
value: '',
options: {
data: [
{ value: '1', label: '有自购(正常领奖)' },
{ value: '0', label: '已暂停(无自购)' },
]
}
},
has_agent: {
type: 'tselect',
label: '收益档案',
value: '',
options: {
data: [
{ value: '1', label: '具备分销/收益档案' },
{ value: '0', label: '普通注册会员' },
]
}
},
parent_user: {
type: 'tinput',
label: '推荐人',
placeholder: '请输入推荐人ID/手机号/昵称',
value: '',
},
createtime: {
type: 'tdatetimerange',
label: '注册时间',
@@ -70,6 +140,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
nickname: 'like',
mobile: 'like',
email: 'like',
identity: '=',
maker_level: '=',
active_30d: '=',
has_agent: '=',
parent_user: '=',
createtime: 'range',
logintime: 'range',
});
@@ -86,6 +161,9 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
}, function (ret, res) {
state.data = res.data.data
pagination.total = res.data.total
if (res.data.stats) {
state.stats = res.data.stats;
}
return false
}, function (ret, res) { })
}
@@ -110,13 +188,44 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
total: 0,
})
function onDetail(id) {
Fast.api.open(`shopro/user/user/detail?id=${id}`, "详情", {
callback() {
getData()
}
})
function onDetail(id, row) {
const url = `shopro/user/user/detail?id=${id}`;
const name = (row && (row.nickname || row.username)) ? (row.nickname || row.username) : '';
const title = name ? `会员详情 - ${name} (#${id})` : `会员详情 #${id}`;
if (typeof Backend !== 'undefined' && Backend.api && typeof Backend.api.addtabs === 'function') {
Backend.api.addtabs(url, title);
} else if (top && top.Backend && top.Backend.api && typeof top.Backend.api.addtabs === 'function') {
top.Backend.api.addtabs(url, title);
} else if (Fast && Fast.api && typeof Fast.api.addtabs === 'function') {
Fast.api.addtabs(url, title);
} else {
window.location.href = Fast.api.fixurl(url);
}
}
function onOpenAgentDetail(id) {
const url = `shopro/commission/agent/detail/id/${id}?id=${id}`;
const title = `分销商详情 #${id}`;
if (typeof Backend !== 'undefined' && Backend.api && typeof Backend.api.addtabs === 'function') {
Backend.api.addtabs(url, title);
} else if (top && top.Backend && top.Backend.api && typeof top.Backend.api.addtabs === 'function') {
top.Backend.api.addtabs(url, title);
} else if (Fast && Fast.api && typeof Fast.api.addtabs === 'function') {
Fast.api.addtabs(url, title);
} else {
window.location.href = Fast.api.fixurl(url);
}
}
function onRecharge(type, row) {
const id = typeof row === 'object' ? row.id : row;
Fast.api.open(`shopro/user/user/recharge?type=${type}&id=${id}`, "账户充值", {
callback: function () {
getData();
}
});
}
function onDelete(id) {
Fast.api.ajax({
url: `shopro/user/user/delete/id/${id}`,
@@ -138,6 +247,8 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
onChangeFilter,
pagination,
onDetail,
onOpenAgentDetail,
onRecharge,
onDelete
}
}
@@ -148,12 +259,37 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
const { reactive, onMounted } = Vue
const detail = {
setup() {
const queryUserId = (typeof Config !== 'undefined' && (Config.id || Config.user_id))
? (Config.id || Config.user_id)
: (new URLSearchParams(location.search).get('id') || (location.pathname.match(/\/id\/(\d+)/i) || [])[1] || Fast.api.query('id'));
const state = reactive({
type: new URLSearchParams(location.search).get('type'),
id: new URLSearchParams(location.search).get('id'),
id: queryUserId,
detail: {}
})
function onBack() {
if (parent && parent.Layer && typeof parent.Layer.getFrameIndex === 'function') {
try {
var index = parent.Layer.getFrameIndex(window.name);
if (index) {
parent.Layer.close(index);
return;
}
} catch (e) {}
}
if (top && top.Backend && top.Backend.api && typeof top.Backend.api.closetabs === 'function') {
top.Backend.api.closetabs();
return;
}
if (typeof Backend !== 'undefined' && Backend.api && typeof Backend.api.closetabs === 'function') {
Backend.api.closetabs();
return;
}
window.history.back();
}
function getDetail() {
Fast.api.ajax({
url: `shopro/user/user/detail/id/${state.id}`,
@@ -206,7 +342,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
}
function onRecharge(type) {
Fast.api.open(`shopro/user/user/recharge?type=${type}&id=${state.detail.id}`, "充值", {
Fast.api.open(`shopro/user/user/recharge?type=${type}&id=${state.detail.id}`, "账户充值", {
callback: function () {
getDetail();
getLog()
@@ -228,33 +364,66 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
}
}
function onOpenUserDetail(id) {
if (!id) return;
const url = `shopro/user/user/detail?id=${id}`;
const title = `会员详情 #${id}`;
if (typeof Backend !== 'undefined' && Backend.api && typeof Backend.api.addtabs === 'function') {
Backend.api.addtabs(url, title);
} else if (top && top.Backend && top.Backend.api && typeof top.Backend.api.addtabs === 'function') {
top.Backend.api.addtabs(url, title);
} else if (Fast && Fast.api && typeof Fast.api.addtabs === 'function') {
Fast.api.addtabs(url, title);
} else {
window.location.href = Fast.api.fixurl(url);
}
}
const formulaDialog = reactive({
visible: false,
row: null,
})
function onShowFormula(row) {
formulaDialog.row = row;
formulaDialog.visible = true;
}
const log = reactive({
tabActive: 'money',
tabActive: 'partner_reward',
showSkip: false,
data: [],
})
function getLog() {
let url
let url = ''
let search = {}
if (log.tabActive == 'money' || log.tabActive == 'score' || log.tabActive == 'commission') {
if (log.tabActive == 'partner_reward') {
url = `shopro/user/user/rewards/id/${state.id}`
search = {
show_skip: log.showSkip ? 1 : 0
}
} else if (log.tabActive == 'voucher_plan') {
url = `shopro/user/user/voucherPlans/id/${state.id}`
} else if (log.tabActive == 'team') {
url = `shopro/user/user/team/id/${state.id}`
} else if (log.tabActive == 'money' || log.tabActive == 'score' || log.tabActive == 'commission') {
url = `shopro/user/wallet_log/${log.tabActive}/id/${state.id}`
}
if (log.tabActive == 'order') {
} else if (log.tabActive == 'order') {
url = `shopro/order/order`
search = {
search: JSON.stringify({
user_id: state.id
})
}
}
if (log.tabActive == 'share') {
} else if (log.tabActive == 'share') {
url = `shopro/share`
search = {
id: state.id
}
}
if (log.tabActive == 'coupon') {
} else if (log.tabActive == 'coupon') {
url = `shopro/user/user/coupon/id/${state.id}`
}
if (!url) return;
Fast.api.ajax({
url: url,
type: 'GET',
@@ -265,11 +434,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
},
}, function (ret, res) {
if (log.tabActive == 'order') {
log.data = res.data.orders.data
pagination.total = res.data.orders.total
log.data = res.data?.orders?.data || []
pagination.total = res.data?.orders?.total || 0
} else {
log.data = res.data.data
pagination.total = res.data.total
log.data = res.data?.data || []
pagination.total = res.data?.total || 0
}
return false
}, function (ret, res) { })
@@ -294,6 +463,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
return {
state,
getDetail,
onBack,
platform,
statusStyle,
onSelectAvatar,
@@ -301,6 +471,9 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
onChangeParentUser,
onRecharge,
onOpenAgentDetail,
onOpenUserDetail,
formulaDialog,
onShowFormula,
log,
getLog,
pagination,

查看文件

@@ -0,0 +1,124 @@
const fs = require('fs');
const path = require('path');
let passed = 0;
let failed = 0;
function assert(condition, testName, detail = '') {
if (condition) {
console.log(` [PASS] ${testName}`);
passed++;
} else {
console.error(` [FAIL] ${testName} - ${detail}`);
failed++;
}
}
console.log("========================================================");
console.log(" 会员列表优化与会员详情补齐 自动化端到端规范断言测试");
console.log("========================================================\n");
// 1. 验证 UserFilter.php 筛选器
const filterPath = path.resolve(__dirname, '../addons/shopro/filter/user/UserFilter.php');
assert(fs.existsSync(filterPath), 'UserFilter.php 文件存在');
const filterContent = fs.readFileSync(filterPath, 'utf-8');
assert(filterContent.includes('function identity('), 'UserFilter.php 包含 identity 多维身份筛选方法');
assert(filterContent.includes("'taster'"), 'UserFilter.php identity 支持品酒师筛选');
assert(filterContent.includes("'maker'"), 'UserFilter.php identity 支持创客筛选');
assert(filterContent.includes("'venue'"), 'UserFilter.php identity 支持酒道馆筛选');
assert(filterContent.includes("'county'"), 'UserFilter.php identity 支持区县代理筛选');
assert(filterContent.includes("'partner'"), 'UserFilter.php identity 支持合伙股东筛选');
assert(filterContent.includes('function makerLevel('), 'UserFilter.php 包含 makerLevel 创客级别细分筛选');
assert(filterContent.includes('function active30d('), 'UserFilter.php 包含 active30d 30天自购活跃状态筛选');
assert(filterContent.includes('function hasAgent('), 'UserFilter.php 包含 hasAgent 分销/收益档案筛选');
assert(filterContent.includes('function parentUser('), 'UserFilter.php 包含 parentUser 推荐人筛选');
// 2. 验证 User.php 控制器
const controllerPath = path.resolve(__dirname, '../application/admin/controller/shopro/user/User.php');
assert(fs.existsSync(controllerPath), 'User.php 控制器文件存在');
const controllerContent = fs.readFileSync(controllerPath, 'utf-8');
assert(controllerContent.includes("'rewards'"), 'User.php noNeedRight 包含 rewards 豁免配置');
assert(controllerContent.includes("'voucherPlans'"), 'User.php noNeedRight 包含 voucherPlans 豁免配置');
assert(controllerContent.includes("'team'"), 'User.php noNeedRight 包含 team 豁免配置');
assert(controllerContent.includes('IdentityService::attachProfile'), 'User.php index/detail 关联装配 IdentityService 身份和资产');
assert(controllerContent.includes('stats'), 'User.php index 返回包含 stats 数据概览');
assert(controllerContent.includes('voucher_available'), 'User.php 装配 voucher_available 可用券资产');
assert(controllerContent.includes('madow_value'), 'User.php 装配 madow_value 马豆预估估值');
assert(controllerContent.includes('push3Amounts'), 'User.php detail 包含推三返一循环金额计算');
assert(controllerContent.includes('next_push3_amount'), 'User.php detail 返回下一单预计返现金额');
assert(controllerContent.includes('active_status_text'), 'User.php detail 包含30天自购活跃状态解析');
assert(controllerContent.includes('income_total_cash'), 'User.php detail 包含累计现金收益统计');
assert(controllerContent.includes('function rewards($id)'), 'User.php 包含 rewards 收益流水查询接口');
assert(controllerContent.includes('function voucherPlans($id)'), 'User.php 包含 voucherPlans 兑换券释放计划查询接口');
assert(controllerContent.includes('function team($id)'), 'User.php 包含 team 团队成员查询接口');
// 3. 验证 sa-user-profile.js
const profileJsPath = path.resolve(__dirname, '../public/assets/addons/shopro/components/sa-user-profile.js');
assert(fs.existsSync(profileJsPath), 'sa-user-profile.js 组件文件存在');
const profileJsContent = fs.readFileSync(profileJsPath, 'utf-8');
assert(profileJsContent.includes('Backend.api.addtabs(url, title)'), 'sa-user-profile.js 点击会员头像/ID 使用 addtabs 打开选项卡');
assert(profileJsContent.includes('top.Backend.api.addtabs(url, title)'), 'sa-user-profile.js 支持 top.Backend.api.addtabs 顶层打开');
// 4. 验证 user.js
const userJsPath = path.resolve(__dirname, '../public/assets/js/backend/shopro/user/user.js');
assert(fs.existsSync(userJsPath), 'user.js 文件存在');
const userJsContent = fs.readFileSync(userJsPath, 'utf-8');
assert(userJsContent.includes('Backend.api.addtabs(url, title)'), 'user.js onDetail 使用 Backend.api.addtabs 选项卡打开');
assert(userJsContent.includes('top.Backend.api.addtabs(url, title)'), 'user.js onDetail 兼容 top.Backend.api.addtabs');
assert(userJsContent.includes('function onOpenAgentDetail(id)'), 'user.js 包含 onOpenAgentDetail 打开收益档案');
assert(userJsContent.includes('function onRecharge('), 'user.js 包含 onRecharge 快速充值方法');
assert(userJsContent.includes('queryUserId'), 'user.js detail 支持 queryUserId 灵活参数解析');
assert(userJsContent.includes('function onBack()'), 'user.js detail 包含 onBack 返回/关闭选项卡方法');
assert(userJsContent.includes('top.Backend.api.closetabs()'), 'user.js detail onBack 支持 closetabs 关闭当前选项卡');
assert(userJsContent.includes('partner_reward'), 'user.js detail 包含 partner_reward 收益流水 Tab 逻辑');
assert(userJsContent.includes('voucher_plan'), 'user.js detail 包含 voucher_plan 兑换券释放计划 Tab 逻辑');
assert(userJsContent.includes('team'), 'user.js detail 包含 team 团队下级 Tab 逻辑');
assert(userJsContent.includes('onShowFormula'), 'user.js detail 包含 onShowFormula 公式弹窗查看');
// 5. 验证 index.html
const indexPath = path.resolve(__dirname, '../application/admin/view/shopro/user/user/index.html');
assert(fs.existsSync(indexPath), 'user index.html 视图文件存在');
const indexContent = fs.readFileSync(indexPath, 'utf-8');
assert(indexContent.includes('stats-row'), 'index.html 包含数据概览 KPI 统计栏');
assert(indexContent.includes('state.stats.total_users'), 'index.html 统计栏展示全平台会员数');
assert(indexContent.includes('state.stats.maker_count'), 'index.html 统计栏展示创客团队骨干');
assert(indexContent.includes('state.stats.venue_count'), 'index.html 统计栏展示线下酒道馆');
assert(indexContent.includes('state.stats.county_count'), 'index.html 统计栏展示区县代理数');
assert(indexContent.includes('state.stats.partner_count'), 'index.html 统计栏展示合伙股东数');
assert(indexContent.includes('state.stats.active_count'), 'index.html 统计栏展示30日自购活跃数');
assert(indexContent.includes('推荐人'), 'index.html 包含推荐人列');
assert(indexContent.includes('业务身份'), 'index.html 包含业务身份列');
assert(indexContent.includes('30日自购活跃'), 'index.html 包含30日自购活跃状态列');
assert(indexContent.includes('active-dot'), 'index.html 包含活跃状态指示灯样式');
assert(indexContent.includes('收益中心资产'), 'index.html 包含收益中心资产列');
assert(indexContent.includes('商城钱包与消费'), 'index.html 包含商城钱包与消费列');
assert(indexContent.includes('onDetail(scope.row.id, scope.row)'), 'index.html 详情按钮正确传递参数触发选项卡');
assert(indexContent.includes('onOpenAgentDetail(scope.row.id)'), 'index.html 包含收益档案快捷入口');
// 6. 验证 detail.html
const detailPath = path.resolve(__dirname, '../application/admin/view/shopro/user/user/detail.html');
assert(fs.existsSync(detailPath), 'user detail.html 视图文件存在');
const detailContent = fs.readFileSync(detailPath, 'utf-8');
assert(detailContent.includes('height: 100vh'), 'detail.html 样式适配 100vh 全屏选项卡页面');
assert(detailContent.includes('detail-header-bar'), 'detail.html 包含顶部导航栏 detail-header-bar');
assert(detailContent.includes('@click="onBack"'), 'detail.html 包含 onBack 返回/关闭选项卡按钮');
assert(detailContent.includes('业务身份与二开权益体系'), 'detail.html 包含业务身份与二开权益看板');
assert(detailContent.includes('30天自购活跃窗口'), 'detail.html 包含30天自购活跃窗口看板');
assert(detailContent.includes('推三返一'), 'detail.html 包含推三返一循环进度展示');
assert(detailContent.includes('二开核心资产中枢'), 'detail.html 包含四大核心资产中枢看板');
assert(detailContent.includes('name="partner_reward"'), 'detail.html 包含收益中心流水 Tab');
assert(detailContent.includes('name="voucher_plan"'), 'detail.html 包含兑换券释放计划 Tab');
assert(detailContent.includes('name="team"'), 'detail.html 包含直推团队成员 Tab');
assert(detailContent.includes('formulaDialog'), 'detail.html 包含奖励测算公式与跳过记录详情弹窗');
console.log("\n--------------------------------------------------------");
console.log(`测试执行结果: 通过 ${passed} 项, 失败 ${failed} 项`);
console.log("========================================================");
process.exit(failed === 0 ? 0 : 1);