feat(commission): 移除二级间推并支持一级直推多层级下钻与防越权鉴权
这个提交包含在:
@@ -123,37 +123,62 @@ class Agent extends Commission
|
||||
$this->success('提交成功');
|
||||
}
|
||||
|
||||
// 我的团队
|
||||
// 我的团队(支持直推查看与安全下钻)
|
||||
public function team()
|
||||
{
|
||||
$agentId = $this->service->user->id;
|
||||
$levelType = $this->request->param('level_type', '1'); // 1=直推, 2=间推, all=全部
|
||||
$drillUserId = (int) $this->request->param('drill_user_id', 0);
|
||||
$filterType = $this->request->param('filter_type', 'all'); // all, partner, active, expiring, inactive
|
||||
$keyword = trim((string) $this->request->param('keyword', ''));
|
||||
$sort = $this->request->param('sort', 'createtime'); // createtime, child_count, consume
|
||||
$listRows = (int) $this->request->param('list_rows', 10);
|
||||
|
||||
// 1. 获取直推一级成员 ID 列表
|
||||
$level1Ids = UserModel::where('parent_user_id', $agentId)
|
||||
$targetUserId = $agentId;
|
||||
$drillUser = null;
|
||||
|
||||
// 1. 如果传了下钻目标成员,严格执行树状归属鉴权,防越权防注入
|
||||
if ($drillUserId > 0 && $drillUserId !== $agentId) {
|
||||
$targetUser = UserModel::where('id', $drillUserId)->where('status', 'normal')->find();
|
||||
if (!$targetUser) {
|
||||
$this->error('指定成员不存在或已注销');
|
||||
}
|
||||
|
||||
// 向上追溯层级,确保目标成员在当前用户的团队树中
|
||||
$isDownline = false;
|
||||
$currParentId = $targetUser->parent_user_id;
|
||||
$depth = 0;
|
||||
while ($currParentId > 0 && $depth < 30) {
|
||||
if ($currParentId == $agentId) {
|
||||
$isDownline = true;
|
||||
break;
|
||||
}
|
||||
$currParentId = UserModel::where('id', $currParentId)->value('parent_user_id') ?: 0;
|
||||
$depth++;
|
||||
}
|
||||
|
||||
if (!$isDownline) {
|
||||
$this->error('无权查看非直属团队成员信息');
|
||||
}
|
||||
|
||||
$targetUserId = $drillUserId;
|
||||
$drillUser = [
|
||||
'id' => $targetUser->id,
|
||||
'nickname' => $targetUser->nickname ?: '微信用户',
|
||||
'avatar' => $targetUser->avatar,
|
||||
'parent_user_id' => $targetUser->parent_user_id,
|
||||
];
|
||||
}
|
||||
|
||||
// 2. 获取目标用户的直推一级成员 ID 列表(全面采用一级直推模型)
|
||||
$level1Ids = UserModel::where('parent_user_id', $targetUserId)
|
||||
->where('status', 'normal')
|
||||
->column('id');
|
||||
|
||||
// 2. 根据 level_type 确定父级范围
|
||||
// 3. 构建直推成员查询
|
||||
$query = UserModel::alias('u')->where('u.status', 'normal');
|
||||
if ($levelType == '2') {
|
||||
if (empty($level1Ids)) {
|
||||
$query->whereRaw('1=0');
|
||||
} else {
|
||||
$query->whereIn('u.parent_user_id', $level1Ids);
|
||||
}
|
||||
} elseif ($levelType == 'all') {
|
||||
$allParentIds = array_merge([$agentId], $level1Ids);
|
||||
$query->whereIn('u.parent_user_id', $allParentIds);
|
||||
} else {
|
||||
$query->where('u.parent_user_id', $agentId);
|
||||
}
|
||||
$query->where('u.parent_user_id', $targetUserId);
|
||||
|
||||
// 3. 关键字搜索 (昵称/手机号/ID)
|
||||
// 4. 关键字搜索 (昵称/手机号/ID)
|
||||
if ($keyword !== '') {
|
||||
$query->where(function ($q) use ($keyword) {
|
||||
if (is_numeric($keyword)) {
|
||||
@@ -166,12 +191,12 @@ class Agent extends Commission
|
||||
});
|
||||
}
|
||||
|
||||
// 4. 计算活跃度时间窗口 (默认30天保活,剩余<=5天为临期)
|
||||
// 5. 计算活跃度时间窗口 (默认30天保活,剩余<=5天为临期)
|
||||
$windowDays = \addons\shopro\service\partner\PartnerConfig::activeDays();
|
||||
$windowStart = (new \addons\shopro\service\partner\ActiveService())->windowStart();
|
||||
$expiringStart = time() - ($windowDays - 5) * 86400;
|
||||
|
||||
// 5. 身份与活跃状态过滤
|
||||
// 6. 身份与活跃状态过滤
|
||||
if ($filterType === 'partner') {
|
||||
$partnerUserIds = AgentModel::where('status', 'in', ['normal', 'freeze'])
|
||||
->where(function ($q) {
|
||||
@@ -207,7 +232,7 @@ class Agent extends Commission
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 排序处理
|
||||
// 7. 排序处理
|
||||
if ($sort === 'child_count') {
|
||||
$agentTable = (new AgentModel())->getTable();
|
||||
$query->join($agentTable . ' a', 'u.id = a.user_id', 'LEFT')
|
||||
@@ -220,7 +245,7 @@ class Agent extends Commission
|
||||
}
|
||||
$query->field('u.*');
|
||||
|
||||
// 7. 分页并加载推荐人与分销商信息
|
||||
// 8. 分页并加载推荐人与分销商信息
|
||||
$data = $query->with([
|
||||
'parentUser' => function ($q) {
|
||||
$q->field('id, nickname, avatar, mobile');
|
||||
@@ -267,14 +292,14 @@ class Agent extends Commission
|
||||
$user->child_user_count_1 = 0;
|
||||
}
|
||||
|
||||
$user->team_level = ($user->parent_user_id == $agentId) ? 1 : 2;
|
||||
$user->team_level = 1;
|
||||
$rawMobile = $user->mobile ? (string) $user->mobile : '';
|
||||
$maskedMobile = $rawMobile ? (strlen($rawMobile) == 11 ? substr_replace($rawMobile, '****', 3, 4) : $rawMobile) : '';
|
||||
$user->mobile = $maskedMobile;
|
||||
$user->mobile_hide = $maskedMobile;
|
||||
$user->mobile_raw = $rawMobile;
|
||||
|
||||
// 间推所属推荐人手机号脱敏
|
||||
// 所属推荐人手机号脱敏
|
||||
if ($user->parentUser && $user->parentUser->mobile) {
|
||||
$pMobile = (string) $user->parentUser->mobile;
|
||||
$pMasked = strlen($pMobile) == 11 ? substr_replace($pMobile, '****', 3, 4) : $pMobile;
|
||||
@@ -283,28 +308,26 @@ class Agent extends Commission
|
||||
}
|
||||
});
|
||||
|
||||
// 9. 计算整体团队健康度与综合收益业绩统计
|
||||
$summary = $this->getTeamHealthSummary($agentId, $level1Ids, $windowStart, $expiringStart);
|
||||
// 9. 计算整体团队健康度与综合收益业绩统计(基于当前直推用户)
|
||||
$summary = $this->getTeamHealthSummary($targetUserId, $level1Ids, $windowStart, $expiringStart);
|
||||
|
||||
$result = $data->toArray();
|
||||
$result['summary'] = $summary;
|
||||
$result['drill_user'] = $drillUser;
|
||||
|
||||
$this->success("", $result);
|
||||
$this->success("获取成功", $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算团队保活健康度与综合收益统计
|
||||
* 计算团队保活健康度与综合收益统计(全面直推口径)
|
||||
*/
|
||||
protected function getTeamHealthSummary($agentId, $level1Ids, $windowStart, $expiringStart)
|
||||
protected function getTeamHealthSummary($targetUserId, $level1Ids, $windowStart, $expiringStart)
|
||||
{
|
||||
$allMembers = array_merge([$agentId], $level1Ids);
|
||||
$teamUserIds = UserModel::whereIn('parent_user_id', $allMembers)
|
||||
->where('status', 'normal')
|
||||
->column('id');
|
||||
$teamUserIds = !empty($level1Ids) ? $level1Ids : [];
|
||||
|
||||
if (empty($teamUserIds)) {
|
||||
$selfAgent = AgentModel::where('user_id', $agentId)->find();
|
||||
$selfUser = UserModel::where('id', $agentId)->find();
|
||||
$selfAgent = AgentModel::where('user_id', $targetUserId)->find();
|
||||
$selfUser = UserModel::where('id', $targetUserId)->find();
|
||||
return [
|
||||
'total_count' => 0,
|
||||
'active_count' => 0,
|
||||
@@ -351,8 +374,8 @@ class Agent extends Commission
|
||||
}
|
||||
|
||||
// 1. 团队累计自购总业绩与团队订单总数(统一使用包含本人自购的分销团队全口径)
|
||||
$selfAgent = AgentModel::where('user_id', $agentId)->find();
|
||||
$selfUser = UserModel::where('id', $agentId)->find();
|
||||
$selfAgent = AgentModel::where('user_id', $targetUserId)->find();
|
||||
$selfUser = UserModel::where('id', $targetUserId)->find();
|
||||
|
||||
$teamTotalConsume = isset($selfAgent->child_order_money_all) && floatval($selfAgent->child_order_money_all) > 0
|
||||
? floatval($selfAgent->child_order_money_all)
|
||||
@@ -362,13 +385,13 @@ class Agent extends Commission
|
||||
? intval($selfAgent->child_order_count_all)
|
||||
: ($teamOrderCount + intval($selfAgent->child_order_count_0 ?? 0));
|
||||
|
||||
// 2. 团队成员为当前用户贡献的累计收益与本月收益
|
||||
// 2. 团队成员为目标用户贡献的累计收益与本月收益
|
||||
$monthStart = strtotime(date('Y-m-01 00:00:00'));
|
||||
$teamTotalReward = RewardModel::where('user_id', $agentId)
|
||||
$teamTotalReward = RewardModel::where('user_id', $targetUserId)
|
||||
->whereIn('buyer_id', $teamUserIds)
|
||||
->where('status', 1)
|
||||
->sum('amount');
|
||||
$teamMonthReward = RewardModel::where('user_id', $agentId)
|
||||
$teamMonthReward = RewardModel::where('user_id', $targetUserId)
|
||||
->whereIn('buyer_id', $teamUserIds)
|
||||
->where('status', 1)
|
||||
->where('createtime', '>=', $monthStart)
|
||||
|
||||
208
tests/verify_team_drilldown_spec.js
普通文件
208
tests/verify_team_drilldown_spec.js
普通文件
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* 测试用例:验证团队中心移除二级间推并支持一级直推多层级下钻功能
|
||||
* 覆盖范围:
|
||||
* 1. 后端 Agent.php team() 接口:全面移除二级查询分支,默认一级直推
|
||||
* 2. 后端防越权链条鉴权机制:追溯 parent_user_id 防止跨团队越权下钻
|
||||
* 3. 前端 team.vue 界面:彻底清除“二级间推”、“二级成员”概念
|
||||
* 4. 前端下钻状态管理:drillStack 栈驱动、currentDrillUser 计算属性与 API 参数透传
|
||||
* 5. 前端导航与交互:面包屑导航栏、返回上一层、回到根团队、卡片下钻按钮、指标下钻与抽屉下钻卡片
|
||||
* 6. 完整业务流程模拟:多级下钻、路径回溯与越权拦截测试
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const assert = require('assert');
|
||||
|
||||
console.log('=== 开始执行团队中心一级直推与多层级下钻自动化测试 ===\n');
|
||||
|
||||
// 1. 验证后端 Agent.php 接口改造
|
||||
const agentPhpPath = path.resolve(__dirname, '../addons/shopro/controller/commission/Agent.php');
|
||||
assert(fs.existsSync(agentPhpPath), 'Agent.php 文件必须存在');
|
||||
const agentPhpContent = fs.readFileSync(agentPhpPath, 'utf-8');
|
||||
|
||||
// 1.1 检查下钻参数与鉴权
|
||||
assert(agentPhpContent.includes("drill_user_id"), 'Agent.php 必须接收 drill_user_id 参数');
|
||||
assert(agentPhpContent.includes("isDownline = true"), 'Agent.php 必须实现父级链条归属鉴权');
|
||||
assert(agentPhpContent.includes("无权查看非直属团队成员信息"), 'Agent.php 必须对非团队成员下钻进行严格权限拦截');
|
||||
assert(agentPhpContent.includes("drill_user"), 'Agent.php 必须在返回结果中包含下钻用户信息 drill_user');
|
||||
|
||||
// 1.2 检查彻底移除二级间推分支
|
||||
assert(!agentPhpContent.includes("$levelType == '2'"), 'Agent.php 必须彻底移除 levelType == 2 的二级间推分支');
|
||||
assert(agentPhpContent.includes("where('u.parent_user_id', $targetUserId)"), 'Agent.php 必须采用直推一级关系查询');
|
||||
|
||||
console.log('✅ 测试点 1 通过: 后端 Agent.php team() 接口直推与下钻链条鉴权逻辑正确');
|
||||
|
||||
// 2. 模拟后端向上追溯防越权鉴权算法测试
|
||||
function verifyDownlinePermission(loginAgentId, targetUserId, userHierarchyMap) {
|
||||
if (targetUserId === loginAgentId) return true; // 查看自己
|
||||
let curr = userHierarchyMap[targetUserId];
|
||||
if (!curr) return false; // 用户不存在
|
||||
|
||||
let depth = 0;
|
||||
let currParentId = curr.parent_user_id;
|
||||
while (currParentId > 0 && depth < 30) {
|
||||
if (currParentId === loginAgentId) {
|
||||
return true; // 确认是自己的下属团队链条
|
||||
}
|
||||
const parentUser = userHierarchyMap[currParentId];
|
||||
if (!parentUser) break;
|
||||
currParentId = parentUser.parent_user_id || 0;
|
||||
depth++;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 构建模拟团队关系树:
|
||||
// 登录用户: 100
|
||||
// 100 -> 101 (直推1级)
|
||||
// 101 -> 102 (直推下级,相对100是下属)
|
||||
// 102 -> 103 (继续深层直推下级)
|
||||
// 外部人员: 200 -> 201 (与100无关联)
|
||||
const mockUserDb = {
|
||||
100: { id: 100, parent_user_id: 0, nickname: '根用户' },
|
||||
101: { id: 101, parent_user_id: 100, nickname: '直推A' },
|
||||
102: { id: 102, parent_user_id: 101, nickname: '直推A的下级B' },
|
||||
103: { id: 103, parent_user_id: 102, nickname: '下级B的下级C' },
|
||||
200: { id: 200, parent_user_id: 0, nickname: '其他团队长' },
|
||||
201: { id: 201, parent_user_id: 200, nickname: '其他团队成员' },
|
||||
};
|
||||
|
||||
// 验证正常直属下钻
|
||||
assert.strictEqual(verifyDownlinePermission(100, 101, mockUserDb), true, '登录用户查看自己的一级直推 101 必须鉴权通过');
|
||||
assert.strictEqual(verifyDownlinePermission(100, 102, mockUserDb), true, '登录用户下钻二级成员 102 必须鉴权通过');
|
||||
assert.strictEqual(verifyDownlinePermission(100, 103, mockUserDb), true, '登录用户多级下钻三级成员 103 必须鉴权通过');
|
||||
|
||||
// 验证越权拦截
|
||||
assert.strictEqual(verifyDownlinePermission(100, 200, mockUserDb), false, '尝试越权下钻其他团队长 200 必须被拦截');
|
||||
assert.strictEqual(verifyDownlinePermission(100, 201, mockUserDb), false, '尝试越权下钻其他团队成员 201 必须被拦截');
|
||||
assert.strictEqual(verifyDownlinePermission(100, 9999, mockUserDb), false, '下钻不存在的用户必须被拦截');
|
||||
|
||||
console.log('✅ 测试点 2 通过: 防越权追溯鉴权算法通过(正常下钻放行、越权调用均被准确拦截)');
|
||||
|
||||
// 3. 验证前端 team.vue 页面
|
||||
const teamVuePath = path.resolve(__dirname, '../uniapp/pages/commission/team.vue');
|
||||
assert(fs.existsSync(teamVuePath), 'team.vue 文件必须存在');
|
||||
const teamVueContent = fs.readFileSync(teamVuePath, 'utf-8');
|
||||
|
||||
// 3.1 验证彻底移除“二级”文本
|
||||
const hasSecondaryText = teamVueContent.includes('二级间推') || teamVueContent.includes('二级成员');
|
||||
assert.strictEqual(hasSecondaryText, false, 'team.vue 页面中严禁存在任何“二级间推”或“二级成员”字样');
|
||||
|
||||
// 3.2 验证移除 levelTabs
|
||||
assert(!teamVueContent.includes('currentLevelTab'), 'team.vue 必须移除 currentLevelTab 状态');
|
||||
assert(!teamVueContent.includes('levelTabs'), 'team.vue 必须移除 levelTabs 变量');
|
||||
|
||||
// 3.3 验证下钻状态管理
|
||||
assert(teamVueContent.includes('drillStack: []'), 'team.vue 必须声明 drillStack 状态');
|
||||
assert(teamVueContent.includes('currentDrillUser = computed'), 'team.vue 必须包含 currentDrillUser 计算属性');
|
||||
assert(teamVueContent.includes('onDrillDown'), 'team.vue 必须包含 onDrillDown 下钻方法');
|
||||
assert(teamVueContent.includes('onDrillBack'), 'team.vue 必须包含 onDrillBack 返回上一层方法');
|
||||
assert(teamVueContent.includes('onDrillRoot'), 'team.vue 必须包含 onDrillRoot 回到根团队方法');
|
||||
assert(teamVueContent.includes('onJumpDrillIndex'), 'team.vue 必须包含 onJumpDrillIndex 面包屑跳跃方法');
|
||||
assert(teamVueContent.includes('onDrillFromDrawer'), 'team.vue 必须包含从抽屉中下钻的方法');
|
||||
assert(teamVueContent.includes('drill_user_id: currentDrillUser.value'), 'team.vue getTeamList 必须传递 drill_user_id');
|
||||
|
||||
// 3.4 验证下钻 UI 元素渲染
|
||||
assert(teamVueContent.includes('drill-nav-bar'), 'team.vue 必须渲染 drill-nav-bar 下钻导航栏');
|
||||
assert(teamVueContent.includes('drill-back-btn'), 'team.vue 必须包含返回上一层按钮');
|
||||
assert(teamVueContent.includes('drill-breadcrumb'), 'team.vue 必须包含层级面包屑路径');
|
||||
assert(teamVueContent.includes('btn-drill'), 'team.vue 成员卡片必须包含下钻团队按钮');
|
||||
assert(teamVueContent.includes('drawer-drill-card'), 'team.vue 详情抽屉必须包含下钻团队卡片');
|
||||
|
||||
console.log('✅ 测试点 3 通过: 前端 team.vue 状态管理、下钻面包屑导航与交互按钮规范');
|
||||
|
||||
// 4. 模拟前端下钻堆栈业务状态机流转测试
|
||||
class MockTeamDrillController {
|
||||
constructor() {
|
||||
this.drillStack = [];
|
||||
this.currentFilter = 'all';
|
||||
this.keyword = '';
|
||||
this.lastParams = null;
|
||||
}
|
||||
|
||||
get currentDrillUser() {
|
||||
if (this.drillStack.length === 0) return null;
|
||||
return this.drillStack[this.drillStack.length - 1];
|
||||
}
|
||||
|
||||
getTeamList(page = 1) {
|
||||
this.lastParams = {
|
||||
page,
|
||||
drill_user_id: this.currentDrillUser ? this.currentDrillUser.id : 0,
|
||||
filter_type: this.currentFilter,
|
||||
keyword: this.keyword,
|
||||
};
|
||||
return this.lastParams;
|
||||
}
|
||||
|
||||
onDrillDown(member) {
|
||||
if (!member || !member.id) return;
|
||||
if (this.drillStack.some((item) => item.id === member.id)) return;
|
||||
this.drillStack.push({
|
||||
id: member.id,
|
||||
nickname: member.nickname || '直推成员',
|
||||
child_user_count_1: member.child_user_count_1 || 0,
|
||||
});
|
||||
this.keyword = '';
|
||||
return this.getTeamList(1);
|
||||
}
|
||||
|
||||
onDrillBack() {
|
||||
if (this.drillStack.length === 0) return;
|
||||
this.drillStack.pop();
|
||||
this.keyword = '';
|
||||
return this.getTeamList(1);
|
||||
}
|
||||
|
||||
onDrillRoot() {
|
||||
if (this.drillStack.length === 0) return;
|
||||
this.drillStack = [];
|
||||
this.keyword = '';
|
||||
return this.getTeamList(1);
|
||||
}
|
||||
|
||||
onJumpDrillIndex(index) {
|
||||
if (index >= this.drillStack.length - 1) return;
|
||||
this.drillStack = this.drillStack.slice(0, index + 1);
|
||||
this.keyword = '';
|
||||
return this.getTeamList(1);
|
||||
}
|
||||
}
|
||||
|
||||
const controller = new MockTeamDrillController();
|
||||
|
||||
// 步骤 1: 初始化根列表
|
||||
const initReq = controller.getTeamList(1);
|
||||
assert.strictEqual(initReq.drill_user_id, 0, '根列表请求 drill_user_id 必须为 0');
|
||||
assert.strictEqual(controller.currentDrillUser, null, '根目录下 currentDrillUser 必须为 null');
|
||||
|
||||
// 步骤 2: 点击成员 101 下钻
|
||||
const drill1Req = controller.onDrillDown({ id: 101, nickname: '张三', child_user_count_1: 5 });
|
||||
assert.strictEqual(drill1Req.drill_user_id, 101, '一级下钻请求 drill_user_id 必须为 101');
|
||||
assert.strictEqual(controller.drillStack.length, 1, '下钻栈深度应为 1');
|
||||
assert.strictEqual(controller.currentDrillUser.nickname, '张三', '当前下钻用户应为张三');
|
||||
|
||||
// 步骤 3: 在 101 名下继续点击成员 102 下钻
|
||||
const drill2Req = controller.onDrillDown({ id: 102, nickname: '李四', child_user_count_1: 2 });
|
||||
assert.strictEqual(drill2Req.drill_user_id, 102, '二级下钻请求 drill_user_id 必须为 102');
|
||||
assert.strictEqual(controller.drillStack.length, 2, '下钻栈深度应为 2');
|
||||
assert.strictEqual(controller.currentDrillUser.nickname, '李四', '当前下钻用户应为李四');
|
||||
|
||||
// 步骤 4: 点击“返回上一层”
|
||||
const backReq = controller.onDrillBack();
|
||||
assert.strictEqual(backReq.drill_user_id, 101, '返回上一层后请求 drill_user_id 必须回到 101');
|
||||
assert.strictEqual(controller.drillStack.length, 1, '返回后下钻栈深度应为 1');
|
||||
assert.strictEqual(controller.currentDrillUser.nickname, '张三', '当前下钻用户应恢复为张三');
|
||||
|
||||
// 步骤 5: 再次深层下钻并一键“回到根团队”
|
||||
controller.onDrillDown({ id: 102, nickname: '李四' });
|
||||
controller.onDrillDown({ id: 103, nickname: '王五' });
|
||||
assert.strictEqual(controller.drillStack.length, 3);
|
||||
const rootReq = controller.onDrillRoot();
|
||||
assert.strictEqual(rootReq.drill_user_id, 0, '一键回到根团队后 drill_user_id 必须为 0');
|
||||
assert.strictEqual(controller.drillStack.length, 0, '下钻栈必须被清空');
|
||||
assert.strictEqual(controller.currentDrillUser, null, '当前下钻用户必须为 null');
|
||||
|
||||
console.log('✅ 测试点 4 通过: 前端下钻堆栈与状态流转业务逻辑全部验证通过');
|
||||
|
||||
console.log('\n🎉 所有测试均通过!团队中心一级直推与多层级下钻功能验证完毕!');
|
||||
@@ -41,65 +41,47 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 团队核心数据总览卡片 -->
|
||||
<!-- 团队核心数据总览卡片(一级直推与合伙人看板) -->
|
||||
<view class="team-data-box ss-flex ss-col-center ss-row-between">
|
||||
<!-- 团队总人数卡片 -->
|
||||
<!-- 直推成员总数卡片 -->
|
||||
<view
|
||||
class="data-card"
|
||||
:class="{ 'card-active': state.currentFilter === 'all' && state.currentLevelTab === 'all' }"
|
||||
@tap="onSelectHeaderTab('all', 'all')"
|
||||
class="data-card ss-flex-1 ss-m-r-16"
|
||||
:class="{ 'card-active': state.currentFilter === 'all' }"
|
||||
@tap="onSelectHeaderTab('all')"
|
||||
>
|
||||
<view class="total-item">
|
||||
<view class="item-title">团队总人数(人)</view>
|
||||
<view class="total-num">{{ headerNum('child_user_count_all') }}</view>
|
||||
<view class="item-title">
|
||||
{{ currentDrillUser ? (currentDrillUser.nickname + '的直推(人)') : '直推成员总数(人)' }}
|
||||
</view>
|
||||
<view class="total-num">
|
||||
{{ currentDrillUser ? (state.summary.total_count || currentDrillUser.child_user_count_1 || 0) : headerNum('child_user_count_1') }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="category-item ss-flex">
|
||||
<view
|
||||
class="ss-flex-1 sub-tab-item"
|
||||
:class="{ 'sub-active': state.currentLevelTab === '1' && state.currentFilter !== 'partner' }"
|
||||
@tap.stop="onSelectHeaderTab('1', 'all')"
|
||||
>
|
||||
<view class="item-title">一级直推</view>
|
||||
<view class="category-num">{{ headerNum('child_user_count_1') }}</view>
|
||||
</view>
|
||||
<view
|
||||
class="ss-flex-1 sub-tab-item"
|
||||
:class="{ 'sub-active': state.currentLevelTab === '2' && state.currentFilter !== 'partner' }"
|
||||
@tap.stop="onSelectHeaderTab('2', 'all')"
|
||||
>
|
||||
<view class="item-title">二级间推</view>
|
||||
<view class="category-num">{{ headerNum('child_user_count_2') }}</view>
|
||||
</view>
|
||||
<view class="category-item ss-flex ss-col-center ss-row-between">
|
||||
<text class="category-label">活跃保活:</text>
|
||||
<text class="category-val text-green">{{ state.summary.active_count || 0 }} 人</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 团队成员人数卡片 -->
|
||||
<!-- 直推分销合伙人卡片 -->
|
||||
<view
|
||||
class="data-card"
|
||||
class="data-card ss-flex-1"
|
||||
:class="{ 'card-active': state.currentFilter === 'partner' }"
|
||||
@tap="onSelectHeaderTab('all', 'partner')"
|
||||
@tap="onSelectHeaderTab('partner')"
|
||||
>
|
||||
<view class="total-item">
|
||||
<view class="item-title">分销成员总数(人)</view>
|
||||
<view class="total-num text-gold">{{ headerNum('child_agent_count_all') }}</view>
|
||||
<view class="item-title">
|
||||
{{ currentDrillUser ? (currentDrillUser.nickname + '的合伙人') : '直推合伙股东(人)' }}
|
||||
</view>
|
||||
<view class="total-num text-gold">
|
||||
{{ currentDrillUser ? (state.summary.partner_count || 0) : headerNum('child_agent_count_1') }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="category-item ss-flex">
|
||||
<view
|
||||
class="ss-flex-1 sub-tab-item"
|
||||
:class="{ 'sub-active': state.currentFilter === 'partner' && state.currentLevelTab === '1' }"
|
||||
@tap.stop="onSelectHeaderTab('1', 'partner')"
|
||||
>
|
||||
<view class="item-title">一级成员</view>
|
||||
<view class="category-num text-gold">{{ headerNum('child_agent_count_1') }}</view>
|
||||
</view>
|
||||
<view
|
||||
class="ss-flex-1 sub-tab-item"
|
||||
:class="{ 'sub-active': state.currentFilter === 'partner' && state.currentLevelTab === '2' }"
|
||||
@tap.stop="onSelectHeaderTab('2', 'partner')"
|
||||
>
|
||||
<view class="item-title">二级成员</view>
|
||||
<view class="category-num text-gold">{{ headerNum('child_agent_count_2') }}</view>
|
||||
</view>
|
||||
<view class="category-item ss-flex ss-col-center ss-row-between">
|
||||
<text class="category-label">临期/暂停:</text>
|
||||
<text class="category-val text-yellow">
|
||||
{{ Number(state.summary.expiring_count || 0) + Number(state.summary.inactive_count || 0) }} 人
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -173,17 +155,44 @@
|
||||
<button class="ss-reset-button search-btn" @tap="onSearchConfirm">搜索</button>
|
||||
</view>
|
||||
|
||||
<!-- 层级 Tab 切换栏 -->
|
||||
<view class="level-tabs ss-flex">
|
||||
<view
|
||||
v-for="tab in levelTabs"
|
||||
:key="tab.value"
|
||||
class="level-tab-item ss-flex-1"
|
||||
:class="{ 'level-tab-active': state.currentLevelTab === tab.value }"
|
||||
@tap="onLevelTabChange(tab.value)"
|
||||
>
|
||||
{{ tab.name }}
|
||||
<!-- 层级下钻路径与导航栏 -->
|
||||
<view v-if="state.drillStack.length === 0" class="drill-nav-bar root-nav ss-flex ss-row-between ss-col-center">
|
||||
<view class="nav-left ss-flex ss-col-center">
|
||||
<view class="nav-icon-box ss-flex ss-row-center ss-col-center">
|
||||
<uni-icons type="personadd-filled" size="14" color="#ff6000"></uni-icons>
|
||||
</view>
|
||||
<text class="nav-root-title ss-m-l-10">我的直推团队</text>
|
||||
<text class="nav-root-count ss-m-l-8">({{ state.pagination.total || 0 }}人)</text>
|
||||
</view>
|
||||
<view class="nav-hint ss-flex ss-col-center">
|
||||
<text class="hint-text">点击成员可下钻其直推</text>
|
||||
<uni-icons type="forward" size="12" color="#999999"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="drill-nav-bar in-drill-nav ss-flex ss-row-between ss-col-center">
|
||||
<view class="drill-back-btn ss-flex ss-col-center" @tap="onDrillBack">
|
||||
<uni-icons type="left" size="16" color="#ff6000"></uni-icons>
|
||||
<text class="back-text">返回上一层</text>
|
||||
</view>
|
||||
<view class="drill-breadcrumb ss-flex ss-col-center ss-flex-1 ss-m-x-12">
|
||||
<scroll-view scroll-x :show-scrollbar="false" class="breadcrumb-scroll">
|
||||
<view class="breadcrumb-inner ss-flex ss-col-center">
|
||||
<text class="crumb-link" @tap="onDrillRoot">我的直推</text>
|
||||
<template v-for="(node, index) in state.drillStack" :key="node.id">
|
||||
<text class="crumb-sep">/</text>
|
||||
<text
|
||||
class="crumb-node"
|
||||
:class="{ 'crumb-current': index === state.drillStack.length - 1 }"
|
||||
@tap="onJumpDrillIndex(index)"
|
||||
>
|
||||
{{ node.nickname }}
|
||||
</text>
|
||||
</template>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<button class="ss-reset-button drill-reset-btn" @tap="onDrillRoot">回到我的</button>
|
||||
</view>
|
||||
|
||||
<!-- 快捷多维胶囊筛选条 -->
|
||||
@@ -232,11 +241,8 @@
|
||||
<view class="member-profile ss-m-l-16">
|
||||
<view class="member-name-row ss-flex ss-col-center">
|
||||
<text class="member-name ss-line-1">{{ item.nickname || '微信用户' }}</text>
|
||||
<text
|
||||
class="level-badge"
|
||||
:class="item.team_level === 1 ? 'badge-direct' : 'badge-indirect'"
|
||||
>
|
||||
{{ item.team_level === 1 ? '一级直推' : '二级间推' }}
|
||||
<text class="level-badge badge-direct">
|
||||
{{ state.drillStack.length > 0 ? '直推成员' : '一级直推' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="member-sub-info ss-flex ss-col-center ss-m-t-4" v-if="item.mobile_hide || item.id">
|
||||
@@ -293,25 +299,20 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 间推成员归属信息条 -->
|
||||
<view
|
||||
v-if="item.team_level === 2 && item.parent_user"
|
||||
class="referral-trace-bar ss-flex ss-col-center ss-m-t-16"
|
||||
>
|
||||
<text class="trace-label">所属推荐人:</text>
|
||||
<image
|
||||
class="trace-avatar ss-m-r-8"
|
||||
:src="sheep.$url.cdn(item.parent_user.avatar)"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<text class="trace-name">{{ item.parent_user.nickname }}(直推一级)</text>
|
||||
</view>
|
||||
|
||||
<!-- 经营指标看板 -->
|
||||
<view class="metrics-grid ss-flex ss-col-center ss-row-between ss-m-t-20">
|
||||
<view class="metric-col ss-flex-col ss-col-center">
|
||||
<view class="metric-val">{{ item.child_user_count_1 || 0 }}</view>
|
||||
<view class="metric-lbl">推荐好友(人)</view>
|
||||
<view
|
||||
class="metric-col ss-flex-col ss-col-center"
|
||||
:class="{ 'metric-clickable': Number(item.child_user_count_1) > 0 }"
|
||||
@tap.stop="Number(item.child_user_count_1) > 0 && onDrillDown(item)"
|
||||
>
|
||||
<view class="metric-val-wrap ss-flex ss-col-center">
|
||||
<text class="metric-val" :class="{ 'text-orange': Number(item.child_user_count_1) > 0 }">
|
||||
{{ item.child_user_count_1 || 0 }}
|
||||
</text>
|
||||
<text v-if="Number(item.child_user_count_1) > 0" class="drill-tag ss-m-l-6">下钻</text>
|
||||
</view>
|
||||
<view class="metric-lbl">直推好友(人)</view>
|
||||
</view>
|
||||
<view class="metric-divider"></view>
|
||||
<view class="metric-col ss-flex-col ss-col-center">
|
||||
@@ -338,31 +339,39 @@
|
||||
注册时间:{{ formatTime(item.createtime) }}
|
||||
</view>
|
||||
<view class="actions-wrap ss-flex ss-col-center">
|
||||
<button
|
||||
v-if="Number(item.child_user_count_1) > 0"
|
||||
class="ss-reset-button action-btn btn-drill"
|
||||
@tap.stop="onDrillDown(item)"
|
||||
>
|
||||
<uni-icons type="staff-filled" size="13" color="#ff6000"></uni-icons>
|
||||
<text class="ss-m-l-4">下钻团队</text>
|
||||
</button>
|
||||
<button
|
||||
v-if="item.mobile_raw"
|
||||
class="ss-reset-button action-btn btn-call"
|
||||
class="ss-reset-button action-btn btn-call ss-m-l-10"
|
||||
@tap.stop="onMakePhoneCall(item.mobile_raw)"
|
||||
>
|
||||
<uni-icons type="phone-filled" size="14" color="#ff6000"></uni-icons>
|
||||
<text class="ss-m-l-6">拨打</text>
|
||||
<uni-icons type="phone-filled" size="13" color="#ff6000"></uni-icons>
|
||||
<text class="ss-m-l-4">拨打</text>
|
||||
</button>
|
||||
<button
|
||||
v-if="item.mobile_raw || item.mobile_hide"
|
||||
class="ss-reset-button action-btn btn-copy ss-m-l-12"
|
||||
class="ss-reset-button action-btn btn-copy ss-m-l-10"
|
||||
@tap.stop="onCopyText(item.mobile_raw || item.mobile_hide, '手机号已复制')"
|
||||
>
|
||||
<uni-icons type="paperclip" size="14" color="#666666"></uni-icons>
|
||||
<text class="ss-m-l-6">复制手机</text>
|
||||
<uni-icons type="paperclip" size="13" color="#666666"></uni-icons>
|
||||
<text class="ss-m-l-4">复制</text>
|
||||
</button>
|
||||
<button
|
||||
class="ss-reset-button action-btn btn-reward ss-m-l-12"
|
||||
class="ss-reset-button action-btn btn-reward ss-m-l-10"
|
||||
@tap.stop="onJumpRewards(item.id, item.nickname)"
|
||||
>
|
||||
<uni-icons type="wallet" size="14" color="#ff6000"></uni-icons>
|
||||
<text class="ss-m-l-6">收益</text>
|
||||
<uni-icons type="wallet" size="13" color="#ff6000"></uni-icons>
|
||||
<text class="ss-m-l-4">收益</text>
|
||||
</button>
|
||||
<button
|
||||
class="ss-reset-button action-btn btn-detail ss-m-l-12"
|
||||
class="ss-reset-button action-btn btn-detail ss-m-l-10"
|
||||
@tap.stop="onOpenMemberDetail(item)"
|
||||
>
|
||||
<text>详情</text>
|
||||
@@ -376,7 +385,7 @@
|
||||
<!-- 空状态处理与行动引导 -->
|
||||
<view v-if="state.pagination.total === 0 && state.loadStatus !== 'loading'" class="empty-wrap">
|
||||
<!-- 搜索或筛选结果为空 -->
|
||||
<view v-if="state.keyword || state.currentFilter !== 'all' || state.currentLevelTab !== 'all'" class="filter-empty ss-flex-col ss-col-center">
|
||||
<view v-if="state.keyword || state.currentFilter !== 'all' || state.drillStack.length > 0" class="filter-empty ss-flex-col ss-col-center">
|
||||
<s-empty icon="/static/data-empty.png" text="未找到符合条件的成员"></s-empty>
|
||||
<button class="ss-reset-button reset-filter-btn ss-m-t-20" @tap="resetFilters">
|
||||
重置所有筛选
|
||||
@@ -421,12 +430,7 @@
|
||||
<view class="drawer-user-info">
|
||||
<view class="drawer-nickname ss-flex ss-col-center">
|
||||
<text class="ss-font-32 ss-bold ss-m-r-12">{{ state.currentMember.nickname }}</text>
|
||||
<text
|
||||
class="level-badge"
|
||||
:class="state.currentMember.team_level === 1 ? 'badge-direct' : 'badge-indirect'"
|
||||
>
|
||||
{{ state.currentMember.team_level === 1 ? '直推一级' : '间推二级' }}
|
||||
</text>
|
||||
<text class="level-badge badge-direct">直推成员</text>
|
||||
</view>
|
||||
<view class="drawer-sub ss-flex ss-col-center ss-m-t-8">
|
||||
<text class="user-id">ID: {{ state.currentMember.id }}</text>
|
||||
@@ -513,7 +517,7 @@
|
||||
</view>
|
||||
|
||||
<!-- 经营产出看板 -->
|
||||
<view class="drawer-section ss-m-b-30">
|
||||
<view class="drawer-section ss-m-b-24">
|
||||
<view class="section-title">团队与业绩产出</view>
|
||||
<view class="section-box performance-box ss-flex ss-row-between">
|
||||
<view class="perf-col ss-flex-col ss-col-center">
|
||||
@@ -531,6 +535,27 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 下钻查看直推团队卡片 -->
|
||||
<view
|
||||
v-if="Number(state.currentMember.child_user_count_1) > 0"
|
||||
class="drawer-drill-card ss-m-b-24 ss-flex ss-row-between ss-col-center"
|
||||
@tap="onDrillFromDrawer(state.currentMember)"
|
||||
>
|
||||
<view class="drill-card-left ss-flex ss-col-center">
|
||||
<view class="drill-icon-box ss-flex ss-row-center ss-col-center">
|
||||
<uni-icons type="staff-filled" size="20" color="#ff6000"></uni-icons>
|
||||
</view>
|
||||
<view class="drill-info ss-m-l-16">
|
||||
<view class="drill-title">下钻查看 Ta 的直推团队</view>
|
||||
<view class="drill-desc">Ta 名下共有 {{ state.currentMember.child_user_count_1 }} 位直推成员</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="drill-card-right ss-flex ss-col-center">
|
||||
<text class="drill-btn-text">立即进入</text>
|
||||
<uni-icons type="right" size="14" color="#ff6000"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 触达与管理动作条 -->
|
||||
<view class="drawer-actions ss-flex ss-col-center ss-row-between">
|
||||
<button
|
||||
@@ -589,12 +614,6 @@
|
||||
state.scrollTop = e.scrollTop > 100;
|
||||
});
|
||||
|
||||
const levelTabs = [
|
||||
{ name: '全部成员', value: 'all' },
|
||||
{ name: '一级直推', value: '1' },
|
||||
{ name: '二级间推', value: '2' },
|
||||
];
|
||||
|
||||
const filterPills = computed(() => [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: roleNames.value.partner || '合伙股东', value: 'partner' },
|
||||
@@ -612,8 +631,8 @@
|
||||
const state = reactive({
|
||||
scrollTop: false,
|
||||
keyword: '',
|
||||
currentLevelTab: 'all', // all, '1', '2'
|
||||
currentFilter: 'all', // all, partner, active, expiring, inactive
|
||||
drillStack: [], // 下钻栈:[{ id, nickname, avatar, child_user_count_1 }]
|
||||
currentFilter: 'all', // all, partner, active, expiring, inactive
|
||||
currentSort: 'createtime', // createtime, child_count, consume
|
||||
showDetail: false,
|
||||
currentMember: null,
|
||||
@@ -637,6 +656,12 @@
|
||||
loadStatus: '',
|
||||
});
|
||||
|
||||
// 当前正在下钻的目标成员
|
||||
const currentDrillUser = computed(() => {
|
||||
if (!state.drillStack || state.drillStack.length === 0) return null;
|
||||
return state.drillStack[state.drillStack.length - 1];
|
||||
});
|
||||
|
||||
function headerNum(field) {
|
||||
const stored = Number(agentInfo.value?.[field] || 0);
|
||||
if (stored > 0) {
|
||||
@@ -697,8 +722,7 @@
|
||||
}
|
||||
|
||||
// 头部卡片联动
|
||||
function onSelectHeaderTab(level, filter) {
|
||||
state.currentLevelTab = level;
|
||||
function onSelectHeaderTab(filter) {
|
||||
state.currentFilter = filter;
|
||||
state.currentSort = 'createtime';
|
||||
getTeamList(1);
|
||||
@@ -714,13 +738,6 @@
|
||||
getTeamList(1);
|
||||
}
|
||||
|
||||
// 层级Tab切换
|
||||
function onLevelTabChange(level) {
|
||||
if (state.currentLevelTab === level) return;
|
||||
state.currentLevelTab = level;
|
||||
getTeamList(1);
|
||||
}
|
||||
|
||||
// 筛选胶囊切换
|
||||
function onFilterChipChange(filter) {
|
||||
if (state.currentFilter === filter && state.currentSort === 'createtime') return;
|
||||
@@ -748,12 +765,55 @@
|
||||
|
||||
function resetFilters() {
|
||||
state.keyword = '';
|
||||
state.currentLevelTab = 'all';
|
||||
state.currentFilter = 'all';
|
||||
state.currentSort = 'createtime';
|
||||
state.drillStack = [];
|
||||
getTeamList(1);
|
||||
}
|
||||
|
||||
// 成员下钻逻辑
|
||||
function onDrillDown(member) {
|
||||
if (!member || !member.id) return;
|
||||
if (state.drillStack.some((item) => item.id === member.id)) {
|
||||
sheep.$helper.toast('当前已在此成员层级中');
|
||||
return;
|
||||
}
|
||||
state.drillStack.push({
|
||||
id: member.id,
|
||||
nickname: member.nickname || '直推成员',
|
||||
avatar: member.avatar,
|
||||
child_user_count_1: member.child_user_count_1 || 0,
|
||||
});
|
||||
state.keyword = '';
|
||||
getTeamList(1);
|
||||
}
|
||||
|
||||
function onDrillBack() {
|
||||
if (state.drillStack.length === 0) return;
|
||||
state.drillStack.pop();
|
||||
state.keyword = '';
|
||||
getTeamList(1);
|
||||
}
|
||||
|
||||
function onDrillRoot() {
|
||||
if (state.drillStack.length === 0) return;
|
||||
state.drillStack = [];
|
||||
state.keyword = '';
|
||||
getTeamList(1);
|
||||
}
|
||||
|
||||
function onJumpDrillIndex(index) {
|
||||
if (index >= state.drillStack.length - 1) return;
|
||||
state.drillStack = state.drillStack.slice(0, index + 1);
|
||||
state.keyword = '';
|
||||
getTeamList(1);
|
||||
}
|
||||
|
||||
function onDrillFromDrawer(member) {
|
||||
state.showDetail = false;
|
||||
onDrillDown(member);
|
||||
}
|
||||
|
||||
// 触达动作
|
||||
function onMakePhoneCall(phone) {
|
||||
if (!phone) {
|
||||
@@ -816,7 +876,7 @@
|
||||
const params = {
|
||||
page,
|
||||
list_rows,
|
||||
level_type: state.currentLevelTab,
|
||||
drill_user_id: currentDrillUser.value ? currentDrillUser.value.id : 0,
|
||||
filter_type: state.currentFilter,
|
||||
sort: state.currentSort,
|
||||
keyword: state.keyword,
|
||||
@@ -837,6 +897,7 @@
|
||||
state.loadStatus = 'noMore';
|
||||
}
|
||||
} else {
|
||||
sheep.$helper.toast(res.msg || '获取团队列表失败');
|
||||
state.loadStatus = 'noMore';
|
||||
}
|
||||
}
|
||||
@@ -995,30 +1056,23 @@
|
||||
border-top: 1rpx solid #f2f2f2;
|
||||
padding-top: 14rpx;
|
||||
|
||||
.sub-tab-item {
|
||||
padding: 4rpx 8rpx;
|
||||
border-radius: 8rpx;
|
||||
transition: all 0.2s;
|
||||
.category-label {
|
||||
font-size: 22rpx;
|
||||
color: #999999;
|
||||
}
|
||||
.category-val {
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
font-family: OPPOSANS;
|
||||
|
||||
&.sub-active {
|
||||
background: #fff5eb;
|
||||
.item-title {
|
||||
color: #ff6000;
|
||||
font-weight: bold;
|
||||
}
|
||||
.category-num {
|
||||
color: #ff6000;
|
||||
}
|
||||
&.text-green {
|
||||
color: #10b981;
|
||||
}
|
||||
&.text-yellow {
|
||||
color: #f59e0b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.category-num {
|
||||
font-size: 26rpx;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
font-family: OPPOSANS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1164,33 +1218,105 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 层级 Tab
|
||||
.level-tabs {
|
||||
// 下钻导航与面包屑
|
||||
.drill-nav-bar {
|
||||
border-bottom: 1rpx solid #f2f2f2;
|
||||
padding: 16rpx 24rpx;
|
||||
|
||||
.level-tab-item {
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: #666666;
|
||||
padding: 18rpx 0;
|
||||
position: relative;
|
||||
|
||||
&.level-tab-active {
|
||||
color: #ff6000;
|
||||
font-weight: bold;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 48rpx;
|
||||
height: 4rpx;
|
||||
background: #ff6000;
|
||||
border-radius: 2rpx;
|
||||
&.root-nav {
|
||||
background: #fafbfc;
|
||||
.nav-left {
|
||||
.nav-icon-box {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
border-radius: 50%;
|
||||
background: #fff0e6;
|
||||
}
|
||||
.nav-root-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: bold;
|
||||
color: #222222;
|
||||
}
|
||||
.nav-root-count {
|
||||
font-size: 24rpx;
|
||||
color: #ff6000;
|
||||
font-weight: 500;
|
||||
font-family: OPPOSANS;
|
||||
}
|
||||
}
|
||||
.nav-hint {
|
||||
.hint-text {
|
||||
font-size: 22rpx;
|
||||
color: #999999;
|
||||
margin-right: 4rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.in-drill-nav {
|
||||
background: #fff8f2;
|
||||
.drill-back-btn {
|
||||
padding: 8rpx 14rpx;
|
||||
border-radius: 24rpx;
|
||||
background: #ffffff;
|
||||
border: 1rpx solid #ffd8bf;
|
||||
box-shadow: 0 2rpx 6rpx rgba(255, 96, 0, 0.08);
|
||||
|
||||
.back-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
color: #ff6000;
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.drill-breadcrumb {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.breadcrumb-scroll {
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.breadcrumb-inner {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
.crumb-link {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.crumb-sep {
|
||||
font-size: 22rpx;
|
||||
color: #bbbbbb;
|
||||
margin: 0 8rpx;
|
||||
}
|
||||
|
||||
.crumb-node {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
|
||||
&.crumb-current {
|
||||
color: #ff6000;
|
||||
font-weight: bold;
|
||||
background: #fff0e6;
|
||||
padding: 2rpx 12rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.drill-reset-btn {
|
||||
font-size: 22rpx;
|
||||
color: #ff6000;
|
||||
padding: 6rpx 14rpx;
|
||||
border-radius: 20rpx;
|
||||
background: rgba(255, 96, 0, 0.1);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1343,26 +1469,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 间推推荐人轨迹条
|
||||
.referral-trace-bar {
|
||||
background: #f9fafb;
|
||||
border-radius: 10rpx;
|
||||
padding: 8rpx 14rpx;
|
||||
font-size: 22rpx;
|
||||
color: #6b7280;
|
||||
|
||||
.trace-avatar {
|
||||
width: 30rpx;
|
||||
height: 30rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.trace-name {
|
||||
color: #374151;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 指标网格
|
||||
.metrics-grid {
|
||||
background: #fafafc;
|
||||
@@ -1373,12 +1479,33 @@
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
|
||||
&.metric-clickable {
|
||||
cursor: pointer;
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.metric-val {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #222222;
|
||||
font-family: OPPOSANS;
|
||||
line-height: 34rpx;
|
||||
|
||||
&.text-orange {
|
||||
color: #ff6000;
|
||||
}
|
||||
}
|
||||
|
||||
.drill-tag {
|
||||
font-size: 18rpx;
|
||||
color: #ff6000;
|
||||
background: #fff0e6;
|
||||
padding: 2rpx 8rpx;
|
||||
border-radius: 6rpx;
|
||||
line-height: 22rpx;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.metric-lbl {
|
||||
@@ -1414,6 +1541,12 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
&.btn-drill {
|
||||
background: linear-gradient(90deg, #fff0e6, #ffe6d6);
|
||||
color: #ff6000;
|
||||
font-weight: bold;
|
||||
border: 1rpx solid #ffd8bf;
|
||||
}
|
||||
&.btn-call {
|
||||
background: #fff0e6;
|
||||
color: #ff6000;
|
||||
@@ -1594,6 +1727,53 @@
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-drill-card {
|
||||
background: linear-gradient(135deg, #fff7ed 0%, #ffedd5 100%);
|
||||
border: 1rpx solid #fed7aa;
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:active {
|
||||
opacity: 0.9;
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
.drill-card-left {
|
||||
.drill-icon-box {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 28rpx;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 4rpx 10rpx rgba(255, 96, 0, 0.15);
|
||||
}
|
||||
|
||||
.drill-info {
|
||||
.drill-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: bold;
|
||||
color: #9a3412;
|
||||
line-height: 34rpx;
|
||||
}
|
||||
.drill-desc {
|
||||
font-size: 22rpx;
|
||||
color: #c2410c;
|
||||
margin-top: 4rpx;
|
||||
line-height: 28rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.drill-card-right {
|
||||
.drill-btn-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
color: #ea580c;
|
||||
margin-right: 4rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-actions {
|
||||
.drawer-btn {
|
||||
height: 72rpx;
|
||||
|
||||
在新工单中引用
屏蔽一个用户