修改河南项目的管理后台ui
This commit is contained in:
@@ -160,6 +160,133 @@ SELECT COUNT(*) FROM sys_role_menu WHERE menu_id=3208;
|
|||||||
2. 水位从 0 起步。`enabled=true` 时前几件货会落在收紧档(水位低 → 随机数高),属预期行为;
|
2. 水位从 0 起步。`enabled=true` 时前几件货会落在收紧档(水位低 → 随机数高),属预期行为;
|
||||||
需要立刻放水用后台「奖池水位 → 注水」。
|
需要立刻放水用后台「奖池水位 → 注水」。
|
||||||
|
|
||||||
|
## 管理后台按钮级权限补全:`sys_menu` 补 24 行 `F`(按钮)记录
|
||||||
|
|
||||||
|
**背景**:后台按钮的显隐走前端 `v-hasPermi`(`ruoyi-ui/src/directive/permission/hasPermi.js`),
|
||||||
|
接口放行走后端 `@PreAuthorize("@ss.hasPermi('...')")`,**两边比对的是同一个 `sys_menu.perms`**
|
||||||
|
(超管 `user_id=1` 拿 `*:*:*`,其他人拿「已授权菜单行的 `perms` 去重集合」,见
|
||||||
|
`ruoyi-framework/.../SysPermissionService.java:55`)。
|
||||||
|
|
||||||
|
历史遗留:**从 `menu_id` 31xx 段开始,新增页面只建了 `C` 行、没建 `F` 行**
|
||||||
|
(`F` 行分布:1000 段 61 行、2000 段 18 行、3100 段仅 9 行、3200+ 段 0 行)。
|
||||||
|
后果是这些页面的按钮在角色授权树里**根本勾不到** ⇒ 除超管外谁都看不见;
|
||||||
|
更严重的是部分 **列表/查询接口也会 403**,页面进去是空的。
|
||||||
|
|
||||||
|
盘点结果:代码在用、但 `sys_menu` 未登记的权限串共 **43 个**。其中 **23 个属于 4 个无菜单入口的孤儿页面**
|
||||||
|
(`ttOrnaments/boxOpenChance`、`ttBonus/firstRecharge`、`ttWelfare/monthlyRecharges`、`ttReplacementRecord/index`
|
||||||
|
—— 已核实无菜单、无组件引用、无路由、无接口复用方,**不可达,本期不动**);
|
||||||
|
**其余 20 个 + 复用组补 4 行 = 24 行**,本次补齐。
|
||||||
|
|
||||||
|
> 为什么 `menu_id` 用 `3209~3232`:`MAX(menu_id)=3208`(麻将胡了 3200~3207、幸运升级 3208),按顺序往后取。
|
||||||
|
|
||||||
|
**本次数据库变更 3 处**(2026-09-27 已在库 `henan` 执行):
|
||||||
|
|
||||||
|
| 对象 | 变更 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `sys_menu` | 新增 **24 行** | `menu_id=3209~3232`,`menu_type='F'`,挂到 6 个 C 行下(2035/2038/2086/2092/2123/3118)|
|
||||||
|
| `sys_menu` | 修正 **3 行** | `menu_id=3160~3162` 的 `parent_id`:`3156`(目录 `M`「流水抽奖」)→ `3157`(页面 `C`「抽奖活动」)。**同属 F 行挂载错误**,不改的话勾「抽奖活动」联动带不到这 3 个按钮 |
|
||||||
|
| `sys_role_menu` | 新增 **24 行** | 把上述 F 行补授权给「**已授权其父 C 行**」的角色(动态 SQL,执行时按实际授权现状生成)。执行时这些父 C 行**仅 `role_id=104`(测试人员)**已授权 ⇒ 24 行全落给 104(`sys_role_menu` 总行数 222 → 246)|
|
||||||
|
|
||||||
|
> 关于 `3160~3162`:这三个串(`system:turnoverLottery:add/edit/remove`)**当前代码里无人使用**
|
||||||
|
> (`turnoverLottery/list.vue` 的新增/修改/删除按钮没写 `v-hasPermi`,后端也没有对应 `@PreAuthorize`),
|
||||||
|
> 所以本次修正只是把菜单树层级摆正、消除「F 行挂在目录下」的隐患,不影响线上行为。
|
||||||
|
> 若将来给该页面补按钮权限,直接复用这三个串即可。
|
||||||
|
|
||||||
|
**变更 SQL**:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
/* ---- 1. 24 行按钮权限(F)---- */
|
||||||
|
INSERT INTO `sys_menu`
|
||||||
|
(`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`,
|
||||||
|
`menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
|
||||||
|
VALUES
|
||||||
|
/* 奖池详情(ttRoll/prizedetail)—— 该页「删除」按钮历史上绑的就是 admin:prize:edit */
|
||||||
|
(3209, '修改奖池', 2038, 1, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:prize:edit', '#', 'admin', NOW(), '', NULL, '奖池详情·修改'),
|
||||||
|
/* CDK管理(ttRedPack/index)—— 整套新增,此前 0 行 F */
|
||||||
|
(3210, '新增红包', 2086, 1, '', NULL, NULL, 1, 0, 'F', '0', '0', 'skinsback:ttRedPack:add', '#', 'admin', NOW(), '', NULL, 'CDK管理·新增'),
|
||||||
|
(3211, '删除红包', 2086, 2, '', NULL, NULL, 1, 0, 'F', '0', '0', 'skinsback:ttRedPack:remove', '#', 'admin', NOW(), '', NULL, 'CDK管理·删除'),
|
||||||
|
(3212, '修改红包', 2086, 3, '', NULL, NULL, 1, 0, 'F', '0', '0', 'skinsback:ttRedPack:edit', '#', 'admin', NOW(), '', NULL, 'CDK管理·修改'),
|
||||||
|
(3213, '导出红包', 2086, 4, '', NULL, NULL, 1, 0, 'F', '0', '0', 'skinsback:ttRedPack:export', '#', 'admin', NOW(), '', NULL, 'CDK管理·导出'),
|
||||||
|
(3214, '红包查询', 2086, 5, '', NULL, NULL, 1, 0, 'F', '0', '0', 'skinsback:ttRedPack:query', '#', 'admin', NOW(), '', NULL, 'CDK管理·详情'),
|
||||||
|
(3215, '红包列表', 2086, 6, '', NULL, NULL, 1, 0, 'F', '0', '0', 'skinsback:ttRedPack:list', '#', 'admin', NOW(), '', NULL, 'CDK管理·列表接口'),
|
||||||
|
(3216, '红包全部列表', 2086, 7, '', NULL, NULL, 1, 0, 'F', '0', '0', 'skinsback:ttRedPack:listAll', '#', 'admin', NOW(), '', NULL, 'CDK管理·不分页列表接口'),
|
||||||
|
/* 对战排行榜奖励(ttFight/fightRankingReward)—— 此前 0 行 F */
|
||||||
|
(3217, '新增奖励', 2092, 1, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:reward:add', '#', 'admin', NOW(), '', NULL, '对战排行榜奖励·生成'),
|
||||||
|
(3218, '删除奖励', 2092, 2, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:reward:remove', '#', 'admin', NOW(), '', NULL, '对战排行榜奖励·清空'),
|
||||||
|
(3219, '修改奖励', 2092, 3, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:reward:edit', '#', 'admin', NOW(), '', NULL, '对战排行榜奖励·修改'),
|
||||||
|
(3220, '奖励查询', 2092, 4, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:reward:query', '#', 'admin', NOW(), '', NULL, '对战排行榜奖励·详情'),
|
||||||
|
(3221, '奖励列表', 2092, 5, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:reward:list', '#', 'admin', NOW(), '', NULL, '对战排行榜奖励·列表接口'),
|
||||||
|
/* 机器人对战配置(ttFight/robotFightConfig)—— 该 C 行下原有 4 行(3119~3122),价格配置这套是漏的,故 order 从 5 起 */
|
||||||
|
(3222, '新增价格配置', 3118, 5, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:robotFightPriceConfig:add', '#', 'admin', NOW(), '', NULL, '机器人对战配置·价格配置新增'),
|
||||||
|
(3223, '删除价格配置', 3118, 6, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:robotFightPriceConfig:remove', '#', 'admin', NOW(), '', NULL, '机器人对战配置·价格配置删除'),
|
||||||
|
(3224, '修改价格配置', 3118, 7, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:robotFightPriceConfig:edit', '#', 'admin', NOW(), '', NULL, '机器人对战配置·价格配置修改'),
|
||||||
|
(3225, '价格配置查询', 3118, 8, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:robotFightPriceConfig:query', '#', 'admin', NOW(), '', NULL, '机器人对战配置·价格配置详情'),
|
||||||
|
(3226, '价格配置列表', 3118, 9, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:robotFightPriceConfig:list', '#', 'admin', NOW(), '', NULL, '机器人对战配置·价格配置列表接口'),
|
||||||
|
/* 货币列表(ttRechargeList/index)—— 该页只有详情接口校验;列表接口本身无 @PreAuthorize */
|
||||||
|
(3227, '货币查询', 2035, 1, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:rechargeList:query', '#', 'admin', NOW(), '', NULL, '货币列表·详情'),
|
||||||
|
/* 机器人对战分组宝箱(ttFight/robotFightGroupBox)
|
||||||
|
⚠️ 复用组:该页前端按钮用 admin:box:{add,edit,remove}、后端接口用 admin:box:{list,query},
|
||||||
|
而这 4 个串原先只挂在 2019「宝箱列表」下 —— 父子联动带不过来,
|
||||||
|
所以必须在 2123 下再挂一遍,才能做到「勾这一个页面 = 该页完整可用」。*/
|
||||||
|
(3228, '新增分组宝箱', 2123, 1, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:box:add', '#', 'admin', NOW(), '', NULL, '机器人对战分组宝箱·新增(与 2020 同 perms,刻意重复)'),
|
||||||
|
(3229, '删除分组宝箱', 2123, 2, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:box:remove', '#', 'admin', NOW(), '', NULL, '机器人对战分组宝箱·删除(与 2021 同 perms,刻意重复)'),
|
||||||
|
(3230, '修改分组宝箱', 2123, 3, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:box:edit', '#', 'admin', NOW(), '', NULL, '机器人对战分组宝箱·修改(与 2022 同 perms,刻意重复)'),
|
||||||
|
(3231, '分组宝箱查询', 2123, 4, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:box:query', '#', 'admin', NOW(), '', NULL, '机器人对战分组宝箱·详情接口'),
|
||||||
|
(3232, '分组宝箱列表', 2123, 5, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:box:list', '#', 'admin', NOW(), '', NULL, '机器人对战分组宝箱·列表接口(与 2019/3139 同 perms,刻意重复)');
|
||||||
|
|
||||||
|
/* ---- 2. 授权补齐:把新 F 行补给「已授权其父 C 行」的角色 ----
|
||||||
|
动态写法的好处:不依赖执行时刻的角色授权现状,也不用在角色界面逐个重存。
|
||||||
|
注意:只补 F 行;C 行(页面本身)是否可见仍由业务在「角色管理」里决定。*/
|
||||||
|
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
|
||||||
|
SELECT DISTINCT `rm`.`role_id`, `f`.`menu_id`
|
||||||
|
FROM `sys_menu` `f`
|
||||||
|
JOIN `sys_role_menu` `rm` ON `rm`.`menu_id` = `f`.`parent_id`
|
||||||
|
WHERE `f`.`menu_id` BETWEEN 3209 AND 3232
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM `sys_role_menu` `x`
|
||||||
|
WHERE `x`.`role_id` = `rm`.`role_id` AND `x`.`menu_id` = `f`.`menu_id`);
|
||||||
|
|
||||||
|
/* ---- 3. 修正 3 行挂错父节点的 F(3160~3162:目录 3156 → 页面 3157)---- */
|
||||||
|
UPDATE `sys_menu`
|
||||||
|
SET `parent_id` = 3157
|
||||||
|
WHERE `menu_id` BETWEEN 3160 AND 3162
|
||||||
|
AND `menu_type` = 'F'
|
||||||
|
AND `parent_id` = 3156;
|
||||||
|
```
|
||||||
|
|
||||||
|
**验证**:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- ① 24 行 F 都进去了、且 perms 无重复录入
|
||||||
|
SELECT COUNT(*) FROM sys_menu WHERE menu_id BETWEEN 3209 AND 3232 AND menu_type='F'; -- 期望 24
|
||||||
|
SELECT perms, COUNT(*) FROM sys_menu WHERE menu_id BETWEEN 3209 AND 3232
|
||||||
|
GROUP BY perms HAVING COUNT(*) > 1; -- 期望 0 行(2123 组与 2019 组不同 id,不算重复)
|
||||||
|
|
||||||
|
-- ② 全库 F 行的 parent 必须是指向 C 行的 menu_id(防挂错;本次修掉了 3160~3162 这组)
|
||||||
|
SELECT f.menu_id, f.menu_name, f.perms, f.parent_id, p.menu_type
|
||||||
|
FROM sys_menu f LEFT JOIN sys_menu p ON p.menu_id = f.parent_id
|
||||||
|
WHERE f.menu_type = 'F' AND (p.menu_id IS NULL OR p.menu_type <> 'C'); -- 期望 0 行
|
||||||
|
|
||||||
|
-- ③ 授权补齐后:每个新 F 行,其父 C 行的每个授权角色都应能查到
|
||||||
|
SELECT f.menu_id, f.perms, rm.role_id
|
||||||
|
FROM sys_menu f
|
||||||
|
JOIN sys_role_menu rm ON rm.menu_id = f.parent_id
|
||||||
|
WHERE f.menu_id BETWEEN 3209 AND 3232
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM sys_role_menu x
|
||||||
|
WHERE x.role_id = rm.role_id AND x.menu_id = f.menu_id); -- 期望 0 行
|
||||||
|
|
||||||
|
-- ④ 端到端:模拟某角色的 perms 集合,确认缺串已消失
|
||||||
|
SELECT DISTINCT m.perms FROM sys_menu m
|
||||||
|
JOIN sys_role_menu rm ON m.menu_id = rm.menu_id
|
||||||
|
WHERE rm.role_id = <角色ID> AND m.perms IS NOT NULL AND m.perms <> '';
|
||||||
|
```
|
||||||
|
|
||||||
|
**代码侧配套(非 SQL)**:新增 `.workbuddy/verify-perms.cjs`,做「代码在用的权限串 ↔ `sys_menu.perms`」双向差集,
|
||||||
|
防止后续新增页面再次只建 C 行、漏建 F 行。跑法见脚本头部注释。
|
||||||
|
|
||||||
|
**注意:权限生效需要用户重新登录**(`permissions` 是登录时从 `/getInfo` 拉取的,存在前端 store 里,不会热更新)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
-----------------------对战接口优化--------------------------
|
-----------------------对战接口优化--------------------------
|
||||||
|
|
||||||
SHOW INDEX FROM tt_fight;
|
SHOW INDEX FROM tt_fight;
|
||||||
|
|||||||
+251
@@ -7,6 +7,257 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-09-27
|
||||||
|
|
||||||
|
- **管理后台按钮级权限补全:`sys_menu` 补 24 行 `F` + 防漂移校验脚本**
|
||||||
|
|
||||||
|
**起因**:用户问「管理后台会不会有只有超管能看到、非超管看不见的页面内按钮」。核查结论:**会,而且比按钮更严重**。
|
||||||
|
|
||||||
|
**机制**:按钮显隐走前端 `v-hasPermi`、接口放行走后端 `@PreAuthorize("@ss.hasPermi(...)")`,
|
||||||
|
两边比对的是**同一份 `sys_menu.perms`**;超管(`user_id==1`)拿 `*:*:*` 所以永远看得见。
|
||||||
|
⇒ **某权限串只要在 `sys_menu` 里没有对应行,就永远授权不到任何非超管角色**:按钮看不见、部分列表/详情接口直接 403。
|
||||||
|
|
||||||
|
**盘出的问题**(代码在用、库里未登记,并集 43 串):
|
||||||
|
- 4 个孤儿页面(开箱概率 / 首充奖励 / 月度充值 / 换装记录)占 23 串 —— 已核实无菜单、无组件引用、无路由、无接口复用方,
|
||||||
|
**不可达,本期不动**(手输 URL 也 404,因 RuoYi 路由由 `/getRouters` 动态注册)。
|
||||||
|
- 其余 20 串 + `admin:box:*` 复用组补 4 行 = **24 行**,本次补齐。
|
||||||
|
- 结构性漂移:**从 `menu_id` 31xx 段起,新增页面只建 `C` 行、不建 `F` 行**(F 行分布:1000 段 61、2000 段 18、3100 段 9、3200+ 段 0)。
|
||||||
|
|
||||||
|
**改动清单**
|
||||||
|
1. `sys_menu` 新增 24 行 `F`(`menu_id 3209~3232`,挂 2035/2038/2086/2092/2123/3118 六个 C 行下)。
|
||||||
|
2. `sys_role_menu` 动态补齐:把新 F 行补给「已授权其父 C 行」的角色(本次实写 24 行,全给 `role_id=104`)。
|
||||||
|
3. `sys_menu` 修正 3 行(`3160~3162`)`parent_id`:目录 `3156` → 页面 `3157`(F 行挂错层级,父子联动带不到)。
|
||||||
|
4. **代码侧配套**:新增 `.workbuddy/verify-perms.cjs` —— 扫前端 `v-hasPermi` + 后端 `@PreAuthorize`,
|
||||||
|
与 `sys_menu.perms` 做双向差集 + 校验全库 F 行 parent 必须指向 C 行。非 0 退出码,建议挂 `publish.sh` 前防复发。
|
||||||
|
|
||||||
|
**验证**:库层 5 项全对(24 行 / 0 重复 / 0 挂错父节点 / 0 未授权 / `role_menu` 222→246);
|
||||||
|
`node .workbuddy/verify-perms.cjs` ⇒ **ALL_OK 6 passed / 0 failed**。
|
||||||
|
|
||||||
|
**注意**:① 权限生效需用户**重新登录**(`permissions` 登录时从 `/getInfo` 拉取,不热更新)。
|
||||||
|
② 执行时库内授权已被重排 —— 只有 `role_id=104`(测试人员)持有这些页面,`三恒` 等角色**0 菜单**,
|
||||||
|
且多个启用用户(三恒/国荣/s1/zzyzzy123/Fdily)**未分配任何角色**,这是业务配置问题,本次未动。
|
||||||
|
|
||||||
|
细节 SQL 见 `docs/updata_sql.md`「管理后台按钮级权限补全」,过程与踩坑见 `.workbuddy/memory/2026-09-27.md`。
|
||||||
|
|
||||||
|
- **推广后台「佣金明细记录」等 3 张报表右侧留白 → 全列 `width` 改 `min-width`**
|
||||||
|
|
||||||
|
**起因**:用户截图 `promo-ui2/src/views/CommissionReport/index.vue` 的「佣金明细记录」表,说「这里表格填充不满」。
|
||||||
|
|
||||||
|
**根因**(读 Element Plus 源码定论,不是猜):`element-plus/es/components/table/src/table-layout.mjs:100-171`
|
||||||
|
的 `updateColumnsWidth()` 只在 **存在弹性列** 时才把表格撑满容器
|
||||||
|
(`flexColumns = flattenColumns.filter(c => !isNumber(c.width))`);
|
||||||
|
全列都是数字 `width` 时走 else 分支 ⇒ `bodyWidth = Σ realWidth` ⇒ **表格宽恒等于列宽之和,容器再宽右侧也留白**。
|
||||||
|
⚠️ 关键陷阱:`width="170"` 这种**静态字符串**也会被 `table-column/render-helper.mjs:39` 的
|
||||||
|
`parseWidth()`(= `Number.parseInt`)转成数字 170 ⇒ 那是**固定列**;
|
||||||
|
反过来只写 `min-width` 时 `setColumnWidth()` 会执行 `column.width = void 0` ⇒ 那才是**弹性列**。
|
||||||
|
|
||||||
|
**改动清单**(`promo-ui2/src/views/CommissionReport/index.vue`,3 张表共 16 列;另有 6 张表见本节末):
|
||||||
|
1. 「佣金明细记录」6 列(本地时间170 / 手机号130 / 该次消费120 / 佣金比例90 / 佣金金额100 / 来源120)
|
||||||
|
2. 「佣金日报」5 列(手机号130 / 今日消费120 / 来源120 / 佣金金额120 / 日期130)
|
||||||
|
3. 「佣金汇总」5 列(手机号130 / 来源120 / 消费金额120 / 本次佣金120 / 参加游戏次数140)
|
||||||
|
—— **一律 `width` → `min-width`,数值不变**。选「全列都变弹性」而不是「只挑一列变弹性」:
|
||||||
|
弹性的那一列会独吞全部余量(1280 视口下 170px 的时间列会涨到 ≈590px),全列按 min-width 等比摊才均匀。
|
||||||
|
模板顶部加了注释说明为什么不能改回 `width`。脚本/接口/分页/过滤逻辑一行未动。
|
||||||
|
|
||||||
|
**验证**(3 件,全部 ALL_OK):
|
||||||
|
- SFC 编译:`node .workbuddy/verify-promo-vue-compile.cjs src/views/CommissionReport/index.vue` ⇒ **ALL_OK 4/0**
|
||||||
|
(连带跑 Home ⇒ 11/0,未受影响)。
|
||||||
|
- 算法级多视口回归:`node .workbuddy/verify-ep-table-stretch.cjs src/views/CommissionReport/index.vue`
|
||||||
|
⇒ **ALL_OK 15/0**。1280/1440/1600/1920/2560 五个视口下 3 张表**留白全为 0px、`scrollX` 全 false**。
|
||||||
|
- 真产物:`node .workbuddy/check-promo-modules.cjs src/views/CommissionReport/index.vue --has=min-width --prop=min-width:16`
|
||||||
|
⇒ **ALL_OK**(用户常驻的 `:4000` dev server 上主模块 200、`"min-width"` prop 恰好 **16** 次、0 处编译错误串)。
|
||||||
|
- 全站审计:`node .workbuddy/audit-table-fill-ep.cjs` ⇒ CommissionReport 3 张表从「✗ 不能撑满」转为有弹性列。
|
||||||
|
|
||||||
|
**新增脚本(3 个,都是 promo-ui2 / Element Plus 侧原先缺的)**:
|
||||||
|
`.workbuddy/audit-table-fill-ep.cjs`(全站扫「全固定列 ⇒ 留白」)、
|
||||||
|
`.workbuddy/verify-ep-table-stretch.cjs`(复现 `updateColumnsWidth` 做多视口定量回归)、
|
||||||
|
`.workbuddy/check-promo-modules.cjs`(从 `:4000` 拉真实编译产物做断言)。
|
||||||
|
原有的 `sim-table-width.cjs` / `verify-table-fill.cjs` 只适用 ruoyi-ui(Element UI 2.x),**不要混用**。
|
||||||
|
|
||||||
|
**注意 / 一并修的另外 6 张**:审计出全站共 **9 张**同样会留白的表,用户拍板「6 张全改」,已一并完成:
|
||||||
|
`BusinessManage/index.vue` 3 张(招商 Σ1128 / 主播 Σ828 / 普通用户 Σ920,后两张在展开行内)、
|
||||||
|
`CardMerchant/index.vue:44`(Σ1210)、`subFlow/index.vue:20`(Σ1570)、`SubordinateRecharge/index.vue:23`(Σ760)。
|
||||||
|
- 改法一致:**数据列 `width` → `min-width`**;**「操作」列保持 `width`**(CardMerchant 100、subFlow 120)——
|
||||||
|
操作列固定宽是设计意图,不该被拉宽。BusinessManage 的 `type="expand"` 列也不动(`cellStarts` 强制 48px 固定)。
|
||||||
|
- **嵌套子表(BusinessManage 二/三级)**:容器宽度由父级展开行决定,**不能用「视口 − 288」推算**。
|
||||||
|
`verify-ep-table-stretch.cjs` 已加 `depth` 判定,嵌套表只做「有没有弹性列」的定性断言,不套用视口断言。
|
||||||
|
- **两张宽表仍会横向滚动**(改 min-width 治不了,属列宽总量问题):`CardMerchant` Σ1210 ⇒ 视口 ≤1440 横滚(差 58px);
|
||||||
|
`subFlow` Σ1570 ⇒ 视口 ≤1600 横滚。**这两个视口下改前改后都是横滚**,未新增;≥1600 / ≥1920 起改为撑满(改前是留白)。
|
||||||
|
若要 1440 也不横滚,得再压列宽(`使用时间/创建时间` 180→165、`卡密` 200→190 之类),本次未做,等确认。
|
||||||
|
- 复验:全站审计 **不能撑满 0 / 34**;SFC 编译 **19/0 ALL_OK**;列宽回归 **22/0 ALL_OK**(6 张表 × 视口);
|
||||||
|
真产物(`:4000`)4 个文件主模块全 200、`"min-width"` prop 计数 **22 / 8 / 6 / 13** 逐一吻合、0 错误串。
|
||||||
|
|
||||||
|
⚠️ **未经浏览器实测**(本机 Chrome 起不来,见 `.workbuddy/memory/theme-skin.md`)⇒ 效果请自己刷 `:4000` 确认。
|
||||||
|
|
||||||
|
- **修复:幸运升级「水位驱动」因 mapper 里 `SUM(x, 0)` 语法错而整条不可用**
|
||||||
|
|
||||||
|
**现象**:启动日志 `TtUpgradeRewardLogMapper.xml` → `sumRecordTotals` 报
|
||||||
|
`java.sql.SQLSyntaxErrorException: ... right syntax to use near '' at line 4`,
|
||||||
|
外层 WARN「幸运升级水位驱动初始化失败(不阻断启动,首次拉取时会自动重试)」。
|
||||||
|
|
||||||
|
**根因**:`TtUpgradeRewardLogMapper.xml` 的 `blankBet` / `blankWin` 两行把外层 `COALESCE(..., 0)` 的写法抄进了 `SUM()` ——
|
||||||
|
`SUM(CASE WHEN is_blank THEN r.amount_consumed END, 0)`。**`SUM()` 只接受一个参数**,第二个 `, 0` 直接语法错。
|
||||||
|
(同文件 `totalBet` / `totalWin` 两行写的是 `CASE WHEN is_blank THEN 0 ELSE ... END`,是对的,可作对照。)
|
||||||
|
|
||||||
|
**改动**:删掉 `SUM(...)` 里多余的 `, 0`(外层 `COALESCE(SUM(...), 0)` 保留,仍是 null-safe)。全仓库仅此一处。
|
||||||
|
|
||||||
|
**影响面(不是"只是个 WARN")**:
|
||||||
|
- `ensureInit(false)`(`@PostConstruct` 预热)异常 → Redis 累计量 `KEY_BET/KEY_WIN/KEY_BLANK_*` **从未写入**,
|
||||||
|
`KEY_INIT_AT` 也没落 ⇒ 每次调用都重新全表扫并再次失败,**水位恒为 0**,水位驱动实际等于停摆。
|
||||||
|
- 后台「奖池水位 → 按历史重算」`/recalc` 调 `ensureInit(true)` 且**没有 try/catch** ⇒ 该按钮**必 500**。
|
||||||
|
- `calibrationFactor()` 有 try/catch → 失败返回 0 → 回落兜底系数 1.19(这条不受影响)。
|
||||||
|
|
||||||
|
**验证**:库里实测 `SELECT SUM(CASE WHEN 1 THEN 2 END, 0)` 复现同一 `ERROR 1064`;
|
||||||
|
改成 `SUM(CASE WHEN ... END)` 后整条 `sumRecordTotals` 跑通出数。XML 重新解析通过。
|
||||||
|
|
||||||
|
**上线动作**:`./ry.sh stop` → `mvn clean install`(见 `USER.md` 的「必须 clean install,否则嵌套旧 jar」)→ `./ry.sh start`。
|
||||||
|
|
||||||
|
- **推广后台登录页新增「开发环境账号密码登录」**
|
||||||
|
|
||||||
|
**起因**:推广后台唯一登录入口是 `/promo/smsLogin`(手机号 + 4 位短信验证码 + 滑块),本地联调要连短信通道、拖滑块;
|
||||||
|
后端此前已明确下线账号密码登录(`PromoLoginServiceImpl` 注释:「账号密码登录已下线……避免用密码绕过短信校验被撞库」)。
|
||||||
|
|
||||||
|
**改动清单**(前后端配合,**默认关闭**):
|
||||||
|
|
||||||
|
1. **后端新增 `POST /promo/devLogin`**(`@Anonymous`):账号解析直接复用 `createUserToken` 的 `promo_user_` 入口
|
||||||
|
⇒ 推广账号(`tt_user` 的主播/招商/卡商,登录名=手机号)与超管(`promo_admin` 的 `username` 或 `phone_number`)
|
||||||
|
**都能登录**,区别只是「短信验证码」换成了 `SecurityUtils.matchesPassword` 的 BCrypt 密码校验。
|
||||||
|
账号不存在 / 停用 / 密码错统一返回「账号或密码错误」(防账号枚举,真实原因走 `log.warn`)。
|
||||||
|
2. **开关走配置文件而非数据库**:新增 `promo.dev-login.enabled` —— `application.yml` 默认 `false`,`application-dev.yml` 置 `true`,
|
||||||
|
生产 profile 不覆盖 ⇒ 接口恒返回「账号密码登录未开启」,且后台管理员无法误开。
|
||||||
|
3. `PromoWebConfig` 放行 `/promo/devLogin`(维护拦截器不拦登录类接口,与 smsLogin/register 同口径)。
|
||||||
|
4. **前端** `LoginForm.vue` 只在 `import.meta.env.DEV`(即 `pnpm run dev`)下渲染「短信验证码 / 账号密码」切换与密码表单;
|
||||||
|
登录成功后复用原有的 `completeLogin` + `enterSystem` 落地流程。`api/login` 增加 `devLoginApi` 与 `DevLoginType`。
|
||||||
|
|
||||||
|
**验证**:`vue-tsc --noEmit` 改动文件 0 报错(仓库其余 30+ 条为既存错误);`mvn -B -DskipTests compile` ⇒ `BUILD SUCCESS`。
|
||||||
|
|
||||||
|
**无需数据库变更**:开关是配置文件项,不是 `sys_config`(相对 `promoMaintenance` 的取舍见 `.workbuddy/memory/2026-09-27.md`)。
|
||||||
|
|
||||||
|
**使用注意**:密码按 BCrypt 校验,测试账号若不知明文,查 `tt_user.remark`(格式 `明文密码:xxx`)或 `promo_admin` 表。
|
||||||
|
|
||||||
|
- **管理后台首页改排版:11 张等宽统计卡 → 「左窄轨 + 右主区」两栏**
|
||||||
|
|
||||||
|
**起因**:用户反馈首页「排版还是和原来一样,只是改了 UI 的风格」——换肤把配色圆角阴影改了,但骨架仍是
|
||||||
|
「页头 → 卡片网格 → 区块 → 区块」的垂直堆叠模板,要求**排版差异化、风格照旧**。
|
||||||
|
|
||||||
|
**改动清单**(`ruoyi-ui/src/views/index.vue`,单文件)
|
||||||
|
1. **骨架拆两栏**:`grid-template-columns: 300px minmax(0,1fr)` + `align-items:start`。
|
||||||
|
- **左窄轨**(`position:sticky; top:16px`,跟随滚动):3 个核心大数字(在线用户 / 总充值 / 总流水,字号 28px)
|
||||||
|
+ 待办区(待处理提货申请 / 提货余额)。待处理 >0 时雏菊金描边 + 数值变色。
|
||||||
|
- **右侧主区**:实时 RTP → 「经营数据」紧凑指标带(6 项 `mini-stat`,小图标 + 标签 + 数值)→ 流水统计图
|
||||||
|
(顺序按用户看完后的要求定:图表 400px 最高,放最底部滚到底正好)。
|
||||||
|
2. **指标一项不丢**:原 `statCards` 11 项拆成 `railMetrics`(3) + `miniStats`(6) + 待办区硬编码 2 项 = 11。
|
||||||
|
原「图标 + 大数值 + 标签」的等权卡片形态取消,改为「大数字 / 紧凑条」两级,主次拉开。
|
||||||
|
3. **RTP 网格** `repeat(5,1fr)` → `repeat(auto-fit, minmax(160px,1fr))`:右区宽度随屏幕浮动(左轨占了 300px),
|
||||||
|
写死 5 列会在 1440 以下挤出 4+1。
|
||||||
|
4. 旧骨架 CSS(`.stat-grid` / `.stat-card` / `.stat-icon` / `.stat-info` / `.stat-arrow`)与 `statCards` computed **一并删除**。
|
||||||
|
|
||||||
|
**风格**:色板 / 圆角 / 阴影 / 图标浅底规则全部沿用,**未新增任何色值** —— cottagecore 配色纪律不动。
|
||||||
|
|
||||||
|
**验证**:`node .workbuddy/verify-home-layout.cjs`(新增,cwd=ruoyi-ui)⇒ **ALL_OK 28 条 0 失败**
|
||||||
|
(含 vue-template-compiler 编译模板、dart-sass 编译样式、新旧骨架核对、区块顺序、指标总数 11、交互绑定未丢);
|
||||||
|
`node .workbuddy/audit-palette.cjs` ⇒ **离版文件 0/185、离版色值 0 次**;
|
||||||
|
dev server(`:80`)`/static/js/app.js` ⇒ 200 / 7.1MB / 0 处 `Module build failed`。
|
||||||
|
⚠️ 本机助手拉不起 Chrome(安全策略)⇒ **渲染后实际观感未经实测**,需用户刷新页面肉眼确认。
|
||||||
|
|
||||||
|
**后续补丁(用户看完截图提的)**:左轨「待处理提货申请 / **提货余额**」那格里,提货余额写的是 `$9259.64`
|
||||||
|
(**美元符号**),而页面其余金额(总充值 `¥1800`、总流水 `¥0.00`、RTP 投注/奖励 `¥0.00`、经营数据 6 项 `¥…`)
|
||||||
|
一律 `¥` ⇒ 统一为 `¥{{ balance }}`(`ruoyi-ui/src/views/index.vue:55`,全站唯一一处 `$` 货币写法,已 grep 确认)。
|
||||||
|
`verify-home-layout.cjs` 补 2 条断言(模板内不得出现 `$` 紧跟插值;提货余额那行前缀必须是 `¥`)⇒ 现 **30 条 ALL_OK**。
|
||||||
|
真产物复核(新增 `.workbuddy/check-ruoyi-bundle.cjs`,从 `:80` 拉 `app.js`):编译后为
|
||||||
|
`_vm._v("¥" + _vm._s(_vm.balance))`,`$` 版本 0 处。
|
||||||
|
|
||||||
|
- **推广后台首页改排版:「基本信息 / 5 张返利卡 / 汇总表」三段堆叠 → 「推广工具置顶」**
|
||||||
|
|
||||||
|
**起因**:与上一轮管理端首页同类问题(换肤只改配色,骨架没动)。用户在 3 轮高保真稿里选定 **版本 3 · 推广工具置顶**。
|
||||||
|
|
||||||
|
**改动清单**(`promo-ui2/src/views/Home/index.vue`,单文件)
|
||||||
|
1. **① 我的推广(置顶通栏)**:`hero` 行放**推广链接**(15px 加粗、超长省略、整条可复制)+ 主色「复制链接」按钮(无链接时 `disabled`);
|
||||||
|
下面一条虚线分隔的三格:**我的邀请码**(复制 / 修改 / 已修改)、**手机号码**(绑 `phoneNumber`)、**上级邀请码**。
|
||||||
|
2. **② 返利 KPI 横带**(4 格,19px 数字):**累计返利 / 今日返利 / 本周返利 / 累计下级** ——
|
||||||
|
全部从已有的 `getGameCommissionData()` 的 `periods` 里取(`总`/`今日`/`本周` 行),**不新增任何接口**。
|
||||||
|
3. **③ 底部两栏** `260px minmax(0,1fr)`:左 = **返利比例**(5 行「玩法名 ⋯⋯ 百分比」点线清单,替掉原来 5 张等宽卡);
|
||||||
|
右 = **周期返利汇总**表格(列一项没删,仍是 10 列)。`@media(max-width:1200px)` 收成一栏让表格拿满宽度。
|
||||||
|
4. **④ 账号信息降级到最底部**:用户账号 / 用户类型 / 金币余额 / 积分余额 / 注册时间 / Steam交易链接(6 项)。
|
||||||
|
样式上**做成非卡片**(`background/border` 透明、去阴影),避免和上面的数据抢注意力。
|
||||||
|
5. 旧骨架(`.section` / `.section-title` / `.game-rate-cards` / `.rate-card*`)与那两条**从未命中过**的
|
||||||
|
`.el-table__row tr:nth-child(odd)` 伪条纹规则一并删除(Element 的 `stripe` 已负责隔行)。
|
||||||
|
6. 颜色从残留的旧 Element 灰(`#f5f7fa` / `#e4e7ed` / `#303133` / `#67c23a` / `#606266` / `#909399` / `#fafafa` / `#e6a23c`)
|
||||||
|
收敛到田园核白名单 —— 该页 scoped 样式是换肤时漏掉的一份。
|
||||||
|
|
||||||
|
**一字未动的**:`username: userInfo.userName`(改邀请码的**入参**,后端拿它做「只能改自己」校验,与展示用的 `phoneNumber` 语义不同);
|
||||||
|
返利比例出处仍是 `getInfo()` 的三个 `*CommissionRate`;表格 10 列 / 弹窗 / 复制 / `onMounted` 双并发加载全部保留。
|
||||||
|
|
||||||
|
**验证**(新增 2 个脚本)
|
||||||
|
- `node .workbuddy/verify-promo-home.cjs` ⇒ **ALL_OK 54 条 0 失败**:新骨架 12 项、旧骨架 4 项须绝迹、区块顺序、
|
||||||
|
表格 10 列 / 账号 6 项 / KPI 4 项 / 玩法 5 项齐全、交互绑定 7 项、旧 Element 色 0 处、色值全在白名单、8 组对比度 ≥4.5:1。
|
||||||
|
- `node .workbuddy/verify-promo-vue-compile.cjs`(新增,用 promo 自带的 `vue/compiler-sfc` 走
|
||||||
|
parse → compileScript → compileTemplate → compileStyle)⇒ **ALL_OK 7 条 0 失败**。
|
||||||
|
- `node .workbuddy/verify-home-page.cjs` ⇒ **ALL_OK 5 条 0 失败**(该脚本①的结构性正则随「手机号码」移位已改写为与结构无关的定位)。
|
||||||
|
- **真机 dev 产物**(用户的 vite dev server 正在 `:4000` 跑,热更新):`/src/views/Home/index.vue` 的
|
||||||
|
style / template / script-setup / main 四个模块全 200,0 处 `Transform failed` / `Internal server error`;
|
||||||
|
编译后 15/15 个新骨架选择器都带上了 `[data-v-a763a6ae]` 前缀,旧类名 0 残留。
|
||||||
|
|
||||||
|
⚠️ 两点未做实测:**渲染后观感**(本机 Chrome 拉不起来);**窄屏横向滚动** —— 底部两栏时表格可用宽度 ≈ 屏宽-200(侧栏)-80,
|
||||||
|
10 列 `min-width` 合计 1090px,**视口 < 约 1600px 时右侧表格会出现横向滚动**(≥1600 正常),已在 `<1200px` 断点收成一栏规避。
|
||||||
|
**数据全是 0 是账号本身无下级/无消费所致**,不是渲染问题(`periodData` 的 4 行都在,只是值都为 0.00)。
|
||||||
|
|
||||||
|
过程与踩坑见 `.workbuddy/memory/2026-09-27.md`。
|
||||||
|
|
||||||
|
- **管理后台站点名「河南项目」→「凯科佰娱」(ruoyi-ui)**
|
||||||
|
|
||||||
|
**起因**:用户截图(`localhost/#/index`)圈出左下角 Logo 旁标题与浏览器 tab 标题,要求改成「凯科佰娱」。
|
||||||
|
|
||||||
|
**定位**:截图端口 80 + hash 路由 ⇒ 是 **`ruoyi-ui`(Vue2 管理后台,`vue.config.js` 默认 port 80)**,不是 promo-ui2(:4000)。
|
||||||
|
站点名只有一个源:`VUE_APP_TITLE`(`.env.*`)。消费方两处 ——
|
||||||
|
`src/layout/components/Sidebar/Logo.vue:23`(侧栏标题)、`src/App.vue:20-29` 的 `metaInfo.titleTemplate`
|
||||||
|
(`${路由名} - ${VUE_APP_TITLE}`);`public/index.html` 的 `<title>` 由 `vue.config.js:10` 的 `name` 注入。
|
||||||
|
⇒ **改一处 env 即可同时改掉侧栏 + tab**。另有 login/register 两页是硬编码同名,一并改。
|
||||||
|
|
||||||
|
**改动清单(6 处,全是字符串替换)**
|
||||||
|
1. `.env.development` / `.env.production` / `.env.staging`:`VUE_APP_TITLE = 河南项目` → `凯科佰娱`。
|
||||||
|
2. `vue.config.js:10` 兜底值 `|| '河南项目'` → `|| '凯科佰娱'`。
|
||||||
|
3. `src/views/login.vue:5`、`src/views/register.vue:4` 的 `<h3 class="title">`。
|
||||||
|
|
||||||
|
**注意**:① `.env` 在**进程启动时**注入 `process.env`,**改完必须重启 dev server**(webpack 不会热更 env)。
|
||||||
|
② `ruoyi-ui/dist/` 是构建产物,仍含旧串,需 `npm run build:prod` 重新生成才会更新(未手动改 dist)。
|
||||||
|
③ 后端 `application-*.yml` 里的「河南」是注释(控制台账号备注),与站点名无关,未动;
|
||||||
|
`docs/修改记录.md:910` 等处的「河南项目」是历史叙述,保留。
|
||||||
|
|
||||||
|
过程见 `.workbuddy/memory/2026-09-27.md`。
|
||||||
|
|
||||||
|
- **推广后台顶栏「分享链接」删除(与首页「我的推广链接」重复)**
|
||||||
|
|
||||||
|
**起因**:用户截图 `promo-ui2` 首页(`localhost:4000/#/home/index`),圈出**顶栏**的 `分享链接:https://…?code=…`
|
||||||
|
与**页面里**的「我的推广链接」hero 卡是同一份内容,要求「把上面的去掉」。
|
||||||
|
|
||||||
|
**定位**:两处不同来源 ——
|
||||||
|
① **顶栏**:`src/layout/components/ToolHeader.vue:45-47` 的 `<span>分享链接:{userStore.getUserInfo?.shareLink}</span>`
|
||||||
|
(**布局层组件,所有页面都有**,`role !== '99' && role !== '05'` 才显示);
|
||||||
|
② **页面**:`src/views/Home/index.vue:4-21` 的 hero 卡 —— 本次改版(版本 3「推广工具置顶」)**新加的**。
|
||||||
|
⇒ hero 一上线,顶栏那份就成了重复。按用户口径删**顶栏**(上面的)。
|
||||||
|
|
||||||
|
**改动清单**
|
||||||
|
1. `ToolHeader.vue`:删掉该 `<span>`(原地留注释说明为何删)。
|
||||||
|
2. 同文件连带清理:`useUserStore` 的 import 与 `const userStore = useUserStore()` —— 全文件只有这一处在用,
|
||||||
|
不删会变成未使用变量(`vue-tsc` / eslint 会报)。
|
||||||
|
3. `Home/index.vue`:删掉「改邀请码成功后同步顶部 header 分享链接」的 `userStore.setUserInfo({...})` 块 + 其 import/实例——
|
||||||
|
该块**存在的唯一目的**就是喂被删掉的顶栏,属同一改动的死代码,不是重构。
|
||||||
|
|
||||||
|
**验证**:给 `.workbuddy/check-promo-modules.cjs` **新增 `--absent=<子串>`**(原先只有 `--has` 用于"必须有",
|
||||||
|
删元素没法验),从用户常驻的 `:4000` 拉 **vite 真产物**断言:
|
||||||
|
- `layout/components/ToolHeader.vue` → 主模块 200、style 子模块 200、`useUserStore` / `shareLink` / `getUserInfo` **全部已消失**,**ALL_OK 6/0**。
|
||||||
|
- `views/Home/index.vue` → 主模块 200、`useUserStore` / `setUserInfo` **已消失**,**ALL_OK 4/0**。
|
||||||
|
⚠️ `--absent` 的坑:dev 构建**保留模板注释** ⇒ 注释里**别写被删的那个词**,否则 `_createCommentVNode("…")` 会命中假 FAIL。
|
||||||
|
|
||||||
|
**影响面**:改的是**布局层** ⇒ 所有页面顶栏都不再显示分享链接(不只首页)。
|
||||||
|
`userStore.userInfo.shareLink` 仍由 `Login/completeLogin.ts` 与 `views/User/index.vue` 写入,只是**不再有展示方**(无其他读取处,已 grep 确认);
|
||||||
|
改邀请码后 store 里那份不再自动刷新,因无消费方,无影响。
|
||||||
|
|
||||||
|
过程见 `.workbuddy/memory/2026-09-27.md`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2026-09-24
|
## 2026-09-24
|
||||||
|
|
||||||
- **头像管理:「是否默认」可同时多条为「是」的缺陷修复(唯一性约束 + 存量数据清洗 + 开关交互)**
|
- **头像管理:「是否默认」可同时多条为「是」的缺陷修复(唯一性约束 + 存量数据清洗 + 开关交互)**
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import request from '@/axios'
|
import request from '@/axios'
|
||||||
import type { RegisterUserType, SmsLoginType, UserType } from './types'
|
import type { DevLoginType, RegisterUserType, SmsLoginType, UserType } from './types'
|
||||||
|
|
||||||
interface RoleParams {
|
interface RoleParams {
|
||||||
roleName: string
|
roleName: string
|
||||||
@@ -9,6 +9,14 @@ export const smsLoginApi = (data: SmsLoginType): Promise<IResponse<UserType>> =>
|
|||||||
return request.post({ url: '/promo/smsLogin', data })
|
return request.post({ url: '/promo/smsLogin', data })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开发环境专用的账号密码登录(仅 vite dev server 下可见)。
|
||||||
|
* 后端需在 sys_config 里把 promoDevLoginEnabled 置为 '1',否则返回「账号密码登录未开启」。
|
||||||
|
*/
|
||||||
|
export const devLoginApi = (data: DevLoginType): Promise<IResponse<UserType>> => {
|
||||||
|
return request.post({ url: '/promo/devLogin', data })
|
||||||
|
}
|
||||||
|
|
||||||
export const registerApi = (data: RegisterUserType): Promise<IResponse<UserType>> => {
|
export const registerApi = (data: RegisterUserType): Promise<IResponse<UserType>> => {
|
||||||
return request.post({ url: '/promo/register', data })
|
return request.post({ url: '/promo/register', data })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ export interface SmsLoginType {
|
|||||||
parentInvitationCode?: string
|
parentInvitationCode?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 开发环境专用的账号密码登录入参(后端 promoDevLoginEnabled=1 才可用) */
|
||||||
|
export interface DevLoginType {
|
||||||
|
/** 推广账号(主播/招商/卡商)填手机号;超管可填登录账号或手机号 */
|
||||||
|
account: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface UserType {
|
export interface UserType {
|
||||||
uid: number
|
uid: number
|
||||||
username: string
|
username: string
|
||||||
|
|||||||
@@ -5,14 +5,12 @@ import { UserInfo } from '@/components/UserInfo'
|
|||||||
import { Breadcrumb } from '@/components/Breadcrumb'
|
import { Breadcrumb } from '@/components/Breadcrumb'
|
||||||
import { useAppStore } from '@/store/modules/app'
|
import { useAppStore } from '@/store/modules/app'
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
import { useDesign } from '@/hooks/web/useDesign'
|
||||||
import { useUserStore } from '@/store/modules/user'
|
|
||||||
|
|
||||||
const { getPrefixCls, variables } = useDesign()
|
const { getPrefixCls, variables } = useDesign()
|
||||||
|
|
||||||
const prefixCls = getPrefixCls('tool-header')
|
const prefixCls = getPrefixCls('tool-header')
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
const userStore = useUserStore()
|
|
||||||
|
|
||||||
// 面包屑
|
// 面包屑
|
||||||
const breadcrumb = computed(() => appStore.getBreadcrumb)
|
const breadcrumb = computed(() => appStore.getBreadcrumb)
|
||||||
@@ -42,9 +40,7 @@ export default defineComponent({
|
|||||||
{breadcrumb.value ? <Breadcrumb class="<md:hidden"></Breadcrumb> : undefined}
|
{breadcrumb.value ? <Breadcrumb class="<md:hidden"></Breadcrumb> : undefined}
|
||||||
</div>
|
</div>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
{userStore.getUserInfo?.role !== '99' && userStore.getUserInfo?.role !== '05' ? (
|
{/* 顶栏的「分享链接」已移除:与首页「我的推广链接」hero 卡重复(2026-09-27) */}
|
||||||
<span>分享链接:{userStore.getUserInfo?.shareLink}</span>
|
|
||||||
) : undefined}
|
|
||||||
<div class="h-full flex items-center">
|
<div class="h-full flex items-center">
|
||||||
<UserInfo></UserInfo>
|
<UserInfo></UserInfo>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export const useAppStore = defineStore('app', {
|
|||||||
tagsViewIcon: true, // 是否显示标签图标
|
tagsViewIcon: true, // 是否显示标签图标
|
||||||
logo: true, // logo
|
logo: true, // logo
|
||||||
fixedHeader: true, // 固定toolheader
|
fixedHeader: true, // 固定toolheader
|
||||||
footer: true, // 显示页脚
|
footer: false, // 不显示页脚(推广后台不需要底部版权栏)
|
||||||
greyMode: false, // 是否开始灰色模式,用于特殊悼念日
|
greyMode: false, // 是否开始灰色模式,用于特殊悼念日
|
||||||
dynamicRouter: true, // 是否动态路由
|
dynamicRouter: true, // 是否动态路由
|
||||||
serverDynamicRouter: false, // 是否服务端渲染动态路由
|
serverDynamicRouter: false, // 是否服务端渲染动态路由
|
||||||
@@ -336,7 +336,13 @@ export const useAppStore = defineStore('app', {
|
|||||||
newTitle !== this.getTitle && this.setTitle(newTitle)
|
newTitle !== this.getTitle && this.setTitle(newTitle)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
persist: true
|
persist: {
|
||||||
|
/* footer 不进持久化:本 store 原先 persist: true 会把 footer: true 写进 localStorage,
|
||||||
|
老用户下次进来 hydrate 又把它盖回 true ⇒ 光改上面的默认值页脚去不掉。
|
||||||
|
omit 在「存」和「取」两侧都生效(见 pinia-plugin-persistedstate dist/index.js:23),
|
||||||
|
所以旧数据里的 footer 会被直接忽略,永远取 state 默认值。 */
|
||||||
|
omit: ['footer']
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const useAppStoreWithOut = () => {
|
export const useAppStoreWithOut = () => {
|
||||||
|
|||||||
@@ -5,6 +5,12 @@
|
|||||||
<p class="desc">查看所有招商及其下属主播、普通用户的汇总数据</p>
|
<p class="desc">查看所有招商及其下属主播、普通用户的汇总数据</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
三级表格的列一律用 min-width,不要改回 width:
|
||||||
|
Element Plus 只在存在弹性列时才把表格撑满容器,全列写死 width ⇒ 右侧留白。
|
||||||
|
(二级/三级表在展开行内,容器宽度由父级决定,所以更要用 min-width 跟随容器。)
|
||||||
|
详见 docs/修改记录.md「表格右侧留白」。
|
||||||
|
-->
|
||||||
<!-- 一级:招商列表 -->
|
<!-- 一级:招商列表 -->
|
||||||
<ElTable v-loading="loading" :data="pagedData" border stripe row-key="userId">
|
<ElTable v-loading="loading" :data="pagedData" border stripe row-key="userId">
|
||||||
<ElTableColumn type="expand">
|
<ElTableColumn type="expand">
|
||||||
@@ -21,26 +27,26 @@
|
|||||||
|
|
||||||
<!-- 三级:普通用户列表 -->
|
<!-- 三级:普通用户列表 -->
|
||||||
<ElTable :data="anchor.users || []" border size="small">
|
<ElTable :data="anchor.users || []" border size="small">
|
||||||
<ElTableColumn prop="userId" label="用户ID" width="90" />
|
<ElTableColumn prop="userId" label="用户ID" min-width="90" />
|
||||||
<ElTableColumn prop="nickName" label="昵称" width="110" />
|
<ElTableColumn prop="nickName" label="昵称" min-width="110" />
|
||||||
<ElTableColumn prop="userName" label="用户名" width="120" />
|
<ElTableColumn prop="userName" label="用户名" min-width="120" />
|
||||||
<ElTableColumn prop="createTime" label="创建时间" width="155" />
|
<ElTableColumn prop="createTime" label="创建时间" min-width="155" />
|
||||||
<ElTableColumn label="总充值" width="105">
|
<ElTableColumn label="总充值" min-width="105">
|
||||||
<template #default="{ row: u }">
|
<template #default="{ row: u }">
|
||||||
{{ formatMoney(u.totalRecharge) }}
|
{{ formatMoney(u.totalRecharge) }}
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn label="总流水" width="110">
|
<ElTableColumn label="总流水" min-width="110">
|
||||||
<template #default="{ row: u }">
|
<template #default="{ row: u }">
|
||||||
{{ formatMoney(u.totalTurnover) }}
|
{{ formatMoney(u.totalTurnover) }}
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn label="特殊游戏流水" width="115">
|
<ElTableColumn label="特殊游戏流水" min-width="115">
|
||||||
<template #default="{ row: u }">
|
<template #default="{ row: u }">
|
||||||
{{ formatMoney(u.specialGameTurnover) }}
|
{{ formatMoney(u.specialGameTurnover) }}
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn label="经典游戏流水" width="115">
|
<ElTableColumn label="经典游戏流水" min-width="115">
|
||||||
<template #default="{ row: u }">
|
<template #default="{ row: u }">
|
||||||
{{ formatMoney(u.classicGameTurnover) }}
|
{{ formatMoney(u.classicGameTurnover) }}
|
||||||
</template>
|
</template>
|
||||||
@@ -49,16 +55,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn prop="userId" label="主播ID" width="120" />
|
<ElTableColumn prop="userId" label="主播ID" min-width="120" />
|
||||||
<ElTableColumn prop="nickName" label="昵称" width="140" />
|
<ElTableColumn prop="nickName" label="昵称" min-width="140" />
|
||||||
<ElTableColumn prop="userName" label="用户名" width="150" />
|
<ElTableColumn prop="userName" label="用户名" min-width="150" />
|
||||||
<ElTableColumn prop="userCount" label="下属用户数" width="110" />
|
<ElTableColumn prop="userCount" label="下属用户数" min-width="110" />
|
||||||
<ElTableColumn label="主播充值" width="130">
|
<ElTableColumn label="主播充值" min-width="130">
|
||||||
<template #default="{ row: a }">
|
<template #default="{ row: a }">
|
||||||
{{ formatMoney(a.selfRecharge) }}
|
{{ formatMoney(a.selfRecharge) }}
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn label="总收益" width="130">
|
<ElTableColumn label="总收益" min-width="130">
|
||||||
<template #default="{ row: a }">
|
<template #default="{ row: a }">
|
||||||
{{ formatMoney(a.totalEarnings) }}
|
{{ formatMoney(a.totalEarnings) }}
|
||||||
</template>
|
</template>
|
||||||
@@ -68,26 +74,26 @@
|
|||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
|
|
||||||
<ElTableColumn prop="userId" label="招商ID" width="120" />
|
<ElTableColumn prop="userId" label="招商ID" min-width="120" />
|
||||||
<ElTableColumn prop="nickName" label="昵称" width="140" />
|
<ElTableColumn prop="nickName" label="昵称" min-width="140" />
|
||||||
<ElTableColumn prop="userName" label="用户名" width="150" />
|
<ElTableColumn prop="userName" label="用户名" min-width="150" />
|
||||||
<ElTableColumn prop="anchorCount" label="下属主播数" width="110" />
|
<ElTableColumn prop="anchorCount" label="下属主播数" min-width="110" />
|
||||||
<ElTableColumn label="总充值" width="130">
|
<ElTableColumn label="总充值" min-width="130">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ formatMoney(row.totalRecharge) }}
|
{{ formatMoney(row.totalRecharge) }}
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn label="总流水" width="130">
|
<ElTableColumn label="总流水" min-width="130">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ formatMoney(row.totalTurnover) }}
|
{{ formatMoney(row.totalTurnover) }}
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn label="总收益" width="130">
|
<ElTableColumn label="总收益" min-width="130">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ formatMoney(row.totalEarnings) }}
|
{{ formatMoney(row.totalEarnings) }}
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn prop="createTime" label="创建时间" width="170" />
|
<ElTableColumn prop="createTime" label="创建时间" min-width="170" />
|
||||||
</ElTable>
|
</ElTable>
|
||||||
|
|
||||||
<div class="pagination-wrap">
|
<div class="pagination-wrap">
|
||||||
|
|||||||
@@ -36,6 +36,11 @@
|
|||||||
<ElButton type="primary" @click="handleSearch">搜索</ElButton>
|
<ElButton type="primary" @click="handleSearch">搜索</ElButton>
|
||||||
<ElButton @click="handleReset">重置</ElButton>
|
<ElButton @click="handleReset">重置</ElButton>
|
||||||
</div>
|
</div>
|
||||||
|
<!--
|
||||||
|
数据列一律用 min-width(「操作」列除外,它要固定宽):
|
||||||
|
Element Plus 只在存在弹性列时才把表格撑满容器,全列写死 width ⇒ 右侧留白。
|
||||||
|
详见 docs/修改记录.md「表格右侧留白」。
|
||||||
|
-->
|
||||||
<ElTable
|
<ElTable
|
||||||
:data="cardList"
|
:data="cardList"
|
||||||
row-key="cardCode"
|
row-key="cardCode"
|
||||||
@@ -44,8 +49,8 @@
|
|||||||
v-loading="tableLoading"
|
v-loading="tableLoading"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<ElTableColumn prop="id" label="ID" width="80" align="center" />
|
<ElTableColumn prop="id" label="ID" min-width="80" align="center" />
|
||||||
<ElTableColumn prop="cardCode" label="卡密" width="200" align="center">
|
<ElTableColumn prop="cardCode" label="卡密" min-width="200" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span class="card-code">{{ row.cardCode }}</span>
|
<span class="card-code">{{ row.cardCode }}</span>
|
||||||
<ElButton type="primary" link size="small" @click="copyText(row.cardCode)">
|
<ElButton type="primary" link size="small" @click="copyText(row.cardCode)">
|
||||||
@@ -53,26 +58,26 @@
|
|||||||
</ElButton>
|
</ElButton>
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn prop="amount" label="金额" width="100" align="center" />
|
<ElTableColumn prop="amount" label="金额" min-width="100" align="center" />
|
||||||
<ElTableColumn prop="status" label="状态" width="90" align="center">
|
<ElTableColumn prop="status" label="状态" min-width="90" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<ElTag v-if="row.status === 0" type="success" size="small">未使用</ElTag>
|
<ElTag v-if="row.status === 0" type="success" size="small">未使用</ElTag>
|
||||||
<ElTag v-else-if="row.status === 2" type="danger" size="small">已销毁</ElTag>
|
<ElTag v-else-if="row.status === 2" type="danger" size="small">已销毁</ElTag>
|
||||||
<ElTag v-else type="info" size="small">已使用</ElTag>
|
<ElTag v-else type="info" size="small">已使用</ElTag>
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn prop="usedByName" label="使用者" width="140" align="center">
|
<ElTableColumn prop="usedByName" label="使用者" min-width="140" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ row.usedByName || '-' }}
|
{{ row.usedByName || '-' }}
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn prop="usedTime" label="使用时间" width="180" align="center">
|
<ElTableColumn prop="usedTime" label="使用时间" min-width="180" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ row.usedTime || '-' }}
|
{{ row.usedTime || '-' }}
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn prop="creatorName" label="创建者" width="140" align="center" />
|
<ElTableColumn prop="creatorName" label="创建者" min-width="140" align="center" />
|
||||||
<ElTableColumn prop="createTime" label="创建时间" width="180" align="center" />
|
<ElTableColumn prop="createTime" label="创建时间" min-width="180" align="center" />
|
||||||
<ElTableColumn label="操作" width="100" align="center" fixed="right">
|
<ElTableColumn label="操作" width="100" align="center" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<ElPopconfirm
|
<ElPopconfirm
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
|
<!--
|
||||||
|
三张报表的列一律用 min-width,不要改回 width:
|
||||||
|
Element Plus 只在「存在弹性列」时才把表格撑满容器
|
||||||
|
(element-plus/es/components/table/src/table-layout.mjs 的 updateColumnsWidth)。
|
||||||
|
全列写死 width 时表格宽 = 各列宽之和 ⇒ 容器再宽右侧也会留白。
|
||||||
|
注意 setColumnWidth 会把 `width` 静态字符串 parseInt 成数字 ⇒ 那是固定列;
|
||||||
|
只写 `min-width` 才会把 column.width 置空 ⇒ 成为弹性列。详见 docs/修改记录.md。
|
||||||
|
-->
|
||||||
<ElTabs v-model="activeTab">
|
<ElTabs v-model="activeTab">
|
||||||
<ElTabPane label="佣金明细记录" name="detail">
|
<ElTabPane label="佣金明细记录" name="detail">
|
||||||
<div class="search-bar">
|
<div class="search-bar">
|
||||||
@@ -10,21 +18,21 @@
|
|||||||
<ElButton type="primary" :icon="Search" @click="loadDetail">查询</ElButton>
|
<ElButton type="primary" :icon="Search" @click="loadDetail">查询</ElButton>
|
||||||
</div>
|
</div>
|
||||||
<ElTable v-loading="loading" :data="pagedDetailData" border stripe>
|
<ElTable v-loading="loading" :data="pagedDetailData" border stripe>
|
||||||
<ElTableColumn prop="localTime" label="本地时间" sortable width="170" />
|
<ElTableColumn prop="localTime" label="本地时间" sortable min-width="170" />
|
||||||
<ElTableColumn prop="phone" label="手机号" width="130" />
|
<ElTableColumn prop="phone" label="手机号" min-width="130" />
|
||||||
<ElTableColumn prop="consumeAmount" sortable width="120">
|
<ElTableColumn prop="consumeAmount" sortable min-width="120">
|
||||||
<template #header>
|
<template #header>
|
||||||
<span>该次消费/充值金额</span>
|
<span>该次消费/充值金额</span>
|
||||||
</template>
|
</template>
|
||||||
<template #default="{ row }"><span style="color:#f56c6c">{{ formatNum(row.consumeAmount) }}</span></template>
|
<template #default="{ row }"><span style="color:#f56c6c">{{ formatNum(row.consumeAmount) }}</span></template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn label="佣金比例" width="90">
|
<ElTableColumn label="佣金比例" min-width="90">
|
||||||
<template #default="{ row }">{{ formatRate(row.commissionRate, row.sourceGame) }}</template>
|
<template #default="{ row }">{{ formatRate(row.commissionRate, row.sourceGame) }}</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn prop="commissionAmount" label="佣金金额" sortable width="100">
|
<ElTableColumn prop="commissionAmount" label="佣金金额" sortable min-width="100">
|
||||||
<template #default="{ row }"><span style="color:#67c23a">{{ formatNum(row.commissionAmount) }}</span></template>
|
<template #default="{ row }"><span style="color:#67c23a">{{ formatNum(row.commissionAmount) }}</span></template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn prop="sourceGame" label="来源" width="120">
|
<ElTableColumn prop="sourceGame" label="来源" min-width="120">
|
||||||
<template #default="{ row }">{{ gameTypeLabel(row.sourceGame, row.originalSource) }}</template>
|
<template #default="{ row }">{{ gameTypeLabel(row.sourceGame, row.originalSource) }}</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
</ElTable>
|
</ElTable>
|
||||||
@@ -42,17 +50,17 @@
|
|||||||
<ElButton type="primary" :icon="Search" @click="loadDaily">查询</ElButton>
|
<ElButton type="primary" :icon="Search" @click="loadDaily">查询</ElButton>
|
||||||
</div>
|
</div>
|
||||||
<ElTable v-loading="loading" :data="pagedDailyData" border stripe>
|
<ElTable v-loading="loading" :data="pagedDailyData" border stripe>
|
||||||
<ElTableColumn prop="phone" label="手机号" width="130" />
|
<ElTableColumn prop="phone" label="手机号" min-width="130" />
|
||||||
<ElTableColumn prop="consumeAmount" label="今日消费" sortable width="120">
|
<ElTableColumn prop="consumeAmount" label="今日消费" sortable min-width="120">
|
||||||
<template #default="{ row }"><span style="color:#f56c6c">{{ formatNum(row.consumeAmount) }}</span></template>
|
<template #default="{ row }"><span style="color:#f56c6c">{{ formatNum(row.consumeAmount) }}</span></template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn prop="gameType" label="来源" width="120">
|
<ElTableColumn prop="gameType" label="来源" min-width="120">
|
||||||
<template #default="{ row }">{{ gameTypeLabel(row.gameType) }}</template>
|
<template #default="{ row }">{{ gameTypeLabel(row.gameType) }}</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn prop="commissionAmount" label="佣金金额" sortable width="120">
|
<ElTableColumn prop="commissionAmount" label="佣金金额" sortable min-width="120">
|
||||||
<template #default="{ row }"><span style="color:#67c23a">{{ formatNum(row.commissionAmount) }}</span></template>
|
<template #default="{ row }"><span style="color:#67c23a">{{ formatNum(row.commissionAmount) }}</span></template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn prop="date" label="日期" sortable width="130" />
|
<ElTableColumn prop="date" label="日期" sortable min-width="130" />
|
||||||
</ElTable>
|
</ElTable>
|
||||||
<div class="pagination-wrap">
|
<div class="pagination-wrap">
|
||||||
<ElPagination v-model:current-page="dailyPage" v-model:page-size="dailySize" :page-sizes="[10,20,50]" :total="dailyData.length" layout="total, sizes, prev, pager, next, jumper" @size-change="dailyPage = 1" />
|
<ElPagination v-model:current-page="dailyPage" v-model:page-size="dailySize" :page-sizes="[10,20,50]" :total="dailyData.length" layout="total, sizes, prev, pager, next, jumper" @size-change="dailyPage = 1" />
|
||||||
@@ -69,17 +77,17 @@
|
|||||||
<ElButton type="primary" :icon="Search" @click="loadSummary">查询</ElButton>
|
<ElButton type="primary" :icon="Search" @click="loadSummary">查询</ElButton>
|
||||||
</div>
|
</div>
|
||||||
<ElTable v-loading="loading" :data="pagedSummaryData" border stripe>
|
<ElTable v-loading="loading" :data="pagedSummaryData" border stripe>
|
||||||
<ElTableColumn prop="phone" label="手机号" width="130" />
|
<ElTableColumn prop="phone" label="手机号" min-width="130" />
|
||||||
<ElTableColumn prop="gameType" label="来源" width="120">
|
<ElTableColumn prop="gameType" label="来源" min-width="120">
|
||||||
<template #default="{ row }">{{ gameTypeLabel(row.gameType) }}</template>
|
<template #default="{ row }">{{ gameTypeLabel(row.gameType) }}</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn prop="totalFlow" label="消费金额" sortable width="120">
|
<ElTableColumn prop="totalFlow" label="消费金额" sortable min-width="120">
|
||||||
<template #default="{ row }"><span style="color:#f56c6c">{{ formatNum(row.totalFlow) }}</span></template>
|
<template #default="{ row }"><span style="color:#f56c6c">{{ formatNum(row.totalFlow) }}</span></template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn prop="totalCommission" label="本次佣金" sortable width="120">
|
<ElTableColumn prop="totalCommission" label="本次佣金" sortable min-width="120">
|
||||||
<template #default="{ row }"><span style="color:#67c23a">{{ formatNum(row.totalCommission) }}</span></template>
|
<template #default="{ row }"><span style="color:#67c23a">{{ formatNum(row.totalCommission) }}</span></template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn prop="gameCount" label="参加游戏次数" sortable width="140" />
|
<ElTableColumn prop="gameCount" label="参加游戏次数" sortable min-width="140" />
|
||||||
</ElTable>
|
</ElTable>
|
||||||
<div class="pagination-wrap">
|
<div class="pagination-wrap">
|
||||||
<ElPagination v-model:current-page="summaryPage" v-model:page-size="summarySize" :page-sizes="[10,20,50]" :total="summaryData.length" layout="total, sizes, prev, pager, next, jumper" @size-change="summaryPage = 1" />
|
<ElPagination v-model:current-page="summaryPage" v-model:page-size="summarySize" :page-sizes="[10,20,50]" :total="summaryData.length" layout="total, sizes, prev, pager, next, jumper" @size-change="summaryPage = 1" />
|
||||||
|
|||||||
+379
-188
@@ -1,8 +1,118 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="home-page">
|
<div class="home-page">
|
||||||
<!-- 基本信息 -->
|
<!-- ① 我的推广(置顶通栏):把每天真正要用的「链接 / 邀请码」提到最显眼处 -->
|
||||||
<div class="section">
|
<section class="cc-card promo">
|
||||||
<h3 class="section-title">基本信息</h3>
|
<div class="promo-hero">
|
||||||
|
<div class="hero-main">
|
||||||
|
<div class="hero-label">我的推广链接</div>
|
||||||
|
<div class="hero-link" :title="shareLink">
|
||||||
|
{{ shareLink || '暂未生成推广链接' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ElButton
|
||||||
|
type="primary"
|
||||||
|
class="hero-btn"
|
||||||
|
:disabled="!shareLink"
|
||||||
|
@click="copyText(shareLink)"
|
||||||
|
>
|
||||||
|
<ElIcon :size="15"><DocumentCopy /></ElIcon>
|
||||||
|
<span class="hero-btn-text">复制链接</span>
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="promo-cells">
|
||||||
|
<div class="pcell">
|
||||||
|
<div class="pcell-label">我的邀请码</div>
|
||||||
|
<div class="pcell-value">
|
||||||
|
<span class="pcell-strong">{{ userInfo.invitationCode || '-' }}</span>
|
||||||
|
<ElButton
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
size="small"
|
||||||
|
@click="copyText(userInfo.invitationCode)"
|
||||||
|
>
|
||||||
|
<ElIcon :size="14"><DocumentCopy /></ElIcon>
|
||||||
|
</ElButton>
|
||||||
|
<ElButton
|
||||||
|
v-if="!userInfo.invitationCodeModified"
|
||||||
|
type="warning"
|
||||||
|
link
|
||||||
|
size="small"
|
||||||
|
@click="editDialogVisible = true"
|
||||||
|
>
|
||||||
|
修改
|
||||||
|
</ElButton>
|
||||||
|
<span v-else class="pcell-tip">(已修改)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pcell">
|
||||||
|
<div class="pcell-label">手机号码</div>
|
||||||
|
<div class="pcell-value">{{ userInfo.phoneNumber || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="pcell">
|
||||||
|
<div class="pcell-label">上级邀请码</div>
|
||||||
|
<div class="pcell-value">{{ userInfo.parentInvitationCode || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ② 返利 KPI 横带:累计 / 今日 / 本周 / 下级 -->
|
||||||
|
<div class="kpi-band">
|
||||||
|
<div v-for="k in kpis" :key="k.label" class="kpi">
|
||||||
|
<div class="kpi-label">{{ k.label }}</div>
|
||||||
|
<div class="kpi-value" :class="k.tone">{{ k.value }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ③ 底部两栏:返利比例(左窄) | 周期返利汇总(右宽) -->
|
||||||
|
<div class="bottom-grid">
|
||||||
|
<section class="cc-card rate-panel">
|
||||||
|
<h3 class="cc-title">返利比例</h3>
|
||||||
|
<ul class="rate-list">
|
||||||
|
<li v-for="g in rateRows" :key="g.name" class="rate-row">
|
||||||
|
<span class="rate-name">{{ g.name }}</span>
|
||||||
|
<span class="rate-dots" aria-hidden="true"></span>
|
||||||
|
<span class="rate-val">{{ formatRate(g.rate) }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="cc-card table-panel">
|
||||||
|
<h3 class="cc-title">周期返利汇总</h3>
|
||||||
|
<ElTable :data="periodData" border stripe class="commission-table">
|
||||||
|
<ElTableColumn prop="period" label="周期" align="center" width="100" />
|
||||||
|
<ElTableColumn prop="classicBox" label="经典开箱" align="center" min-width="110">
|
||||||
|
<template #default="{ row }">{{ formatNum(row.classicBox) }}</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn prop="luckyUpgrade" label="幸运升级" align="center" min-width="110">
|
||||||
|
<template #default="{ row }">{{ formatNum(row.luckyUpgrade) }}</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn prop="battle" label="对战" align="center" min-width="110">
|
||||||
|
<template #default="{ row }">{{ formatNum(row.battle) }}</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn prop="crazyWheel" label="疯狂转盘" align="center" min-width="110">
|
||||||
|
<template #default="{ row }">{{ formatNum(row.crazyWheel) }}</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn prop="mahjong" label="麻将胡了" align="center" min-width="110">
|
||||||
|
<template #default="{ row }">{{ formatNum(row.mahjong) }}</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn prop="totalConsume" label="总消费" align="center" min-width="110">
|
||||||
|
<template #default="{ row }">{{ formatNum(row.totalConsume) }}</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn prop="recharge" label="充值" align="center" min-width="110">
|
||||||
|
<template #default="{ row }">{{ formatNum(row.recharge) }}</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn prop="newSub" label="新增下级" align="center" min-width="100" />
|
||||||
|
<ElTableColumn prop="totalRebate" label="总返利" align="center" min-width="110">
|
||||||
|
<template #default="{ row }">{{ formatNum(row.totalRebate) }}</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
</ElTable>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ④ 账号信息(降级到底部):日常用不到的身份/余额信息 -->
|
||||||
|
<section class="cc-card account-panel">
|
||||||
|
<h3 class="cc-title">账号信息</h3>
|
||||||
<ElDescriptions :column="3" border class="custom-desc">
|
<ElDescriptions :column="3" border class="custom-desc">
|
||||||
<ElDescriptionsItem label="用户账号">
|
<ElDescriptionsItem label="用户账号">
|
||||||
{{ userInfo.userId }}
|
{{ userInfo.userId }}
|
||||||
@@ -16,121 +126,18 @@
|
|||||||
<ElDescriptionsItem label="积分余额">
|
<ElDescriptionsItem label="积分余额">
|
||||||
{{ userInfo.accountCredits ?? 0 }}
|
{{ userInfo.accountCredits ?? 0 }}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="手机号码">
|
|
||||||
{{ userInfo.userName }}
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="注册时间">
|
<ElDescriptionsItem label="注册时间">
|
||||||
{{ userInfo.createTime }}
|
{{ userInfo.createTime }}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="Steam交易链接">
|
<ElDescriptionsItem label="Steam交易链接">
|
||||||
{{ userInfo.transactionLink || '-' }}
|
{{ userInfo.transactionLink || '-' }}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="上级邀请码">
|
|
||||||
{{ userInfo.parentInvitationCode || '-' }}
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="我的邀请码">
|
|
||||||
{{ userInfo.invitationCode }}
|
|
||||||
<ElButton
|
|
||||||
type="primary"
|
|
||||||
link
|
|
||||||
size="small"
|
|
||||||
style="margin-left: 8px"
|
|
||||||
@click="copyText(userInfo.invitationCode)"
|
|
||||||
>
|
|
||||||
<ElIcon :size="14"><DocumentCopy /></ElIcon>
|
|
||||||
</ElButton>
|
|
||||||
<ElButton
|
|
||||||
v-if="!userInfo.invitationCodeModified"
|
|
||||||
type="warning"
|
|
||||||
link
|
|
||||||
size="small"
|
|
||||||
style="margin-left: 8px"
|
|
||||||
@click="editDialogVisible = true"
|
|
||||||
>
|
|
||||||
修改
|
|
||||||
</ElButton>
|
|
||||||
<span v-else style="margin-left: 8px; color: #909399; font-size: 12px">(已修改)</span>
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="推广链接">
|
|
||||||
{{ shareLink }}
|
|
||||||
<ElButton
|
|
||||||
v-if="shareLink"
|
|
||||||
type="primary"
|
|
||||||
link
|
|
||||||
size="small"
|
|
||||||
style="margin-left: 8px"
|
|
||||||
@click="copyText(shareLink)"
|
|
||||||
>
|
|
||||||
<ElIcon :size="14"><DocumentCopy /></ElIcon>
|
|
||||||
</ElButton>
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem label="" />
|
|
||||||
</ElDescriptions>
|
</ElDescriptions>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<!-- 游戏返利看板 -->
|
|
||||||
<div class="section">
|
|
||||||
<h3 class="section-title">游戏返利比例</h3>
|
|
||||||
<div class="game-rate-cards">
|
|
||||||
<div class="rate-card">
|
|
||||||
<div class="rate-card-title">经典开箱</div>
|
|
||||||
<div class="rate-card-value">{{ formatRate(gameRates.classicGameRate) }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="rate-card">
|
|
||||||
<div class="rate-card-title">幸运升级</div>
|
|
||||||
<div class="rate-card-value">{{ formatRate(gameRates.classicGameRate) }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="rate-card">
|
|
||||||
<div class="rate-card-title">对战</div>
|
|
||||||
<div class="rate-card-value">{{ formatRate(gameRates.classicGameRate) }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="rate-card">
|
|
||||||
<div class="rate-card-title">疯狂转盘</div>
|
|
||||||
<div class="rate-card-value">{{ formatRate(gameRates.classicGameRate) }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="rate-card">
|
|
||||||
<div class="rate-card-title">麻将胡了</div>
|
|
||||||
<div class="rate-card-value">{{ formatRate(gameRates.newGameRate) }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 周期返利汇总 -->
|
|
||||||
<div class="section">
|
|
||||||
<h3 class="section-title">周期返利汇总 </h3>
|
|
||||||
<ElTable :data="periodData" border stripe class="commission-table">
|
|
||||||
<ElTableColumn prop="period" label="周期" align="center" width="100" />
|
|
||||||
<ElTableColumn prop="classicBox" label="经典开箱" align="center" min-width="110">
|
|
||||||
<template #default="{ row }">{{ formatNum(row.classicBox) }}</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn prop="luckyUpgrade" label="幸运升级" align="center" min-width="110">
|
|
||||||
<template #default="{ row }">{{ formatNum(row.luckyUpgrade) }}</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn prop="battle" label="对战" align="center" min-width="110">
|
|
||||||
<template #default="{ row }">{{ formatNum(row.battle) }}</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn prop="crazyWheel" label="疯狂转盘" align="center" min-width="110">
|
|
||||||
<template #default="{ row }">{{ formatNum(row.crazyWheel) }}</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn prop="mahjong" label="麻将胡了" align="center" min-width="110">
|
|
||||||
<template #default="{ row }">{{ formatNum(row.mahjong) }}</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn prop="totalConsume" label="总消费" align="center" min-width="110">
|
|
||||||
<template #default="{ row }">{{ formatNum(row.totalConsume) }}</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn prop="recharge" label="充值" align="center" min-width="110">
|
|
||||||
<template #default="{ row }">{{ formatNum(row.recharge) }}</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn prop="newSub" label="新增下级" align="center" min-width="100" />
|
|
||||||
<ElTableColumn prop="totalRebate" label="总返利" align="center" min-width="110">
|
|
||||||
<template #default="{ row }">{{ formatNum(row.totalRebate) }}</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
</ElTable>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 修改邀请码弹窗 -->
|
<!-- 修改邀请码弹窗 -->
|
||||||
<ElDialog v-model="editDialogVisible" title="修改邀请码" width="400px">
|
<ElDialog v-model="editDialogVisible" title="修改邀请码" width="400px">
|
||||||
<p style="color: #e6a23c; margin-bottom: 12px">注意:邀请码只能修改一次,请谨慎操作!</p>
|
<p style="color: #8f6418; margin-bottom: 12px">注意:邀请码只能修改一次,请谨慎操作!</p>
|
||||||
<ElForm>
|
<ElForm>
|
||||||
<ElFormItem label="新邀请码">
|
<ElFormItem label="新邀请码">
|
||||||
<ElInput v-model="newInvitationCode" placeholder="请输入 6 位邀请码" maxlength="6" :pattern="/^[a-zA-Z0-9]*$/" :rules="[{ required: true, message: '邀请码只能包含英文字母和数字', trigger: 'blur' }]" />
|
<ElInput v-model="newInvitationCode" placeholder="请输入 6 位邀请码" maxlength="6" :pattern="/^[a-zA-Z0-9]*$/" :rules="[{ required: true, message: '邀请码只能包含英文字母和数字', trigger: 'blur' }]" />
|
||||||
@@ -150,12 +157,10 @@ import { ElDescriptions, ElDescriptionsItem, ElButton, ElMessage, ElDialog, ElFo
|
|||||||
import { DocumentCopy } from '@element-plus/icons-vue'
|
import { DocumentCopy } from '@element-plus/icons-vue'
|
||||||
import { getInfo, getShareDomain, updateInvitationCode } from '@/api/user'
|
import { getInfo, getShareDomain, updateInvitationCode } from '@/api/user'
|
||||||
import { getGameCommissionData } from '@/api/turnover'
|
import { getGameCommissionData } from '@/api/turnover'
|
||||||
import { useUserStore } from '@/store/modules/user'
|
|
||||||
|
|
||||||
// 用户基本信息
|
// 用户基本信息
|
||||||
const userInfo = reactive<any>({})
|
const userInfo = reactive<any>({})
|
||||||
const shareLink = ref('')
|
const shareLink = ref('')
|
||||||
const userStore = useUserStore()
|
|
||||||
|
|
||||||
// 修改邀请码弹窗
|
// 修改邀请码弹窗
|
||||||
const editDialogVisible = ref(false)
|
const editDialogVisible = ref(false)
|
||||||
@@ -183,13 +188,6 @@ const submitInvitationCode = async () => {
|
|||||||
editDialogVisible.value = false
|
editDialogVisible.value = false
|
||||||
newInvitationCode.value = ''
|
newInvitationCode.value = ''
|
||||||
await loadUserInfo()
|
await loadUserInfo()
|
||||||
// 同步更新顶部 header 中的分享链接
|
|
||||||
if (userStore.getUserInfo) {
|
|
||||||
userStore.setUserInfo({
|
|
||||||
...userStore.getUserInfo,
|
|
||||||
shareLink: shareLink.value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(res.msg || '修改失败')
|
ElMessage.error(res.msg || '修改失败')
|
||||||
}
|
}
|
||||||
@@ -205,7 +203,7 @@ const gameRates = reactive({
|
|||||||
classicGameRate: 0
|
classicGameRate: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
// 周期返利汇总数据
|
// 周期返利汇总数据(后端顺序固定:今日 / 昨日 / 本周 / 总)
|
||||||
const periodData = ref([] as any[])
|
const periodData = ref([] as any[])
|
||||||
|
|
||||||
// 用户类型映射
|
// 用户类型映射
|
||||||
@@ -221,6 +219,38 @@ const userTypeLabel = computed(() => {
|
|||||||
return map[userInfo.userType] || '未知'
|
return map[userInfo.userType] || '未知'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返利比例列表。后端只下发三个比例(充值 / 新游戏 / 经典玩法),
|
||||||
|
* 5 个玩法里「麻将胡了」走新游戏比例,其余 4 个经典玩法共用经典比例
|
||||||
|
* —— 与后端 buildPeriod 里 classicConsume / mahjong 的算法口径一致。
|
||||||
|
*/
|
||||||
|
const rateRows = computed(() => [
|
||||||
|
{ name: '经典开箱', rate: gameRates.classicGameRate },
|
||||||
|
{ name: '幸运升级', rate: gameRates.classicGameRate },
|
||||||
|
{ name: '对战', rate: gameRates.classicGameRate },
|
||||||
|
{ name: '疯狂转盘', rate: gameRates.classicGameRate },
|
||||||
|
{ name: '麻将胡了', rate: gameRates.newGameRate }
|
||||||
|
])
|
||||||
|
|
||||||
|
// 按周期名取 periodData 里的一行
|
||||||
|
const periodOf = (name: string) => periodData.value.find((p: any) => p.period === name)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顶部 KPI 横带:累计返利 / 今日返利 / 本周返利 / 累计下级。
|
||||||
|
* 全部取自周期返利汇总的「总 / 今日 / 本周」行,不额外发请求。
|
||||||
|
*/
|
||||||
|
const kpis = computed(() => {
|
||||||
|
const total = periodOf('总')
|
||||||
|
const today = periodOf('今日')
|
||||||
|
const week = periodOf('本周')
|
||||||
|
return [
|
||||||
|
{ label: '累计返利', value: formatNum(total?.totalRebate ?? 0), tone: 'is-primary' },
|
||||||
|
{ label: '今日返利', value: formatNum(today?.totalRebate ?? 0), tone: '' },
|
||||||
|
{ label: '本周返利', value: formatNum(week?.totalRebate ?? 0), tone: '' },
|
||||||
|
{ label: '累计下级', value: String(total?.newSub ?? 0), tone: 'is-success' }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
// 复制到剪贴板(兼容非 HTTPS 环境)
|
// 复制到剪贴板(兼容非 HTTPS 环境)
|
||||||
const copyText = (text: string) => {
|
const copyText = (text: string) => {
|
||||||
if (navigator.clipboard) {
|
if (navigator.clipboard) {
|
||||||
@@ -322,97 +352,258 @@ onMounted(async () => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.home-page {
|
.home-page {
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #303133;
|
|
||||||
margin: 0 0 15px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-type-anchor {
|
|
||||||
color: #e6a23c;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 覆盖 ElDescriptions 默认的 label 灰色背景 */
|
|
||||||
.custom-desc :deep(.el-descriptions__label) {
|
|
||||||
background-color: #fff;
|
|
||||||
color: #606266;
|
|
||||||
font-weight: normal;
|
|
||||||
width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-desc :deep(.el-descriptions__content) {
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 游戏返利比例卡片 */
|
|
||||||
.game-rate-cards {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 15px;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
gap: 16px;
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rate-card {
|
/* 通用卡片外壳(田园核:奶油面 + 亚麻描边) */
|
||||||
flex: 1;
|
.cc-card {
|
||||||
min-width: 150px;
|
background-color: #fffdfa;
|
||||||
max-width: calc(20% - 12px);
|
border: 1px solid #eadfd0;
|
||||||
background-color: #f5f7fa;
|
border-radius: 12px;
|
||||||
border: 1px solid #e4e7ed;
|
padding: 18px 20px;
|
||||||
border-radius: 6px;
|
box-shadow: 0 2px 10px rgba(139, 115, 85, 0.06);
|
||||||
padding: 20px 15px;
|
|
||||||
text-align: center;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rate-card:hover {
|
.cc-title {
|
||||||
background-color: #fbf5ea;
|
|
||||||
border-color: #9a5c35;
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 2px 12px rgba(154, 92, 53, 0.10);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rate-card-title {
|
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
color: #303133;
|
color: #4f4130;
|
||||||
|
margin: 0 0 14px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== ① 我的推广 ===== */
|
||||||
|
.promo-hero {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background-color: #fbf5ea;
|
||||||
|
border: 1px solid #eadfd0;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #7a664e;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-link {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #4f4130;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-btn {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-btn-text {
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promo-cells {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 14px;
|
||||||
|
border-top: 1px dashed #eadfd0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcell {
|
||||||
|
padding: 0 18px;
|
||||||
|
border-left: 1px solid #eadfd0;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcell:first-child {
|
||||||
|
padding-left: 0;
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcell-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #7a664e;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcell-value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #4f4130;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcell-strong {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #9a5c35;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcell-tip {
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #7a664e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== ② KPI 横带 ===== */
|
||||||
|
.kpi-band {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
background-color: #fffdfa;
|
||||||
|
border: 1px solid #eadfd0;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 10px rgba(139, 115, 85, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi {
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-left: 1px solid #eadfd0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi:first-child {
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #7a664e;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rate-card-sub {
|
.kpi-value {
|
||||||
font-size: 12px;
|
font-size: 19px;
|
||||||
color: #909399;
|
font-weight: 700;
|
||||||
margin-bottom: 12px;
|
line-height: 1.2;
|
||||||
|
color: #4f4130;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rate-card-value {
|
.kpi-value.is-primary {
|
||||||
font-size: 20px;
|
color: #9a5c35;
|
||||||
font-weight: bold;
|
}
|
||||||
color: #67c23a;
|
|
||||||
|
.kpi-value.is-success {
|
||||||
|
color: #3f6b45;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== ③ 底部两栏 ===== */
|
||||||
|
.bottom-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 260px minmax(0, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px dashed #eadfd0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate-name {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #5f4f3d;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate-dots {
|
||||||
|
flex: 1;
|
||||||
|
border-bottom: 1px dotted #d8c3b0;
|
||||||
|
transform: translateY(-3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate-val {
|
||||||
|
flex: none;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #3f6b45;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-panel {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 返利汇总表格 */
|
|
||||||
.commission-table {
|
.commission-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commission-table :deep(.el-table__header th) {
|
.commission-table :deep(.el-table__header th) {
|
||||||
background-color: #fafafa !important;
|
background-color: #f6efe3 !important;
|
||||||
color: #606266;
|
color: #5f4f3d;
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commission-table :deep(.el-table__row tr:nth-child(odd)) {
|
/* ===== ④ 账号信息(降级:不做成卡片,别和上面的数据抢注意力) ===== */
|
||||||
background-color: #fafafa;
|
.account-panel {
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: transparent;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commission-table :deep(.el-table__row tr:nth-child(even)) {
|
.user-type-anchor {
|
||||||
background-color: #fff;
|
color: #8f6418;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-desc :deep(.el-descriptions__label) {
|
||||||
|
background-color: #f6efe3;
|
||||||
|
color: #7a664e;
|
||||||
|
font-weight: normal;
|
||||||
|
width: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-desc :deep(.el-descriptions__content) {
|
||||||
|
background-color: #fffdfa;
|
||||||
|
color: #4f4130;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 窄屏:底部两栏收成一栏,表格拿满宽度 */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.bottom-grid {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.kpi-band {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.promo-cells {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pcell {
|
||||||
|
padding: 0;
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -215,86 +215,15 @@ const inviteCode = computed(() => {
|
|||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
验证码行
|
验证码行
|
||||||
只改按钮,不碰 input / row / col 的布局
|
「获取验证码」按钮的配色写在 LoginForm.vue 里(.login-code-btn)。
|
||||||
|
这里原本有一条 `.el-form-item .el-row .el-col:last-child .el-button`
|
||||||
|
和一组 .send-code / .code-button / .get-code-btn,都是死代码:
|
||||||
|
Form.vue 的实际结构是 ElRow > ElCol > ElFormItem > 插槽内容,
|
||||||
|
el-row 是 el-form-item 的祖先而不是后代,选择器永远匹配不到;
|
||||||
|
类名也对不上(真实类名是 .login-code-btn)。
|
||||||
|
不要再往这里加按钮样式,改 LoginForm.vue。
|
||||||
========================= */
|
========================= */
|
||||||
|
|
||||||
/*
|
|
||||||
LoginForm 常见结构:
|
|
||||||
el-form-item
|
|
||||||
el-row
|
|
||||||
el-col -> input
|
|
||||||
el-col -> button
|
|
||||||
|
|
||||||
这里仅让右侧按钮与输入框同高、同圆角、同玻璃质感。
|
|
||||||
*/
|
|
||||||
:deep(.el-form-item .el-row .el-col:last-child .el-button) {
|
|
||||||
width: 100%;
|
|
||||||
height: 48px;
|
|
||||||
|
|
||||||
padding: 0 16px;
|
|
||||||
|
|
||||||
border-radius: 12px;
|
|
||||||
|
|
||||||
border: 1px solid rgba(212, 160, 160, 0.6);
|
|
||||||
|
|
||||||
background: rgba(245, 215, 95, 0.22);
|
|
||||||
|
|
||||||
color: #9a5c35;
|
|
||||||
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
|
|
||||||
box-shadow: none;
|
|
||||||
|
|
||||||
transition:
|
|
||||||
background 0.2s ease,
|
|
||||||
border-color 0.2s ease,
|
|
||||||
color 0.2s ease,
|
|
||||||
transform 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #7d4a2a;
|
|
||||||
|
|
||||||
border-color: #c08a8a;
|
|
||||||
|
|
||||||
background: rgba(245, 215, 95, 0.34);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
transform: scale(0.97);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-disabled,
|
|
||||||
&:disabled {
|
|
||||||
color: #9a886e;
|
|
||||||
|
|
||||||
border-color: #eadfd0;
|
|
||||||
|
|
||||||
background: #f6efe3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
如果验证码按钮不是放在 el-row/el-col 里,
|
|
||||||
兼容常见的 send-code / code-button 类。
|
|
||||||
*/
|
|
||||||
:deep(.send-code),
|
|
||||||
:deep(.code-button),
|
|
||||||
:deep(.get-code-btn) {
|
|
||||||
height: 48px;
|
|
||||||
|
|
||||||
border-radius: 12px;
|
|
||||||
|
|
||||||
border: 1px solid rgba(212, 160, 160, 0.6);
|
|
||||||
|
|
||||||
background: rgba(245, 215, 95, 0.22);
|
|
||||||
|
|
||||||
color: #9a5c35;
|
|
||||||
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
登录主按钮
|
登录主按钮
|
||||||
========================= */
|
========================= */
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { getAdminRoleApi, getCodeApi, precheckPhoneApi, smsLoginApi } from '@/api/login'
|
import {
|
||||||
import type { SmsLoginType, UserType } from '@/api/login/types'
|
devLoginApi,
|
||||||
|
getAdminRoleApi,
|
||||||
|
getCodeApi,
|
||||||
|
precheckPhoneApi,
|
||||||
|
smsLoginApi
|
||||||
|
} from '@/api/login'
|
||||||
|
import type { DevLoginType, SmsLoginType, UserType } from '@/api/login/types'
|
||||||
import { getMyMenus } from '@/api/permission'
|
import { getMyMenus } from '@/api/permission'
|
||||||
import { PATH_URL } from '@/axios/service'
|
import { PATH_URL } from '@/axios/service'
|
||||||
import { BaseButton } from '@/components/Button'
|
import { BaseButton } from '@/components/Button'
|
||||||
@@ -25,12 +31,20 @@ const { currentRoute, addRoute, push, resolve } = router
|
|||||||
const { required } = useValidator()
|
const { required } = useValidator()
|
||||||
const { formRegister, formMethods } = useForm()
|
const { formRegister, formMethods } = useForm()
|
||||||
const { getFormData, getElFormExpose, setValues } = formMethods
|
const { getFormData, getElFormExpose, setValues } = formMethods
|
||||||
|
// 账号密码登录用独立表单实例:与短信表单互斥渲染(v-if 切换),各自持有自己的 el-form
|
||||||
|
const { formRegister: pwdFormRegister, formMethods: pwdFormMethods } = useForm()
|
||||||
|
const { getFormData: getPwdFormData, getElFormExpose: getPwdElFormExpose } = pwdFormMethods
|
||||||
|
|
||||||
// 招商邀请链接里带的邀请码(/login?code=xxx),进入后自动填到邀请码框
|
// 招商邀请链接里带的邀请码(/login?code=xxx),进入后自动填到邀请码框
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
inviteCode?: string
|
inviteCode?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
// 账号密码登录只在本地 dev server(pnpm run dev)下提供:
|
||||||
|
// 生产/测试用 vite build 打包时 import.meta.env.DEV 为 false,入口与表单整块不渲染
|
||||||
|
const devPasswordLogin = import.meta.env.DEV
|
||||||
|
const loginMode = ref<'sms' | 'password'>('sms')
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const redirect = ref('')
|
const redirect = ref('')
|
||||||
const getCodeTime = ref(60)
|
const getCodeTime = ref(60)
|
||||||
@@ -62,19 +76,27 @@ const rules = {
|
|||||||
code: [required()]
|
code: [required()]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const passwordRules = {
|
||||||
|
account: [required()],
|
||||||
|
password: [required()]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 两种登录模式共用的标题,保证卡片标题样式完全一致
|
||||||
|
const renderLoginTitle = () => (
|
||||||
|
<div class="login-form-title">
|
||||||
|
<div class="title-decoration"></div>
|
||||||
|
<h2>登录</h2>
|
||||||
|
<div class="title-decoration"></div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
const schema = reactive<FormSchema[]>([
|
const schema = reactive<FormSchema[]>([
|
||||||
{
|
{
|
||||||
field: 'title',
|
field: 'title',
|
||||||
colProps: { span: 24 },
|
colProps: { span: 24 },
|
||||||
formItemProps: {
|
formItemProps: {
|
||||||
slots: {
|
slots: {
|
||||||
default: () => (
|
default: renderLoginTitle
|
||||||
<div class="login-form-title">
|
|
||||||
<div class="title-decoration"></div>
|
|
||||||
<h2>登录</h2>
|
|
||||||
<div class="title-decoration"></div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -162,6 +184,82 @@ const schema = reactive<FormSchema[]>([
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// 账号密码登录表单:字段口径与后端 /promo/devLogin 一致(推广账号填手机号,超管可填账号名或手机号)
|
||||||
|
const passwordSchema = reactive<FormSchema[]>([
|
||||||
|
{
|
||||||
|
field: 'title',
|
||||||
|
colProps: { span: 24 },
|
||||||
|
formItemProps: {
|
||||||
|
slots: {
|
||||||
|
default: renderLoginTitle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'account',
|
||||||
|
label: '',
|
||||||
|
value: '',
|
||||||
|
colProps: { span: 24 },
|
||||||
|
formItemProps: {
|
||||||
|
slots: {
|
||||||
|
default: (formData) => (
|
||||||
|
<ElInput
|
||||||
|
v-model={formData.account}
|
||||||
|
maxlength={32}
|
||||||
|
clearable
|
||||||
|
placeholder="请输入账号(推广账号填手机号)"
|
||||||
|
onKeydown={(event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Enter') passwordSignIn()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'password',
|
||||||
|
label: '',
|
||||||
|
value: '',
|
||||||
|
colProps: { span: 24 },
|
||||||
|
formItemProps: {
|
||||||
|
slots: {
|
||||||
|
default: (formData) => (
|
||||||
|
<ElInput
|
||||||
|
v-model={formData.password}
|
||||||
|
type="password"
|
||||||
|
showPassword
|
||||||
|
maxlength={32}
|
||||||
|
placeholder="请输入密码"
|
||||||
|
onKeydown={(event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Enter') passwordSignIn()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'login',
|
||||||
|
colProps: { span: 24 },
|
||||||
|
formItemProps: {
|
||||||
|
slots: {
|
||||||
|
default: () => (
|
||||||
|
<div class="login-form-btn w-[100%]">
|
||||||
|
<BaseButton
|
||||||
|
loading={loading.value}
|
||||||
|
type="primary"
|
||||||
|
class="full-width"
|
||||||
|
onClick={passwordSignIn}
|
||||||
|
>
|
||||||
|
登录
|
||||||
|
</BaseButton>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => currentRoute.value,
|
() => currentRoute.value,
|
||||||
(route: RouteLocationNormalizedLoaded) => {
|
(route: RouteLocationNormalizedLoaded) => {
|
||||||
@@ -287,6 +385,37 @@ const signIn = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 账号密码登录(仅 dev server 可见):后端 /promo/devLogin 校验 BCrypt 密码后直接签发 token,
|
||||||
|
// 之后的落地流程与短信登录完全一致
|
||||||
|
const passwordSignIn = async () => {
|
||||||
|
if (loading.value) return
|
||||||
|
const formRef = await getPwdElFormExpose()
|
||||||
|
if (
|
||||||
|
!(await formRef
|
||||||
|
?.validateField(['account', 'password'])
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false))
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const formData = await getPwdFormData<DevLoginType>()
|
||||||
|
const res = await devLoginApi({
|
||||||
|
account: formData.account,
|
||||||
|
password: formData.password
|
||||||
|
})
|
||||||
|
if (!res) return
|
||||||
|
|
||||||
|
userStore.setLoginInfo(undefined)
|
||||||
|
userStore.setRememberMe(false)
|
||||||
|
await completeLogin(formData.account, (res as any).token)
|
||||||
|
await enterSystem(formData.account)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getRoleRouters = async () => {
|
const getRoleRouters = async () => {
|
||||||
const res = (await getMyMenus()) as any
|
const res = (await getMyMenus()) as any
|
||||||
return { code: 0, data: res.data || [] }
|
return { code: 0, data: res.data || [] }
|
||||||
@@ -329,18 +458,96 @@ const enterSystem = async (username: string) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<!-- 登录方式切换:仅本地 dev server 渲染,生产/测试构建包不会出现该入口 -->
|
||||||
|
<div v-if="devPasswordLogin" class="login-mode-switch">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="login-mode-item"
|
||||||
|
:class="{ 'is-active': loginMode === 'sms' }"
|
||||||
|
@click="loginMode = 'sms'"
|
||||||
|
>
|
||||||
|
短信验证码登录
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="login-mode-item"
|
||||||
|
:class="{ 'is-active': loginMode === 'password' }"
|
||||||
|
@click="loginMode = 'password'"
|
||||||
|
>
|
||||||
|
账号密码登录
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Form
|
<Form
|
||||||
:schema="schema"
|
v-if="devPasswordLogin && loginMode === 'password'"
|
||||||
:rules="rules"
|
:schema="passwordSchema"
|
||||||
|
:rules="passwordRules"
|
||||||
label-position="top"
|
label-position="top"
|
||||||
hide-required-asterisk
|
hide-required-asterisk
|
||||||
size="large"
|
size="large"
|
||||||
@register="formRegister"
|
@register="pwdFormRegister"
|
||||||
/>
|
/>
|
||||||
<div id="captcha-box" class="captcha-box"></div>
|
<template v-else>
|
||||||
|
<Form
|
||||||
|
:schema="schema"
|
||||||
|
:rules="rules"
|
||||||
|
label-position="top"
|
||||||
|
hide-required-asterisk
|
||||||
|
size="large"
|
||||||
|
@register="formRegister"
|
||||||
|
/>
|
||||||
|
<div id="captcha-box" class="captcha-box"></div>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
/* =========================
|
||||||
|
登录方式切换(仅 dev server 渲染)
|
||||||
|
与卡片同色系:默认低调暖棕文字,选中态用奶油底 + 花粉细边
|
||||||
|
对比度:默认 5.9:1 / hover 5.3:1 / 选中 6.3:1,均过 WCAG AA
|
||||||
|
========================= */
|
||||||
|
.login-mode-switch {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-mode-item {
|
||||||
|
padding: 7px 16px;
|
||||||
|
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
color: #7a664e;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 999px;
|
||||||
|
|
||||||
|
transition:
|
||||||
|
color 0.2s ease,
|
||||||
|
background 0.2s ease,
|
||||||
|
border-color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #9a5c35;
|
||||||
|
border-color: rgba(212, 160, 160, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
color: #854c2a;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
background: #fbf5ea;
|
||||||
|
border-color: rgba(212, 160, 160, 0.55);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.captcha-box {
|
.captcha-box {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
@@ -361,32 +568,39 @@ const enterSystem = async (username: string) => {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 独立圆角按钮:拼接式(el-input-group__append)左侧必须是直角,四角圆角做不到 */
|
/* 独立圆角按钮:拼接式(el-input-group__append)左侧必须是直角,四角圆角做不到。
|
||||||
|
原紫蓝半透明底(rgba(126,132,240,.3))+ 近白字(#eef0ff)是给深色背景配的,
|
||||||
|
搬到奶油卡片上后字面比底色还亮(对比度约 0.8:1),按钮看上去就是一块空白;
|
||||||
|
改用主题色板内色值:默认 6.31:1 / hover 5.99:1 / 倒计时 5.00:1,均过 WCAG AA */
|
||||||
:deep(.sms-code-row .login-code-btn) {
|
:deep(.sms-code-row .login-code-btn) {
|
||||||
flex: 0 0 124px;
|
flex: 0 0 124px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
margin: 0 0 0 10px;
|
margin: 0 0 0 10px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color: #eef0ff;
|
color: #854c2a;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
border: 1px solid rgba(150, 158, 255, 0.42);
|
border: 1px solid rgba(212, 160, 160, 0.55);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: rgba(126, 132, 240, 0.3);
|
background: #fbf5ea;
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14);
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6);
|
||||||
|
transition:
|
||||||
|
background 0.2s ease,
|
||||||
|
border-color 0.2s ease,
|
||||||
|
color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.sms-code-row .login-code-btn:hover) {
|
:deep(.sms-code-row .login-code-btn:hover) {
|
||||||
color: #fff;
|
color: #854c2a;
|
||||||
border-color: rgba(172, 180, 255, 0.62);
|
border-color: #c08a8a;
|
||||||
background: rgba(126, 132, 240, 0.44);
|
background: #f6efe3;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.sms-code-row .login-code-btn.is-disabled),
|
:deep(.sms-code-row .login-code-btn.is-disabled),
|
||||||
:deep(.sms-code-row .login-code-btn.is-disabled:hover) {
|
:deep(.sms-code-row .login-code-btn.is-disabled:hover) {
|
||||||
color: rgba(230, 232, 255, 0.45);
|
color: #6b6152;
|
||||||
border-color: rgba(255, 255, 255, 0.12);
|
border-color: #eadfd0;
|
||||||
background: rgba(126, 132, 240, 0.14);
|
background: #f0e8dc;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.login-form-btn .el-button) {
|
:deep(.login-form-btn .el-button) {
|
||||||
|
|||||||
@@ -19,21 +19,22 @@
|
|||||||
<ElButton type="primary" :icon="Search" @click="loadList">查询</ElButton>
|
<ElButton type="primary" :icon="Search" @click="loadList">查询</ElButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 列宽一律用 min-width:全列写死 width 时 Element Plus 不拉伸,表格右侧会留白(见 docs/修改记录.md) -->
|
||||||
<ElTable v-loading="loading" :data="pagedData" border stripe>
|
<ElTable v-loading="loading" :data="pagedData" border stripe>
|
||||||
<ElTableColumn prop="rechargeTime" label="充值时间" sortable width="170" />
|
<ElTableColumn prop="rechargeTime" label="充值时间" sortable min-width="170" />
|
||||||
<ElTableColumn prop="userId" label="用户ID" width="120" />
|
<ElTableColumn prop="userId" label="用户ID" min-width="120" />
|
||||||
<ElTableColumn prop="parentId" label="上级ID" width="120">
|
<ElTableColumn prop="parentId" label="上级ID" min-width="120">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ row.parentId || '-' }}
|
{{ row.parentId || '-' }}
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn prop="phone" label="手机号" width="130" />
|
<ElTableColumn prop="phone" label="手机号" min-width="130" />
|
||||||
<ElTableColumn prop="userType" label="用户类型" width="100">
|
<ElTableColumn prop="userType" label="用户类型" min-width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ row.userType === '01' ? '主播' : row.userType === '02' ? '玩家' : row.userType === '04' ? '招商' : row.userType || '-' }}
|
{{ row.userType === '01' ? '主播' : row.userType === '02' ? '玩家' : row.userType === '04' ? '招商' : row.userType || '-' }}
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn prop="rechargeAmount" label="充值金额" sortable width="120">
|
<ElTableColumn prop="rechargeAmount" label="充值金额" sortable min-width="120">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span style="color: #67c23a">{{ formatNum(row.rechargeAmount) }}</span>
|
<span style="color: #67c23a">{{ formatNum(row.rechargeAmount) }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -13,6 +13,10 @@
|
|||||||
>
|
>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
<!--
|
||||||
|
数据列一律用 min-width(「操作」列除外,它要固定宽):
|
||||||
|
全列写死 width 时 Element Plus 不拉伸表格 ⇒ 右侧留白(见 docs/修改记录.md)。
|
||||||
|
-->
|
||||||
<el-table
|
<el-table
|
||||||
:data="alltableData"
|
:data="alltableData"
|
||||||
style="width: 100%; margin-bottom: 20px; margin-top: 10px"
|
style="width: 100%; margin-bottom: 20px; margin-top: 10px"
|
||||||
@@ -21,25 +25,25 @@
|
|||||||
default-expand-all
|
default-expand-all
|
||||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||||
>
|
>
|
||||||
<el-table-column prop="userId" label="用户ID" width="120" />
|
<el-table-column prop="userId" label="用户ID" min-width="120" />
|
||||||
<el-table-column prop="userName" label="用户名" width="140" />
|
<el-table-column prop="userName" label="用户名" min-width="140" />
|
||||||
<el-table-column prop="nickName" label="昵称" width="120" />
|
<el-table-column prop="nickName" label="昵称" min-width="120" />
|
||||||
<el-table-column prop="userType" label="用户类型" width="100">
|
<el-table-column prop="userType" label="用户类型" min-width="100">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag v-if="scope.row.userType === '01'" type="warning" size="small">主播</el-tag>
|
<el-tag v-if="scope.row.userType === '01'" type="warning" size="small">主播</el-tag>
|
||||||
<el-tag v-else-if="scope.row.userType === '02'" type="info" size="small">玩家</el-tag>
|
<el-tag v-else-if="scope.row.userType === '02'" type="info" size="small">玩家</el-tag>
|
||||||
<el-tag v-else size="small">未知</el-tag>
|
<el-tag v-else size="small">未知</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="createTime" label="创建时间" width="170" />
|
<el-table-column prop="createTime" label="创建时间" min-width="170" />
|
||||||
<el-table-column prop="totalRecharge" label="总充值" width="100" />
|
<el-table-column prop="totalRecharge" label="总充值" min-width="100" />
|
||||||
<el-table-column prop="classicBoxConsume" label="经典开箱" width="100" />
|
<el-table-column prop="classicBoxConsume" label="经典开箱" min-width="100" />
|
||||||
<el-table-column prop="battleConsume" label="对战" width="100" />
|
<el-table-column prop="battleConsume" label="对战" min-width="100" />
|
||||||
<el-table-column prop="luckUpgradeConsume" label="幸运升级" width="100" />
|
<el-table-column prop="luckUpgradeConsume" label="幸运升级" min-width="100" />
|
||||||
<el-table-column prop="crazyWheelConsume" label="疯狂转盘" width="100" />
|
<el-table-column prop="crazyWheelConsume" label="疯狂转盘" min-width="100" />
|
||||||
<el-table-column prop="westDuelConsume" label="荒野争锋" width="100" />
|
<el-table-column prop="westDuelConsume" label="荒野争锋" min-width="100" />
|
||||||
<el-table-column prop="sweetConsume" label="极速糖果" width="100" />
|
<el-table-column prop="sweetConsume" label="极速糖果" min-width="100" />
|
||||||
<el-table-column prop="totalCommission" label="总佣金" width="100" />
|
<el-table-column prop="totalCommission" label="总佣金" min-width="100" />
|
||||||
<el-table-column label="操作" align="center" width="120" class-name="small-padding fixed-width">
|
<el-table-column label="操作" align="center" width="120" class-name="small-padding fixed-width">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button
|
<el-button
|
||||||
|
|||||||
@@ -202,6 +202,12 @@ alipay:
|
|||||||
apispace:
|
apispace:
|
||||||
carriersAuthToken: rcpg373exsddqb8p24nixfo6zgdzw35c
|
carriersAuthToken: rcpg373exsddqb8p24nixfo6zgdzw35c
|
||||||
carriersAuthUrl: https://eolink.o.apispace.com/carriers/carriers-auth
|
carriersAuthUrl: https://eolink.o.apispace.com/carriers/carriers-auth
|
||||||
|
|
||||||
|
# 推广后台配置
|
||||||
|
promo:
|
||||||
|
dev-login:
|
||||||
|
# 开发用账号密码登录(POST /promo/devLogin):dev 环境打开,配合前端 pnpm run dev 的登录页入口
|
||||||
|
enabled: true
|
||||||
#
|
#
|
||||||
#hipay:
|
#hipay:
|
||||||
# payUrl: https://xahnob7yqke3.hipay.one/pay/pay/createpay
|
# payUrl: https://xahnob7yqke3.hipay.one/pay/pay/createpay
|
||||||
|
|||||||
@@ -94,6 +94,14 @@ aes:
|
|||||||
# AES 向量(16位,与前端 AES_IV 保持一致)
|
# AES 向量(16位,与前端 AES_IV 保持一致)
|
||||||
iv: ruoyi@aes@iv2024
|
iv: ruoyi@aes@iv2024
|
||||||
|
|
||||||
|
# 推广后台配置
|
||||||
|
promo:
|
||||||
|
dev-login:
|
||||||
|
# 开发用账号密码登录(POST /promo/devLogin)开关。
|
||||||
|
# 仅本地/测试联调使用:置 true 后可用「账号 + 密码」登录,绕过短信验证码。
|
||||||
|
# 默认 false;生产 profile 不覆盖该项,即天然关闭。
|
||||||
|
enabled: false
|
||||||
|
|
||||||
# 防止XSS攻击
|
# 防止XSS攻击
|
||||||
xss:
|
xss:
|
||||||
# 过滤开关
|
# 过滤开关
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# 页面标题
|
# 页面标题
|
||||||
VUE_APP_TITLE = 河南项目
|
VUE_APP_TITLE = 凯科佰娱
|
||||||
|
|
||||||
# 开发环境配置
|
# 开发环境配置
|
||||||
ENV = 'development'
|
ENV = 'development'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# 页面标题
|
# 页面标题
|
||||||
VUE_APP_TITLE = 河南项目
|
VUE_APP_TITLE = 凯科佰娱
|
||||||
|
|
||||||
# 生产环境配置
|
# 生产环境配置
|
||||||
ENV = 'production'
|
ENV = 'production'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# 页面标题
|
# 页面标题
|
||||||
VUE_APP_TITLE = 河南项目
|
VUE_APP_TITLE = 凯科佰娱
|
||||||
|
|
||||||
NODE_ENV = production
|
NODE_ENV = production
|
||||||
|
|
||||||
|
|||||||
@@ -190,9 +190,15 @@ $line: $cc-border-3;
|
|||||||
box-shadow: 0 0 0 3px rgba(154, 92, 53, .12);
|
box-shadow: 0 0 0 3px rgba(154, 92, 53, .12);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 搜索条不再显示 label,用 placeholder 表达(与设计图一致) */
|
/* 搜索条恢复显示 label(2026-09-27 用户反馈「搜索框旁边的提示词不显示了」)。
|
||||||
|
此前这里把 inline 表单的 label 整片 display:none、只用 placeholder 表达,实测丢语境:
|
||||||
|
①「最小值 / 最大值」两个框没有前面的「价格区间」就不知道在填什么(同一处还有「请选择」×3 看不出是外观/类型/是否上架);
|
||||||
|
② 只写了 label、没写 placeholder 的框会变成没有任何说明的空白框;
|
||||||
|
③ 清空/选中后 placeholder 消失,条件就再也认不出来了。
|
||||||
|
⇒ 交回 Element 默认的 inline label(float:none + inline-block),只把字号/颜色对齐 size="small" 的控件。 */
|
||||||
.el-form--inline .el-form-item__label {
|
.el-form--inline .el-form-item__label {
|
||||||
display: none;
|
font-size: 13px;
|
||||||
|
color: $ink-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-form--inline .el-form-item {
|
.el-form--inline .el-form-item {
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ export default {
|
|||||||
poolStatsList: [],
|
poolStatsList: [],
|
||||||
multiplierTable: [
|
multiplierTable: [
|
||||||
{ color: 'gold', A: 1, B: 1, C: 1, D: 1 },
|
{ color: 'gold', A: 1, B: 1, C: 1, D: 1 },
|
||||||
{ color: '#a4485a', A: 1, B: 1, C: 1, D: 1 },
|
{ color: 'red', A: 1, B: 1, C: 1, D: 1 },
|
||||||
{ color: 'purple', A: 1, B: 1, C: 1, D: 1 }
|
{ color: 'purple', A: 1, B: 1, C: 1, D: 1 }
|
||||||
],
|
],
|
||||||
// 奖池调节相关
|
// 奖池调节相关
|
||||||
@@ -353,7 +353,9 @@ export default {
|
|||||||
},
|
},
|
||||||
syncMultiplierTable() {
|
syncMultiplierTable() {
|
||||||
const multipliers = this.config.multipliers || [];
|
const multipliers = this.config.multipliers || [];
|
||||||
const colors = ['gold', '#a4485a', 'purple'];
|
// 这里的 color 必须与后端契约一致(gold/red/purple),也是 cellCounts() 的 key:
|
||||||
|
// 写成颜色值(如 #a4485a)会同时导致标签显示成色号、红色那一行算不到盘面格数(整行倍率被跳过)
|
||||||
|
const colors = ['gold', 'red', 'purple'];
|
||||||
const symbols = ['A', 'B', 'C', 'D'];
|
const symbols = ['A', 'B', 'C', 'D'];
|
||||||
this.multiplierTable = colors.map(color => {
|
this.multiplierTable = colors.map(color => {
|
||||||
const row = { color };
|
const row = { color };
|
||||||
|
|||||||
+355
-180
@@ -16,87 +16,133 @@
|
|||||||
>刷新数据</el-button>
|
>刷新数据</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 统计卡片网格 -->
|
<!-- 主体:左窄轨 + 右侧主区 -->
|
||||||
<div class="stat-grid">
|
<div class="home-body">
|
||||||
<div
|
<!-- 左窄轨:核心指标 + 待办 -->
|
||||||
v-for="card in statCards"
|
<div class="rail">
|
||||||
:key="card.key"
|
<div class="rail-title">核心指标</div>
|
||||||
class="stat-card"
|
|
||||||
:class="{ clickable: !!card.route }"
|
|
||||||
@click="card.route && handto(card.route)"
|
|
||||||
>
|
|
||||||
<div class="stat-icon" :class="'theme-' + card.theme">
|
|
||||||
<i :class="card.icon"></i>
|
|
||||||
</div>
|
|
||||||
<div class="stat-info">
|
|
||||||
<div class="stat-value">{{ card.value }}</div>
|
|
||||||
<div class="stat-label">
|
|
||||||
{{ card.label }}
|
|
||||||
<el-tooltip v-if="card.note" :content="card.note" placement="top">
|
|
||||||
<i class="el-icon-info stat-note-icon"></i>
|
|
||||||
</el-tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<i v-if="card.route" class="el-icon-arrow-right stat-arrow"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 实时 RTP -->
|
<div class="rail-metrics">
|
||||||
<div class="section-card">
|
<div
|
||||||
<div class="section-header">
|
v-for="m in railMetrics"
|
||||||
<div class="section-title">
|
:key="m.key"
|
||||||
<span class="title-bar"></span>
|
class="rail-metric"
|
||||||
<span>实时 RTP</span>
|
:class="{ clickable: !!m.route }"
|
||||||
</div>
|
@click="m.route && handto(m.route)"
|
||||||
<span class="section-sub">每 30 秒自动刷新</span>
|
>
|
||||||
</div>
|
<div class="rail-metric-label">
|
||||||
<div class="rtp-grid">
|
<span>{{ m.label }}</span>
|
||||||
<div
|
<el-tooltip v-if="m.note" :content="m.note" placement="top">
|
||||||
v-for="(item, idx) in visibleRtpList"
|
<i class="el-icon-info rail-note"></i>
|
||||||
:key="item.name || idx"
|
</el-tooltip>
|
||||||
class="rtp-card"
|
|
||||||
:class="'rtp-theme-' + (idx % 5)"
|
|
||||||
>
|
|
||||||
<div class="rtp-name">
|
|
||||||
<span class="rtp-dot"></span>
|
|
||||||
{{ item.name }}
|
|
||||||
</div>
|
|
||||||
<div class="rtp-value">
|
|
||||||
{{ item.rtp !== null && item.rtp !== undefined ? item.rtp.toFixed(2) + '%' : '--' }}
|
|
||||||
</div>
|
|
||||||
<div class="rtp-progress">
|
|
||||||
<div class="rtp-progress-inner" :style="{ width: Math.min(Number(item.rtp) || 0, 100) + '%' }"></div>
|
|
||||||
</div>
|
|
||||||
<div class="rtp-rows">
|
|
||||||
<div class="rtp-row">
|
|
||||||
<span>投注</span>
|
|
||||||
<span>¥{{ item.bet !== null && item.bet !== undefined ? Number(item.bet).toFixed(2) : '--' }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="rtp-row">
|
<div class="rail-metric-value">{{ m.value }}</div>
|
||||||
<span>奖励</span>
|
<i v-if="m.route" class="el-icon-arrow-right rail-arrow"></i>
|
||||||
<span>¥{{ item.reward !== null && item.reward !== undefined ? Number(item.reward).toFixed(2) : '--' }}</span>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rail-todo">
|
||||||
|
<div
|
||||||
|
class="rail-todo-item clickable"
|
||||||
|
:class="{ alert: totaldeliver > 0 }"
|
||||||
|
@click="handto(3)"
|
||||||
|
>
|
||||||
|
<span class="rail-todo-label">待处理提货申请</span>
|
||||||
|
<span class="rail-todo-value">{{ totaldeliver === null ? '--' : totaldeliver }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="rail-todo-item">
|
||||||
|
<span class="rail-todo-label">提货余额</span>
|
||||||
|
<span class="rail-todo-value">¥{{ balance }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧主区 -->
|
||||||
|
<div class="main-col">
|
||||||
|
<!-- 实时 RTP -->
|
||||||
|
<div class="section-card">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-title">
|
||||||
|
<span class="title-bar"></span>
|
||||||
|
<span>实时 RTP</span>
|
||||||
|
</div>
|
||||||
|
<span class="section-sub">每 30 秒自动刷新</span>
|
||||||
|
</div>
|
||||||
|
<div class="rtp-grid">
|
||||||
|
<div
|
||||||
|
v-for="(item, idx) in visibleRtpList"
|
||||||
|
:key="item.name || idx"
|
||||||
|
class="rtp-card"
|
||||||
|
:class="'rtp-theme-' + (idx % 5)"
|
||||||
|
>
|
||||||
|
<div class="rtp-name">
|
||||||
|
<span class="rtp-dot"></span>
|
||||||
|
{{ item.name }}
|
||||||
|
</div>
|
||||||
|
<div class="rtp-value">
|
||||||
|
{{ item.rtp !== null && item.rtp !== undefined ? item.rtp.toFixed(2) + '%' : '--' }}
|
||||||
|
</div>
|
||||||
|
<div class="rtp-progress">
|
||||||
|
<div class="rtp-progress-inner" :style="{ width: Math.min(Number(item.rtp) || 0, 100) + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
<div class="rtp-rows">
|
||||||
|
<div class="rtp-row">
|
||||||
|
<span>投注</span>
|
||||||
|
<span>¥{{ item.bet !== null && item.bet !== undefined ? Number(item.bet).toFixed(2) : '--' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="rtp-row">
|
||||||
|
<span>奖励</span>
|
||||||
|
<span>¥{{ item.reward !== null && item.reward !== undefined ? Number(item.reward).toFixed(2) : '--' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!visibleRtpList.length" class="rtp-empty">暂无 RTP 数据</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 经营数据(次级指标带)—— 放在图表上方:图表最高,放最后滚到底正好 -->
|
||||||
|
<div class="section-card">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-title">
|
||||||
|
<span class="title-bar"></span>
|
||||||
|
<span>经营数据</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mini-stats">
|
||||||
|
<div v-for="s in miniStats" :key="s.key" class="mini-stat">
|
||||||
|
<div class="mini-icon" :class="'theme-' + s.theme">
|
||||||
|
<i :class="s.icon"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mini-info">
|
||||||
|
<div class="mini-label">
|
||||||
|
<span>{{ s.label }}</span>
|
||||||
|
<el-tooltip v-if="s.note" :content="s.note" placement="top">
|
||||||
|
<i class="el-icon-info mini-note"></i>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="mini-value">{{ s.value }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!visibleRtpList.length" class="rtp-empty">暂无 RTP 数据</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 流水统计图表 -->
|
<!-- 流水统计图表 -->
|
||||||
<div class="section-card">
|
<div class="section-card">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
<span class="title-bar"></span>
|
<span class="title-bar"></span>
|
||||||
<span>流水统计</span>
|
<span>流水统计</span>
|
||||||
<span class="section-sub chart-sub">{{ chartTypeLabel }}</span>
|
<span class="section-sub chart-sub">{{ chartTypeLabel }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-tabs">
|
<div class="chart-tabs">
|
||||||
<span :class="['tab-item', chartType === 'hour' ? 'active' : '']" @click="switchChartType('hour')">小时</span>
|
<span :class="['tab-item', chartType === 'hour' ? 'active' : '']" @click="switchChartType('hour')">小时</span>
|
||||||
<span :class="['tab-item', chartType === 'day' ? 'active' : '']" @click="switchChartType('day')">天</span>
|
<span :class="['tab-item', chartType === 'day' ? 'active' : '']" @click="switchChartType('day')">天</span>
|
||||||
<span :class="['tab-item', chartType === 'week' ? 'active' : '']" @click="switchChartType('week')">周</span>
|
<span :class="['tab-item', chartType === 'week' ? 'active' : '']" @click="switchChartType('week')">周</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="lineChart" style="width: 100%; height: 400px;"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="lineChart" style="width: 100%; height: 400px;"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -155,17 +201,39 @@ export default {
|
|||||||
chartTypeLabel() {
|
chartTypeLabel() {
|
||||||
return this.chartType === "hour" ? "今日(按小时)" : this.chartType === "week" ? "近 4 周" : "近 7 天";
|
return this.chartType === "hour" ? "今日(按小时)" : this.chartType === "week" ? "近 4 周" : "近 7 天";
|
||||||
},
|
},
|
||||||
statCards() {
|
/* 左窄轨:只放「平台体量」三项,给足字号做层级;
|
||||||
|
其余指标下沉到右侧主区底部的「经营数据」带。 */
|
||||||
|
railMetrics() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: "online",
|
||||||
|
label: "在线用户",
|
||||||
|
note: "当前在线的用户数,点击可查看在线用户列表",
|
||||||
|
value: this.total === null ? "--" : this.total,
|
||||||
|
route: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "totalRecharge",
|
||||||
|
label: "总充值",
|
||||||
|
note: "历史累计充值总额(排除无效用户)",
|
||||||
|
value: "¥" + this.totalRecord,
|
||||||
|
route: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "totalFlow",
|
||||||
|
label: "总流水",
|
||||||
|
note: "历史累计全部游戏投注消费总额(已排除无效用户)",
|
||||||
|
value: "¥" + Number(this.totalFlow).toFixed(2)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
},
|
||||||
|
/* 右侧主区底部:其余 6 项,紧凑成一条指标带 */
|
||||||
|
miniStats() {
|
||||||
return [
|
return [
|
||||||
{ key: "online", label: "在线用户", note: "当前在线的用户数,点击可查看在线用户列表", value: this.total === null ? "--" : this.total, icon: "el-icon-user-solid", theme: "blue", route: 1 },
|
|
||||||
{ key: "totalRecharge", label: "总充值", note: "历史累计充值总额(排除无效用户)", value: "¥" + this.totalRecord, icon: "el-icon-s-order", theme: "orange", route: 2 },
|
|
||||||
{ key: "deliver", label: "待处理提货申请", note: "待审核处理的提货申请数量,点击可前往处理", value: this.totaldeliver === null ? "--" : this.totaldeliver, icon: "el-icon-shopping-cart-full", theme: "green", route: 3 },
|
|
||||||
{ key: "balance", label: "提货余额", note: "第三方发货平台账户余额,用于采购发货", value: "$" + this.balance, icon: "el-icon-wallet", theme: "red" },
|
|
||||||
{ key: "todayRecharge", label: "今日总充值", note: "今日0点至今的充值总额(已排除无效用户)", value: "¥" + this.todayRecharge, icon: "el-icon-money", theme: "purple" },
|
{ key: "todayRecharge", label: "今日总充值", note: "今日0点至今的充值总额(已排除无效用户)", value: "¥" + this.todayRecharge, icon: "el-icon-money", theme: "purple" },
|
||||||
{ key: "todayBet", label: "今日总投注", note: "今日全部游戏的投注消费总额(已排除无效用户)", value: "¥" + this.todayBet, icon: "el-icon-coin", theme: "cyan" },
|
{ key: "todayBet", label: "今日总投注", note: "今日全部游戏的投注消费总额(已排除无效用户)", value: "¥" + this.todayBet, icon: "el-icon-coin", theme: "cyan" },
|
||||||
{ key: "todayReward", label: "今日总返奖", note: "今日全游戏产出饰品总价值(按背包产出统计,含已分解)", value: "¥" + this.todayReward, icon: "el-icon-present", theme: "pink" },
|
{ key: "todayReward", label: "今日总返奖", note: "今日全游戏产出饰品总价值(按背包产出统计,含已分解)", value: "¥" + this.todayReward, icon: "el-icon-present", theme: "pink" },
|
||||||
{ key: "deliveryAmount", label: "总发货金额", note: "已完成发货订单的采购价合计(已排除无效用户)", value: "¥" + Number(this.totalDeliveryAmount).toFixed(2), icon: "el-icon-box", theme: "amber" },
|
{ key: "deliveryAmount", label: "总发货金额", note: "已完成发货订单的采购价合计(已排除无效用户)", value: "¥" + Number(this.totalDeliveryAmount).toFixed(2), icon: "el-icon-box", theme: "amber" },
|
||||||
{ key: "totalFlow", label: "总流水", note: "历史累计全部游戏投注消费总额(已排除无效用户)", value: "¥" + Number(this.totalFlow).toFixed(2), icon: "el-icon-data-line", theme: "indigo" },
|
|
||||||
{ key: "historyReward", label: "历史总返奖", note: "全游戏产出饰品总价值(按背包产出统计,含已分解)", value: "¥" + Number(this.historyReward).toFixed(2), icon: "el-icon-trophy", theme: "teal" },
|
{ key: "historyReward", label: "历史总返奖", note: "全游戏产出饰品总价值(按背包产出统计,含已分解)", value: "¥" + Number(this.historyReward).toFixed(2), icon: "el-icon-trophy", theme: "teal" },
|
||||||
{ key: "rollRoom", label: "Roll 房今日返奖", note: "不含下注(仅饰品产出)", value: "¥" + Number(this.rollRoomReward || 0).toFixed(2), icon: "el-icon-house", theme: "sky" }
|
{ key: "rollRoom", label: "Roll 房今日返奖", note: "不含下注(仅饰品产出)", value: "¥" + Number(this.rollRoomReward || 0).toFixed(2), icon: "el-icon-house", theme: "sky" }
|
||||||
];
|
];
|
||||||
@@ -480,107 +548,95 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========== 统计卡片网格 ========== */
|
/* ========== 主体两栏:左窄轨 + 右主区 ========== */
|
||||||
.stat-grid {
|
.home-body {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: 300px minmax(0, 1fr);
|
||||||
gap: 16px;
|
gap: 20px;
|
||||||
margin-bottom: 20px;
|
/* start:左轨按内容高度,不跟着右列拉长(否则底部空一大截) */
|
||||||
|
align-items: start;
|
||||||
|
|
||||||
@media (max-width: 1400px) {
|
@media (max-width: 1200px) {
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
}
|
|
||||||
@media (max-width: 1000px) {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card {
|
/* ========== 左窄轨 ========== */
|
||||||
position: relative;
|
.rail {
|
||||||
display: flex;
|
/* 右列很长,左轨跟着滚,随时能看到核心数字 */
|
||||||
align-items: center;
|
position: sticky;
|
||||||
|
top: 16px;
|
||||||
|
padding: 20px;
|
||||||
background: #fffdfa;
|
background: #fffdfa;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 20px;
|
|
||||||
box-shadow: 0 4px 14px rgba(139, 115, 85, 0.10);
|
box-shadow: 0 4px 14px rgba(139, 115, 85, 0.10);
|
||||||
border: 1px solid rgba(212, 160, 160, 0.55);
|
border: 1px solid rgba(212, 160, 160, 0.55);
|
||||||
transition:
|
|
||||||
transform 0.5s ease-in-out,
|
|
||||||
box-shadow 0.5s ease-in-out,
|
|
||||||
border-color 0.5s ease-in-out;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&:hover {
|
@media (max-width: 1200px) {
|
||||||
/* 规范:悬停用「小幅放大 + ≤1deg 微旋」,避免机械式直上直下 */
|
position: static;
|
||||||
transform: scale(1.015) rotate(-0.3deg);
|
}
|
||||||
border-color: #c08a8a;
|
}
|
||||||
box-shadow: 0 10px 26px rgba(139, 115, 85, 0.16);
|
|
||||||
|
|
||||||
.stat-icon {
|
.rail-title {
|
||||||
transform: rotate(-1deg) scale(1.04);
|
font-size: 13px;
|
||||||
}
|
font-weight: 600;
|
||||||
.stat-arrow {
|
color: #7a664e;
|
||||||
opacity: 1;
|
letter-spacing: 0.5px;
|
||||||
transform: translateX(0);
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rail-metric {
|
||||||
|
position: relative;
|
||||||
|
padding: 16px 0;
|
||||||
|
border-top: 1px solid #f3ece0;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-top: none;
|
||||||
|
padding-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.clickable {
|
&.clickable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.stat-icon {
|
&.clickable:hover {
|
||||||
flex-shrink: 0;
|
.rail-metric-value {
|
||||||
width: 54px;
|
color: #9a5c35;
|
||||||
height: 54px;
|
}
|
||||||
border-radius: 16px;
|
.rail-arrow {
|
||||||
display: flex;
|
opacity: 1;
|
||||||
align-items: center;
|
transform: translateX(0);
|
||||||
justify-content: center;
|
|
||||||
margin-right: 16px;
|
|
||||||
/* 底色与图标色由 .theme-* 提供:浅底 + 同色深图标(水彩感),
|
|
||||||
不再是一格一色的高饱和实心块 */
|
|
||||||
background: rgba(154, 92, 53, 0.22);
|
|
||||||
transition: transform 0.5s ease-in-out;
|
|
||||||
|
|
||||||
i {
|
|
||||||
font-size: 26px;
|
|
||||||
color: #9a5c35;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-info {
|
|
||||||
min-width: 0;
|
|
||||||
|
|
||||||
.stat-value {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #4f4130;
|
|
||||||
line-height: 1.2;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
.stat-label {
|
|
||||||
margin-top: 4px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #7a664e;
|
|
||||||
|
|
||||||
.stat-note-icon {
|
|
||||||
margin-left: 4px;
|
|
||||||
color: #7a664e;
|
|
||||||
cursor: help;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-arrow {
|
.rail-metric-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #7a664e;
|
||||||
|
|
||||||
|
.rail-note {
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rail-metric-value {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.15;
|
||||||
|
letter-spacing: -0.4px;
|
||||||
|
color: #4f4130;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rail-arrow {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 16px;
|
right: 0;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
margin-top: -7px;
|
margin-top: -7px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -590,11 +646,74 @@ export default {
|
|||||||
transition: all 0.25s ease;
|
transition: all 0.25s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 统计卡图标配色
|
/* ========== 左轨待办区 ========== */
|
||||||
11 个卡片只用 5 个田园色(陶土 / 草绿 / 苔绿 / 雏菊金 / 玫瑰粉),循环使用。
|
.rail-todo {
|
||||||
浅底 + 同色深图标。
|
display: flex;
|
||||||
tint 从 .14~.18 提到 .22~.26:原来太淡,小图标几乎"看不见"(用户反馈对比度不够)。
|
flex-direction: column;
|
||||||
类名(blue/orange/...)只是 card.theme 的旧键名,沿用不改,免得动模板。 */
|
gap: 10px;
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #f3ece0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rail-todo-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #f6efe3;
|
||||||
|
border: 1px solid rgba(212, 160, 160, 0.40);
|
||||||
|
transition:
|
||||||
|
background-color 0.25s ease,
|
||||||
|
border-color 0.25s ease;
|
||||||
|
|
||||||
|
&.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.clickable:hover {
|
||||||
|
background: #fbf5ea;
|
||||||
|
border-color: #c08a8a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 有积压时用雏菊金描边 + 数值变色提醒。
|
||||||
|
刻意不给 background:留空才不会和 hover 的底色打架(同权重,靠顺序定胜负太脆) */
|
||||||
|
&.alert {
|
||||||
|
border-color: rgba(168, 121, 31, 0.32);
|
||||||
|
|
||||||
|
.rail-todo-value {
|
||||||
|
color: #7d5512;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rail-todo-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #7a664e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rail-todo-value {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: #4f4130;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 右侧主区 ========== */
|
||||||
|
.main-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 统计卡图标配色 ==========
|
||||||
|
浅底 + 同色深图标(水彩感)。五个田园色循环使用:
|
||||||
|
陶土 / 草绿 / 苔绿 / 雏菊金 / 玫瑰粉。
|
||||||
|
tint 用 .22~.26:太淡小图标会"看不见"。
|
||||||
|
类名(blue/orange/...)沿用旧键名不改,免得动逻辑。 */
|
||||||
.theme-blue,
|
.theme-blue,
|
||||||
.theme-indigo {
|
.theme-indigo {
|
||||||
background: rgba(154, 92, 53, 0.22) !important;
|
background: rgba(154, 92, 53, 0.22) !important;
|
||||||
@@ -627,14 +746,13 @@ export default {
|
|||||||
i { color: #8f3f50 !important; }
|
i { color: #8f3f50 !important; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========== 区块卡片(RTP / 图表) ========== */
|
/* ========== 区块卡片(RTP / 图表 / 经营数据) ========== */
|
||||||
.section-card {
|
.section-card {
|
||||||
background: #faf6f0;
|
background: #faf6f0;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 20px 24px;
|
padding: 20px 24px;
|
||||||
box-shadow: 0 6px 18px rgba(139, 115, 85, 0.11);
|
box-shadow: 0 6px 18px rgba(139, 115, 85, 0.11);
|
||||||
border: 1px solid rgba(212, 160, 160, 0.55);
|
border: 1px solid rgba(212, 160, 160, 0.55);
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
.section-header {
|
||||||
@@ -675,21 +793,13 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========== RTP 卡片 ========== */
|
/* ========== RTP 卡片 ==========
|
||||||
|
右侧主区宽度随屏幕浮动(有左轨占位),用 auto-fit 自适应
|
||||||
|
—— 宽屏一行 5 张,窄屏自动折行,不再写死 5 列。 */
|
||||||
.rtp-grid {
|
.rtp-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(5, 1fr);
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
|
||||||
@media (max-width: 1400px) {
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
}
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rtp-card {
|
.rtp-card {
|
||||||
@@ -774,9 +884,7 @@ export default {
|
|||||||
|
|
||||||
/* RTP 主题色(只给圆点 + 进度条上色)
|
/* RTP 主题色(只给圆点 + 进度条上色)
|
||||||
★ 与上方折线图的颜色一一对应:同一游戏在哪儿都是同一个色,
|
★ 与上方折线图的颜色一一对应:同一游戏在哪儿都是同一个色,
|
||||||
这样「颜色」本身就带信息,不用每次重新认。
|
这样「颜色」本身就带信息,不用每次重新认。 */
|
||||||
(原来是品红 #ec4899 / 青 #14b8a6 / 紫 #8b5cf6 / 翠绿 #10b981 / 琥珀 #f59e0b,
|
|
||||||
属于规范禁止的高饱和荧光色,已换掉。) */
|
|
||||||
.rtp-theme-0 { /* 疯狂转盘 */
|
.rtp-theme-0 { /* 疯狂转盘 */
|
||||||
.rtp-dot { background: #7a6a9c; }
|
.rtp-dot { background: #7a6a9c; }
|
||||||
.rtp-progress-inner { background: linear-gradient(90deg, #a89cc0, #7a6a9c); }
|
.rtp-progress-inner { background: linear-gradient(90deg, #a89cc0, #7a6a9c); }
|
||||||
@@ -798,6 +906,73 @@ export default {
|
|||||||
.rtp-progress-inner { background: linear-gradient(90deg, #c69bb8, #a35790); }
|
.rtp-progress-inner { background: linear-gradient(90deg, #c69bb8, #a35790); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========== 经营数据(次级指标带) ========== */
|
||||||
|
.mini-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-stat {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #f6efe3;
|
||||||
|
border: 1px solid rgba(212, 160, 160, 0.40);
|
||||||
|
transition:
|
||||||
|
background-color 0.5s ease-in-out,
|
||||||
|
border-color 0.5s ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fbf5ea;
|
||||||
|
border-color: #c08a8a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 11px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-info {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #7a664e;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
.mini-note {
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-value {
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.25;
|
||||||
|
color: #4f4130;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
/* ========== 图表切换标签 ========== */
|
/* ========== 图表切换标签 ========== */
|
||||||
.chart-tabs {
|
.chart-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="login">
|
<div class="login">
|
||||||
|
|
||||||
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
|
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
|
||||||
<h3 class="title">河南项目</h3>
|
<h3 class="title">凯科佰娱</h3>
|
||||||
<el-form-item prop="username">
|
<el-form-item prop="username">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="loginForm.username"
|
v-model="loginForm.username"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="register">
|
<div class="register">
|
||||||
<el-form ref="registerForm" :model="registerForm" :rules="registerRules" class="register-form">
|
<el-form ref="registerForm" :model="registerForm" :rules="registerRules" class="register-form">
|
||||||
<h3 class="title">河南项目</h3>
|
<h3 class="title">凯科佰娱</h3>
|
||||||
<el-form-item prop="username">
|
<el-form-item prop="username">
|
||||||
<el-input v-model="registerForm.username" type="text" auto-complete="off" placeholder="账号">
|
<el-input v-model="registerForm.username" type="text" auto-complete="off" placeholder="账号">
|
||||||
<svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" />
|
<svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" />
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ function resolve(dir) {
|
|||||||
|
|
||||||
const CompressionPlugin = require('compression-webpack-plugin')
|
const CompressionPlugin = require('compression-webpack-plugin')
|
||||||
|
|
||||||
const name = process.env.VUE_APP_TITLE || '河南项目' // 网页标题
|
const name = process.env.VUE_APP_TITLE || '凯科佰娱' // 网页标题
|
||||||
|
|
||||||
const port = process.env.port || process.env.npm_config_port || 80 // 端口
|
const port = process.env.port || process.env.npm_config_port || 80 // 端口
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
|||||||
/**
|
/**
|
||||||
* 推广后台 Web 配置:注册维护拦截器。
|
* 推广后台 Web 配置:注册维护拦截器。
|
||||||
* <p>
|
* <p>
|
||||||
* 对 /promo/** 生效,放行登录/注册接口(登录/注册由 PromoLoginController 自行提示维护)。
|
* 对 /promo/** 生效,放行登录/注册/开发用登录接口(这几条由 PromoLoginController 自行提示维护)。
|
||||||
*
|
*
|
||||||
* @author ruoyi
|
* @author ruoyi
|
||||||
*/
|
*/
|
||||||
@@ -23,6 +23,6 @@ public class PromoWebConfig implements WebMvcConfigurer {
|
|||||||
public void addInterceptors(InterceptorRegistry registry) {
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
registry.addInterceptor(promoMaintenanceInterceptor)
|
registry.addInterceptor(promoMaintenanceInterceptor)
|
||||||
.addPathPatterns("/promo/**")
|
.addPathPatterns("/promo/**")
|
||||||
.excludePathPatterns("/promo/smsLogin", "/promo/register");
|
.excludePathPatterns("/promo/smsLogin", "/promo/register", "/promo/devLogin");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.ruoyi.promo.contract.request;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推广后台「开发用」账号密码登录请求体。
|
||||||
|
* <p>
|
||||||
|
* 仅供本地 / 测试环境联调使用,由 sys_config 的 {@code promoDevLoginEnabled} 控制是否可用。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class DevLoginRequest {
|
||||||
|
|
||||||
|
/** 登录账号:推广账号(主播/招商/卡商)填手机号;超管可填登录账号或手机号 */
|
||||||
|
private String account;
|
||||||
|
|
||||||
|
/** 登录密码(明文,服务端按 BCrypt 校验) */
|
||||||
|
private String password;
|
||||||
|
}
|
||||||
@@ -21,12 +21,14 @@ import com.ruoyi.admin.mapper.PromoAdminMapper;
|
|||||||
import com.ruoyi.admin.mapper.TtUserMapper;
|
import com.ruoyi.admin.mapper.TtUserMapper;
|
||||||
import com.ruoyi.common.redis.config.RedisLock;
|
import com.ruoyi.common.redis.config.RedisLock;
|
||||||
import com.ruoyi.domain.other.ApiVerificationCodeLoginBody;
|
import com.ruoyi.domain.other.ApiVerificationCodeLoginBody;
|
||||||
|
import com.ruoyi.promo.contract.request.DevLoginRequest;
|
||||||
import com.ruoyi.promo.contract.request.RegisterRequest;
|
import com.ruoyi.promo.contract.request.RegisterRequest;
|
||||||
import com.ruoyi.promo.contract.request.UpdateInvitationCodeRequest;
|
import com.ruoyi.promo.contract.request.UpdateInvitationCodeRequest;
|
||||||
import com.ruoyi.promo.contract.response.GetShareDomainResponse;
|
import com.ruoyi.promo.contract.response.GetShareDomainResponse;
|
||||||
import com.ruoyi.promo.service.PromoLoginService;
|
import com.ruoyi.promo.service.PromoLoginService;
|
||||||
import com.ruoyi.system.service.ISysConfigService;
|
import com.ruoyi.system.service.ISysConfigService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@@ -59,6 +61,10 @@ public class PromoLoginController extends BaseController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private RedisLock redisLock;
|
private RedisLock redisLock;
|
||||||
|
|
||||||
|
/** 开发用账号密码登录开关:application-dev.yml 置 true,其余环境不配置即为 false */
|
||||||
|
@Value("${promo.dev-login.enabled:false}")
|
||||||
|
private boolean devLoginEnabled;
|
||||||
|
|
||||||
@PostMapping("/getInfo")
|
@PostMapping("/getInfo")
|
||||||
public R<Map<String, Object>> getPromoInfo() {
|
public R<Map<String, Object>> getPromoInfo() {
|
||||||
var loginUser = getLoginUser();
|
var loginUser = getLoginUser();
|
||||||
@@ -271,6 +277,30 @@ public class PromoLoginController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开发用账号密码登录:仅本地/测试联调使用,绕过短信验证码直接按密码登录。
|
||||||
|
* <p>
|
||||||
|
* 由配置项 {@code promo.dev-login.enabled} 控制(默认 false,只有 dev profile 置 true);
|
||||||
|
* 生产 profile 不配置该项 ⇒ 接口恒返回「账号密码登录未开启」。
|
||||||
|
*/
|
||||||
|
@Anonymous
|
||||||
|
@PostMapping("/devLogin")
|
||||||
|
public AjaxResult devLogin(@RequestBody DevLoginRequest body) {
|
||||||
|
if (!devLoginEnabled) {
|
||||||
|
return AjaxResult.error("账号密码登录未开启");
|
||||||
|
}
|
||||||
|
if ("1".equals(configService.selectConfigByKey("promoMaintenance"))) {
|
||||||
|
return AjaxResult.error("系统维护中,请稍后再试");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
AjaxResult ajax = AjaxResult.success();
|
||||||
|
ajax.put(Constants.TOKEN, promoLoginService.devLogin(body.getAccount(), body.getPassword()));
|
||||||
|
return ajax;
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return AjaxResult.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Anonymous
|
@Anonymous
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
public AjaxResult register(@RequestBody RegisterRequest request) {
|
public AjaxResult register(@RequestBody RegisterRequest request) {
|
||||||
|
|||||||
@@ -18,6 +18,14 @@ public interface PromoLoginService {
|
|||||||
|
|
||||||
String createUserToken(String phoneNumber);
|
String createUserToken(String phoneNumber);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开发用账号密码登录:仅本地/测试环境联调使用,由 sys_config 的 promoDevLoginEnabled 控制是否可用。
|
||||||
|
* 账号口径与 {@link #createUserToken} 一致(推广账号手机号 / 超管账号),只是把短信验证码换成密码校验。
|
||||||
|
*
|
||||||
|
* @return 推广后台 token
|
||||||
|
*/
|
||||||
|
String devLogin(String account, String password);
|
||||||
|
|
||||||
R<TtUser> register(RegisterRequest request);
|
R<TtUser> register(RegisterRequest request);
|
||||||
|
|
||||||
R<TtUser> updateInvitationCode(UpdateInvitationCodeRequest request);
|
R<TtUser> updateInvitationCode(UpdateInvitationCodeRequest request);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import com.ruoyi.admin.service.TtUserAvatarService;
|
|||||||
import com.ruoyi.admin.service.TtUserService;
|
import com.ruoyi.admin.service.TtUserService;
|
||||||
import com.ruoyi.admin.util.RandomUtils;
|
import com.ruoyi.admin.util.RandomUtils;
|
||||||
import com.ruoyi.common.core.domain.R;
|
import com.ruoyi.common.core.domain.R;
|
||||||
|
import com.ruoyi.common.core.domain.entity.UserData;
|
||||||
import com.ruoyi.common.core.domain.model.LoginUser;
|
import com.ruoyi.common.core.domain.model.LoginUser;
|
||||||
import com.ruoyi.common.core.redis.RedisCache;
|
import com.ruoyi.common.core.redis.RedisCache;
|
||||||
import com.ruoyi.common.utils.DateUtils;
|
import com.ruoyi.common.utils.DateUtils;
|
||||||
@@ -214,8 +215,48 @@ public class PromoLoginServiceImpl implements PromoLoginService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String createUserToken(String phoneNumber) {
|
public String createUserToken(String phoneNumber) {
|
||||||
sysLoginService.validateBlackIp(phoneNumber);
|
|
||||||
LoginUser loginUser = (LoginUser) apiUserDetailsService.loadUserByUsername("promo_user_" + phoneNumber);
|
LoginUser loginUser = (LoginUser) apiUserDetailsService.loadUserByUsername("promo_user_" + phoneNumber);
|
||||||
|
return issueToken(loginUser, phoneNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开发用账号密码登录(仅本地/测试联调,生产 profile 未配置 promo.dev-login.enabled 即为关闭)。
|
||||||
|
* <p>
|
||||||
|
* 账号解析完全复用 {@link #createUserToken} 的 {@code promo_user_} 入口,
|
||||||
|
* 因此推广账号(主播/招商/卡商,登录名即手机号)与超管(promo_admin 的 username 或 phone_number)
|
||||||
|
* 都能登录,区别只是「短信验证码」这一步换成了 BCrypt 密码校验。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String devLogin(String account, String password) {
|
||||||
|
String acc = StringUtils.trim(account);
|
||||||
|
String rawPassword = password == null ? "" : password;
|
||||||
|
if (StringUtils.isEmpty(acc) || StringUtils.isEmpty(rawPassword)) {
|
||||||
|
throw new IllegalArgumentException("账号和密码不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
LoginUser loginUser;
|
||||||
|
try {
|
||||||
|
loginUser = (LoginUser) apiUserDetailsService.loadUserByUsername("promo_user_" + acc);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 账号不存在 / 已停用 / 已删除 一律收敛为同一句提示,避免这个接口被拿来枚举账号
|
||||||
|
log.warn("[推广后台·开发用密码登录] 账号加载失败 account={} reason={}", acc, e.getMessage());
|
||||||
|
throw new IllegalArgumentException("账号或密码错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
UserData userData = loginUser.getUserData();
|
||||||
|
if (userData == null || StringUtils.isEmpty(userData.getPassword())
|
||||||
|
|| !SecurityUtils.matchesPassword(rawPassword, userData.getPassword())) {
|
||||||
|
log.warn("[推广后台·开发用密码登录] 密码校验失败 account={}", acc);
|
||||||
|
throw new IllegalArgumentException("账号或密码错误");
|
||||||
|
}
|
||||||
|
return issueToken(loginUser, acc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验黑名单 IP 并签发推广后台 token(短信登录与开发用密码登录共用)
|
||||||
|
*/
|
||||||
|
private String issueToken(LoginUser loginUser, String blackIpAccount) {
|
||||||
|
sysLoginService.validateBlackIp(blackIpAccount);
|
||||||
// 超级管理员来自 promo_admin 表,其 id 与 tt_user 不同表,不能按 id 回写 tt_user 登录信息
|
// 超级管理员来自 promo_admin 表,其 id 与 tt_user 不同表,不能按 id 回写 tt_user 登录信息
|
||||||
if (!UserType.SUPER_ADMIN.getCode().equals(loginUser.getUserData().getUserType())) {
|
if (!UserType.SUPER_ADMIN.getCode().equals(loginUser.getUserData().getUserType())) {
|
||||||
recordLoginInfo(loginUser.getUserId());
|
recordLoginInfo(loginUser.getUserId());
|
||||||
|
|||||||
+2
-2
@@ -18,8 +18,8 @@
|
|||||||
SELECT
|
SELECT
|
||||||
COALESCE(SUM(CASE WHEN is_blank THEN 0 ELSE r.amount_consumed END), 0) AS totalBet,
|
COALESCE(SUM(CASE WHEN is_blank THEN 0 ELSE r.amount_consumed END), 0) AS totalBet,
|
||||||
COALESCE(SUM(CASE WHEN is_blank THEN 0 ELSE r.gain_ornaments_price END), 0) AS totalWin,
|
COALESCE(SUM(CASE WHEN is_blank THEN 0 ELSE r.gain_ornaments_price END), 0) AS totalWin,
|
||||||
COALESCE(SUM(CASE WHEN is_blank THEN r.amount_consumed END, 0), 0) AS blankBet,
|
COALESCE(SUM(CASE WHEN is_blank THEN r.amount_consumed END), 0) AS blankBet,
|
||||||
COALESCE(SUM(CASE WHEN is_blank THEN r.gain_ornaments_price END, 0), 0) AS blankWin,
|
COALESCE(SUM(CASE WHEN is_blank THEN r.gain_ornaments_price END), 0) AS blankWin,
|
||||||
COALESCE(SUM(CASE
|
COALESCE(SUM(CASE
|
||||||
WHEN NOT is_blank AND r.round_target IS NOT NULL THEN r.amount_consumed
|
WHEN NOT is_blank AND r.round_target IS NOT NULL THEN r.amount_consumed
|
||||||
ELSE 0 END), 0) AS calBet,
|
ELSE 0 END), 0) AS calBet,
|
||||||
|
|||||||
Reference in New Issue
Block a user