修改河南项目的管理后台ui和删除之前不需要的游戏

This commit is contained in:
2026-09-22 14:57:09 +08:00
parent 8f8d2e042c
commit e61399ecd6
106 changed files with 1202 additions and 4100 deletions
+130
View File
@@ -0,0 +1,130 @@
# 数据库变更台账
> **数据库改动只写这里**:中文标题 + 完整可执行 SQL。不再单独建 `docs/*.sql`(全量基线除外)。
> 每条都要能回得去 —— **回滚方案与验证步骤必写**。
---
## 删除「奥林匹斯 / 荒野争锋 / 极速糖果」三个玩法的后台菜单
**背景**:这三个玩法不再需要,要求从后台左侧菜单移除。
**为什么先动数据库**:RuoYi 的左侧菜单是**数据库驱动**的(`sys_menu` → `/getRouters` 接口 → 前端 `sidebarRouters`),
代码里没有菜单清单。所以顺序是:**先删数据,再删前端页面文件**。
**影响面**(实查本机库 `henan`,非估算):
| 表 | 行数 | 说明 |
|---|---|---|
| `sys_menu` | **22** | 3 个顶级菜单(`menu_type='M'`)+ 19 个子页面(`'C'`);**无** `'F'` 按钮权限行 |
| `sys_role_menu` | **22** | 全部属于 `role_id = 103` 这一个角色 |
| 顶级 menu_id | 菜单 | path | 子菜单 | 子菜单 menu_id | 前端 component |
|---|---|---|---|---|---|
| 3130 | 奥林匹斯 | `olympus` | 7 个 | 3131~3137 | `game/olympus/*` |
| 2105 | 荒野争锋 | `bullet` | 6 个 | 2106~2110、2112 | `game/bullet/*` |
| 2125 | 极速糖果 | `sweet` | 6 个 | 2126~2131 | `game/sweet/*` |
⚠️ **`2111`(宝箱管理→临时倍率)不在删除范围内** —— 它虽然编号紧邻 `2110`,但父级是 `2018`(宝箱管理),
且其 component 是 `game/box/mock/index`(被 `3141` 经典开箱的「临时爆率」也在用),**不要动**。
**变更 SQL**(2026-09-22 已在库 `henan` 执行):
```sql
-- 1) 先清角色-菜单授权,避免留下孤儿行
DELETE FROM sys_role_menu
WHERE menu_id IN (3130,3131,3132,3133,3134,3135,3136,3137,2105,2106,2107,2108,2109,2110,2112,2125,2126,2127,2128,2129,2130,2131);
-- 2) 再删菜单本身
DELETE FROM sys_menu
WHERE menu_id IN (3130,3131,3132,3133,3134,3135,3136,3137,2105,2106,2107,2108,2109,2110,2112,2125,2126,2127,2128,2129,2130,2131);
```
**验证**(执行后实测):
```sql
-- 两个都必须是 0
SELECT COUNT(*) FROM sys_menu
WHERE menu_id IN (3130,3131,3132,3133,3134,3135,3136,3137,2105,2106,2107,2108,2109,2110,2112,2125,2126,2127,2128,2129,2130,2131)
OR parent_id IN (3130, 2105, 2125); -- 实测 0
SELECT COUNT(*) FROM sys_role_menu
WHERE menu_id IN (3130,3131,3132,3133,3134,3135,3136,3137,2105,2106,2107,2108,2109,2110,2112,2125,2126,2127,2128,2129,2130,2131); -- 实测 0
```
一级菜单复查(`SELECT menu_id, menu_name, path FROM sys_menu WHERE parent_id = 0 ORDER BY order_num, menu_id`)
实测剩余 20 项,已无「奥林匹斯 / 荒野争锋 / 极速糖果」。
**回滚 SQL**(22 条 `sys_menu` + 22 条 `sys_role_menu`,由 `mysqldump --no-create-info --complete-insert` 导出):
```sql
-- 事务包裹更安全;menu_id 是显式指定,不需要自增
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 (2105,'荒野争锋',0,7,'bullet',NULL,NULL,1,0,'M','0','0','','excel','admin','2026-05-14 14:36:15','admin','2026-08-01 13:37:13','');
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 (2106,'饰品列表',2105,1,'ornament','game/bullet/ornament/index',NULL,1,0,'C','0','0','game:bullet:ornament:list','button','admin','2026-05-14 14:49:48','admin','2026-05-14 14:55:16','');
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 (2107,'箱子管理',2105,2,'box','game/bullet/box/index',NULL,1,0,'C','0','0','game:bullet:box:list','bug','admin','2026-05-14 14:56:11','',NULL,'');
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 (2108,'回合管理',2105,3,'round','game/bullet/round/index',NULL,1,0,'C','0','0','game:bullet:round:list','build','admin','2026-05-14 14:56:49','',NULL,'');
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 (2109,'玩家记录',2105,4,'record','game/bullet/record/index',NULL,1,0,'C','0','0','game:bullet:record:list','button','admin','2026-05-14 14:57:29','',NULL,'');
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 (2110,'每日统计',2105,5,'dailywin','game/bullet/dailywin/index',NULL,1,0,'C','0','0','game:bullet:dailywin:list','chart','admin','2026-05-14 14:58:07','',NULL,'');
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 (2112,'奖品管理',2105,6,'prize','game/bullet/prize/index',NULL,1,0,'C','0','0','game:bullet:prize:list','component','admin','2026-05-26 04:47:02','admin','2026-05-26 05:34:40','');
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 (2125,'极速糖果',0,7,'sweet',NULL,NULL,1,0,'M','0','0','','star','admin','2026-06-18 10:15:36','admin','2026-08-01 13:29:08','');
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 (2126,'箱子管理',2125,2,' box','game/sweet/box/index',NULL,1,0,'C','0','0','game:sweet:box:list','404','admin','2026-06-18 10:17:18','admin','2026-06-18 14:34:47','');
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 (2127,'饰品列表',2125,1,'ornament','game/sweet/ornament/index',NULL,1,0,'C','0','0','game:sweet:ornament:list','bug','admin','2026-06-18 10:18:01','admin','2026-06-18 14:34:29','');
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 (2128,'奖品管理',2125,6,'prize','game/sweet/prize/index',NULL,1,0,'C','0','0','game:sweet:prize:list','build','admin','2026-06-18 10:18:41','admin','2026-06-18 14:35:55','');
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 (2129,'每日统计',2125,5,'dailywin','game/sweet/dailywin/index',NULL,1,0,'C','0','0','game:sweet:dailywin:list','button','admin','2026-06-18 10:19:39','admin','2026-06-18 14:35:39','');
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 (2130,'回合管理',2125,3,'round','game/sweet/round/index',NULL,1,0,'C','0','0','game:sweet:round:list','cascader','admin','2026-06-18 10:20:13','admin','2026-06-18 14:35:03','');
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 (2131,'玩家记录',2125,4,'record','game/sweet/record/index',NULL,1,0,'C','0','0','game:sweet:record:list','cascader','admin','2026-06-18 10:21:20','admin','2026-06-18 14:35:17','');
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 (3130,'奥林匹斯',0,5,'olympus','',NULL,1,0,'M','0','0','','star','admin','2026-08-01 13:41:53','',NULL,'奥林匹斯电子游戏管理');
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 (3131,'箱子配置',3130,1,'box','game/olympus/box/index',NULL,1,0,'C','0','0','game:olympus:box','shopping','admin','2026-08-01 13:41:53','',NULL,'奥林匹斯箱子配置与身份桶分布');
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 (3132,'饰品列表',3130,2,'ornament','game/olympus/ornament/index',NULL,1,0,'C','0','0','game:olympus:ornament','star','admin','2026-08-01 13:41:53','',NULL,'奥林匹斯 17 档饰品桶定义');
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 (3133,'回合管理',3130,3,'round','game/olympus/round/index',NULL,1,0,'C','0','0','game:olympus:round','documentation','admin','2026-08-01 13:41:53','',NULL,'奥林匹斯预生成回合查询');
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 (3134,'奖池水位',3130,4,'rewardPool','game/olympus/rewardPool/index',NULL,1,0,'C','0','0','game:olympus:pool','money','admin','2026-08-01 13:41:53','',NULL,'奥林匹斯按箱奖池水位(注水/抽水/批量/重置)');
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 (3135,'玩家记录',3130,5,'record','game/olympus/record/index',NULL,1,0,'C','0','0','game:olympus:record','list','admin','2026-08-01 13:41:53','',NULL,'奥林匹斯游戏记录');
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 (3136,'每日统计',3130,6,'dailywin','game/olympus/dailywin/index',NULL,1,0,'C','0','0','game:olympus:win','chart','admin','2026-08-01 13:41:53','',NULL,'奥林匹斯每日盈亏统计');
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 (3137,'奖品管理',3130,7,'prize','game/olympus/prize/index',NULL,1,0,'C','0','0','game:olympus:prize','guide','admin','2026-08-01 13:41:53','admin','2026-08-04 13:46:18','奥林匹斯返奖饰品与区间配置');
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES (103,2105);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES (103,2106);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES (103,2107);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES (103,2108);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES (103,2109);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES (103,2110);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES (103,2112);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES (103,2125);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES (103,2126);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES (103,2127);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES (103,2128);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES (103,2129);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES (103,2130);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES (103,2131);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES (103,3130);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES (103,3131);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES (103,3132);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES (103,3133);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES (103,3134);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES (103,3135);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES (103,3136);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES (103,3137);
```
**配套的前端文件删除**(38 个文件,全部在 git 里,`git checkout` 可恢复):
| 目录 | 文件数 |
|---|---|
| `ruoyi-ui/src/views/game/olympus/` | 7 |
| `ruoyi-ui/src/views/game/bullet/` | 6 |
| `ruoyi-ui/src/views/game/sweet/` | 6 |
| `ruoyi-ui/src/api/game/olympus/` | 7 |
| `ruoyi-ui/src/api/game/bullet/` | 6 |
| `ruoyi-ui/src/api/game/sweet/` | 6 |
**刻意没动**:
- **后端 Java 代码**。`olympus / bullet / sweet / sugar` 在后端出现在**跨模块共享**的位置 ——
`TtAccountRecordSource`(流水来源码:60=奥林匹斯…)、`TtboxRecordSource`、`RedisConstants`、
`MessageCategoryEnum`、`PromoTurnover*`、`ApiUserPackSackServiceImpl`、`WebsiteSetupServiceImpl` 等。
删这些会波及**其他玩法与推广/发货链路**,不在「删页面」范围内。
- **数据库业务表**(`game_olympus_*`、`game_sweet_*` 等)原样保留,未 TRUNCATE、未 DROP。
- `ruoyi-ui/src/views/game/sugar/`(5 个 api + 5 个页面)—— **没有任何菜单引用它**:
菜单里的「极速糖果」指向的是 `game/sweet/*`,而 `game_sugar_*` 表在 `docs/cleanup_records.sql` 里被标注为「极速糖果」。
即 `sugar` 与 `sweet` 是两套并存的东西,`sugar` 未被任何菜单挂载(后端却存在 `GameSugarWin/User/StepInfo/Spin` 实体)。
**疑似遗留或未接入的开发分支,未确认前不删。**
+29
View File
@@ -30,3 +30,32 @@
**上线前必做两件事**:① 卷是 `external: true`,**必须先 `docker volume create henan_mysql_data`**,否则报 `external volume not found` 直接起不来(`deploy.sh up -d` 会自动建,手敲 `docker-compose up` 不会);② 新卷首次初始化会建出**空**的 `henan` 库 —— `MYSQL_DATABASE` / `MYSQL_USER` / `MYSQL_PASSWORD` **只在数据卷首次初始化时生效**,原有数据需另行导入,否则应用连上的是空库。
**遗留待定**:网络没固定 `name:`,实际名会随执行目录变(在 `docker/compose` 下执行时是 `compose_henan_app_network`)。卷已用 `external` + 固定 `name` 规避「换目录丢数据」,网络是否同样钉死成 `henan_app_network` 待定。
- **下架三个玩法:删除「奥林匹斯 / 荒野争锋 / 极速糖果」的后台菜单与页面**
**起因**:这三个玩法不再需要,要求从后台左侧菜单移除。
**关键前提**:RuoYi 的左侧菜单是**数据库驱动**的(`sys_menu` → `/getRouters` → 前端 `sidebarRouters`),**代码里没有菜单清单**,所以必须"先删数据、再删页面文件",只删前端文件的话菜单依然在。
**改动清单**(完整 SQL 与回滚语句见 `docs/updata_sql.md`):
1. **数据库**(库 `henan`,已执行):`sys_menu` 删 **22 行**(3 个顶级 M + 19 个子页面 C);`sys_role_menu` 删 **22 行**授权(全属 `role_id=103`)。**无 `'F'` 按钮权限行**。
- 3130 奥林匹斯 `olympus` → 子菜单 3131~3137
- 2105 荒野争锋 `bullet` → 子菜单 2106~2110、2112
- 2125 极速糖果 `sweet` → 子菜单 2126~2131
2. **前端文件**(38 个,删前已确认全部被 git 跟踪):`ruoyi-ui/src/views/game/{olympus,bullet,sweet}/`(7+6+6)与 `ruoyi-ui/src/api/game/{olympus,bullet,sweet}/`(7+6+6)。
3. **文档**:本条 + `docs/updata_sql.md` 新条目(含 44 条回滚 INSERT)。
**验证**:
- 删除后 `sys_menu` / `sys_role_menu` 命中行数**均为 0**(`printf` 实测);`SELECT ... WHERE parent_id = 0` 复查剩余 20 个一级菜单,三个玩法已消失。
- 全量搜索 `game/olympus|game/bullet|game/sweet|api/game/...` 残留引用 **= 0**。
- `ruoyi-ui` 生产构建 **`exitcode=0` / `DONE Build complete`**(删掉页面后仍能编译,说明没有别处静态引用它们)。
**刻意没动**:
1. **后端 Java 代码**。这些玩法名在后端出现在**跨模块共享**的文件里 —— `TtAccountRecordSource`(流水来源码 60=奥林匹斯)、`TtboxRecordSource`、`RedisConstants`、`MessageCategoryEnum`、`PromoTurnover*`、`ApiUserPackSackServiceImpl`、`WebsiteSetupServiceImpl`。删它们会波及**其他玩法与推广/发货链路**,不在「删页面」范围内。
2. **数据库业务表**:`game_olympus_*` / `game_sweet_*` 等原样保留,未 TRUNCATE、未 DROP。数据还在,随时可回。
3. **`ruoyi-ui/src/views/game/sugar/`**(5 api + 5 页面)—— **没有任何菜单引用**。菜单「极速糖果」指向的是 `game/sweet/*`,而 `docs/cleanup_records.sql` 又把 `game_sugar_*` 标注为「极速糖果」,两套并存;后端还存在 `GameSugarWin/User/StepInfo/Spin` 实体。**疑似遗留或未接入的开发分支,未确认前不删。**
**遗留待定**:① 仪表盘「实时 RTP」卡片与「流水统计」折线里仍有 **玫瑰女郎(=olympus)** 一条 —— 那是后端 `/getRtpData` 返回的玩法列表,**与菜单无关**,删菜单不会让它消失;若要一并去掉得动后端。② `game/sugar/*` 是否清理待定。③ 是否需要把 `game_olympus_*` / `game_sweet_*` 业务表也清掉(现在只是入口没了,数据仍在库里占空间)。
@@ -177,7 +177,7 @@ const clear = () => {
<ColorRadioPicker
v-model="systemTheme"
:schema="[
'#409eff',
'#9a5c35',
'#009688',
'#536dfe',
'#ff5c93',
@@ -212,7 +212,7 @@ const clear = () => {
v-model="menuTheme"
:schema="[
'#fff',
'#001529',
'#faf6f0',
'#212121',
'#273352',
'#191b24',
+22 -18
View File
@@ -64,34 +64,38 @@ export const useAppStore = defineStore('app', {
isDark: false, // 是否是暗黑模式
currentSize: 'default', // 组件尺寸
theme: {
// 主题色
elColorPrimary: '#409eff',
/* Cottagecore(田园核风)
⚠️ 本 store 是 persist: true,老用户 localStorage 里的 'app' 会覆盖这里的默认值。
换肤后如果看到旧配色,清掉 localStorage 的 'app' 键(或在设置抽屉里重新选一次色)即可。
色值与 src/styles/var.css 保持一致,两处要一起改。 */
// 主题色(陶土棕 · 与 success 草绿拉开色相,避免新增/修改按钮同色)
elColorPrimary: '#9a5c35',
// 左侧菜单边框颜色
leftMenuBorderColor: 'inherit',
// 左侧菜单背景颜色
leftMenuBgColor: '#001529',
// 左侧菜单背景颜色(奶油)
leftMenuBgColor: '#faf6f0',
// 左侧菜单浅色背景颜色
leftMenuBgLightColor: '#0f2438',
// 左侧菜单选中背景颜色
leftMenuBgActiveColor: 'var(--el-color-primary)',
leftMenuBgLightColor: '#f6efe3',
// 左侧菜单选中背景颜色(陶土浅色,不用实心,免得整栏太重)
leftMenuBgActiveColor: 'rgba(154, 92, 53, 0.16)',
// 左侧菜单收起选中背景颜色
leftMenuCollapseBgActiveColor: 'var(--el-color-primary)',
leftMenuCollapseBgActiveColor: 'rgba(154, 92, 53, 0.16)',
// 左侧菜单字体颜色
leftMenuTextColor: '#bfcbd9',
leftMenuTextColor: '#5f4f3d',
// 左侧菜单选中字体颜色
leftMenuTextActiveColor: '#fff',
leftMenuTextActiveColor: '#9a5c35',
// logo字体颜色
logoTitleTextColor: '#fff',
logoTitleTextColor: '#4f4130',
// logo边框颜色
logoBorderColor: 'inherit',
// 头部背景颜色
topHeaderBgColor: '#fff',
topHeaderBgColor: '#faf6f0',
// 头部字体颜色
topHeaderTextColor: 'inherit',
// 头部悬停颜色
topHeaderHoverColor: '#f6f6f6',
topHeaderHoverColor: '#f6efe3',
// 头部边框颜色
topToolBorderColor: '#eee'
topToolBorderColor: '#eadfd0'
}
}
},
@@ -279,7 +283,7 @@ export const useAppStore = defineStore('app', {
const isDarkColor = colorIsDark(color)
const theme: Recordable = {
// 左侧菜单边框颜色
leftMenuBorderColor: isDarkColor ? 'inherit' : '#eee',
leftMenuBorderColor: isDarkColor ? 'inherit' : '#eadfd0',
// 左侧菜单背景颜色
leftMenuBgColor: color,
// 左侧菜单浅色背景颜色
@@ -293,13 +297,13 @@ export const useAppStore = defineStore('app', {
? 'var(--el-color-primary)'
: hexToRGB(unref(primaryColor) as string, 0.1),
// 左侧菜单字体颜色
leftMenuTextColor: isDarkColor ? '#bfcbd9' : '#333',
leftMenuTextColor: isDarkColor ? '#bfcbd9' : '#5f4f3d',
// 左侧菜单选中字体颜色
leftMenuTextActiveColor: isDarkColor ? '#fff' : 'var(--el-color-primary)',
// logo字体颜色
logoTitleTextColor: isDarkColor ? '#fff' : 'inherit',
// logo边框颜色
logoBorderColor: isDarkColor ? color : '#eee'
logoBorderColor: isDarkColor ? color : '#eadfd0'
}
this.setTheme(theme)
this.setCssVarTheme()
@@ -308,7 +312,7 @@ export const useAppStore = defineStore('app', {
const isDarkColor = colorIsDark(color)
const textColor = isDarkColor ? '#fff' : 'inherit'
const textHoverColor = isDarkColor ? lighten(color!, 6) : '#f6f6f6'
const topToolBorderColor = isDarkColor ? color : '#eee'
const topToolBorderColor = isDarkColor ? color : '#eadfd0'
setCssVar('--top-header-bg-color', color)
setCssVar('--top-header-text-color', textColor)
setCssVar('--top-header-hover-color', textHoverColor)
+68 -13
View File
@@ -1,36 +1,39 @@
:root {
--login-bg-color: #293146;
/* Cottagecore(田园核风)· 奶油底 + 暖棕字,去掉深蓝冷调 */
--login-bg-color: #f3ece0;
/* left menu start */
--left-menu-max-width: 200px;
--left-menu-min-width: 64px;
--left-menu-bg-color: #001529;
--left-menu-bg-color: #faf6f0;
--left-menu-bg-light-color: #0f2438;
--left-menu-bg-light-color: #f6efe3;
--left-menu-bg-active-color: var(--el-color-primary);
--left-menu-bg-active-color: rgba(154, 92, 53, 0.16);
--left-menu-text-color: #bfcbd9;
--left-menu-text-color: #5f4f3d;
--left-menu-text-active-color: #fff;
--left-menu-text-active-color: #9a5c35;
--left-menu-collapse-bg-active-color: var(--el-color-primary);
--left-menu-collapse-bg-active-color: rgba(154, 92, 53, 0.16);
/* left menu end */
/* logo start */
--logo-height: 50px;
--logo-title-text-color: #fff;
--logo-title-text-color: #4f4130;
/* logo end */
/* header start */
--top-header-bg-color: '#fff';
/* header start
注意:原文件这几个值是带引号的字符串('#fff' / 'inherit'),引号会被当成值的一部分,
已去掉。 */
--top-header-bg-color: #faf6f0;
--top-header-text-color: 'inherit';
--top-header-text-color: inherit;
--top-header-hover-color: #f6f6f6;
--top-header-hover-color: #f6efe3;
--top-tool-height: var(--logo-height);
@@ -49,13 +52,65 @@
--app-content-padding: 20px;
--app-content-bg-color: #f5f7f9;
--app-content-bg-color: #f3ece0;
--app-footer-height: 50px;
--transition-time-02: 0.2s;
}
/* ==========================================================================
Element Plus 设计令牌 · Cottagecore
用 html:root:not(.dark) 把特异性抬到 (0,2,1):
1) 确保压得住后加载的 element-plus 默认 :root;
2) 只在浅色模式生效,`.dark` 下交还给 element-plus 的 dark/css-vars,
否则暗色模式会出现「奶油底 + 浅色字」不可读。
========================================================================== */
html:root:not(.dark) {
--el-color-primary: #9a5c35; /* 陶土棕 · 白字 5.31:1 */
--el-color-success: #457450; /* 草绿 */
--el-color-warning: #8f6418; /* 深雏菊金 */
--el-color-danger: #a4485a; /* 莓红 */
--el-color-info: #6b6152; /* 暖褐灰 */
--el-text-color-primary: #4f4130;
--el-text-color-regular: #5f4f3d;
--el-text-color-secondary: #7a664e;
--el-text-color-placeholder: #9a886e;
--el-text-color-disabled: #9a886e;
--el-border-color: #d8c3b0;
--el-border-color-light: #e2d3c2;
--el-border-color-lighter: #eadfd0;
--el-border-color-extra-light: #f0e8dc;
--el-border-color-hover: #c08a8a;
--el-fill-color: #f6efe3;
--el-fill-color-light: #f6efe3;
--el-fill-color-lighter: #fbf5ea;
--el-fill-color-blank: #faf6f0;
--el-bg-color: #faf6f0;
--el-bg-color-page: #f3ece0;
--el-bg-color-overlay: #fffdfa;
--el-border-radius-base: 12px;
--el-border-radius-small: 10px;
--el-border-radius-round: 999px;
--el-box-shadow-light: 0 2px 10px rgba(139, 115, 85, 0.12);
--el-box-shadow: 0 6px 18px rgba(139, 115, 85, 0.13);
--el-box-shadow-dark: 0 12px 32px rgba(139, 115, 85, 0.17);
}
/* 标题类走衬线(只在大号字上),正文/表单/表格保持无衬线,
理由同 ruoyi-ui:13px 宋体在密集表格里可读性会掉。 */
.el-dialog__title,
.el-message-box__title,
.el-drawer__title {
font-family: 'Songti SC', 'Source Han Serif SC', 'Noto Serif SC', STSong, SimSun, Georgia, 'Times New Roman', serif;
letter-spacing: 0.2px;
}
.dark {
--app-content-bg-color: var(--el-bg-color);
}
+1 -1
View File
@@ -392,7 +392,7 @@ const copyText = (text: string) => {
.card-code {
font-family: 'Courier New', monospace;
font-weight: 600;
color: #409eff;
color: #9a5c35;
margin-right: 6px;
}
+4 -4
View File
@@ -174,7 +174,7 @@
</ElTableColumn>
<ElTableColumn label="招商收益" width="120">
<template #default="{ row }">
<span style="color: #409eff; font-weight: 600">{{ formatNum(row.totalCommission) }}</span>
<span style="color: #9a5c35; font-weight: 600">{{ formatNum(row.totalCommission) }}</span>
</template>
</ElTableColumn>
</ElTable>
@@ -415,7 +415,7 @@ onMounted(() => {
}
.summary-card {
background: #fff;
border: 1px solid #409eff;
border: 1px solid #9a5c35;
border-radius: 4px;
padding: 12px 24px;
color: #303133;
@@ -455,7 +455,7 @@ onMounted(() => {
}
.daily-card {
background: #fff;
border: 1px solid #409eff;
border: 1px solid #9a5c35;
border-radius: 6px;
padding: 10px 12px;
display: flex;
@@ -475,7 +475,7 @@ onMounted(() => {
}
.weekly-total-card {
background: #fff;
border: 1px solid #409eff;
border: 1px solid #9a5c35;
border-radius: 6px;
padding: 12px 24px;
color: #303133;
+3 -3
View File
@@ -372,10 +372,10 @@ onMounted(async () => {
}
.rate-card:hover {
background-color: #ecf5ff;
border-color: #409eff;
background-color: #fbf5ea;
border-color: #9a5c35;
transform: translateY(-2px);
box-shadow: 0 2px 12px rgba(64, 158, 255, 0.1);
box-shadow: 0 2px 12px rgba(154, 92, 53, 0.10);
}
.rate-card-title {
+76 -70
View File
@@ -53,16 +53,20 @@ const inviteCode = computed(() => {
z-index: 0;
overflow: hidden;
background: radial-gradient(circle at 72% 18%, rgba(92, 86, 176, 0.15), transparent 30%),
linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%);
/* 原为 #0f0c29 → #302b63 → #24243e 的紫蓝渐变(规范明令禁止),
改成亚麻→奶油暖渐变 + 雏菊黄/草绿柔光斑 */
background: radial-gradient(circle at 72% 18%, rgba(245, 215, 95, 0.24), transparent 34%),
radial-gradient(circle at 12% 88%, rgba(90, 143, 90, 0.14), transparent 36%),
linear-gradient(135deg, #fdfaf5 0%, #f7f0e4 52%, #f3ece0 100%);
}
.grid-overlay {
position: absolute;
inset: 0;
background-image: linear-gradient(rgba(255, 255, 255, 0.028) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.028) 1px, transparent 1px);
/* 亚麻织纹:暖棕细线,不再是白色科技网格 */
background-image: linear-gradient(rgba(139, 115, 85, 0.055) 1px, transparent 1px),
linear-gradient(90deg, rgba(139, 115, 85, 0.055) 1px, transparent 1px);
background-size: 50px 50px;
}
@@ -89,23 +93,21 @@ const inviteCode = computed(() => {
② 受光放在右上角,与背景光斑方向一致
③ 投影反方向往左下,棱线只亮右上
*/
background-color: rgba(126, 132, 240, 0.24);
/* 原来是半透明靛蓝玻璃卡 + backdrop-filter;
Cottagecore 不把玻璃态当默认风格,改成不透明奶油卡 + 花粉细边 + 暖棕柔影 */
background-color: #fffdfa;
background-image: radial-gradient(
130% 110% at 100% 0%,
rgba(152, 158, 255, 0.22),
rgba(245, 215, 95, 0.20),
transparent 62%
);
border: 1px solid rgba(255, 255, 255, 0.12);
border: 1px solid rgba(212, 160, 160, 0.45);
box-shadow:
-20px 24px 56px -20px rgba(5, 4, 24, 0.68),
inset -1px 1px 0 rgba(255, 255, 255, 0.2),
inset 1px -1px 0 rgba(255, 255, 255, 0.02);
backdrop-filter: blur(24px) saturate(150%);
-webkit-backdrop-filter: blur(24px) saturate(150%);
0 18px 48px -18px rgba(139, 115, 85, 0.34),
inset 0 1px 0 rgba(255, 255, 255, 0.9);
/* =========================
标题
@@ -124,10 +126,11 @@ const inviteCode = computed(() => {
h2 {
margin: 0 16px;
font-family: 'Songti SC', 'Source Han Serif SC', 'Noto Serif SC', STSong, SimSun, Georgia, serif;
font-size: 24px;
font-weight: 650;
color: #f7f8ff;
color: #4f4130;
}
.title-decoration {
@@ -136,10 +139,10 @@ const inviteCode = computed(() => {
border-radius: 2px;
background: linear-gradient(90deg, transparent, rgba(184, 194, 232, 0.55));
background: linear-gradient(90deg, transparent, rgba(192, 138, 138, 0.8));
&:last-child {
background: linear-gradient(90deg, rgba(184, 194, 232, 0.55), transparent);
background: linear-gradient(90deg, rgba(192, 138, 138, 0.8), transparent);
}
}
}
@@ -152,13 +155,13 @@ const inviteCode = computed(() => {
}
:deep(.el-form-item__label) {
color: rgba(255, 255, 255, 0.72);
color: #5f4f3d;
font-size: 13px;
}
/* =========================
输入框
玻璃上的凹槽:比卡片暗,形成内嵌感
奶油底上的凹槽:比卡片略深,形成内嵌感
不再改内部 Row / Col 结构
========================= */
:deep(.el-input__wrapper) {
@@ -168,11 +171,11 @@ const inviteCode = computed(() => {
border-radius: 12px;
background: rgba(10, 8, 34, 0.34);
background: #faf6f0;
border: 1px solid rgba(255, 255, 255, 0.1);
border: 1px solid rgba(212, 160, 160, 0.45);
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.22);
box-shadow: inset 0 1px 2px rgba(139, 115, 85, 0.08);
transition:
border-color 0.22s ease,
@@ -180,34 +183,34 @@ const inviteCode = computed(() => {
box-shadow 0.22s ease;
&:hover {
border-color: rgba(255, 255, 255, 0.2);
background: rgba(10, 8, 34, 0.26);
border-color: #c08a8a;
background: #fdfaf5;
}
&.is-focus {
border-color: rgba(150, 158, 255, 0.62);
background: rgba(10, 8, 34, 0.22);
border-color: rgba(154, 92, 53, 0.55);
background: #fffdfa;
box-shadow:
inset 0 1px 2px rgba(0, 0, 0, 0.22),
0 0 0 3px rgba(120, 128, 255, 0.16);
inset 0 1px 2px rgba(139, 115, 85, 0.08),
0 0 0 3px rgba(154, 92, 53, 0.14);
}
}
:deep(.el-input__inner) {
height: 46px;
color: #fff;
color: #4f4130;
font-size: 14px;
&::placeholder {
color: rgba(255, 255, 255, 0.4);
color: #9a886e;
}
}
:deep(.el-input__prefix),
:deep(.el-input__suffix) {
color: rgba(255, 255, 255, 0.55);
color: #7a664e;
}
/* =========================
@@ -232,16 +235,16 @@ const inviteCode = computed(() => {
border-radius: 12px;
border: 1px solid rgba(133, 150, 255, 0.38);
border: 1px solid rgba(212, 160, 160, 0.6);
background: linear-gradient(135deg, rgba(112, 131, 255, 0.28), rgba(121, 91, 200, 0.24));
background: rgba(245, 215, 95, 0.22);
color: #e3e7ff;
color: #9a5c35;
font-size: 13px;
font-weight: 600;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
box-shadow: none;
transition:
background 0.2s ease,
@@ -250,24 +253,24 @@ const inviteCode = computed(() => {
transform 0.2s ease;
&:hover {
color: #fff;
color: #7d4a2a;
border-color: rgba(151, 164, 255, 0.62);
border-color: #c08a8a;
background: linear-gradient(135deg, rgba(112, 131, 255, 0.4), rgba(121, 91, 200, 0.34));
background: rgba(245, 215, 95, 0.34);
}
&:active {
transform: scale(0.98);
transform: scale(0.97);
}
&.is-disabled,
&:disabled {
color: rgba(255, 255, 255, 0.34);
color: #9a886e;
border-color: rgba(255, 255, 255, 0.1);
border-color: #eadfd0;
background: rgba(255, 255, 255, 0.05);
background: #f6efe3;
}
}
@@ -282,11 +285,11 @@ const inviteCode = computed(() => {
border-radius: 12px;
border: 1px solid rgba(133, 150, 255, 0.38);
border: 1px solid rgba(212, 160, 160, 0.6);
background: linear-gradient(135deg, rgba(112, 131, 255, 0.28), rgba(121, 91, 200, 0.24));
background: rgba(245, 215, 95, 0.22);
color: #e3e7ff;
color: #9a5c35;
font-size: 13px;
font-weight: 600;
@@ -321,19 +324,21 @@ const inviteCode = computed(() => {
color: #fff;
background: linear-gradient(100deg, #667eea 0%, #7075dc 48%, #875bbb 100%);
/* 原为 #667eea → #7075dc → #875bbb 的紫蓝渐变(规范明令禁止),改陶土;
渐变两端白字对比度 5.31:1 / 7.28:1,均过 AA */
background: linear-gradient(100deg, #9a5c35 0%, #8a5230 48%, #7d4a2a 100%);
box-shadow: 0 7px 20px rgba(91, 98, 220, 0.27);
box-shadow: 0 7px 20px rgba(154, 92, 53, 0.24);
&:hover {
transform: translateY(-1px);
filter: brightness(1.06);
/* 去掉原来的 filter: brightness(硬闪),改规范要求的微风感:轻微旋转 + 小幅放大 */
transform: rotate(-0.6deg) scale(1.02);
box-shadow: 0 10px 24px rgba(91, 98, 220, 0.36);
box-shadow: 0 10px 24px rgba(154, 92, 53, 0.32);
}
&:active {
transform: translateY(0);
transform: scale(0.98);
}
}
}
@@ -344,44 +349,45 @@ const inviteCode = computed(() => {
:deep(.el-select__wrapper) {
min-height: 48px;
background: rgba(255, 255, 255, 0.08);
background: #faf6f0;
border: 1px solid rgba(255, 255, 255, 0.15);
border: 1px solid rgba(212, 160, 160, 0.45);
border-radius: 12px;
box-shadow: none;
color: #fff;
color: #4f4130;
}
:deep(.el-select__placeholder) {
color: rgba(255, 255, 255, 0.7);
color: #9a886e;
}
:deep(.el-checkbox__label) {
color: rgba(255, 255, 255, 0.72);
color: #5f4f3d;
}
:deep(.el-checkbox__inner) {
background: rgba(255, 255, 255, 0.08);
background: #faf6f0;
border-color: rgba(255, 255, 255, 0.3);
border-color: #d8c3b0;
}
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
background: #667eea;
border-color: #667eea;
background: #9a5c35;
border-color: #9a5c35;
}
/* 密码强度条 */
/* 密码强度条:弱→强 用 莓红 → 雏菊金 → 草绿 → 深草绿,色相递进而不是明度递进,
保证色盲用户也能分辨(不靠单一颜色维度传递状态) */
:deep(.@{adminNamespace}-input-password__bar) {
height: 4px;
margin-top: 8px;
margin-bottom: 0;
background-color: rgba(255, 255, 255, 0.12);
background-color: #eadfd0;
border-radius: 999px;
@@ -398,19 +404,19 @@ const inviteCode = computed(() => {
&[data-score='0'],
&[data-score='1'] {
background-color: #ff6b81;
background-color: #a4485a;
}
&[data-score='2'] {
background-color: #ffc069;
background-color: #8f6418;
}
&[data-score='3'] {
background-color: #7c8cff;
background-color: #457450;
}
&[data-score='4'] {
background: linear-gradient(90deg, #667eea, #4facfe);
background: linear-gradient(90deg, #457450, #3f6b45);
}
}
@@ -419,14 +425,14 @@ const inviteCode = computed(() => {
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.24);
border: 1px solid #d8c3b0;
color: rgba(255, 255, 255, 0.82);
color: #5f4f3d;
&:hover {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.42);
color: #fff;
background: #f6efe3;
border-color: #c08a8a;
color: #9a5c35;
}
}
+7 -7
View File
@@ -275,14 +275,14 @@ const handleClearAll = () => {
}
.role-card:hover {
border-color: #b3d8ff;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.1);
border-color: #c08a8a;
box-shadow: 0 2px 8px rgba(154, 92, 53, 0.10);
}
.role-card.active {
border-color: #409eff;
background: linear-gradient(135deg, #ecf5ff 0%, #f0f9ff 100%);
box-shadow: 0 2px 12px rgba(64, 158, 255, 0.15);
border-color: #9a5c35;
background: linear-gradient(135deg, #fbf5ea 0%, #f6efe3 100%);
box-shadow: 0 2px 12px rgba(154, 92, 53, 0.14);
}
.role-icon {
@@ -344,8 +344,8 @@ const handleClearAll = () => {
}
.menu-item:hover {
border-color: #c6e2ff;
box-shadow: 0 1px 4px rgba(64, 158, 255, 0.08);
border-color: #d8c3b0;
box-shadow: 0 1px 4px rgba(154, 92, 53, 0.08);
}
.drag-handle {
+1 -1
View File
@@ -890,7 +890,7 @@ const handleSaveTemplate = async () => {
.ornament-grid { display: flex; flex-wrap: wrap; gap: 10px; max-height: 240px; overflow-y: auto; padding: 4px; }
.ornament-card { display: flex; align-items: center; gap: 8px; padding: 6px 10px; border: 1px solid #e4e7ed;
border-radius: 6px; cursor: pointer; transition: all 0.2s; width: 250px; }
.ornament-card:hover { border-color: #409eff; background: #f0f9ff; }
.ornament-card:hover { border-color: #9a5c35; background: #fbf5ea; }
.ornament-img { width: 50px; height: 38px; object-fit: cover; border-radius: 4px; flex-shrink: 0; background: #f5f5f5; }
.ornament-info { flex: 1; min-width: 0; }
.ornament-name { font-size: 12px; line-height: 1.3; word-break: break-all; }
+10 -9
View File
@@ -1,16 +1,22 @@
<template>
<div id="app">
<router-view />
<theme-picker />
</div>
</template>
<script>
import ThemePicker from "@/components/ThemePicker";
/**
* 说明:这里曾经挂着一个隐藏的 <theme-picker />(用 #app .theme-picker{display:none} 藏起来)。
* 它虽然不可见,组件照样会 mount,ThemePicker.created() 里只要 localStorage 的 theme 不等于
* ThemePicker 自己那个 ORIGINAL_THEME 常量,就会:
* 1) XHR 去 https://unpkg.com/element-ui@<version>/lib/theme-chalk/index.css 拉整包默认主题(外网依赖)
* 2) 把拉回来的 CSS 注入成 <style id="chalk-style"> 追加到 head 末尾
* 3) 用正则把页面上所有含旧色的 <style> 标签逐个做字符串替换
* 第 2 步注入的默认主题与本项目皮肤层 skin.scss 同权重但更靠后,会把圆角/配色改回企业蓝方角,
* 导致换肤被部分覆盖。故移除。如需恢复主题色选择器,请连带处理上述覆盖问题。
*/
export default {
name: "App",
components: { ThemePicker },
metaInfo() {
return {
title:
@@ -25,8 +31,3 @@ export default {
}
};
</script>
<style scoped lang="scss">
#app .theme-picker {
display: none;
}
</style>
-63
View File
@@ -1,63 +0,0 @@
import request from '@/utils/request'
// 箱子列表 (支持分页+筛选)
export function boxList(params) {
return request({
url: '/admin/bullet/box/list',
method: 'get',
params
})
}
// 新增/编辑箱子
export function boxSave(data) {
return request({
url: '/admin/bullet/box/save',
method: 'post',
data: data
})
}
// 删除箱子
export function boxDelete(boxId) {
return request({
url: '/admin/bullet/box/delete',
method: 'post',
params: { boxId }
})
}
// 查看箱子饰品配置
export function boxOrnaments(boxId) {
return request({
url: '/admin/bullet/box/ornaments',
method: 'get',
params: { boxId }
})
}
// 一键重算全部箱子饰品分布
export function recalcAll() {
return request({ url: '/admin/bullet/box/recalc_all', method: 'post' })
}
// 新增/编辑饰品数量 (upsert)
export function saveOrnaments(boxId, data) {
return request({
url: '/admin/bullet/box/ornaments/save',
method: 'post',
params: { boxId },
data: data
})
}
// 版本管理
export function getBoxVersions(boxId) {
return request({ url: '/admin/bullet/box/versions', method: 'get', params: { boxId } })
}
export function publishVersion(boxId, version) {
return request({ url: '/admin/bullet/box/publish', method: 'post', params: { boxId, version } })
}
export function rollbackVersion(boxId, version) {
return request({ url: '/admin/bullet/box/rollback', method: 'post', params: { boxId, version } })
}
@@ -1,9 +0,0 @@
import request from '@/utils/request'
export function winStats(params) {
return request({ url: '/admin/bullet/winstats', method: 'get', params })
}
export function rewardPoolReset() {
return request({ url: '/admin/bullet/rewardpool/reset', method: 'post' })
}
@@ -1,5 +0,0 @@
import request from '@/utils/request'
export function ornamentList() {
return request({ url: '/admin/bullet/ornament/list', method: 'get' })
}
@@ -1,9 +0,0 @@
import request from '@/utils/request'
export function prizeList() {
return request({ url: '/admin/bullet/prize/list', method: 'get' })
}
export function prizeSave(data) {
return request({ url: '/admin/bullet/prize/save', method: 'post', data })
}
@@ -1,13 +0,0 @@
import request from '@/utils/request'
export function recordList(params) {
return request({ url: '/admin/bullet/records', method: 'get', params })
}
export function deleteRecord(recordId) {
return request({ url: '/admin/bullet/records/delete', method: 'post', params: { recordId } })
}
export function clearRecords() {
return request({ url: '/admin/bullet/records/clear', method: 'post' })
}
-13
View File
@@ -1,13 +0,0 @@
import request from '@/utils/request'
export function roundList(params) {
return request({ url: '/admin/bullet/round/list', method: 'get', params })
}
export function roundByOrnament(params) {
return request({ url: '/admin/bullet/round/byOrnament', method: 'get', params })
}
export function roundDelete(roundId) {
return request({ url: '/admin/bullet/round/delete', method: 'post', params: { roundId } })
}
-56
View File
@@ -1,56 +0,0 @@
import request from '@/utils/request'
// 箱子列表(分页 + 按模式筛选)
export function getBoxList(params) {
return request({ url: '/admin/olympus/box/list', method: 'get', params })
}
// 新增/编辑箱子
export function saveBox(data) {
return request({ url: '/admin/olympus/box/save', method: 'post', data })
}
// 某箱某身份的饰品桶分布(含 convergeCount / fixedRounds / convergeRounds)
export function getBoxOrnaments(boxId, audience) {
return request({ url: `/admin/olympus/box/ornaments/${boxId}`, method: 'get', params: { audience }})
}
// 保存某箱某身份饰品回合分布
export function saveBoxOrnaments(data) {
return request({ url: '/admin/olympus/box/ornaments/save', method: 'post', data })
}
// 按 RTP 重算某箱某身份分布
export function calcDistribution(data) {
return request({ url: '/admin/olympus/box/ornaments/calc', method: 'post', data })
}
// 模拟 payoutCap 约束下的预估实际 RTP
export function simulateRtp(data) {
return request({ url: '/admin/olympus/box/ornaments/simulate-rtp', method: 'post', data })
}
// ==================== 分布模板(按身份) ====================
export function getBoxTemplates(boxId, audience) {
return request({ url: `/admin/olympus/box/templates/${boxId}`, method: 'get', params: { audience }})
}
export function saveBoxTemplate(data) {
return request({ url: '/admin/olympus/box/template/save', method: 'post', data })
}
export function deleteBoxTemplate(id) {
return request({ url: `/admin/olympus/box/template/${id}`, method: 'delete' })
}
export function applyBoxTemplate(data) {
return request({ url: '/admin/olympus/box/template/apply', method: 'post', data })
}
// 一键重算所有箱子 × 所有身份(按各箱现有配置)
export function recalcAll() {
return request({ url: '/admin/olympus/box/recalc_all', method: 'post' })
}
// 重算分布:按参数批量重算(boxType 可选=全部模式;audience 可选=全部身份;rtp/totalRounds/convergeRatio 必传统一设置)
export function recalcBatch(data) {
return request({ url: '/admin/olympus/box/recalcBatch', method: 'post', data })
}
@@ -1,11 +0,0 @@
import request from '@/utils/request'
// 每日统计列表(分页)
export function getWinList(params) {
return request({ url: '/admin/olympus/win/list', method: 'get', params })
}
// 汇总(按日期区间)
export function getWinSummary(params) {
return request({ url: '/admin/olympus/win/summary', method: 'get', params })
}
@@ -1,6 +0,0 @@
import request from '@/utils/request'
// 饰品桶定义列表(17 桶)
export function getOrnamentList() {
return request({ url: '/admin/olympus/ornament/list', method: 'get' })
}
@@ -1,11 +0,0 @@
import request from '@/utils/request'
// 返奖饰品列表(含返奖区间)
export function prizeList() {
return request({ url: '/admin/olympus/prize/list', method: 'get' })
}
// 新增/编辑返奖饰品(含区间)
export function prizeSave(data) {
return request({ url: '/admin/olympus/prize/save', method: 'post', data })
}
@@ -1,11 +0,0 @@
import request from '@/utils/request'
// 游戏记录列表(分页 + 按玩家ID/身份筛选;identity=common/anchor/robot/white,空=全部身份)
export function getRecordList(params) {
return request({ url: '/admin/olympus/record/list', method: 'get', params })
}
// 返奖汇总(按日期区间)
export function getWinSummary(params) {
return request({ url: '/admin/olympus/win/summary', method: 'get', params })
}
@@ -1,26 +0,0 @@
import request from '@/utils/request'
// 奖池配置快照(总池 + 各箱子池),按身份 audience=common/anchor/robot/white
export function getPoolConfig(params) {
return request({ url: '/admin/olympus/rewardPool/config', method: 'get', params })
}
// 逐箱注水/抽水(type=1 注水 / 2 抽水,含 audience、remark)
export function adjustPool(data) {
return request({ url: '/admin/olympus/rewardPool/adjust', method: 'post', data })
}
// 批量按一轮花费比例注水/抽水:{ audience, type, ratio, remark },每箱金额=一轮总花费(底注×模式倍数×总回合数)×ratio%
export function adjustPoolBatch(data) {
return request({ url: '/admin/olympus/rewardPool/adjustBatch', method: 'post', data })
}
// 重置人工调整:该身份所有箱子注水/抽水累计归零,只保留真实水位(下注−返奖):{ audience, remark }
export function adjustPoolReset(data) {
return request({ url: '/admin/olympus/rewardPool/adjustReset', method: 'post', data })
}
// 奖池调整历史(分页,params 含 audience)
export function getPoolLogs(params) {
return request({ url: '/admin/olympus/rewardPool/adjustLog', method: 'get', params })
}
@@ -1,6 +0,0 @@
import request from '@/utils/request'
// 预生成回合列表(分页 + 按回合ID/模式筛选)
export function getRoundList(params) {
return request({ url: '/admin/olympus/round/list', method: 'get', params })
}
-97
View File
@@ -1,97 +0,0 @@
import request from '@/utils/request'
export function getBoxList(params) {
return request({
url: '/admin/sweet/box/list',
method: 'get',
params
})
}
export function saveBox(data) {
return request({
url: '/admin/sweet/box/save',
method: 'post',
data
})
}
export function getBoxOrnaments(boxId) {
return request({
url: `/admin/sweet/box/ornaments/${boxId}`,
method: 'get'
})
}
export function saveBoxOrnaments(data) {
return request({
url: '/admin/sweet/box/ornaments/save',
method: 'post',
data
})
}
export function calcDistribution(data) {
return request({
url: '/admin/sweet/box/ornaments/calc',
method: 'post',
data
})
}
// 模拟 payoutCap 约束下的预估实际 RTP
export function simulateRtp(data) {
return request({
url: '/admin/sweet/box/ornaments/simulate-rtp',
method: 'post',
data
})
}
// ==================== 箱子饰品分布模板(后端持久化) ====================
// 查询某箱子的模板列表
export function getBoxTemplates(boxId) {
return request({
url: `/admin/sweet/box/templates/${boxId}`,
method: 'get'
})
}
// 保存当前分布为模板
export function saveBoxTemplate(data) {
return request({
url: '/admin/sweet/box/template/save',
method: 'post',
data
})
}
// 删除模板
export function deleteBoxTemplate(id) {
return request({
url: `/admin/sweet/box/template/${id}`,
method: 'delete'
})
}
// 应用模板(写入分布 + 标记为生效模板)
export function applyBoxTemplate(data) {
return request({
url: '/admin/sweet/box/template/apply',
method: 'post',
data
})
}
// ==================== 奖池水位 ====================
export function getPoolStatus() {
return request({ url: '/admin/sweet/pool/status', method: 'get' })
}
export function addPoolBet(data) {
return request({ url: '/admin/sweet/pool/addBet', method: 'post', data })
}
export function addPoolWin(data) {
return request({ url: '/admin/sweet/pool/addWin', method: 'post', data })
}
@@ -1,17 +0,0 @@
import request from '@/utils/request'
export function getWinList(params) {
return request({
url: '/admin/sweet/win/list',
method: 'get',
params
})
}
export function getWinSummary(params) {
return request({
url: '/admin/sweet/win/summary',
method: 'get',
params
})
}
@@ -1,8 +0,0 @@
import request from '@/utils/request'
export function getOrnamentList() {
return request({
url: '/admin/sweet/ornament/list',
method: 'get'
})
}
-16
View File
@@ -1,16 +0,0 @@
import request from '@/utils/request'
export function prizeList() {
return request({
url: '/admin/sweet/prize/list',
method: 'get'
})
}
export function prizeSave(data) {
return request({
url: '/admin/sweet/prize/save',
method: 'post',
data
})
}
@@ -1,9 +0,0 @@
import request from '@/utils/request'
export function getRecordList(params) {
return request({
url: '/admin/sweet/record/list',
method: 'get',
params
})
}
-17
View File
@@ -1,17 +0,0 @@
import request from '@/utils/request'
export function getRoundList(params) {
return request({
url: '/admin/sweet/round/list',
method: 'get',
params
})
}
export function roundDelete(ids) {
return request({
url: '/admin/sweet/round/delete',
method: 'delete',
data: ids
})
}
@@ -0,0 +1,56 @@
/* ==========================================================================
Cottagecore(田园核风)色板 —— 全站唯一色源
改色只改这里;skin.scss / sidebar.scss / variables.scss / element-variables.scss
全部引用本文件的变量,不再各自写死十六进制。
★ 无障碍(已按 WCAG 2.1 AA 实算,承载文字的组合均 ≥ 4.5:1)
- 规范原值 #8b7355 作正文只有 4.17:1 ⇒ 正文用 $cc-ink-2
- 规范原值 #5a8f5a 作浅底文字只有 3.56:1、白字只有 3.81:1
⇒ 它只做装饰与大面积色块,绿色文字一律用 $cc-grass-text
========================================================================== */
/* ---- 底色 ---- */
$cc-cream: #faf6f0; // 卡片底(规范「背景主色」)
$cc-cream-hi: #fffdfa; // 卡片悬停/更亮
$cc-linen: #f3ece0; // 页面底(比卡片深一档,卡片才浮得起来)
$cc-linen-2: #ece2d3; // 表头 / 浅槽 / 斑马纹
/* ---- 绿:草绿只做装饰,绿色文字用深值 ---- */
$cc-grass: #5a8f5a; // 装饰、大色块
$cc-grass-deep: #457450; // 绿底填充(白字 5.43:1)
$cc-grass-text: #3f6b45; // 浅底上的绿色文字(5.73:1)
/* ---- 雏菊黄 ---- */
$cc-daisy: #f5d75f; // 强调块 / 区块底(不承载小字)
$cc-daisy-deep: #8f6418; // 深雏菊金 · 警告(白字 5.24:1)
/* ---- 大地色 ---- */
$cc-earth: #8b7355; // 土棕 · 仅大号字与装饰
$cc-terra: #9a5c35; // 陶土棕 · 主色 primary(白字 5.31:1)
$cc-rose: #a4485a; // 莓红 · 危险(白字 5.75:1)
$cc-ash: #6b6152; // 暖褐灰 · 信息(白字 6.08:1)
$cc-blossom: #d4a0a0; // 花粉 · 描边 / 徽章
$cc-blossom-deep: #c08a8a; // 花粉加深 · 悬停描边
/* ---- 墨色(文字)---- */
$cc-ink: #4f4130; // 主文字(奶油底 9.16:1)
$cc-ink-2: #5f4f3d; // 正文(6.37:1)
$cc-ink-3: #7a664e; // 次级 / 弱化(5.08:1)
$cc-ink-4: #8b755a; // 占位符 / 禁用(原 #9a886e 太淡,看不出还有内容)
/* ---- 线 ---- */
$cc-line: rgba(212, 160, 160, 0.55); // 花粉细边(原 .42 的卡片描边几乎看不见)
$cc-line-2: rgba(139, 115, 85, 0.20); // 暖棕分隔线
/* ---- 柔影:暖棕扩散,不用灰黑硬影 ---- */
$cc-shadow-sm: 0 2px 8px rgba(139, 115, 85, 0.10);
$cc-shadow-md: 0 6px 18px rgba(139, 115, 85, 0.13);
$cc-shadow-lg: 0 12px 32px rgba(139, 115, 85, 0.17);
/* ---- 字体 ----
Cottagecore 要求衬线,但后台是拿表格干活的:13px 宋体在密集表格里可读性明显下降。
因此只在「标题类」(≥ 16px 的页面标题 / 弹窗标题)用衬线,
正文、表单、表格一律保持系统无衬线,数字用 tabular-nums 保证列对齐(不是等宽字体)。 */
$cc-serif: 'Songti SC', 'Source Han Serif SC', 'Noto Serif SC', STSong, SimSun, Georgia, 'Times New Roman', serif;
$cc-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
+10 -8
View File
@@ -52,13 +52,15 @@
left: 0;
position: relative;
margin: 0 auto;
border-radius: 12px;
border-radius: 18px;
border: 1px solid $cc-line;
box-shadow: $cc-shadow-lg;
overflow: hidden;
}
// 优化遮罩层
// 优化遮罩层:暖棕调,不再是纯黑
.v-modal {
background: rgba(0, 0, 0, 0.4) !important;
background: rgba(79, 65, 48, 0.42) !important;
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
@@ -73,25 +75,25 @@
// 对话框头部
.el-dialog__header {
padding: 20px 24px 16px;
border-bottom: 1px solid rgba(115, 133, 166, 0.12);
border-bottom: 1px solid $cc-line-2;
}
.el-dialog__title {
font-size: 18px;
font-weight: 600;
color: #263654;
color: $cc-ink;
}
// 对话框内容
.el-dialog__body {
padding: 24px;
color: #5a6478;
color: $cc-ink-2;
}
// 对话框底部
.el-dialog__footer {
padding: 16px 24px;
border-top: 1px solid rgba(115, 133, 166, 0.12);
border-top: 1px solid $cc-line-2;
}
// refine element ui upload
@@ -129,4 +131,4 @@
> .el-submenu__title
.el-submenu__icon-arrow {
display: none;
}
}
@@ -1,23 +1,62 @@
/**
* I think element-ui's default theme color is too light for long-term use.
* So I modified the default color and you can modify it to your liking.
**/
* Element UI 主题变量(Cottagecore / 田园核风)
*
* 这里覆盖的变量会在 @import element-ui 主题源码之前生效,
* 因此整包 theme-chalk 是「从源码编译」出来的新主题,而不是事后打补丁。
* 色值全部来自 cottagecore.scss(唯一色源),本文件只做「语义 → Element」的映射。
*
* ★ 主色用陶土棕而不是草绿:Element 的 success 已经被工具栏的「修改」按钮占用为绿色,
* 若 primary 也用绿色,「新增 / 修改」两个按钮就变成同色系分不清。
* 想改回草绿主色 → 把下面 $--color-primary 换成 $cc-grass-deep 即可(链接文字同样达标)。
**/
@import './cottagecore.scss';
/* theme color */
$--color-primary: #1890ff;
$--color-success: #13ce66;
$--color-warning: #ffba00;
$--color-danger: #ff4949;
// $--color-info: #1E1E1E;
$--color-primary: $cc-terra; // 陶土棕 · 白字 5.31:1 / 奶油底文字 4.93:1
$--color-success: $cc-grass-deep; // 草绿 · 白字 5.43:1 / 奶油底文字 5.05:1
$--color-warning: $cc-daisy-deep; // 深雏菊金 · 白字 5.24:1
$--color-danger: $cc-rose; // 莓红 · 白字 5.75:1
$--color-info: $cc-ash; // 暖褐灰 · 白字 6.08:1
$--button-font-weight: 400;
$--button-font-weight: 500;
// $--color-text-regular: #1f2d3d;
/* 文字层级:一律暖棕,不用冷灰 */
$--color-text-primary: $cc-ink;
$--color-text-regular: $cc-ink-2;
$--color-text-secondary: $cc-ink-3;
$--color-text-placeholder: $cc-ink-4;
$--border-color-light: #dfe4ed;
$--border-color-lighter: #e6ebf5;
/* 描边:花粉细边,禁止纯黑硬线 */
$--border-color-base: #d8c3b0;
$--border-color-light: #e2d3c2;
$--border-color-lighter: #eadfd0;
$--border-color-extra-light: #f0e8dc;
$--table-border: 1px solid #dfe6ec;
/* 底色 */
$--background-color-base: $cc-linen;
$--disabled-fill-base: $cc-linen;
$--disabled-color-base: $cc-ink-4;
$--disabled-border-base: #e2d3c2;
/* 圆润:Cottagecore 的圆角一律大于 Element 默认的 4px */
$--border-radius-base: 12px;
$--border-radius-small: 10px;
$--border-radius-circle: 100%;
/* 柔影:暖棕扩散 */
$--box-shadow-base: $cc-shadow-md;
$--box-shadow-dark: $cc-shadow-lg;
$--box-shadow-light: $cc-shadow-sm;
/* 表格:柔和暖线,禁用 #dfe6ec 冷灰 */
$--table-border: 1px solid #eadfd0;
$--table-border-color: #eadfd0;
$--table-header-background-color: $cc-linen;
$--table-header-font-color: $cc-ink-3;
$--table-row-hover-background-color: #fbf5ea;
$--table-current-row-background-color: $cc-linen;
$--table-font-color: $cc-ink-2;
/* icon font path, required */
$--font-path: '~element-ui/lib/theme-chalk/fonts';
+33 -31
View File
@@ -1,3 +1,4 @@
@import './cottagecore.scss';
@import './variables.scss';
@import './mixin.scss';
@import './transition.scss';
@@ -11,7 +12,8 @@ body {
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
background: #f3f7ff;
background: $cc-linen;
color: $cc-ink-2;
}
label {
@@ -21,7 +23,7 @@ label {
html {
height: 100%;
box-sizing: border-box;
background: #f3f7ff;
background: $cc-linen;
}
#app {
@@ -99,24 +101,24 @@ div:focus {
}
aside {
background: #eef1f6;
background: $cc-linen-2;
padding: 8px 24px;
margin-bottom: 20px;
border-radius: 2px;
border-radius: 12px;
display: block;
line-height: 32px;
font-size: 16px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
color: #2c3e50;
color: $cc-ink;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
a {
color: #337ab7;
color: $cc-grass-text;
cursor: pointer;
&:hover {
color: rgb(32, 160, 255);
color: $cc-grass-deep;
}
}
}
@@ -125,12 +127,15 @@ aside {
// 注意:这两张卡片都不能加 backdrop-filter / filter / transform,
// 它们会创建层叠上下文,把 position:fixed 的 el-dialog 困在卡片里,
// 而 Element UI 的 .v-modal 遮罩挂在 body 上(z-index:2000) → 弹窗被遮罩盖住(整页变黑、点一下才恢复)
// Cottagecore 皮肤下也严格只用 background / border / box-shadow 三样做卡片。
// 另:整页卡片刻意「弱化」——去掉描边、底色贴近页面底、阴影收薄。
// 否则「页卡 > 区块卡 > 小卡」三层卡片套在一起,画面很碎(规范也禁止卡片嵌套)。
.app-container {
padding: 24px;
background: rgba(255, 255, 255, 0.88);
border: 1px solid rgba(255, 255, 255, 0.9);
border-radius: 16px;
box-shadow: 0 8px 24px rgba(72, 105, 166, 0.09), inset 0 1px 0 rgba(255, 255, 255, 0.95);
background: #fdfaf5;
border: 0;
border-radius: 20px;
box-shadow: 0 2px 10px rgba(139, 115, 85, 0.07);
min-height: 500px;
}
@@ -140,37 +145,34 @@ aside {
position: relative;
margin: 6px 10px 6px 0;
padding: 10px 24px 24px;
background: rgba(255, 255, 255, 0.88);
border: 1px solid rgba(255, 255, 255, 0.9);
border-radius: 16px;
box-shadow: 0 8px 24px rgba(72, 105, 166, 0.09), inset 0 1px 0 rgba(255, 255, 255, 0.95);
background: #fdfaf5;
border: 0;
border-radius: 20px;
box-shadow: 0 2px 10px rgba(139, 115, 85, 0.07);
min-height: calc(100vh - 150px);
}
/* 只有「固定顶栏」时顶栏才脱离文档流,卡片才需要给它让位;
fixedHeader=false(本项目默认)时顶栏自己占位置,再加边距就会多出一条空白 */
fixedHeader=false(本项目默认)时顶栏自己占位置,再加边距就会多出一条空白。
注意:标签页已并入顶栏,有/无标签页都只有一条 58px 的栏,不再区分 76 / 126px。 */
.fixed-header + .page-card {
margin-top: 76px;
}
.hasTagsView .fixed-header + .page-card {
margin-top: 126px;
}
// 页面标题通用样式
.page-header {
display: flex;
align-items: baseline;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid rgba(115, 133, 166, 0.12);
border-bottom: 1px solid $cc-line-2;
}
.page-title {
margin: 0;
font-size: 24px;
font-weight: 700;
color: #263654;
color: $cc-ink;
letter-spacing: 0.3px;
}
@@ -178,19 +180,19 @@ aside {
margin-left: 12px;
font-size: 16px;
font-weight: 500;
color: #7385a6;
color: $cc-ink-3;
&::before {
content: '·';
margin-right: 8px;
color: #b0bace;
color: $cc-ink-4;
}
}
.page-description {
margin-top: 8px;
font-size: 14px;
color: #7385a6;
color: $cc-ink-3;
line-height: 1.6;
}
@@ -215,7 +217,7 @@ aside {
text-align: right;
padding-right: 20px;
transition: 600ms ease position;
background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%, rgba(33, 120, 241, 1) 100%);
background: linear-gradient(90deg, $cc-grass-deep 0%, $cc-grass-deep 0%, $cc-grass 100%, $cc-grass 100%);
.subtitle {
font-size: 20px;
@@ -223,21 +225,21 @@ aside {
}
&.draft {
background: #d0d0d0;
background: $cc-ink-4;
}
&.deleted {
background: #d0d0d0;
background: $cc-ink-4;
}
}
.link-type,
.link-type:focus {
color: #337ab7;
color: $cc-terra;
cursor: pointer;
&:hover {
color: rgb(32, 160, 255);
color: $cc-grass-text;
}
}
+109 -97
View File
@@ -1,46 +1,60 @@
/* ==========================================================================
悬浮卡片侧栏(主题核心文件)
悬浮卡片侧栏(主题核心文件)· Cottagecore / 田园核风
- 侧栏容器完全透明:页面上只有一张张独立卡片
- 一级菜单:48px 毛玻璃卡片 + 28px 彩色图标块
- 一级菜单:48px 奶油卡片 + 28px 暖色图标块
- 二/三级:原地向下展开,用竖线 + 圆点区分层级
- 收起态:54px 窄栏只留图标,悬停向右弹出子菜单
- 移动端:整栏抽屉
改动说明(相对上一版蓝色皮肤):
1. 去掉毛玻璃(backdrop-filter)—— 规范明确不把玻璃态当默认风格,
且不透明奶油卡在亚麻底上层次更清楚,也少一层层叠上下文隐患。
2. 8 色图标渐变换成大地/花卉色,不再有一个蓝。
3. 所有选中/悬停色由陶土棕 $cc-terra + 雏菊黄 $cc-daisy 派生。
========================================================================== */
$sidebar-width: $base-sidebar-width;
$menu-h: 48px;
$submenu-h: 36px;
$card-radius: 13px;
$brand: #2f6bff;
$text-main: #263654;
$text-sub: #66738b;
$text-muted: #98a3b7;
$card-bg: rgba(255, 255, 255, .84);
$card-border: rgba(255, 255, 255, .88);
$card-shadow: 0 7px 20px rgba(72, 105, 166, .11), inset 0 1px 0 rgba(255, 255, 255, .92);
@import './cottagecore.scss';
/* 一级菜单图标色板:8 色循环(浅色→深色),避免整栏一个蓝 */
$icon-palette:
(#4e9bff, #2f6bff), // 1 蓝
(#ffb648, #f97316), // 2 橙
(#4ade80, #16a34a), // 3 绿
(#fb7185, #ef4444), // 4 红
(#a78bfa, #7c3aed), // 5 紫
(#38bdf8, #0ea5e9), // 6 青
(#f472b6, #db2777), // 7 粉
(#fcd34d, #f59e0b); // 8 黄
$sidebar-width: $base-sidebar-width;
/* 菜单密度
★ 注意:用户说「拥挤」指的是视觉憋闷,不是占得高。上一轮缩成 42px/3px 反而更挤了。
正确做法是「加大项高 + 加大间距」:现在每项 54px + 上下 7px = 68px。
17 个一级菜单会超一屏 → 走 el-scrollbar 滚动,这是可接受的代价。 */
$menu-h: 54px;
$submenu-h: 38px;
$card-radius: 14px;
$brand: $cc-terra;
$text-main: $cc-ink;
$text-sub: $cc-ink-2;
$text-muted: $cc-ink-3;
$card-bg: $cc-cream-hi;
$card-border: $cc-line;
$card-shadow: $cc-shadow-sm;
/* 一级菜单图标色板:只留 4 个田园色循环(陶土 / 草绿 / 雏菊金 / 玫瑰粉)
原来是 8 色高饱和渐变,15 个菜单项齐上像调色盘;
现在与仪表盘统计卡共用同一套 4 色,整站配色口径统一。 */
$icon-hues:
#9a5c35, // 1 陶土
#457450, // 2 草绿
#a8791f, // 3 雏菊金
#b57474; // 4 玫瑰粉
$icon-hue-count: length($icon-hues);
html,
body {
background: #f3f7ff;
background: $cc-linen;
}
#app {
min-height: 100%;
/* 亚麻底整体加深一档(原来是 #fdfaf5→#f3ece0,顶部近白,
和奶油卡片几乎同色,卡片边界会糊掉)。现在卡片是亮面、页面是麻面,层次清楚。 */
background:
radial-gradient(760px 520px at 4% 100%, rgba(78, 155, 255, .18), transparent 62%),
radial-gradient(760px 520px at 96% 0%, rgba(124, 92, 252, .12), transparent 62%),
linear-gradient(155deg, #f5f8ff 0%, #fbfdff 46%, #edf4ff 100%);
radial-gradient(760px 520px at 4% 100%, rgba(90, 143, 90, .11), transparent 62%),
radial-gradient(760px 520px at 96% 0%, rgba(212, 160, 160, .13), transparent 62%),
radial-gradient(520px 420px at 78% 88%, rgba(245, 215, 95, .14), transparent 60%),
linear-gradient(155deg, #f7f1e6 0%, #f3ece0 48%, #ece2d3 100%);
background-attachment: fixed;
.main-container {
@@ -83,9 +97,9 @@ body {
background: transparent !important;
}
/* Logo 卡:10 + 64 + 6 = 80px */
/* Logo 卡:10 + 60 + 8 = 78px */
&.has-logo .el-scrollbar {
height: calc(100% - 80px);
height: calc(100% - 78px);
}
.scrollbar-wrapper {
@@ -98,7 +112,7 @@ body {
}
.el-scrollbar__thumb {
background: rgba(123, 144, 181, .28);
background: rgba(139, 115, 85, .26);
}
.is-horizontal {
@@ -140,19 +154,17 @@ body {
min-width: 0 !important;
height: $menu-h;
line-height: $menu-h;
margin: 6px 10px !important;
margin: 7px 10px !important;
padding: 0 14px !important;
box-sizing: border-box;
border: 1px solid $card-border;
border-radius: $card-radius;
background: $card-bg !important;
-webkit-backdrop-filter: blur(14px) saturate(155%);
backdrop-filter: blur(14px) saturate(155%);
box-shadow: $card-shadow;
color: $text-main !important;
font-size: 14px;
font-size: 14.5px;
font-weight: 600;
transition:
@@ -164,34 +176,34 @@ body {
.el-menu:not(.el-menu--inline) > div > a > .el-menu-item:hover,
.el-menu:not(.el-menu--inline) > div > .el-submenu > .el-submenu__title:hover {
background: rgba(255, 255, 255, .95) !important;
border-color: rgba(150, 179, 235, .38);
box-shadow: 0 9px 24px rgba(57, 94, 159, .14), inset 0 1px 0 #fff;
background: $cc-cream !important;
border-color: $cc-blossom-deep;
box-shadow: $cc-shadow-md;
transform: translateY(-1px);
}
/* 展开中的一级菜单 */
.el-menu:not(.el-menu--inline) > div > .el-submenu.is-opened > .el-submenu__title,
.el-menu:not(.el-menu--inline) > div > .el-submenu.is-active > .el-submenu__title {
color: #173f94 !important;
border-color: rgba(67, 126, 255, .28);
background: linear-gradient(135deg, rgba(255, 255, 255, .96), rgba(235, 242, 255, .91)) !important;
box-shadow: 0 8px 22px rgba(47, 107, 255, .13), inset 3px 0 0 $brand;
color: $cc-grass-text !important;
border-color: $cc-blossom;
background: linear-gradient(135deg, $cc-cream-hi, rgba(245, 215, 95, .18)) !important;
box-shadow: $cc-shadow-md, inset 3px 0 0 $brand;
}
/* 一级叶子菜单选中态 */
.el-menu:not(.el-menu--inline) > div > a > .el-menu-item.is-active {
color: $brand !important;
border-color: rgba(67, 126, 255, .30);
background: linear-gradient(135deg, rgba(255, 255, 255, .98), rgba(231, 239, 255, .92)) !important;
box-shadow: 0 8px 22px rgba(47, 107, 255, .14), inset 3px 0 0 $brand;
border-color: $cc-blossom;
background: linear-gradient(135deg, $cc-cream-hi, rgba(212, 160, 160, .22)) !important;
box-shadow: $cc-shadow-md, inset 3px 0 0 $brand;
}
/* 展开箭头 */
.el-menu:not(.el-menu--inline) > div > .el-submenu > .el-submenu__title .el-submenu__icon-arrow {
right: 14px;
margin-top: -5px;
color: #7385a6;
color: $cc-ink-3;
font-size: 12px;
font-weight: 700;
}
@@ -206,27 +218,29 @@ body {
align-items: center;
justify-content: center;
box-sizing: border-box;
width: 28px;
height: 28px;
width: 30px;
height: 30px;
margin: 0 11px 0 0 !important;
padding: 5px;
border-radius: 9px;
color: #fff !important;
fill: #fff;
background: linear-gradient(135deg, #4e9bff, #2f6bff);
box-shadow: 0 5px 12px rgba(47, 107, 255, .25);
padding: 6px;
border-radius: 11px;
/* 浅底 + 同色深图标。底色不能太淡,否则小图标「看不见」(用户反馈对比度不够),
所以 tint 用 .22 并加 1px 内描边。 */
color: #9a5c35 !important;
fill: currentColor;
background: rgba(154, 92, 53, .22);
box-shadow: inset 0 0 0 1px rgba(154, 92, 53, .32);
}
/* 按菜单顺序轮换图标配色:第 1~8 个一组,之后循环 */
@for $i from 1 through 8 {
$pair: nth($icon-palette, $i);
$from: nth($pair, 1);
$to: nth($pair, 2);
/* 按菜单顺序在这 4 个色里轮换 */
@for $i from 1 through $icon-hue-count {
$hue: nth($icon-hues, $i);
.el-menu:not(.el-menu--inline) > div:nth-child(8n + #{$i}) > a > .el-menu-item .svg-icon,
.el-menu:not(.el-menu--inline) > div:nth-child(8n + #{$i}) > .el-submenu > .el-submenu__title .svg-icon {
background: linear-gradient(135deg, $from, $to);
box-shadow: 0 5px 12px rgba($to, .28);
.el-menu:not(.el-menu--inline) > div:nth-child(#{$icon-hue-count}n + #{$i}) > a > .el-menu-item .svg-icon,
.el-menu:not(.el-menu--inline) > div:nth-child(#{$icon-hue-count}n + #{$i}) > .el-submenu > .el-submenu__title .svg-icon {
background: rgba($hue, .22);
box-shadow: inset 0 0 0 1px rgba($hue, .32);
color: $hue !important;
fill: currentColor;
}
}
@@ -236,8 +250,8 @@ body {
.el-menu--inline {
position: relative;
width: calc(100% - 30px) !important;
margin: -1px 10px 5px 20px;
padding: 4px 0 5px 17px;
margin: -2px 10px 6px 18px;
padding: 4px 0 6px 17px;
box-sizing: border-box;
background: transparent !important;
border: 0 !important;
@@ -253,7 +267,7 @@ body {
bottom: 7px;
width: 2px;
border-radius: 1px;
background: linear-gradient(180deg, rgba(47, 107, 255, .40), rgba(115, 133, 166, .22));
background: linear-gradient(180deg, rgba(154, 92, 53, .38), rgba(212, 160, 160, .22));
}
.el-menu--inline .el-menu-item,
@@ -265,12 +279,12 @@ body {
width: 100%;
height: $submenu-h;
line-height: $submenu-h;
margin: 1px 0 !important;
padding: 0 12px 0 24px !important;
margin: 2px 0 !important;
padding: 0 12px 0 26px !important;
box-sizing: border-box;
border: 0 !important;
border-radius: 9px;
border-radius: 10px;
background: transparent !important;
box-shadow: none !important;
@@ -293,8 +307,8 @@ body {
margin-top: -4px;
box-sizing: border-box;
border-radius: 50%;
border: 2px solid #7385a6;
background: #fff;
border: 2px solid $cc-ink-4;
background: $cc-cream;
transition: all .2s ease;
}
@@ -306,20 +320,20 @@ body {
.el-menu--inline .el-menu-item:hover,
.el-menu--inline .el-submenu > .el-submenu__title:hover {
color: #2f6bff !important;
background: rgba(234, 241, 255, .72) !important;
color: $brand !important;
background: rgba(245, 215, 95, .20) !important;
transform: none !important;
&::before {
border-color: #2f6bff;
background: rgba(47, 107, 255, .15);
border-color: $brand;
background: rgba(154, 92, 53, .16);
}
}
.el-menu--inline .el-menu-item.is-active {
color: $brand !important;
font-weight: 650;
background: linear-gradient(90deg, rgba(222, 234, 255, .92), rgba(239, 245, 255, .72)) !important;
background: linear-gradient(90deg, rgba(212, 160, 160, .30), rgba(245, 215, 95, .16)) !important;
}
.el-menu--inline .el-menu-item.is-active::before {
@@ -328,7 +342,7 @@ body {
margin-top: -5px;
border-color: $brand;
background: $brand;
box-shadow: 0 0 0 4px rgba(47, 107, 255, .15);
box-shadow: 0 0 0 4px rgba(154, 92, 53, .16);
}
/* 三级:在二级基础上小步缩进,不把内容推到很右边 */
@@ -345,16 +359,16 @@ body {
.el-menu--inline .el-menu--inline .el-menu-item,
.el-menu--inline .el-menu--inline .el-submenu > .el-submenu__title {
height: 34px;
line-height: 34px;
padding-left: 22px !important;
font-size: 12.5px;
color: #7c879a !important;
height: 36px;
line-height: 36px;
padding-left: 24px !important;
font-size: 13px;
color: $cc-ink-3 !important;
}
.el-menu--inline .el-submenu__icon-arrow {
right: 10px;
color: #9aa6bb;
color: $cc-ink-4;
font-size: 11px;
}
}
@@ -438,12 +452,10 @@ body {
.el-menu--vertical {
> .el-menu--popup {
padding: 8px;
border-radius: 12px;
background: rgba(255, 255, 255, .96);
-webkit-backdrop-filter: blur(14px) saturate(155%);
backdrop-filter: blur(14px) saturate(155%);
border: 1px solid rgba(255, 255, 255, .9);
box-shadow: 0 12px 32px rgba(72, 105, 166, .18);
border-radius: 14px;
background: $cc-cream-hi;
border: 1px solid $cc-line;
box-shadow: $cc-shadow-lg;
.el-menu-item {
height: 36px;
@@ -451,21 +463,21 @@ body {
min-width: 150px;
margin: 2px 0;
padding: 0 14px !important;
border-radius: 8px;
border-radius: 10px;
color: $text-sub !important;
font-size: 13px;
font-weight: 500;
background: transparent !important;
&:hover {
background: rgba(234, 241, 255, .85) !important;
background: rgba(245, 215, 95, .24) !important;
color: $brand !important;
}
&.is-active {
color: $brand !important;
font-weight: 650;
background: linear-gradient(90deg, rgba(222, 234, 255, .95), rgba(239, 245, 255, .8)) !important;
background: linear-gradient(90deg, rgba(212, 160, 160, .32), rgba(245, 215, 95, .18)) !important;
}
.svg-icon {
@@ -487,7 +499,7 @@ body {
bottom: 7px;
width: 2px;
border-radius: 1px;
background: linear-gradient(180deg, rgba(47, 107, 255, .35), rgba(115, 133, 166, .2));
background: linear-gradient(180deg, rgba(154, 92, 53, .34), rgba(212, 160, 160, .2));
}
.el-menu-item,
@@ -497,7 +509,7 @@ body {
line-height: 34px;
padding-left: 20px !important;
font-size: 12.5px;
color: #7c879a !important;
color: $cc-ink-3 !important;
background: transparent !important;
&::before {
@@ -510,13 +522,13 @@ body {
margin-top: -4px;
box-sizing: border-box;
border-radius: 50%;
border: 2px solid #9aa6bb;
background: #fff;
border: 2px solid $cc-ink-4;
background: $cc-cream;
}
&:hover {
color: $brand !important;
background: rgba(234, 241, 255, .85) !important;
background: rgba(245, 215, 95, .24) !important;
&::before {
border-color: $brand;
@@ -526,7 +538,7 @@ body {
&.is-active {
color: $brand !important;
font-weight: 650;
background: rgba(234, 241, 255, .9) !important;
background: rgba(212, 160, 160, .26) !important;
&::before {
border-color: $brand;
+175 -69
View File
@@ -1,28 +1,48 @@
/* ==========================================================================
皮肤层:按钮 / 表单 / 表格 / 分页 / 页头 / 水印 / 暗色
皮肤层:按钮 / 表单 / 表格 / 分页 / 页头 / 弹层 · Cottagecore / 田园核风
只覆盖 Element UI 默认样式,不改组件源码;需要整体回滚时删掉本文件的 import 即可
(index.scss 最后一行 @import './skin.scss')
交互节奏按规范走「Gentle / Watercolor」:过渡 .4s ease-in-out,
悬停做轻微旋转(≤0.6deg)与小幅放大,按下柔和回弹 scale(.97),
不使用 bounce / elastic,也不用 filter: brightness 那种硬闪。
========================================================================== */
$brand: #2f6bff;
$ink: #263654;
$ink-2: #5b6880;
$ink-3: #98a3b7;
$line: #eef2f9;
@import './cottagecore.scss';
$brand: $cc-terra;
$ink: $cc-ink;
$ink-2: $cc-ink-2;
$ink-3: $cc-ink-3;
$line: #eadfd0;
/* ------------------------------- 按钮 ------------------------------- */
.el-button {
border-radius: 10px;
border-radius: 12px;
font-size: 13px;
font-weight: 600;
line-height: 1;
transition: filter .16s ease, box-shadow .16s ease;
border-color: $cc-line;
background: $cc-cream-hi;
color: $ink-2;
transition:
transform .4s ease-in-out,
box-shadow .4s ease-in-out,
background-color .4s ease-in-out,
border-color .4s ease-in-out;
> i {
font-size: 14px;
}
&:hover {
filter: brightness(1.06);
&:hover:not(.is-disabled) {
transform: rotate(-0.4deg) scale(1.02);
border-color: $cc-blossom-deep;
box-shadow: $cc-shadow-sm;
}
&:active:not(.is-disabled) {
transform: scale(0.97);
}
}
@@ -31,109 +51,115 @@ $line: #eef2f9;
font-size: 13px;
}
/* 工具栏按钮:实心胶囊(新增蓝 / 修改绿 / 删除红 / 导入青 / 导出橙) */
/* 工具栏按钮:实心胶囊(新增陶土 / 修改草绿 / 删除莓红 / 导入褐灰 / 导出雏菊金)
—— 五种色相刻意拉开,避免「新增 / 修改」变成同色系分不清 */
.el-button--primary.is-plain {
background: #2f6bff;
border-color: #2f6bff;
background: $cc-terra;
border-color: $cc-terra;
color: #fff;
&:hover, &:focus {
background: #1f57e0;
border-color: #1f57e0;
background: #854c2a;
border-color: #854c2a;
color: #fff;
}
&.is-disabled, &.is-disabled:hover, &.is-disabled:focus {
background: #a9c1ff;
border-color: #a9c1ff;
background: #cfa98f;
border-color: #cfa98f;
color: #fff;
}
}
.el-button--success.is-plain {
background: #22c55e;
border-color: #22c55e;
background: $cc-grass-deep;
border-color: $cc-grass-deep;
color: #fff;
&:hover, &:focus {
background: #16a34a;
border-color: #16a34a;
background: #3a6243;
border-color: #3a6243;
color: #fff;
}
&.is-disabled, &.is-disabled:hover, &.is-disabled:focus {
background: #a5e5bf;
border-color: #a5e5bf;
background: #a9c9ae;
border-color: #a9c9ae;
color: #fff;
}
}
.el-button--danger.is-plain {
background: #ef4444;
border-color: #ef4444;
background: $cc-rose;
border-color: $cc-rose;
color: #fff;
&:hover, &:focus {
background: #dc2626;
border-color: #dc2626;
background: #8b3c4c;
border-color: #8b3c4c;
color: #fff;
}
&.is-disabled, &.is-disabled:hover, &.is-disabled:focus {
background: #f7b0b0;
border-color: #f7b0b0;
background: #d9a9b2;
border-color: #d9a9b2;
color: #fff;
}
}
.el-button--info.is-plain {
background: #06b6d4;
border-color: #06b6d4;
background: $cc-ash;
border-color: $cc-ash;
color: #fff;
&:hover, &:focus {
background: #0891b2;
border-color: #0891b2;
background: #5b5244;
border-color: #5b5244;
color: #fff;
}
&.is-disabled, &.is-disabled:hover, &.is-disabled:focus {
background: #9adcea;
border-color: #9adcea;
background: #b8b0a3;
border-color: #b8b0a3;
color: #fff;
}
}
.el-button--warning.is-plain {
background: #f59e0b;
border-color: #f59e0b;
background: $cc-daisy-deep;
border-color: $cc-daisy-deep;
color: #fff;
&:hover, &:focus {
background: #d97706;
border-color: #d97706;
background: #7a5512;
border-color: #7a5512;
color: #fff;
}
&.is-disabled, &.is-disabled:hover, &.is-disabled:focus {
background: #fbd08a;
border-color: #fbd08a;
background: #d6bb86;
border-color: #d6bb86;
color: #fff;
}
}
/* 表格行内文字按钮:编辑蓝、删除红 */
/* 表格行内文字按钮:编辑陶土、删除莓红 */
.el-table .el-button--text {
padding: 4px 6px;
font-size: 13px;
font-weight: 600;
color: $brand;
&:hover, &:focus {
color: $cc-grass-text;
}
}
.el-table .el-button--text.text-danger {
color: #ef4444;
color: $cc-rose;
&:hover, &:focus {
color: #dc2626;
color: #8b3c4c;
}
}
@@ -141,21 +167,27 @@ $line: #eef2f9;
.el-table .more-dot {
padding: 4px 10px;
font-size: 16px;
color: $ink-3;
color: $cc-ink-4;
}
/* ------------------------------- 表单 ------------------------------- */
.el-input__inner,
.el-textarea__inner {
border-radius: 10px;
border-color: #e6ecf6;
border-radius: 12px;
border-color: $cc-line;
background: $cc-cream-hi;
color: $ink;
}
.el-input__inner::placeholder,
.el-textarea__inner::placeholder {
color: $cc-ink-4;
}
.el-input__inner:focus,
.el-textarea__inner:focus {
border-color: rgba(47, 107, 255, .55);
box-shadow: 0 0 0 3px rgba(47, 107, 255, .1);
border-color: rgba(154, 92, 53, .55);
box-shadow: 0 0 0 3px rgba(154, 92, 53, .12);
}
/* 搜索条不再显示 label,用 placeholder 表达(与设计图一致) */
@@ -168,10 +200,20 @@ $line: #eef2f9;
margin-bottom: 6px;
}
/* 下拉/日期浮层的输入框跟着圆 */
.el-select-dropdown__item.selected,
.el-select-dropdown__item.hover,
.el-select-dropdown__item:hover {
background: rgba(245, 215, 95, .26);
}
/* ------------------------------- 表格 ------------------------------- */
.el-table {
font-size: 13px;
color: #2b3a55;
color: $ink-2;
background: transparent;
/* 数字列右对齐时天然对齐,不用等宽字体(等宽被规范列为禁止项) */
font-variant-numeric: tabular-nums;
&::before {
display: none;
@@ -179,8 +221,8 @@ $line: #eef2f9;
th.el-table__cell {
height: 42px;
background: #f7f9fe;
color: $ink-2;
background: $cc-linen;
color: $cc-ink-3;
font-size: 13px;
font-weight: 600;
}
@@ -191,7 +233,7 @@ $line: #eef2f9;
}
.el-table__row:hover > td.el-table__cell {
background: #f8fbff;
background: #fbf5ea;
}
.el-button--text {
@@ -212,7 +254,7 @@ $line: #eef2f9;
.app-container .pagination-container {
margin-top: 6px;
padding: 8px 16px 6px;
border-radius: 12px;
border-radius: 14px;
}
.el-pagination {
@@ -227,13 +269,20 @@ $line: #eef2f9;
height: 32px;
line-height: 30px;
margin: 0 3px;
border: 1px solid #e9eff8;
border-radius: 8px;
background: #fff;
border: 1px solid $line;
border-radius: 10px;
background: $cc-cream-hi;
color: $ink-2;
font-weight: 600;
}
.btn-prev:hover,
.btn-next:hover,
.el-pager li:hover {
border-color: $cc-blossom-deep;
color: $brand;
}
.el-pager li.active {
background: $brand;
border-color: $brand;
@@ -242,7 +291,7 @@ $line: #eef2f9;
.el-pagination__sizes .el-input__inner,
.el-pagination__jump .el-input__inner {
border-radius: 8px;
border-radius: 10px;
}
}
@@ -256,6 +305,14 @@ $line: #eef2f9;
border-bottom: 0;
}
/* 标题类走衬线:只在 ≥16px 的大号字上用,制造田园感而不牺牲表格可读性 */
.page-title,
.el-dialog__title,
.el-message-box__title {
font-family: $cc-serif;
letter-spacing: .2px;
}
/* ------------------------------- 弹层 -------------------------------
$confirm / $alert(el-message-box)之前完全没被皮肤层覆盖,还是 Element
默认的 4px 方角 + 15px 内边距,和 12px 的 el-dialog、10px 的按钮/输入框
@@ -263,14 +320,14 @@ $line: #eef2f9;
------------------------------------------------------------------- */
.el-message-box {
padding-bottom: 0;
border: 1px solid rgba(255, 255, 255, 0.9);
border-radius: 12px;
box-shadow: 0 12px 32px rgba(72, 105, 166, 0.18);
border: 1px solid $cc-line;
border-radius: 18px;
box-shadow: $cc-shadow-lg;
}
.el-message-box__header {
padding: 20px 24px 16px;
border-bottom: 1px solid rgba(115, 133, 166, 0.12);
border-bottom: 1px solid $cc-line-2;
}
.el-message-box__title {
@@ -292,13 +349,13 @@ $line: #eef2f9;
.el-message-box__status {
&.el-icon-warning {
color: #f59e0b;
color: $cc-daisy-deep;
}
&.el-icon-success {
color: #22c55e;
color: $cc-grass-deep;
}
&.el-icon-error {
color: #ef4444;
color: $cc-rose;
}
&.el-icon-info {
color: $brand;
@@ -307,7 +364,7 @@ $line: #eef2f9;
.el-message-box__btns {
padding: 16px 24px;
border-top: 1px solid rgba(115, 133, 166, 0.12);
border-top: 1px solid $cc-line-2;
.el-button {
min-width: 76px;
@@ -322,12 +379,61 @@ $line: #eef2f9;
.el-picker-panel,
.el-message,
.el-notification {
border-color: rgba(115, 133, 166, 0.14);
border-radius: 10px;
box-shadow: 0 8px 24px rgba(72, 105, 166, 0.12);
border-color: $cc-line-2;
border-radius: 14px;
box-shadow: $cc-shadow-lg;
overflow: hidden;
}
.el-message {
min-width: 320px;
}
/* 标签:圆润小球形,配色跟随语义色 */
.el-tag {
border-radius: 999px;
padding: 0 12px;
border-color: $cc-line;
}
/* 复选框/单选框/开关:跟随陶土主色 */
.el-checkbox__input.is-checked .el-checkbox__inner,
.el-checkbox__input.is-indeterminate .el-checkbox__inner,
.el-radio__input.is-checked .el-radio__inner {
background-color: $brand;
border-color: $brand;
}
.el-checkbox__input.is-checked + .el-checkbox__label,
.el-radio__input.is-checked + .el-radio__label {
color: $brand;
}
/* 卡片 / 折叠面板 / 描述列表:统一的奶油卡 */
.el-card,
.el-collapse,
.el-collapse-item__header,
.el-collapse-item__wrap,
.el-descriptions__body {
border-color: $cc-line;
border-radius: 16px;
background: $cc-cream;
}
.el-collapse-item__header,
.el-collapse-item__wrap {
border-radius: 0;
border-bottom-color: $cc-line-2;
}
/* ------------------------------- 无障碍 -------------------------------
规范要求动效提供 reduced-motion 备选:关掉悬停位移/旋转,只留颜色变化
------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
.el-button,
.el-button:hover,
.el-button:active {
transform: none !important;
transition: background-color .01ms linear, border-color .01ms linear !important;
}
}
+22 -33
View File
@@ -1,40 +1,29 @@
// base color
$blue:#324157;
$light-blue:#3A71A8;
$red:#C03639;
$pink: #E65D6E;
$green: #30B08F;
$tiffany: #4AB7BD;
$yellow:#FEC171;
$panGreen: #30B08F;
@import './cottagecore.scss';
// 默认菜单主题风格
$base-menu-color:#bfcbd9;
$base-menu-color-active:#f4f4f5;
$base-menu-background:#304156;
// base color(沿用 cottagecore 色板,避免这里的 $blue/$green 与主题脱节)
$blue: $cc-earth;
$light-blue: $cc-grass;
$red: $cc-rose;
$pink: $cc-blossom;
$green: $cc-grass-deep;
$tiffany: $cc-grass;
$yellow: $cc-daisy;
$panGreen: $cc-grass-deep;
// 深色菜单主题风格(sideTheme: 'theme-dark' 时用;Cottagecore 皮肤默认走下面的浅色)
$base-menu-color: #bfcbd9;
$base-menu-color-active: #f4f4f5;
$base-menu-background: #304156;
$base-logo-title-color: #ffffff;
$base-menu-light-color:rgba(0,0,0,.70);
$base-menu-light-background:#ffffff;
$base-logo-light-title-color: #001529;
// 浅色菜单主题风格(当前使用:奶油底 + 暖棕文字)
$base-menu-light-color: $cc-ink-2;
$base-menu-light-background: $cc-cream;
$base-logo-light-title-color: $cc-ink;
$base-sub-menu-background:#1f2d3d;
$base-sub-menu-hover:#001528;
// 自定义暗色菜单风格
/**
$base-menu-color:hsla(0,0%,100%,.65);
$base-menu-color-active:#fff;
$base-menu-background:#001529;
$base-logo-title-color: #ffffff;
$base-menu-light-color:rgba(0,0,0,.70);
$base-menu-light-background:#ffffff;
$base-logo-light-title-color: #001529;
$base-sub-menu-background:#000c17;
$base-sub-menu-hover:#001528;
*/
// 深色主题的二级菜单底色(未使用,留作回退)
$base-sub-menu-background: #1f2d3d;
$base-sub-menu-hover: #001528;
$base-sidebar-width: 240px;
+9 -9
View File
@@ -65,48 +65,48 @@ export default {
font-size: 14px;
line-height: 58px;
margin-left: 4px;
color: #7385a6;
color: #7a664e;
::v-deep .el-breadcrumb__separator {
color: #b0bace;
color: #9a886e;
margin: 0 6px;
font-weight: 400;
}
::v-deep .el-breadcrumb__item {
.el-breadcrumb__inner {
color: #5a6478;
color: #5f4f3d;
font-weight: 500;
transition: color 0.2s ease;
&:hover {
color: #2f6bff;
color: #9a5c35;
}
&.is-link {
color: #7385a6;
color: #7a664e;
font-weight: 400;
}
}
&:last-child .el-breadcrumb__inner {
color: #2f6bff;
color: #9a5c35;
font-weight: 600;
}
}
.no-redirect {
color: #2f6bff;
color: #9a5c35;
cursor: text;
font-weight: 600;
}
a {
color: #7385a6;
color: #7a664e;
font-weight: 500;
&:hover {
color: #2f6bff;
color: #9a5c35;
}
}
}
+2 -2
View File
@@ -19,8 +19,8 @@
<!-- 上传提示 -->
<div class="el-upload__tip" slot="tip" v-if="showTip">
请上传
<template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template>
<template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
<template v-if="fileSize"> 大小不超过 <b style="color: #a4485a">{{ fileSize }}MB</b> </template>
<template v-if="fileType"> 格式为 <b style="color: #a4485a">{{ fileType.join("/") }}</b> </template>
的文件
</div>
</el-upload>
@@ -83,7 +83,7 @@ export default {
align-items: center;
width: 100%;
height: 100%;
color: #909399;
color: #7a664e;
font-size: 30px;
}
}
@@ -23,8 +23,8 @@
<!-- 上传提示 -->
<div class="el-upload__tip" slot="tip" v-if="showTip">
请上传
<template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template>
<template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
<template v-if="fileSize"> 大小不超过 <b style="color: #a4485a">{{ fileSize }}MB</b> </template>
<template v-if="fileType"> 格式为 <b style="color: #a4485a">{{ fileType.join("/") }}</b> </template>
的文件
</div>
@@ -1,7 +1,7 @@
<template>
<el-color-picker
v-model="theme"
:predefine="['#409EFF', '#1890ff', '#304156','#212121','#11a983', '#13c2c2', '#6959CD', '#f5222d', ]"
:predefine="['#9a5c35', '#1890ff', '#304156','#212121','#11a983', '#13c2c2', '#6959CD', '#f5222d', ]"
class="theme-picker"
popper-class="theme-picker-dropdown"
/>
@@ -9,7 +9,7 @@
<script>
const version = require('element-ui/package.json').version // element-ui version from node_modules
const ORIGINAL_THEME = '#409EFF' // default color
const ORIGINAL_THEME = '#9a5c35' // default color
export default {
data() {
+5 -3
View File
@@ -41,17 +41,19 @@ export default {
height: 8px;
}
/* 全局滚动条:原来是冷白蓝轨 + 石板蓝滑块,悬停还是蓝色
(rgba(47,107,255))—— 换肤时容易漏掉,因为它不在任何卡片里 */
::-webkit-scrollbar-track {
background-color: rgba(248, 250, 254, 0.6);
background-color: rgba(236, 226, 211, 0.6);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, rgba(123, 144, 181, 0.4), rgba(123, 144, 181, 0.3));
background: linear-gradient(180deg, rgba(139, 115, 85, 0.34), rgba(139, 115, 85, 0.24));
border-radius: 4px;
&:hover {
background: linear-gradient(180deg, rgba(47, 107, 255, 0.5), rgba(47, 107, 255, 0.4));
background: linear-gradient(180deg, rgba(154, 92, 53, 0.5), rgba(154, 92, 53, 0.4));
}
}
</style>
+35 -34
View File
@@ -1,7 +1,10 @@
<template>
<div class="navbar">
<breadcrumb id="breadcrumb-container" class="breadcrumb-container" v-if="!topNav"/>
<top-nav id="topmenu-container" class="topmenu-container" v-if="topNav"/>
<!-- 顶栏与标签页合并成一条
原来「面包屑 + 标签页」是两条叠在一起的栏,而且都在显示当前位置
(面包屑显示"首页",紧挨着的标签页也显示"首页")= 重复,还白占 108px 竖向空间。
现在只保留标签页(多标签导航更实用),面包屑去掉。 -->
<tags-view v-if="showTagsView" class="navbar-tags-view" />
<div class="right-menu">
<!-- 任意页面都能一键看"当前这个页面"的操作记录 -->
@@ -35,23 +38,20 @@
<script>
import { mapGetters } from 'vuex'
import Breadcrumb from '@/components/Breadcrumb'
import TopNav from '@/components/TopNav'
import TagsView from './TagsView'
export default {
components: {
Breadcrumb,
TopNav
TagsView
},
computed: {
...mapGetters([
'avatar',
'name'
]),
topNav: {
get() {
return this.$store.state.settings.topNav
}
/** 标签页已并入顶栏,是否显示跟随布局设置 */
showTagsView() {
return this.$store.state.settings.tagsView
},
/** 已经在操作日志页时不显示入口 */
showOperLog() {
@@ -84,26 +84,25 @@ export default {
<style lang="scss" scoped>
.navbar {
display: flex;
align-items: center;
gap: 10px;
height: 58px;
overflow: hidden;
position: relative;
background: rgba(255, 255, 255, 0.86);
backdrop-filter: blur(14px) saturate(145%);
-webkit-backdrop-filter: blur(14px) saturate(145%);
box-shadow: 0 6px 22px rgba(47, 107, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.72);
/* 原来是白色毛玻璃 + 蓝影(rgba(47,107,255,.08)),
与奶油菜单卡不是一套;统一成不透明奶油卡 + 花粉细边 + 暖棕柔影,并去掉 backdrop-filter */
background: #fffdfa;
box-shadow: 0 2px 8px rgba(139, 115, 85, 0.10);
border: 1px solid rgba(212, 160, 160, 0.55);
border-radius: 14px;
margin: 6px 10px 6px 0;
padding-left: 16px;
padding: 0 10px 0 8px;
/* 顶栏不再放置收起按钮/装饰图标,面包屑直接从左侧留白开始 */
.breadcrumb-container {
float: left;
}
.topmenu-container {
position: absolute;
left: 16px;
/* 面包屑与 top-nav 模式不再使用(当前位置由标签页表达) */
.navbar-tags-view {
flex: 1 1 auto;
min-width: 0;
}
.errLog-container {
@@ -112,22 +111,24 @@ export default {
}
.right-menu {
float: right;
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 6px;
height: 100%;
line-height: 58px;
padding-right: 18px;
padding-right: 4px;
&:focus {
outline: none;
}
.right-menu-item {
display: inline-block;
display: inline-flex;
align-items: center;
padding: 0 8px;
height: 100%;
font-size: 18px;
color: #5a6478;
vertical-align: text-bottom;
color: #5f4f3d;
}
/* 操作日志入口:文案按钮,比图标更好找 */
@@ -141,7 +142,7 @@ export default {
}
&:hover {
color: #2f6bff;
color: #9a5c35;
}
}
@@ -151,7 +152,7 @@ export default {
.avatar-wrapper {
display: flex;
align-items: center;
height: 58px;
height: 100%;
.user-avatar {
width: 36px;
@@ -164,7 +165,7 @@ export default {
margin: 0 6px 0 8px;
font-size: 14px;
font-weight: 600;
color: #263654;
color: #4f4130;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
@@ -173,7 +174,7 @@ export default {
.el-icon-caret-bottom {
font-size: 12px;
color: #98a3b7;
color: #9a886e;
}
}
}
@@ -226,7 +226,7 @@ export default {
height: 100%;
padding-top: 15px;
padding-left: 24px;
color: #1890ff;
color: #9a5c35;
font-weight: 700;
font-size: 14px;
}
@@ -28,18 +28,18 @@ export default {
</script>
<style lang="scss" scoped>
/* 悬浮 Logo 卡片:与一级菜单使用同一套毛玻璃语言 */
/* 悬浮 Logo 卡片:与一级菜单同一套语言(不透明奶油卡 + 花粉细边 + 暖棕柔影)
原来还是旧的蓝色毛玻璃:rgba(255,255,255,.86) + blur(14px) + rgba(72,105,166,.11) 蓝影,
换肤时漏了这个文件,结果 Logo 卡和下面的菜单卡不是一套。 */
.sidebar-logo-container {
position: relative;
width: calc(100% - 20px);
height: 64px;
margin: 10px 10px 6px;
background: rgba(255, 255, 255, .86);
-webkit-backdrop-filter: blur(14px) saturate(155%);
backdrop-filter: blur(14px) saturate(155%);
border: 1px solid rgba(255, 255, 255, .88);
height: 60px;
margin: 10px 10px 8px;
background: #fffdfa;
border: 1px solid rgba(212, 160, 160, 0.55);
border-radius: 14px;
box-shadow: 0 7px 20px rgba(72, 105, 166, .11), inset 0 1px 0 rgba(255, 255, 255, .92);
box-shadow: 0 2px 8px rgba(139, 115, 85, 0.10);
.sidebar-logo-link {
display: flex;
@@ -63,7 +63,7 @@ export default {
.sidebar-title {
flex: 0 1 auto;
margin: 0;
color: #263654;
color: #4f4130;
font-size: 16px;
font-weight: 700;
line-height: 22px;
@@ -18,7 +18,7 @@
<span v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
</router-link>
</scroll-pane>
<ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu">
<ul v-show="visible" ref="menu" :style="{left:left+'px',top:top+'px'}" class="contextmenu">
<li @click="refreshSelectedTag(selectedTag)"><i class="el-icon-refresh-right"></i> 刷新页面</li>
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)"><i class="el-icon-close"></i> 关闭当前</li>
<li @click="closeOthersTags"><i class="el-icon-circle-close"></i> 关闭其他</li>
@@ -218,21 +218,34 @@ export default {
}
},
openMenu(tag, e) {
const menuMinWidth = 105
const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
const offsetWidth = this.$el.offsetWidth // container width
const maxLeft = offsetWidth - menuMinWidth // left boundary
const left = e.clientX - offsetLeft + 15 // 15: margin right
if (left > maxLeft) {
this.left = maxLeft
} else {
this.left = left
}
this.top = e.clientY
/* 菜单是 position: fixed(见 <style>),坐标用光标的视口坐标即可。
原实现是 absolute + (clientX - 容器 left),按「相对定位祖先」算:
顶栏合并后祖先变成 .navbar(overflow: hidden),于是菜单既被裁成一条、又整体偏位。
改成 fixed 后就跳出裁切(.navbar 没有 transform / filter,不会给 fixed 造包含块)。 */
this.left = e.clientX + 6
this.top = e.clientY + 6
this.visible = true
this.selectedTag = tag
this.clampMenu()
},
/** 贴边时回收/上翻,保证整个菜单都在视口内(原来 maxLeft 用的是容器宽度,算不对) */
clampMenu() {
this.$nextTick(() => {
const el = this.$refs.menu
if (!el) return
const rect = el.getBoundingClientRect()
const pad = 8
let left = this.left
let top = this.top
if (rect.right > window.innerWidth - pad) {
left = window.innerWidth - rect.width - pad
}
if (rect.bottom > window.innerHeight - pad) {
top = this.top - rect.height - 12
}
this.left = left < pad ? pad : left
this.top = top < pad ? pad : top
})
},
closeMenu() {
this.visible = false
@@ -246,16 +259,18 @@ export default {
<style lang="scss" scoped>
.tags-view-container {
height: 50px;
margin: 0 10px 0 0;
padding: 9px 12px 9px 14px;
/* 已并入顶栏(Navbar):不再自己做一张卡片,只负责横向占位,
否则会变成「顶栏卡片里再套一张标签卡片」 */
display: flex;
align-items: center;
height: 100%;
min-width: 0;
margin: 0;
padding: 0;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.86);
backdrop-filter: blur(14px) saturate(145%);
-webkit-backdrop-filter: blur(14px) saturate(145%);
border: 1px solid rgba(255, 255, 255, 0.72);
border-radius: 14px;
box-shadow: 0 6px 22px rgba(47, 107, 255, 0.08);
background: transparent;
border: 0;
box-shadow: none;
.tags-view-wrapper {
.tags-view-item {
@@ -265,15 +280,15 @@ export default {
cursor: pointer;
height: 32px;
line-height: 32px;
border: 1px solid rgba(214, 225, 241, 0.9);
color: #5a6478;
background: rgba(238, 244, 253, 0.95);
border: 1px solid #eadfd0;
color: #5f4f3d;
background: #f6efe3;
padding: 0 12px;
font-size: 13px;
font-weight: 500;
margin-left: 6px;
border-radius: 8px;
transition: all 0.2s ease;
border-radius: 10px;
transition: all .4s ease-in-out;
&:first-of-type {
margin-left: 0;
@@ -283,16 +298,17 @@ export default {
}
&:hover {
background: rgba(228, 238, 253, 1);
border-color: rgba(47, 107, 255, 0.25);
color: #2f6bff;
background: #fbf5ea;
border-color: #c08a8a;
color: #9a5c35;
}
&.active {
background: linear-gradient(135deg, #2f6bff, #4e9bff);
/* 渐变两端都保证白字 ≥4.5:1(#9a5c35 为 5.31:1,#7d4a2a 为 7.28:1) */
background: linear-gradient(135deg, #9a5c35, #7d4a2a);
color: #fff;
border-color: transparent;
box-shadow: 0 4px 12px rgba(47, 107, 255, 0.28);
box-shadow: 0 4px 12px rgba(139, 115, 85, 0.22);
font-weight: 600;
&::before {
@@ -304,38 +320,39 @@ export default {
.contextmenu {
margin: 0;
background: rgba(255, 255, 255, 0.96);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
background: #fffdfa;
z-index: 3000;
position: absolute;
/* 必须是 fixed:顶栏(.navbar)有 overflow: hidden,
absolute 会被裁成一条、且坐标基准是 .navbar 而不是视口 */
position: fixed;
list-style-type: none;
padding: 6px;
border-radius: 10px;
min-width: 136px;
border-radius: 12px;
font-size: 13px;
font-weight: 500;
color: #333;
border: 1px solid rgba(255, 255, 255, 0.8);
box-shadow: 0 8px 24px rgba(47, 107, 255, 0.12);
color: #4f4130;
border: 1px solid rgba(212, 160, 160, 0.55);
box-shadow: 0 8px 24px rgba(139, 115, 85, 0.16);
li {
margin: 0;
padding: 8px 14px;
cursor: pointer;
border-radius: 6px;
transition: all 0.2s ease;
border-radius: 8px;
transition: all .4s ease-in-out;
i {
margin-right: 6px;
color: #7385a6;
color: #7a664e;
}
&:hover {
background: rgba(47, 107, 255, 0.08);
color: #2f6bff;
background: rgba(154, 92, 53, 0.10);
color: #9a5c35;
i {
color: #2f6bff;
color: #9a5c35;
}
}
}
+3 -4
View File
@@ -4,8 +4,8 @@
<sidebar v-if="!sidebar.hide" class="sidebar-container"/>
<div :class="{hasTagsView:needTagsView,sidebarHide:sidebar.hide}" class="main-container">
<div :class="{'fixed-header':fixedHeader}">
<!-- 标签页已并入 <navbar/>(原来「面包屑 + 标签页」两条栏重复显示当前位置) -->
<navbar/>
<tags-view v-if="needTagsView"/>
</div>
<div class="page-card">
<app-main/>
@@ -19,7 +19,7 @@
<script>
import RightPanel from '@/components/RightPanel'
import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components'
import { AppMain, Navbar, Settings, Sidebar } from './components'
import ResizeMixin from './mixin/ResizeHandler'
import { mapState } from 'vuex'
import variables from '@/assets/styles/variables.scss'
@@ -32,8 +32,7 @@ export default {
Navbar,
RightPanel,
Settings,
Sidebar,
TagsView
Sidebar
},
mixins: [ResizeMixin],
data() {
+1 -1
View File
@@ -73,7 +73,7 @@ export default {
lock: true,
text: content,
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.7)",
background: "rgba(79, 65, 48, 0.7)",
})
},
// 关闭遮罩层
+2 -1
View File
@@ -1,8 +1,9 @@
module.exports = {
/**
* 侧边栏主题 深色主题theme-dark,浅色主题theme-light
* Cottagecore 皮肤下侧栏是奶油浅色卡片,故用 theme-light(配合 variables.scss 的 $base-menu-light-*)
*/
sideTheme: 'theme-dark',
sideTheme: 'theme-light',
/**
* 是否系统布局配置
+1 -1
View File
@@ -5,7 +5,7 @@ const { sideTheme, showSettings, topNav, tagsView, fixedHeader, sidebarLogo, dyn
const storageSetting = JSON.parse(localStorage.getItem('layout-setting')) || ''
const state = {
title: '',
theme: storageSetting.theme || '#409EFF',
theme: storageSetting.theme || '#9a5c35',
sideTheme: storageSetting.sideTheme || sideTheme,
showSettings: showSettings,
topNav: storageSetting.topNav === undefined ? topNav : storageSetting.topNav,
+1 -1
View File
@@ -156,7 +156,7 @@ service.interceptors.response.use(res => {
// 通用下载方法
export function download(url, params, filename, config) {
downloadLoadingInstance = Loading.service({ text: "正在下载数据,请稍候", spinner: "el-icon-loading", background: "rgba(0, 0, 0, 0.7)", })
downloadLoadingInstance = Loading.service({ text: "正在下载数据,请稍候", spinner: "el-icon-loading", background: "rgba(79, 65, 48, 0.7)", })
return service.post(url, params, {
transformRequest: [(params) => { return tansParams(params) }],
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
@@ -1,281 +0,0 @@
<template>
<div class="app-container">
<el-row :gutter="10" style="margin-bottom: 16px;">
<el-select v-model="filter.boxType" placeholder="按分类筛选" clearable @change="getList" style="width:180px;margin-right:10px">
<el-option v-for="t in boxTypes" :key="t" :label="boxTypeLabels[t]" :value="t" />
</el-select>
<el-input v-model="filter.name" placeholder="按名称搜索" clearable @keyup.enter.native="getList" @clear="getList" style="width:200px">
<el-button slot="append" icon="el-icon-search" @click="getList" />
</el-input>
<el-button type="danger" icon="el-icon-refresh" @click="handleRecalc" :loading="recalcLoading" style="float:right">一键重算分布</el-button>
</el-row>
<el-table v-loading="loading" :data="boxList" border>
<el-table-column prop="box_id" label="ID" min-width="50" />
<el-table-column prop="name" label="名称" min-width="150" />
<el-table-column label="箱子类型" min-width="120">
<template slot-scope="scope">{{ boxTypeLabels[scope.row.box_type] || scope.row.box_type }}</template>
</el-table-column>
<el-table-column prop="price" label="价格(€)" min-width="90" />
<el-table-column prop="total_ornaments" label="饰品总数" min-width="90" />
<el-table-column prop="rtp" label="RTP" min-width="80">
<template slot-scope="scope">{{ (scope.row.rtp * 100).toFixed(1) }}%</template>
</el-table-column>
<el-table-column prop="status" label="状态" min-width="60">
<template slot-scope="scope">{{ scope.row.status == '1' ? '启用' : '禁用' }}</template>
</el-table-column>
<el-table-column label="操作" width="260">
<template slot-scope="scope">
<el-button size="mini" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="mini" type="info" @click="handleOrnaments(scope.row)">饰品配置</el-button>
<el-button size="mini" type="warning" @click="handleVersions(scope.row)">版本</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
style="margin-top:16px;text-align:right"
background
layout="total, prev, pager, next, sizes"
:total="total"
:page-size="filter.size"
:current-page="filter.page"
:page-sizes="[5,10,20,50]"
@size-change="s=>{filter.size=s;getList()}"
@current-change="p=>{filter.page=p;getList()}"
/>
<el-dialog title="编辑箱子" :visible.sync="dialogVisible" width="480px">
<el-form ref="form" :model="form" label-width="110px">
<el-form-item label="箱子类型" prop="boxType">
<el-select v-model="form.boxType" disabled>
<el-option v-for="t in boxTypes" :key="t" :label="boxTypeLabels[t]" :value="t" />
</el-select>
</el-form-item>
<el-form-item label="下注额" prop="bet">
<el-input-number v-model="form.bet" :disabled="true" />
</el-form-item>
<el-form-item label="价格 (自动)">
<el-input-number v-model="form.price" :disabled="true" />
</el-form-item>
<el-form-item label="饰品总数" prop="totalOrnaments">
<el-input-number v-model="form.totalOrnaments" :min="0" :step="50" />
</el-form-item>
<el-form-item label="RTP" prop="rtp">
<el-input-number v-model="form.rtp" :min="0" :max="1" :precision="3" :step="0.05" />
</el-form-item>
<el-form-item label="SCATTER权重" prop="scatterWeight" v-if="form.boxType === 'buy1_bonushunt'">
<el-input-number v-model="form.scatterWeight" :min="1" :max="20" :precision="1" :step="0.5" />
<span style="margin-left:8px;color:#999;font-size:12px">幸运加成, 默认2.0</span>
</el-form-item>
<el-form-item label="状态" prop="status" v-if="isEdit">
<el-radio-group v-model="form.status">
<el-radio label="1">启用</el-radio>
<el-radio label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</div>
</el-dialog>
<el-dialog :title="'饰品配置 - ' + (boxTypeLabels[currentBox.box_type] || currentBox.box_type) + ' (€' + currentBox.price + ')'":visible.sync="ornamentVisible" width="650px">
<el-alert type="info" :closable="false" style="margin-bottom:10px">
修改数量后保存即可。<br/>
当前总回拨: {{ ornamentTotal }} &nbsp; 计算RTP: {{ ornamentRTP }}% &nbsp; 目标RTP: {{ (currentBox.rtp * 100).toFixed(1) }}%
<br/>
未分配饰品: <span :style="{ color: ornamentRemaining !== 0 ? '#F56C6C' : '#67C23A', fontWeight: 'bold' }">{{ ornamentRemaining }}</span>
<span v-if="ornamentRemaining > 0" style="color:#909399"> (还需分配)</span>
<span v-else-if="ornamentRemaining === 0" style="color:#67C23A"> (已分配完毕)</span>
<span v-else style="color:#F56C6C"> (超出 {{ Math.abs(ornamentRemaining) }} 个)</span>
</el-alert>
<el-table :data="ornamentList" border>
<el-table-column label="饰品ID" min-width="120">
<template slot-scope="scope">
<span v-if="!scope.row.isNew">{{ getOrnamentName(scope.row.ornamentId) }}</span>
<el-select v-else v-model="scope.row.ornamentId" placeholder="选择饰品" size="small" style="width:130px">
<el-option v-for="o in availableOrnaments" :key="o.id" :label="o.label" :value="o.id" />
</el-select>
</template>
</el-table-column>
<el-table-column label="饰品名称" min-width="120">
<template slot-scope="scope">{{ getOrnamentName(scope.row.ornamentId) }}</template>
</el-table-column>
<el-table-column label="数量" min-width="120">
<template slot-scope="scope">
<el-input-number v-model="scope.row.count" :min="0" :step="10" size="small" controls-position="right" />
</template>
</el-table-column>
<el-table-column label="剩余回合" min-width="100" align="center">
<template slot-scope="scope">
<span :style="{ color: (scope.row.remaining || 0) < 100 ? '#F56C6C' : '#67C23A', fontWeight: 'bold' }">
{{ scope.row.remaining || 0 }}
</span>
</template>
</el-table-column>
</el-table>
<div slot="footer">
<el-button type="primary" @click="saveOrnaments">保存配置</el-button>
</div>
</el-dialog>
<el-dialog :title="'版本管理 - ' + (boxTypeLabels[currentBox.box_type] || currentBox.box_type) + ' (€' + currentBox.price + ')'" :visible.sync="versionVisible" width="650px">
<el-alert type="info" :closable="false" style="margin-bottom:10px">
当前活跃版本: <b>v{{ currentBox.active_version || 1 }}</b> &nbsp;|&nbsp;
SCATTER权重: <b>{{ currentBox.scatter_weight || '2.00' }}</b>
</el-alert>
<el-table :data="versionList" border>
<el-table-column label="版本" prop="version" width="80">
<template slot-scope="scope"><b>v{{ scope.row.version }}</b></template>
</el-table-column>
<el-table-column label="SCATTER权重" prop="scatterWeight" width="130" />
<el-table-column label="状态" width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 'active' ? 'success' : scope.row.status === 'draft' ? 'warning' : 'info'">
{{ scope.row.status === 'active' ? '当前' : scope.row.status === 'draft' ? '草稿' : '归档' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" prop="createTime" width="170" />
<el-table-column label="操作" width="140">
<template slot-scope="scope">
<el-button v-if="scope.row.status === 'draft'" size="mini" type="primary" @click="handlePublish(scope.row)">发布</el-button>
<el-button v-if="scope.row.status === 'archived'" size="mini" type="warning" @click="handleRollback(scope.row)">回退</el-button>
</template>
</el-table-column>
</el-table>
<div slot="footer">
<el-button @click="versionVisible = false">关闭</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { boxList, boxSave, boxOrnaments, saveOrnaments, recalcAll, getBoxVersions, publishVersion, rollbackVersion } from "@/api/game/bullet/box/api";
export default {
name: "BulletBox",
data() {
return {
loading: false, recalcLoading: false, boxList: [], total: 0,
filter: { page: 1, size: 10, boxType: "", name: "" },
dialogVisible: false, ornamentVisible: false, isEdit: false, currentBox: {},
form: { boxType: "", bet: 1, price: 1, totalOrnaments: 500, rtp: 0.8, scatterWeight: 2.0 },
boxTypes: ["normal", "buy1_bonushunt", "buy2_bad_blood", "buy3_draw_or_die", "buy4_true_grit", "buy5_four_shots", "buy6_go_ahead"],
boxTypeLabels: { "normal":"普通", "buy1_bonushunt":"幸运加成", "buy2_bad_blood":"匪徒高发", "buy3_draw_or_die":"赏金频现", "buy4_true_grit":"幸运常驻", "buy5_four_shots":"欧皇旅途", "buy6_go_ahead":"终极之路" },
boxTypeCM: { "normal":1, "buy1_bonushunt":5, "buy2_bad_blood":60, "buy3_draw_or_die":60, "buy4_true_grit":80, "buy5_four_shots":200, "buy6_go_ahead":400 },
ornamentList: [], _ornamentAll: [], _ornamentNames: {},
versionVisible: false, versionList: [], activeVersion: null, pollTimer: null
};
},
created() { this.getList(); },
computed: {
availableOrnaments() {
const usedIds = this.ornamentList.filter(o => o.ornamentId > 0).map(o => o.ornamentId);
return this._ornamentAll.filter(o => !usedIds.includes(o.id));
},
ornamentTotal() {
return this.ornamentList.reduce((s, o) => s + (o.count || 0), 0);
},
ornamentRemaining() {
const total = this.currentBox.total_ornaments || 0;
return total - this.ornamentTotal;
},
ornamentRTP() {
const total = this.ornamentTotal;
if (!total || !this.currentBox.bet) return '0.0';
let contrib = 0;
this.ornamentList.forEach(o => {
const mul = this._ornamentExpects && this._ornamentExpects[o.ornamentId];
if (mul != null) contrib += (o.count || 0) * mul;
});
const cm = this.boxTypeCM[this.currentBox.box_type] || 1;
return (contrib / total / cm * 100).toFixed(1);
}
},
watch: {
ornamentVisible(val) {
if (val) this.startPolling();
else this.stopPolling();
}
},
beforeDestroy() { this.stopPolling(); },
methods: {
handleRecalc() {
this.$modal.confirm('确认重算所有箱子的饰品分布并重置Redis预热回合吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning', closeOnClickModal: false }).then(() => {
this.recalcLoading = true;
recalcAll().then(() => { this.$message.success('重算完成'); this.getList(); }).finally(() => { this.recalcLoading = false; });
}).catch(() => {});
return;
},
_handleRecalcOld() {
this.$modal.confirm('确认重算所有箱子的饰品分布并重置Redis预热回合吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
.then(() => {
this.recalcLoading = true;
recalcAll().then(() => { this.$message.success('重算完成'); this.getList(); }).finally(() => { this.recalcLoading = false; });
}).catch(() => {});
},
getList() {
this.loading = true;
boxList({ page: this.filter.page, size: this.filter.size, boxType: this.filter.boxType || undefined, name: this.filter.name || undefined })
.then(r => { this.boxList = r.data.rows || []; this.total = r.data.total || 0; this.loading = false; });
},
calcPrice() {
const cm = this.boxTypeCM[this.form.boxType] || 1;
this.form.price = +(this.form.bet || 0) * cm;
},
handleEdit(row) { this.isEdit = true; this.form = { boxId: row.box_id, boxType: row.box_type, bet: row.bet, price: row.price, totalOrnaments: row.total_ornaments, rtp: row.rtp, scatterWeight: row.scatter_weight != null ? row.scatter_weight : 2.0, gameType: "bullet", status: row.status }; this.dialogVisible = true; },
submitForm() {
const data = { gameType: "bullet", ...this.form }; if (this.isEdit) data.boxId = this.form.boxId;
boxSave(data).then(r => { if (r.code !== 200) { this.$modal.msgError(r.msg); return; } this.$message.success("保存成功"); this.dialogVisible = false; this.getList(); });
},
handleOrnaments(row) { this.currentBox = row; boxOrnaments(row.box_id).then(r => { this.ornamentList = (r.data || []).map(o => ({ ornamentId: Number(o.ornamentId), count: o.count, remaining: o.remaining || 0, isNew: false })); this.loadOrnamentNames().then(() => { this.ornamentList = [...this.ornamentList]; this.ornamentVisible = true; }); }); },
loadOrnamentNames() { return import("@/api/game/bullet/ornament/api").then(api => api.ornamentList().then(r => { this._ornamentNames = {}; this._ornamentExpects = {}; this._ornamentAll = []; (r.data || []).forEach(o => { this._ornamentNames[o.id] = o.expect_mul + '倍'; this._ornamentExpects[o.id] = Number(o.expect_mul); this._ornamentAll.push({id: o.id, label: 'ID' + o.id + ' ' + o.expect_mul + '倍'}); }); })); },
getOrnamentName(id) { if (!id || id <= 0) return '-'; return (this._ornamentNames && this._ornamentNames[id]) || ('ID:' + id); },
addOrnamentRow() { this.ornamentList.push({ ornamentId: null, count: 0, isNew: true }); },
startPolling() { this.stopPolling(); this.pollRemaining(); this.pollTimer = setInterval(() => { this.pollRemaining(); }, 3000); },
stopPolling() { if (this.pollTimer) { clearInterval(this.pollTimer); this.pollTimer = null; } },
pollRemaining() {
if (!this.currentBox || !this.currentBox.box_id) return;
boxOrnaments(this.currentBox.box_id).then(r => {
const latest = r.data || [];
this.ornamentList.forEach(row => {
const match = latest.find(o => Number(o.ornamentId) === row.ornamentId);
if (match) row.remaining = match.remaining || 0;
});
}).catch(() => {});
},
saveOrnaments() { const data = this.ornamentList.filter(o => o.ornamentId > 0).map(o => ({ ornamentId: Number(o.ornamentId), count: o.count })); if (data.length === 0) { this.$message.warning("请至少添加一个饰品"); return; } saveOrnaments(this.currentBox.box_id, data).then(() => { this.$message.success("保存成功"); this.ornamentVisible = false; this.getList(); }); },
handleVersions(row) {
this.currentBox = row;
getBoxVersions(row.box_id).then(r => {
this.versionList = r.data || [];
this.versionVisible = true;
});
},
handlePublish(row) {
this.$modal.confirm(`确认发布 v${row.version} 吗?发布后玩家将使用该版本数据。`, '提示', { confirmButtonText: '确定', type: 'warning' })
.then(() => {
publishVersion(this.currentBox.box_id, row.version).then(() => {
this.$message.success('发布成功');
this.versionVisible = false;
this.getList();
});
}).catch(() => {});
},
handleRollback(row) {
this.$modal.confirm(`确认回退到 v${row.version} 吗?当前版本将变为归档。`, '提示', { confirmButtonText: '确定', type: 'warning' })
.then(() => {
rollbackVersion(this.currentBox.box_id, row.version).then(() => {
this.$message.success('回退成功');
this.versionVisible = false;
this.getList();
});
}).catch(() => {});
}
}
};
</script>
@@ -1,61 +0,0 @@
<template>
<div class="app-container">
<el-row :gutter="16" style="margin-bottom:16px">
<el-col :span="6"><el-card shadow="hover"><div style="text-align:center"><small>累计投注</small><h3>{{ summary.totalBet }}</h3></div></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><div style="text-align:center"><small>累计赢奖</small><h3>{{ summary.totalWin }}</h3></div></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><div style="text-align:center"><small>整体RTP</small><h3 :style="{color: summary.rtp > 100 ? 'red' : 'green'}">{{ summary.rtp }}%</h3></div></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><div style="text-align:center"><small>总局数</small><h3>{{ summary.totalCount }}</h3></div></el-card></el-col>
</el-row>
<div style="margin-bottom: 16px;">
<el-date-picker v-model="queryParams.dateStart" type="date" placeholder="开始日期" value-format="yyyy-MM-dd" style="width:150px;margin-right:8px" />
<span style="margin:0 4px">至</span>
<el-date-picker v-model="queryParams.dateEnd" type="date" placeholder="结束日期" value-format="yyyy-MM-dd" style="width:150px;margin-right:8px" />
<el-button type="primary" icon="el-icon-search" @click="getList">查询</el-button>
<el-button icon="el-icon-refresh" @click="handleReset">重置</el-button>
</div>
<el-table v-loading="loading" :data="winList" border>
<el-table-column label="日期" min-width="120">
<template slot-scope="scope">{{ formatDate(scope.row.date) }}</template>
</el-table-column>
<el-table-column prop="total_bet" label="总投注" min-width="130" />
<el-table-column prop="total_win" label="总赢奖" min-width="130" />
<el-table-column prop="count" label="总局数" min-width="80" />
<el-table-column prop="bonus_count" label="Bonus购买" min-width="100" />
<el-table-column label="RTP" min-width="90">
<template slot-scope="scope"><span :style="{color: scope.row.total_bet > 0 && scope.row.total_win/scope.row.total_bet > 1 ? 'red' : 'green'}">{{ scope.row.total_bet > 0 ? (scope.row.total_win / scope.row.total_bet * 100).toFixed(1) + '%' : '-' }}</span></template>
</el-table-column>
</el-table>
<pagination v-show="total>0" :total="total" :page.sync="queryParams.page" :limit.sync="queryParams.size" @pagination="getList" />
</div>
</template>
<script>
import { winStats } from "@/api/game/bullet/dailywin/api";
import { parseTime } from "@/utils/ruoyi";
export default {
name: "BulletDailyWin",
data() { return { loading: false, winList: [], total: 0, summary: { totalBet: "0", totalWin: "0", totalCount: 0, rtp: "0.0" }, queryParams: { page: 1, size: 10, dateStart: null, dateEnd: null } }; },
created() { this.getList(); },
methods: {
getList() {
this.loading = true;
winStats({ page: this.queryParams.page, size: this.queryParams.size, dateStart: this.queryParams.dateStart, dateEnd: this.queryParams.dateEnd }).then(r => {
const d = r.data;
this.winList = (d && d.rows) ? d.rows : (d || []);
this.total = (d && d.total) || 0;
if (d) {
this.summary = {
totalBet: Number(d.totalBet || 0).toFixed(2),
totalWin: Number(d.totalWin || 0).toFixed(2),
totalCount: Number(d.totalCount || 0),
rtp: Number(d.totalBet) > 0 ? (Number(d.totalWin) / Number(d.totalBet) * 100).toFixed(1) : "0.0"
};
}
this.loading = false;
});
},
handleReset() { this.queryParams = { page: 1, size: 10, dateStart: null, dateEnd: null }; this.getList(); },
formatDate(v) { if (!v) return ''; const d = new Date(v); return d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0'); }
}
};
</script>
@@ -1,23 +0,0 @@
<template>
<div class="app-container">
<el-alert type="info" :closable="false" style="margin-bottom:10px">
饰品即倍率桶,由 Python 模拟器生成。每个桶的 expect_mul 对应一个预生成的回合数据池。
</el-alert>
<el-table v-loading="loading" :data="list" border>
<el-table-column prop="id" label="ID" min-width="60" />
<el-table-column prop="name" label="名称" min-width="150" />
<el-table-column prop="expect_mul" label="期望倍率" min-width="100" />
<el-table-column prop="game_type" label="游戏" min-width="80" />
</el-table>
</div>
</template>
<script>
import { ornamentList } from "@/api/game/bullet/ornament/api";
export default {
name: "BulletOrnament",
data() { return { loading: false, list: [] }; },
created() { this.getList(); },
methods: { getList() { this.loading = true; ornamentList().then(r => { this.list = r.data || []; this.loading = false; }); } }
};
</script>
@@ -1,74 +0,0 @@
<template>
<div class="app-container">
<el-table v-loading="loading" :data="list" border>
<el-table-column prop="id" label="ID" min-width="100" />
<el-table-column prop="name" label="名称" min-width="400" show-overflow-tooltip align="center" />
<el-table-column prop="grade" label="品阶" min-width="100" align="center">
<template slot-scope="scope">
<el-tag v-if="scope.row.grade === 1" type="warning" size="small">金</el-tag>
<el-tag v-else-if="scope.row.grade === 2" type="danger" size="small">紫</el-tag>
<el-tag v-else-if="scope.row.grade === 3" size="small">红</el-tag>
<el-tag v-else-if="scope.row.grade === 4" type="success" size="small">绿</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="饰品图片" min-width="250" align="center">
<template slot-scope="scope">
<el-image v-if="scope.row.image_url" :src="getImageUrl(scope.row.image_url)" style="width:48px;height:48px" fit="cover" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="exterior_name" label="品质" min-width="120" align="center" />
<el-table-column label="等阶图片" min-width="250" align="center">
<template slot-scope="scope">
<el-image v-if="scope.row.level_image_url" :src="getImageUrl(scope.row.level_image_url)" style="width:48px;height:48px" fit="cover" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="create_time" label="创建时间" min-width="250" />
<el-table-column label="操作" min-width="100">
<template slot-scope="scope">
<el-button size="mini" @click="handleEdit(scope.row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog title="编辑奖品" :visible.sync="dialogVisible" min-width="480px">
<el-form ref="form" :model="form" label-min-width="80px">
<el-form-item label="名称"><el-input v-model="form.name" /></el-form-item>
<el-form-item label="品阶">
<el-select v-model="form.grade" style="width:100%">
<el-option label="无品阶" :value="null" />
<el-option label="1 - 金品" :value="1" />
<el-option label="2 - 紫品" :value="2" />
<el-option label="3 - 红品" :value="3" />
<el-option label="4 - 绿品" :value="4" />
</el-select>
</el-form-item>
<el-form-item label="品质"><el-input v-model="form.exterior_name" placeholder="崭新出厂/战痕累累等" /></el-form-item>
<el-form-item label="图片URL"><el-input v-model="form.image_url" /></el-form-item>
</el-form>
<div slot="footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { prizeList, prizeSave } from "@/api/game/bullet/prize/api";
export default {
name: "BulletPrize",
data() {
return { loading: false, list: [], dialogVisible: false, form: {} };
},
created() { this.getList(); },
methods: {
getList() { this.loading = true; prizeList().then(r => { this.list = r.data || []; this.loading = false; }); },
getImageUrl(url) { if (!url) return ''; return url.startsWith('http') ? url : (process.env.VUE_APP_BASE_API || '') + url; },
handleEdit(row) { this.form = { ...row }; this.dialogVisible = true; },
submitForm() { prizeSave(this.form).then(() => { this.$message.success("保存成功"); this.dialogVisible = false; this.getList(); }); }
}
};
</script>
@@ -1,148 +0,0 @@
<template>
<div class="app-container">
<!-- 汇总卡片 -->
<el-row :gutter="16" style="margin-bottom:16px">
<el-col :span="6">
<el-card shadow="hover"><div style="text-align:center"><small>总投注</small><h3>{{ summary.totalBet }}</h3></div></el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover"><div style="text-align:center"><small>总赢奖</small><h3>{{ summary.totalWin }}</h3></div></el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover"><div style="text-align:center"><small>总局数</small><h3>{{ summary.totalCount }}</h3></div></el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover"><div style="text-align:center"><small>RTP</small><h3 :style="{color: summary.rtp > 100 ? 'red' : 'green'}">{{ summary.rtp }}%</h3></div></el-card>
</el-col>
</el-row>
<el-form :inline="true" :model="queryParams">
<el-form-item label="用户ID">
<el-input v-model="queryParams.userId" placeholder="用户ID" clearable style="width:120px" />
</el-form-item>
<el-form-item label="箱子类型">
<el-select v-model="queryParams.boxType" placeholder="全部" clearable style="width:180px">
<el-option v-for="t in boxTypes" :key="t" :label="boxTypeLabels[t] || t" :value="t" />
</el-select>
</el-form-item>
<el-form-item label="日期范围">
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始" end-placeholder="结束" value-format="yyyy-MM-dd" style="width:260px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getList">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
<el-button type="danger" plain @click="handleClear">清空所有数据</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="recordList" border>
<el-table-column prop="user_id" label="用户ID" min-width="80" />
<el-table-column prop="nick_name" label="昵称" min-width="110" />
<el-table-column prop="bet" label="投注" min-width="90" />
<el-table-column label="赢奖" min-width="90">
<template slot-scope="scope">
<span :style="{color: Number(scope.row.win) > Number(scope.row.bet) ? 'green' : (Number(scope.row.win) > 0 ? '#333' : '#999')}">
{{ scope.row.win }}
</span>
</template>
</el-table-column>
<el-table-column label="倍率" min-width="70">
<template slot-scope="scope">{{ scope.row.multiplier }}x</template>
</el-table-column>
<el-table-column prop="box_type" label="箱子类型" min-width="150">
<template slot-scope="scope">{{ boxTypeLabels[scope.row.box_type] || scope.row.box_type }}</template>
</el-table-column>
<el-table-column prop="ornament_name" label="饰品名称" min-width="120">
<template slot-scope="scope">
<span>{{ scope.row.ornament_name || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="饰品图片" min-width="80">
<template slot-scope="scope">
<el-image v-if="scope.row.image_url" :src="getImageUrl(scope.row.image_url)" style="width:40px;height:40px" fit="cover" :preview-src-list="[getImageUrl(scope.row.image_url)]" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" min-width="70">
<template slot-scope="scope">
<el-tag :type="scope.row.status == '1' ? 'success' : 'info'" size="small">{{ scope.row.status == '1' ? '完成' : '进行' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="create_time" label="时间" min-width="150" />
<el-table-column label="操作" min-width="60">
<template slot-scope="scope">
<el-button size="mini" type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.page" :limit.sync="queryParams.size" @pagination="getList" />
</div>
</template>
<script>
import { recordList, deleteRecord, clearRecords } from "@/api/game/bullet/record/api";
export default {
name: "BulletRecord",
data() {
return {
loading: false,
recordList: [],
total: 0,
dateRange: [],
summary: { totalBet: 0, totalWin: 0, totalCount: 0, rtp: 0 },
boxTypes: ["normal", "buy1_bonushunt", "buy2_bad_blood", "buy3_draw_or_die", "buy4_true_grit", "buy5_four_shots", "buy6_go_ahead"],
boxTypeLabels: { "normal":"普通", "buy1_bonushunt":"幸运加成", "buy2_bad_blood":"匪徒高发", "buy3_draw_or_die":"赏金频现", "buy4_true_grit":"幸运常驻", "buy5_four_shots":"欧皇旅途", "buy6_go_ahead":"终极之路" },
queryParams: { page: 1, size: 10, userId: "", boxType: "" }
};
},
created() { this.getList(); },
methods: {
getImageUrl(url) {
if (!url) return '';
if (url.startsWith('http')) return url;
return (process.env.VUE_APP_BASE_API || '') + url;
},
getList() {
this.loading = true;
const params = { page: this.queryParams.page, size: this.queryParams.size };
if (this.queryParams.userId) params.userId = this.queryParams.userId;
if (this.queryParams.boxType) params.boxType = this.queryParams.boxType;
recordList(params).then(r => {
const d = r.data;
const rows = d.rows || d || [];
this.total = d.total || 0;
this.summary = {
totalBet: Number(d.totalBet || 0).toFixed(2),
totalWin: Number(d.totalWin || 0).toFixed(2),
totalCount: Number(d.totalCount || 0),
rtp: Number(d.totalBet) > 0 ? (Number(d.totalWin) / Number(d.totalBet) * 100).toFixed(1) : "0.0"
};
if (this.dateRange && this.dateRange.length === 2) {
const [start, end] = this.dateRange;
this.recordList = rows.filter(r2 => r2.create_time && r2.create_time >= start && r2.create_time <= end + " 23:59:59");
} else {
this.recordList = rows;
}
this.loading = false;
});
},
resetQuery() {
this.queryParams = { page: 1, size: 10, userId: "", boxType: "" };
this.dateRange = [];
this.getList();
},
handleDelete(row) {
this.$confirm(`确定删除该记录吗?`, "警告", { type: "warning" }).then(() => {
deleteRecord(row.id).then(() => { this.$message.success("删除成功"); this.getList(); });
});
},
handleClear() {
this.$confirm("确定清空所有玩家记录吗?此操作不可恢复!", "严重警告", { type: "warning", confirmButtonText: "确定清空", confirmButtonClass: "el-button--danger" }).then(() => {
clearRecords().then(() => { this.$message.success("已清空全部记录"); this.getList(); });
});
}
}
};
</script>
@@ -1,80 +0,0 @@
<template>
<div class="app-container">
<el-alert type="warning" :closable="false" style="margin-bottom:10px">
删除回合会级联删除关联的 game_step。回合由 Python 模拟器生成,删除后需重新运行模拟器补充。
为保证不误删已去掉删除功能,可执行 Python 模拟器进行覆盖。
</el-alert>
<el-row :gutter="12" style="margin-bottom:16px">
<el-col :span="4"><el-card shadow="hover"><div style="text-align:center"><small>总回合</small><h3>{{ stats.total }}</h3></div></el-card></el-col>
<el-col :span="4" v-for="s in stats.breakdown" :key="s.key"><el-card shadow="hover"><div style="text-align:center"><small>{{ typeLabels[s.key] || s.key }}</small><h3>{{ s.count }}</h3></div></el-card></el-col>
</el-row>
<el-form :inline="true" :model="queryParams">
<el-form-item label="回合ID"><el-input-number v-model="queryParams.roundId" :min="0" style="width:130px" /></el-form-item>
<el-form-item label="Bonus类型">
<el-select v-model="queryParams.bonusType" style="width:150px">
<el-option v-for="b in bonusTypes" :key="b" :label="bonusLabels[b] || b" :value="b" />
</el-select>
</el-form-item>
<el-form-item><el-button type="primary" icon="el-icon-search" @click="getList">查询</el-button></el-form-item>
</el-form>
<el-table v-loading="loading" :data="roundList" border>
<el-table-column type="expand">
<template slot-scope="scope"><div style="padding:10px 20px"><p> ornament={{ scope.row.ornament_id }} | bonus_type={{ scope.row.bonus_type || '普通' }} | scatters={{ scope.row.scatters }} | 总步数={{ scope.row.total_steps }} | gamble={{ scope.row.gamble_result || '无' }} | actual_mul={{ scope.row.actual_mul }}</p></div></template>
</el-table-column>
<el-table-column prop="round_id" label="ID" min-width="70" />
<el-table-column prop="name" label="饰品" min-width="100" />
<el-table-column prop="expect_mul" label="期望倍率" min-width="80" />
<el-table-column prop="actual_mul" label="实际倍率" min-width="80" />
<el-table-column label="偏差" min-width="70">
<template slot-scope="scope">
<span :style="{color: Math.abs(scope.row.actual_mul - scope.row.expect_mul) > scope.row.expect_mul * 0.5 ? 'red' : '#333'}">
{{ scope.row.expect_mul > 0 ? ((scope.row.actual_mul / scope.row.expect_mul * 100 - 100).toFixed(0) + '%') : '-' }}
</span>
</template>
</el-table-column>
<el-table-column prop="box_type" label="Bonus" min-width="130"><template slot-scope="scope">{{ typeLabels[scope.row.box_type] || scope.row.box_type || '普通' }}</template></el-table-column>
<el-table-column prop="scatters" label="FS" min-width="50" />
<el-table-column prop="total_steps" label="步数" min-width="50" />
<el-table-column prop="is_buy" label="模式" min-width="60"><template slot-scope="scope"><el-tag :type="scope.row.is_buy ? 'warning' : 'info'" size="small">{{ scope.row.is_buy ? '购买' : '普通' }}</el-tag></template></el-table-column>
<el-table-column prop="create_time" label="创建时间" min-width="160" />
</el-table>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.page" :limit.sync="queryParams.size" @pagination="getList" />
</div>
</template>
<script>
import { roundList } from "@/api/game/bullet/round/api";
export default {
name: "BulletRound",
data() { return {
loading: false, roundList: [], total: 0,
stats: { total: 0, breakdown: [] },
bonusTypes: ["ALL","normal","buy1_bonushunt","buy2_bad_blood","buy3_draw_or_die","buy4_true_grit","buy5_four_shots","buy6_go_ahead"],
bonusLabels: { "ALL":"全部", "normal":"普通", "buy1_bonushunt":"幸运加成", "buy2_bad_blood":"匪徒高发", "buy3_draw_or_die":"赏金频现", "buy4_true_grit":"幸运常驻", "buy5_four_shots":"欧皇旅途", "buy6_go_ahead":"终极之路" },
typeLabels: { "normal":"普通", "buy1_bonushunt":"幸运加成", "buy2_bad_blood":"匪徒高发", "buy3_draw_or_die":"赏金频现", "buy4_true_grit":"幸运常驻", "buy5_four_shots":"欧皇旅途", "buy6_go_ahead":"终极之路", "BONUSHUNT":"幸运加成", "BAD_BLOOD":"匪徒高发", "DRAW_OR_DIE":"赏金频现", "TRUE_GRIT":"幸运常驻", "FOUR_SHOTS":"欧皇旅途", "GO_AHEAD":"终极之路" },
queryParams: { page: 1, size: 20, roundId: 0, bonusType: "ALL" }
}; },
created() { this.getList(); },
methods: {
getList() {
this.loading = true;
const params = { page: this.queryParams.page, size: this.queryParams.size };
if (this.queryParams.roundId > 0) params.roundId = this.queryParams.roundId;
if (this.queryParams.bonusType) params.bonusType = this.queryParams.bonusType;
roundList(params).then(r => {
const d = r.data;
this.roundList = (d && d.rows) ? d.rows : (d || []);
this.total = (d && d.total) || 0;
if (d && d.stats) {
this.stats = { total: this.total, breakdown: d.stats.map(s => ({ key: s.name, count: s.cnt })) };
}
this.loading = false;
});
}
}
};
</script>
@@ -1,451 +0,0 @@
<template>
<div class="app-container">
<el-row :gutter="10" style="margin-bottom:16px">
<el-select v-model="queryParams.boxType" placeholder="按模式筛选" clearable style="width:180px;margin-right:10px" @change="loadData">
<el-option v-for="t in boxTypes" :key="t" :label="boxTypeLabels[t]" :value="t" />
</el-select>
<el-input v-model="queryParams.name" placeholder="按名称搜索" clearable style="width:200px" @clear="loadData" @keyup.enter.native="loadData">
<el-button slot="append" icon="el-icon-search" @click="loadData" />
</el-input>
<span style="margin-left:10px;margin-right:4px;color:#606266">身份:</span>
<el-radio-group v-model="audienceFilter" size="small" @change="loadData">
<el-radio-button v-for="a in audiences" :key="a.key" :label="a.key">{{ a.name }}</el-radio-button>
</el-radio-group>
<el-button icon="el-icon-refresh" style="margin-left:10px" @click="loadData">刷新</el-button>
<el-button type="danger" icon="el-icon-refresh" :loading="recalcLoading" style="float:right" @click="openRecalcDialog">一键重算分布</el-button>
</el-row>
<el-table v-loading="loading" :data="boxList" border>
<el-table-column prop="boxId" label="ID" min-width="50" align="center" />
<el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
<el-table-column label="模式" min-width="110" align="center">
<template slot-scope="scope">{{ boxTypeLabels[scope.row.boxType] || scope.row.boxType }}</template>
</el-table-column>
<el-table-column prop="bet" label="赌注" min-width="70" align="center" />
<el-table-column prop="price" label="价格" min-width="80" align="center" />
<el-table-column label="总回合" min-width="80" align="center">
<template slot-scope="scope">
<span v-if="totalRoundsOf(scope.row) != null">{{ totalRoundsOf(scope.row) }}</span>
<span v-else style="color:#c0c4cc">-</span>
</template>
</el-table-column>
<el-table-column label="目标RTP" min-width="80" align="center">
<template slot-scope="scope">
<!-- 显示 1 位小数:配比是"整数条回合 × 桶内实际均值"分出来的,颗粒度(≈0.5 倍)比 2 位小数还粗,
真实值只会在 79.97~80.03 之间抖;精确值留在 title 里,鼠标悬停可看 -->
<span v-if="targetRtp(scope.row) != null" :title="targetRtp(scope.row) + '%'">{{ Number(targetRtp(scope.row)).toFixed(1) + '%' }}</span>
<span v-else style="color:#c0c4cc">-</span>
</template>
</el-table-column>
<el-table-column label="实时RTP" min-width="80" align="center">
<template slot-scope="scope">
<span v-if="actualRtp(scope.row) != null" :style="{color: actualRtp(scope.row) > (targetRtp(scope.row) != null ? targetRtp(scope.row) : 0) ? '#f56c6c' : '#67c23a'}">{{ actualRtp(scope.row) }}%</span>
<span v-else style="color:#c0c4cc">-</span>
</template>
</el-table-column>
<el-table-column label="状态" min-width="60" align="center">
<template slot-scope="scope">
<el-tag :type="scope.row.status === '1' ? 'success' : 'danger'" size="small">{{ scope.row.status === '1' ? '启用' : '禁用' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" min-width="270" align="center">
<template slot-scope="scope">
<el-button size="mini" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="mini" type="info" @click="handleOrnaments(scope.row)">饰品配置</el-button>
<el-button size="mini" type="success" @click="openTemplateManager(scope.row)">模板管理</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
style="margin-top:16px;text-align:right"
background
layout="total, prev, pager, next, sizes"
:total="total"
:page-size.sync="queryParams.pageSize"
:current-page.sync="queryParams.pageNum"
:page-sizes="[10,20,50,100]"
@size-change="loadData"
@current-change="loadData"
/>
<!-- 编辑弹窗 -->
<el-dialog :title="'编辑箱子 - ' + (form.name || '')" :visible.sync="dialogVisible" width="480px" append-to-body> <el-form ref="form" :model="form" label-width="110px">
<el-form-item label="名称"><el-input v-model="form.name" disabled /></el-form-item>
<el-form-item label="模式类型"><el-input :value="boxTypeLabels[form.boxType] || form.boxType" disabled /></el-form-item>
<el-form-item label="基本赌注"><el-input-number v-model="form.bet" disabled style="width:100%" /></el-form-item>
<el-form-item label="价格(自动)">
<el-input-number v-model="form.price" disabled style="width:100%" />
<span style="color:#909399;font-size:12px">价格 = 赌注 × 倍率系数({{ boxTypeCM[form.boxType] }}x)</span>
</el-form-item>
<el-form-item label="总回合数">
<el-input-number v-model="form.totalRounds" :min="0" :step="50" style="width:100%" />
<span style="color:#909399;font-size:12px">身份级参数,仅作用于身份【{{ audienceLabel(audienceFilter) }}】</span>
</el-form-item>
<el-form-item label="目标RTP">
<el-input-number v-model="form.rtp" :min="0" :max="1" :precision="3" :step="0.05" style="width:100%" />
<span style="color:#909399;font-size:12px">身份级参数,仅作用于身份【{{ audienceLabel(audienceFilter) }}】</span>
</el-form-item>
<el-form-item label="固定/收敛回合%">
<el-input-number v-model="form.fixedPct" :min="0" :max="100" :step="5" style="width:38%" @change="onFixedPctChange" />
<span style="margin-left:4px">%</span>
<span style="margin:0 4%">/</span>
<el-input-number v-model="form.convergePct" :min="0" :max="100" :step="5" style="width:38%" @change="onConvergePctChange" />
<span style="margin-left:4px">%</span>
<div style="color:#909399;font-size:12px">固定% + 收敛% = 100;收敛回合用于一轮内把实时RTP动态拉回目标,随保存作用于当前身份</div>
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio label="1">启用</el-radio>
<el-radio label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" :loading="saveLoading" @click="submitForm">确 定</el-button>
</div>
</el-dialog>
<!-- 重算分布弹窗 -->
<el-dialog title="重算分布" :visible.sync="recalcDialogVisible" width="480px" append-to-body>
<el-form label-width="110px">
<el-form-item label="身份范围">
<el-radio-group v-model="recalcForm.onlyCurrent">
<el-radio :label="false">全部身份</el-radio>
<el-radio :label="true">仅当前身份【{{ audienceLabel(audienceFilter) }}】</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="模式范围">
<el-select v-model="recalcForm.boxType" clearable placeholder="全部模式" style="width:100%">
<el-option v-for="t in boxTypes" :key="t" :label="boxTypeLabels[t]" :value="t" />
</el-select>
</el-form-item>
<el-form-item label="目标RTP">
<el-input-number v-model="recalcForm.rtp" :min="0" :max="1" :precision="3" :step="0.05" style="width:100%" />
</el-form-item>
<el-form-item label="总回合">
<el-input-number v-model="recalcForm.totalRounds" :min="50" :step="50" style="width:100%" />
</el-form-item>
<el-form-item label="固定/收敛回合%">
<el-input-number v-model="recalcForm.fixedPct" :min="0" :max="100" :step="5" style="width:40%" @change="onRecalcFixedPctChange" />
<span style="margin-left:4px">%</span>
<span style="margin:0 4%">/</span>
<el-input-number v-model="recalcForm.convergePct" :min="0" :max="100" :step="5" style="width:40%" @change="onRecalcConvergePctChange" />
<span style="margin-left:4px">%</span>
<div style="color:#909399;font-size:12px">固定% + 收敛% = 100;收敛回合用于一轮内把实时RTP动态拉回目标</div>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="recalcDialogVisible = false">取 消</el-button>
<el-button type="primary" :loading="recalcLoading" @click="submitRecalc">确认重算</el-button>
</div>
</el-dialog>
<!-- 饰品配置弹窗(跟随列表页当前身份) -->
<el-dialog :title="'饰品配置 - ' + (boxTypeLabels[currentBox.boxType] || currentBox.boxType) + ' (ID:' + currentBox.boxId + ')【' + audienceLabel(audience) + '】'" :visible.sync="ornamentVisible" width="720px" append-to-body>
<div style="display:flex;align-items:center;margin-bottom:10px">
<el-alert type="info" :closable="false" style="flex:1">
身份【{{ audienceLabel(audience) }}】 计算回合(固定): <b>{{ ornamentTotal }}</b> &nbsp;|&nbsp;
收敛回合: <b>{{ convergeRoundsDerived }}</b> &nbsp;|&nbsp; 总回合: <b>{{ totalRoundsDerived }}</b> &nbsp;|&nbsp;
计算RTP(池内实际均值): <b>{{ ornamentRTP }}%</b> &nbsp;|&nbsp; 目标RTP: <b>{{ currentOrnamentTargetRtp }}%</b>
</el-alert>
<el-button type="warning" size="small" icon="el-icon-refresh" style="margin-left:10px;flex-shrink:0" @click="handleOrnamentRecalc">重算分布</el-button>
</div>
<el-table v-loading="ornamentLoading" :data="ornamentList" border max-height="420">
<el-table-column prop="id" label="桶ID" width="60" align="center" />
<el-table-column label="饰品" min-width="110" align="center">
<template slot-scope="scope">{{ getOrnName(scope.row.name) }}</template>
</el-table-column>
<el-table-column label="倍率范围" min-width="120" align="center">
<template slot-scope="scope">{{ scope.row.rangeLow }} - {{ scope.row.rangeHigh }}</template>
</el-table-column>
<el-table-column label="回合数" min-width="140" align="center">
<template slot-scope="scope">
<el-input-number v-model="scope.row.count" :min="0" :step="10" size="small" controls-position="right" />
</template>
</el-table-column>
<el-table-column label="剩余" min-width="70" align="center">
<template slot-scope="scope">
<span :style="{color: (scope.row.remaining != null ? scope.row.remaining : scope.row.count) <= 0 ? '#f56c6c' : '#67c23a'}">{{ scope.row.remaining != null ? scope.row.remaining : scope.row.count }}</span>
</template>
</el-table-column>
<el-table-column label="动态收敛回合" min-width="100" align="center">
<template slot-scope="scope"><span style="color:#e6a23c">{{ scope.row.convergeCount || 0 }}</span></template>
</el-table-column>
</el-table>
<div slot="footer">
<el-button @click="ornamentVisible = false">取 消</el-button>
<el-button type="success" icon="el-icon-document-add" @click="handleSaveAsTemplate">保存为模板</el-button>
<el-button type="primary" @click="saveOrnaments">保存</el-button>
</div>
</el-dialog>
<!-- 模板管理弹窗(跟随列表页当前身份) -->
<el-dialog :title="'模板管理 - ' + (boxTypeLabels[templateBox.boxType] || templateBox.boxType || '') + ' (ID:' + templateBox.boxId + ')【' + audienceLabel(templateAudience) + '】'" :visible.sync="templateDialogVisible" width="720px" append-to-body>
<el-alert type="info" :closable="false" style="margin-bottom:12px">
当前为身份【{{ audienceLabel(templateAudience) }}】的模板。模板保存了该箱子该身份的饰品回合分布。选择后点【应用】可将该身份饰品数量恢复为模板值,并设为抽干补桶依据。
</el-alert>
<el-table v-if="boxTemplates.length > 0" :data="boxTemplates" border highlight-current-row @current-change="onTemplateRowChange">
<el-table-column label="模板名称" min-width="150">
<template slot-scope="scope">
{{ scope.row.name }}
<el-tag v-if="scope.row.isActive === 1" type="success" size="mini" style="margin-left:6px">生效中</el-tag>
</template>
</el-table-column>
<el-table-column label="饰品数" width="80" align="center">
<template slot-scope="scope">{{ scope.row.items ? scope.row.items.length : 0 }}</template>
</el-table-column>
<el-table-column label="固定回合" width="90" align="center">
<template slot-scope="scope">{{ scope.row.totalRounds }}</template>
</el-table-column>
<el-table-column label="保存时间" width="170" align="center">
<template slot-scope="scope">{{ scope.row.createTime }}</template>
</el-table-column>
<el-table-column label="操作" width="90" align="center">
<template slot-scope="scope">
<el-button size="mini" type="danger" @click="handleDeleteTemplate(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-else description="该身份暂无模板,请在饰品配置中修改数量后点【保存为模板】。" />
<div slot="footer">
<el-button @click="templateDialogVisible = false">关 闭</el-button>
<el-button type="primary" :disabled="!selectedTemplateRow" @click="handleApplyTemplate">应用选中模板</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { getBoxList, saveBox, getBoxOrnaments, saveBoxOrnaments, calcDistribution, getBoxTemplates, saveBoxTemplate, deleteBoxTemplate, applyBoxTemplate, recalcBatch } from '@/api/game/olympus/box/api'
import { getOrnamentList } from '@/api/game/olympus/ornament/api'
export default {
name: 'OlympusBox',
data() {
return {
loading: false, recalcLoading: false, saveLoading: false, boxList: [], total: 0,
queryParams: { pageNum: 1, pageSize: 10, name: null, boxType: null },
audienceFilter: 'common',
dialogVisible: false, ornamentVisible: false, currentBox: {},
form: { boxId: null, name: '', bet: 1, boxType: 'normal', rtp: 0.8, status: '1', price: 0, totalRounds: 0, fixedPct: 85, convergePct: 15 },
// 重算分布弹窗
recalcDialogVisible: false,
recalcForm: { onlyCurrent: false, boxType: '', rtp: 0.8, totalRounds: 200, fixedPct: 85, convergePct: 15 },
boxTypes: ['normal', 'ante', 'buy_fs'],
boxTypeLabels: { normal: '普通旋转', ante: '底注', buy_fs: '购买免费旋转' },
boxTypeCM: { normal: 1, ante: 2, buy_fs: 50 },
modeOrnRange: { normal: [1, 13], ante: [1, 14], buy_fs: [7, 17] },
audiences: [{ key: 'common', name: '普通' }, { key: 'anchor', name: '主播' }, { key: 'robot', name: '机器人' }, { key: 'white', name: '白号' }],
audience: 'common', ornamentLoading: false, ornamentList: [], ornamentAll: [], ornamentExpects: {},
// 模板
templateDialogVisible: false, templateBox: {}, templateAudience: 'common', boxTemplates: [], selectedTemplateRow: null
}
},
computed: {
ornamentTotal() { return this.ornamentList.reduce((s, o) => s + (o.count || 0), 0) },
ornamentRTP() {
const total = this.ornamentTotal
if (!total || !this.currentBox.bet) return '0.0'
let contrib = 0
this.ornamentList.forEach(o => { const mul = this.ornamentExpects[o.id]; if (mul != null) contrib += (o.count || 0) * mul })
// 扣费倍率优先用后端返回的 costMultiplier,避免这里的表跟后端脱节(曾漏改 buy_fs 20→50,算出 200%)
const cm = Number(this.currentBox.costMultiplier) || this.boxTypeCM[this.currentBox.boxType] || 1
return (contrib / total / cm * 100).toFixed(1)
},
convergeRatioVal() {
let cr = this.currentBox.rtpConvergeRatio != null ? Number(this.currentBox.rtpConvergeRatio) : 0.15
if (!(cr >= 0)) cr = 0.15
return Math.min(0.9, Math.max(0, cr))
},
totalRoundsDerived() { const fixed = this.ornamentTotal; return fixed > 0 ? Math.round(fixed / (1 - this.convergeRatioVal)) : 0 },
convergeRoundsDerived() { return Math.max(0, this.totalRoundsDerived - this.ornamentTotal) },
// 当前身份目标RTP(%):优先取该身份分布反推值,无则回退箱子统一值
// 统一按 1 位小数显示(后端回的是 2 位小数,例如 80.01),这样弹窗里"计算RTP"和"目标RTP"
// 不会因为一个 1 位一个 2 位而看起来不一致
currentOrnamentTargetRtp() {
const m = this.currentBox.targetRtpByAudience
const v = m && m[this.audience] != null ? m[this.audience] : null
if (v != null) return Number(v).toFixed(1)
return this.currentBox.rtp != null ? (this.currentBox.rtp * 100).toFixed(1) : '-'
}
},
created() { this.loadData() },
methods: {
loadData() {
this.loading = true
const params = { pageNum: this.queryParams.pageNum, pageSize: this.queryParams.pageSize }
if (this.queryParams.name) params.name = this.queryParams.name
if (this.queryParams.boxType) params.boxType = this.queryParams.boxType
getBoxList(params).then(res => {
this.boxList = (res.data && res.data.rows) || []
this.total = (res.data && res.data.total) || 0
this.loading = false
}).catch(() => { this.loading = false })
},
openRecalcDialog() {
// 默认参数取当前列表第一行的当前身份反推值(无数据时用默认)
const row = this.boxList[0] || {}
let rtp = 0.8, totalRounds = 200, convergePct = 15
const t = row.targetRtpByAudience && row.targetRtpByAudience[this.audienceFilter]
if (t != null) rtp = Number(t) / 100
const tr = row.totalRoundsByAudience && row.totalRoundsByAudience[this.audienceFilter]
if (tr != null) totalRounds = Number(tr)
if (row.rtpConvergeRatio != null) convergePct = Math.round(Number(row.rtpConvergeRatio) * 100)
this.recalcForm = { onlyCurrent: false, boxType: '', rtp, totalRounds, fixedPct: 100 - convergePct, convergePct }
this.recalcDialogVisible = true
},
onRecalcFixedPctChange(v) { const val = Math.min(100, Math.max(0, v || 0)); this.recalcForm.fixedPct = val; this.recalcForm.convergePct = 100 - val },
onRecalcConvergePctChange(v) { const val = Math.min(100, Math.max(0, v || 0)); this.recalcForm.convergePct = val; this.recalcForm.fixedPct = 100 - val },
submitRecalc() {
const f = this.recalcForm
const params = {
boxType: f.boxType || null,
audience: f.onlyCurrent ? this.audienceFilter : null,
rtp: f.rtp,
totalRounds: f.totalRounds,
convergeRatio: f.convergePct / 100
}
this.recalcLoading = true
recalcBatch(params).then(() => {
this.$message.success('重算完成')
this.recalcDialogVisible = false
this.loadData()
}).catch(e => this.$message.error('重算失败: ' + (e.message || e)))
.finally(() => { this.recalcLoading = false })
},
handleEdit(row) {
const convergePct = row.rtpConvergeRatio != null ? Math.round(row.rtpConvergeRatio * 100) : 15
// 目标RTP 初始值 = 当前身份分布反推的目标(百分点/100),避免显示成其他身份的值
let rtp = row.rtp != null ? row.rtp : 0.8
const t = row.targetRtpByAudience && row.targetRtpByAudience[this.audienceFilter]
if (t != null) rtp = Number(t) / 100
// 总回合 初始值 = 当前身份分布反推的一轮总回合(身份级独立)
let totalRounds = row.totalOrnaments || 0
const tr = row.totalRoundsByAudience && row.totalRoundsByAudience[this.audienceFilter]
if (tr != null) totalRounds = Number(tr)
this.form = { ...row, totalRounds, rtp, status: row.status || '1', convergePct, fixedPct: 100 - convergePct }
this.dialogVisible = true
},
onFixedPctChange(v) { const val = Math.min(100, Math.max(0, v || 0)); this.form.fixedPct = val; this.form.convergePct = 100 - val },
onConvergePctChange(v) { const val = Math.min(100, Math.max(0, v || 0)); this.form.convergePct = val; this.form.fixedPct = 100 - val },
submitForm() {
const convergeRatio = (this.form.convergePct != null ? this.form.convergePct : 15) / 100
const data = { boxId: this.form.boxId, name: this.form.name, bet: this.form.bet, boxType: this.form.boxType, rtp: this.form.rtp, status: this.form.status, rtpConvergeRatio: convergeRatio, totalOrnaments: this.form.totalRounds }
this.saveLoading = true
saveBox(data).then(res => {
const boxId = this.form.boxId || (res.data && res.data.boxId)
if (!boxId) { this.saveLoading = false; this.$message.success('保存成功'); this.dialogVisible = false; this.loadData(); return }
// 保存后只重算当前身份
calcDistribution({ boxId, audience: this.audienceFilter, totalRounds: this.form.totalRounds, rtp: this.form.rtp, convergeRatio })
.then(() => { this.$message.success('修改成功,已重算【' + this.audienceLabel(this.audienceFilter) + '】身份分布') })
.catch(() => { this.$message.success('修改成功(分布计算失败,可稍后用"重算分布"重试)') })
.finally(() => { this.saveLoading = false; this.dialogVisible = false; this.loadData() })
}).catch(() => { this.saveLoading = false })
},
handleOrnaments(row) {
this.currentBox = row
this.audience = this.audienceFilter // 跟随列表页当前身份
getOrnamentList().then(ornRes => {
this.ornamentAll = (ornRes.data && ornRes.data.list) ? ornRes.data.list : []
this.ornamentExpects = {}
this.ornamentAll.forEach(o => { this.ornamentExpects[o.id] = Number(o.expectMul) })
this.ornamentVisible = true
this._refreshOrnamentList()
})
},
_refreshOrnamentList() {
this.ornamentLoading = true
getBoxOrnaments(this.currentBox.boxId, this.audience).then(res => {
const items = (res.data && res.data.list) ? res.data.list : []
// 「计算RTP」必须与后台「目标RTP」同一口径 = 该桶**池内实际均值**(AVG(actual_mul)),
// 否则会拿桶定义的名义 expect_mul(区间中点)来算,两个数必然对不上(单桶能差 -18%~+11%)。
// 池子里还没有该桶的回合时,后端会回退名义值,这里保持同样回退。
const pe = (res.data && res.data.poolExpect) || {}
const expects = {}
this.ornamentAll.forEach(o => {
expects[o.id] = pe[o.id] != null ? Number(pe[o.id]) : Number(o.expectMul)
})
this.ornamentExpects = expects
const map = {}
items.forEach(it => { map[it.ornamentId] = { count: it.count, remaining: it.remaining != null ? it.remaining : (it.count - (it.used || 0)), convergeCount: it.convergeCount } })
const [lo, hi] = this.modeOrnRange[this.currentBox.boxType] || [1, 17]
this.ornamentList = this.ornamentAll.filter(o => o.id >= lo && o.id <= hi)
.map(o => ({ ...o, count: (map[o.id] && map[o.id].count) || 0, remaining: map[o.id] ? map[o.id].remaining : 0, convergeCount: map[o.id] ? map[o.id].convergeCount : 0 }))
this.ornamentLoading = false
}).catch(() => { this.ornamentLoading = false })
},
getOrnName(name) { return name ? name.replace('x', '倍饰品') : name },
handleOrnamentRecalc() {
const box = this.currentBox
const cr = box.rtpConvergeRatio != null ? box.rtpConvergeRatio : 0.15
// 目标RTP 优先用当前身份分布反推值,与"按身份编辑"语义一致
const t = box.targetRtpByAudience && box.targetRtpByAudience[this.audience] != null ? Number(box.targetRtpByAudience[this.audience]) / 100 : (box.rtp || 0.8)
// 总回合 用当前身份反推值(身份级独立)
const tr = box.totalRoundsByAudience && box.totalRoundsByAudience[this.audience] != null ? Number(box.totalRoundsByAudience[this.audience]) : (box.totalOrnaments || 200)
calcDistribution({ boxId: box.boxId, audience: this.audience, totalRounds: tr, rtp: t, convergeRatio: cr }).then(() => {
this.$message.success('分布已重算并保存')
this._refreshOrnamentList()
this.loadData()
}).catch(() => { this.$message.error('重算失败') })
},
saveOrnaments() {
const items = this.ornamentList.filter(o => o.count > 0).map(o => ({ ornamentId: o.id, count: o.count }))
saveBoxOrnaments({ boxId: this.currentBox.boxId, audience: this.audience, items }).then(() => {
this.$message.success('保存成功')
this.ornamentVisible = false
this.loadData()
})
},
handleSaveAsTemplate() {
const items = this.ornamentList.filter(o => o.count > 0).map(o => ({ ornamentId: o.id, count: o.count }))
if (items.length === 0) return this.$message.warning('当前没有配置任何饰品数量,无法保存模板')
this.$prompt('请输入模板名称(同一箱子+身份内不可重复)', '保存为模板【' + this.audienceLabel(this.audience) + '】', { inputPattern: /\S+/, inputErrorMessage: '名称不能为空' })
.then(({ value }) => {
saveBoxTemplate({ boxId: this.currentBox.boxId, audience: this.audience, name: value.trim(), items })
.then(() => this.$message.success('模板【' + value.trim() + '】已保存'))
.catch(e => this.$message.error('保存失败: ' + (e.message || e)))
}).catch(() => {})
},
// 模板管理
openTemplateManager(row) { this.templateBox = row; this.templateAudience = this.audienceFilter; this.selectedTemplateRow = null; this.templateDialogVisible = true; this._loadTemplates() },
_loadTemplates() {
getBoxTemplates(this.templateBox.boxId, this.templateAudience).then(res => { this.boxTemplates = (res.data && res.data.list) || [] }).catch(() => { this.boxTemplates = [] })
},
onTemplateRowChange(row) { this.selectedTemplateRow = row },
handleDeleteTemplate(tpl) {
this.$confirm('确定删除模板【' + tpl.name + '】吗?', '提示', { type: 'warning' }).then(() => {
deleteBoxTemplate(tpl.id).then(() => { this.selectedTemplateRow = null; this.$message.success('已删除'); this._loadTemplates() })
}).catch(() => {})
},
handleApplyTemplate() {
if (!this.selectedTemplateRow) return
const tpl = this.selectedTemplateRow
this.$confirm('将模板【' + tpl.name + '】应用到身份【' + this.audienceLabel(this.templateAudience) + '】,并设为抽干补桶依据,确认?', '应用模板', { type: 'warning' }).then(() => {
applyBoxTemplate({ templateId: tpl.id }).then(() => {
this.$message.success('模板已应用,饰品分布已更新')
this.templateDialogVisible = false
if (this.ornamentVisible && this.currentBox.boxId === this.templateBox.boxId && this.audience === this.templateAudience) this._refreshOrnamentList()
this.loadData()
}).catch(e => this.$message.error('应用失败: ' + (e.message || e)))
}).catch(() => {})
},
audienceLabel(key) { const a = this.audiences.find(x => x.key === key); return a ? a.name : key },
// 按当前身份筛选取值(后端一次返回4身份)
targetRtp(row) {
const m = row.targetRtpByAudience
return m && m[this.audienceFilter] != null ? m[this.audienceFilter] : null
},
actualRtp(row) {
const m = row.actualRtpByAudience
return m && m[this.audienceFilter] != null ? m[this.audienceFilter] : null
},
// 当前身份一轮总回合(身份级独立)
totalRoundsOf(row) {
const m = row.totalRoundsByAudience
return m && m[this.audienceFilter] != null ? m[this.audienceFilter] : (row.totalOrnaments != null ? row.totalOrnaments : null)
}
}
}
</script>
@@ -1,92 +0,0 @@
<template>
<div class="app-container">
<el-row :gutter="16" style="margin-bottom:16px">
<el-col :span="6"><el-card shadow="hover"><div class="stat"><small>累计投注</small><h3>{{ summary.totalCost || 0 }}</h3></div></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><div class="stat"><small>累计赢奖</small><h3>{{ summary.totalWin || 0 }}</h3></div></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><div class="stat"><small>整体RTP</small><h3 :style="{color: rtp > 100 ? 'red' : 'green'}">{{ rtp }}%</h3></div></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><div class="stat"><small>总局数</small><h3>{{ summary.totalSpins || 0 }}</h3></div></el-card></el-col>
</el-row>
<div style="margin-bottom:16px">
<el-date-picker v-model="queryParams.dateStart" type="date" placeholder="开始日期" value-format="yyyy-MM-dd" style="width:150px;margin-right:8px" />
<span style="margin:0 4px">至</span>
<el-date-picker v-model="queryParams.dateEnd" type="date" placeholder="结束日期" value-format="yyyy-MM-dd" style="width:150px;margin-right:8px" />
<el-button type="primary" icon="el-icon-search" @click="loadAll">查询</el-button>
<el-button icon="el-icon-refresh" @click="handleReset">重置</el-button>
</div>
<el-table v-loading="loading" :data="list" border>
<el-table-column prop="date" label="日期" min-width="120" align="center" />
<el-table-column prop="totalCost" label="总投注" min-width="120" align="center" />
<el-table-column prop="totalWin" label="总赢奖" min-width="120" align="center" />
<el-table-column prop="totalSpins" label="总局数" min-width="90" align="center" />
<el-table-column prop="playerCount" label="玩家数" min-width="90" align="center" />
<el-table-column label="RTP" min-width="100" align="center">
<template slot-scope="scope">
<span :style="{color: scope.row.totalCost > 0 && scope.row.totalWin / scope.row.totalCost > 1 ? 'red' : 'green'}">
{{ scope.row.totalCost > 0 ? (Math.round(scope.row.totalWin / scope.row.totalCost * 10000) / 100).toFixed(2) + '%' : '-' }}
</span>
</template>
</el-table-column>
</el-table>
<el-pagination
style="margin-top:16px;text-align:right"
background
layout="total, prev, pager, next, sizes"
:total="total"
:page-size.sync="queryParams.pageSize"
:current-page.sync="queryParams.pageNum"
:page-sizes="[10,20,50]"
@size-change="loadData"
@current-change="loadData"
/>
</div>
</template>
<script>
import { getWinList, getWinSummary } from '@/api/game/olympus/dailywin/api'
export default {
name: 'OlympusDailyWin',
data() {
return {
loading: false, list: [], total: 0, summary: {},
queryParams: { pageNum: 1, pageSize: 10, dateStart: new Date().toISOString().slice(0, 10), dateEnd: null }
}
},
computed: {
rtp() {
const c = Number(this.summary.totalCost) || 0
const w = Number(this.summary.totalWin) || 0
return c > 0 ? (Math.round(w / c * 10000) / 100).toFixed(2) : '0.00'
}
},
created() { this.loadAll() },
methods: {
loadAll() { this.loadSummary(); this.loadData() },
loadSummary() {
getWinSummary({ dateStart: this.queryParams.dateStart, dateEnd: this.queryParams.dateEnd }).then(res => {
this.summary = res.data || {}
})
},
loadData() {
this.loading = true
getWinList(this.queryParams).then(res => {
this.list = (res.data && res.data.rows) || []
this.total = (res.data && res.data.total) || 0
this.loading = false
}).catch(() => { this.loading = false })
},
handleReset() {
this.queryParams = { pageNum: 1, pageSize: 10, dateStart: new Date().toISOString().slice(0, 10), dateEnd: null }
this.loadAll()
}
}
}
</script>
<style scoped>
.stat { text-align: center; }
.stat small { color: #909399; }
</style>
@@ -1,32 +0,0 @@
<template>
<div class="app-container">
<el-alert type="info" :closable="false" style="margin-bottom:16px">
17 档饰品桶(0x~1000x)的定义与落桶区间。分布由 Python 预生成落库,此处仅供参考查看。
</el-alert>
<el-table v-loading="loading" :data="list" border>
<el-table-column prop="id" label="桶ID" width="80" align="center" />
<el-table-column label="名称" min-width="140" align="center">
<template slot-scope="scope">{{ (scope.row.name || '').replace('x', '倍饰品') }}</template>
</el-table-column>
<el-table-column prop="expectMul" label="期望倍率" min-width="120" align="center" />
<el-table-column prop="rangeLow" label="落桶下限" min-width="120" align="center" />
<el-table-column prop="rangeHigh" label="落桶上限" min-width="120" align="center" />
</el-table>
</div>
</template>
<script>
import { getOrnamentList } from '@/api/game/olympus/ornament/api'
export default {
name: 'OlympusOrnament',
data() { return { loading: false, list: [] } },
created() {
this.loading = true
getOrnamentList().then(res => {
this.list = (res.data && res.data.list) || []
this.loading = false
}).catch(() => { this.loading = false })
}
}
</script>
@@ -1,121 +0,0 @@
<template>
<div class="app-container">
<el-alert type="info" :closable="false" style="margin-bottom:12px">
返奖饰品按"结算赢额"匹配返奖区间展示。区间需连续覆盖;未命中时用兜底饰品。
</el-alert>
<el-table v-loading="loading" :data="list" border>
<el-table-column prop="id" label="ID" width="60" align="center" />
<el-table-column prop="name" label="名称" min-width="160" align="center" show-overflow-tooltip />
<el-table-column label="品质" width="80" align="center">
<template slot-scope="scope">
<el-tag v-if="scope.row.grade" size="small" :type="gradeType(scope.row.grade)" :style="gradeStyle(scope.row.grade)">{{ gradeLabel(scope.row.grade) }}</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="饰品图片" width="80" align="center">
<template slot-scope="scope">
<el-image v-if="scope.row.imageUrl" :src="getImg(scope.row.imageUrl)" style="width:40px;height:40px" fit="cover" />
</template>
</el-table-column>
<el-table-column label="品质图片" width="80" align="center">
<template slot-scope="scope">
<el-image v-if="scope.row.levelImg" :src="getImg(scope.row.levelImg)" style="width:40px;height:40px" fit="contain" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="磨损" width="110" align="center">
<template slot-scope="scope">{{ scope.row.exteriorName || '崭新出厂' }}</template>
</el-table-column>
<el-table-column label="兜底" width="70" align="center">
<template slot-scope="scope">
<el-tag v-if="scope.row.isFallback === 1" type="danger" size="small">是</el-tag>
<el-tag v-else type="info" size="small">否</el-tag>
</template>
</el-table-column>
<el-table-column label="返奖区间" min-width="160" align="center">
<template slot-scope="scope">
<span v-if="scope.row.winLow != null">{{ scope.row.winLow }} ~ {{ scope.row.winHigh }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template slot-scope="scope">
<el-button size="mini" @click="handleEdit(scope.row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog title="编辑奖品" :visible.sync="dialogVisible" width="480px" append-to-body>
<el-form ref="form" :model="form" label-width="80px">
<el-form-item label="名称"><el-input v-model="form.name" /></el-form-item>
<el-form-item label="品质">
<el-select v-model="form.grade" style="width:100%">
<el-option label="1 - 金" :value="1" />
<el-option label="2 - 红" :value="2" />
<el-option label="3 - 紫" :value="3" />
<el-option label="4 - 蓝" :value="4" />
</el-select>
</el-form-item>
<el-form-item label="饰品图片">
<ImageUpload v-model="form.imageUrl" :limit="1" />
</el-form-item>
<el-form-item label="磨损">
<el-select v-model="form.exteriorName" placeholder="请选择磨损度" clearable style="width:100%">
<el-option label="崭新出厂" value="崭新出厂" />
<el-option label="略有磨损" value="略有磨损" />
<el-option label="久经沙场" value="久经沙场" />
<el-option label="破损不堪" value="破损不堪" />
<el-option label="战痕累累" value="战痕累累" />
</el-select>
</el-form-item>
<el-form-item label="兜底饰品">
<el-switch v-model="form.isFallback" :active-value="1" :inactive-value="0" active-text="是" inactive-text="否" />
<div style="color:#909399;font-size:12px;margin-top:4px">未命中区间时随机选择一个兜底饰品返奖</div>
</el-form-item>
<el-form-item label="返奖区间">
<div style="display:flex;align-items:center;gap:8px">
<el-input-number v-model="form.winLow" :min="0" :precision="2" :step="0.01" controls-position="right" placeholder="最小值" style="flex:1" />
<span>~</span>
<el-input-number v-model="form.winHigh" :min="0" :precision="2" :step="0.01" controls-position="right" placeholder="最大值" style="flex:1" />
</div>
<div style="color:#909399;font-size:12px;margin-top:4px">玩家结算赢额落在该区间即获得此饰品</div>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { prizeList, prizeSave } from '@/api/game/olympus/prize/api'
import ImageUpload from '@/components/ImageUpload'
export default {
name: 'OlympusPrize',
components: { ImageUpload },
data() {
return { loading: false, list: [], dialogVisible: false, form: {}}
},
created() { this.getList() },
methods: {
getList() {
this.loading = true
prizeList().then(res => { this.list = (res.data && res.data.list) || []; this.loading = false }).catch(() => { this.loading = false })
},
getImg(u) { return u ? (u.startsWith('http') ? u : (process.env.VUE_APP_BASE_API || '') + u) : '' },
// 品质配色与饰品品质一致:1金/2红/3紫/4蓝
gradeType(g) { const m = { 1: 'warning', 2: 'danger', 3: '', 4: '' }; return m[g] === undefined ? 'info' : m[g] },
gradeStyle(g) { return g === 3 ? { color: '#a16ee8', background: '#f7f1fe', borderColor: '#e4d5f9' } : null },
gradeLabel(g) { return ({ 1: '金', 2: '红', 3: '紫', 4: '蓝' })[g] || ('品质' + g) },
handleEdit(row) { this.form = { ...row }; this.dialogVisible = true },
submitForm() {
if (!this.form.name || !this.form.name.trim()) return this.$message.warning('名称不能为空')
prizeSave(this.form).then(() => { this.$message.success('保存成功'); this.dialogVisible = false; this.getList() })
}
}
}
</script>
@@ -1,108 +0,0 @@
<template>
<div class="app-container">
<el-form :inline="true" class="demo-form-inline">
<el-form-item label="玩家ID">
<el-input v-model="queryParams.userId" placeholder="输入玩家ID" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="用户类型">
<el-select v-model="queryParams.audience" placeholder="全部" clearable style="width:130px" @change="handleQuery">
<el-option v-for="a in audiences" :key="a.key" :label="a.name" :value="a.key" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
<el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="list" border>
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="userId" label="玩家ID" width="90" align="center" />
<el-table-column label="用户类型" width="90" align="center">
<template slot-scope="scope">
<el-tag :type="audienceTagType(scope.row.audience)" size="small">{{ audienceLabel(scope.row.audience) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="boxId" label="箱子" width="70" align="center" />
<el-table-column label="模式" width="120" align="center">
<template slot-scope="scope">{{ boxTypeLabels[scope.row.modeType] || scope.row.modeType }}</template>
</el-table-column>
<el-table-column prop="prizeName" label="饰品名称" min-width="150" align="center" show-overflow-tooltip />
<el-table-column label="饰品图片" width="80" align="center">
<template slot-scope="scope">
<el-image v-if="scope.row.prizeImageUrl" :src="getImg(scope.row.prizeImageUrl)" style="width:40px;height:40px" fit="cover" />
</template>
</el-table-column>
<el-table-column prop="cost" label="花费" width="90" align="center" />
<el-table-column prop="settledWin" label="结算赢额" width="100" align="center" />
<el-table-column label="播放状态" width="110" align="center">
<template slot-scope="scope">
<el-tooltip :content="settleTips[scope.row.settleSource] || '历史值'" placement="top">
<el-tag :type="scope.row.settleSource === 'start' ? 'warning' : 'success'" size="small">
{{ settleLabels[scope.row.settleSource] || scope.row.settleSource || '—' }}
</el-tag>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="createTime" label="时间" width="160" align="center" />
</el-table>
<el-pagination
style="margin-top:16px;text-align:right"
background
layout="total, prev, pager, next, sizes"
:total="total"
:page-size.sync="queryParams.pageSize"
:current-page.sync="queryParams.pageNum"
:page-sizes="[10,20,50]"
@size-change="loadData"
@current-change="loadData"
/>
</div>
</template>
<script>
import { getRecordList } from '@/api/game/olympus/record/api'
export default {
name: 'OlympusRecord',
data() {
return {
loading: false, list: [], total: 0,
queryParams: { pageNum: 1, pageSize: 10, userId: null, audience: null },
boxTypeLabels: { normal: '普通旋转', ante: '底注', buy_fs: '购买免费旋转' },
audiences: [{ key: 'common', name: '普通' }, { key: 'anchor', name: '主播' }, { key: 'robot', name: '机器人' }, { key: 'white', name: '白号' }],
// 播放状态:start=开局即结算(奖已入背包)但动画没播完;与"结算赢额"不矛盾
settleLabels: { start: '未播完', finish: '已播完', reconnect: '重连补结', scheduled: '定时兜底', force_settle: '强制结算' },
settleTips: {
start: '开局就已结算:钱和饰品当场定下并直接进背包(防断网丢奖)。只是整局动画没播到末帧(玩家中途退出/断网),客户端战绩暂不展示,超 30 分钟兜底放行',
finish: '整局已播到末帧,客户端战绩正常展示(站内信也是此刻才发)'
}
}
},
created() { this.loadData() },
methods: {
audienceLabel(key) {
const a = this.audiences.find(x => x.key === key)
return a ? a.name : (key || '—')
},
audienceTagType(key) {
return { common: '', anchor: 'success', robot: 'info', white: 'warning' }[key] || 'info'
},
loadData() {
this.loading = true
const params = { pageNum: this.queryParams.pageNum, pageSize: this.queryParams.pageSize }
if (this.queryParams.userId) params.userId = this.queryParams.userId
if (this.queryParams.audience) params.audience = this.queryParams.audience
getRecordList(params).then(res => {
this.list = (res.data && res.data.rows) || []
this.total = (res.data && res.data.total) || 0
this.loading = false
}).catch(() => { this.loading = false })
},
handleQuery() { this.queryParams.pageNum = 1; this.loadData() },
resetQuery() { this.queryParams = { pageNum: 1, pageSize: 10, userId: null, audience: null }; this.loadData() },
getImg(u) { return u ? (u.startsWith('http') ? u : (process.env.VUE_APP_BASE_API || '') + u) : '' }
}
}
</script>
@@ -1,451 +0,0 @@
<template>
<div class="app-container reward-pool-page">
<div class="page-head">
<div class="page-head-row">
<el-button type="text" size="mini" :icon="showGuide ? 'el-icon-arrow-up' : 'el-icon-question'" @click="showGuide = !showGuide">
{{ showGuide ? '收起说明' : '配置说明' }}
</el-button>
</div>
<p class="head-tip">
奖池按【箱子 × 身份】独立:每箱每身份一套水位 = 该身份箱内下注 − 箱内返奖 + 该箱该身份人工调整;开局按「(本箱该身份水位 + 本局花费) × 目标RTP」算赔付上限,只有预期赔付不超上限的饰品才可被抽中。白号水位仅统计与人工调整,不受上限约束(纯概率)。
</p>
<div class="audience-row">
<span class="aud-label">身份:</span>
<el-radio-group v-model="audience" size="small" @change="switchAudience">
<el-radio-button v-for="a in audiences" :key="a.key" :label="a.key">{{ a.name }}</el-radio-button>
</el-radio-group>
<span class="aud-tip">四个身份各自独立水位、独立注水/抽水、独立调整历史;白号不参与其他身份池。</span>
</div>
<div v-show="showGuide" class="guide-panel">
<div class="guide-col">
<h5><i class="el-icon-info" /> 机制说明</h5>
<ul>
<li>每箱每身份独立水位 = 该身份箱内累计下注 − 累计返奖 + 人工调整;四个身份互不串池。</li>
<li>白号(audience=white)水位独立统计,但按设计不受水位约束,白号出奖=配比即爆率,注水/抽水只影响记录。</li>
<li>抽饰品两阶段:固定阶段按分布抽取(确定性);轮末收敛阶段按实时RTP偏差定向选桶纠偏。</li>
<li>水位随下注升高、随返奖降低;顶部总池为当前身份全站汇总,仅展示不可调。</li>
</ul>
</div>
<div class="guide-col">
<h5><i class="el-icon-setting" /> 如何配置</h5>
<ul>
<li>新箱上线或水位偏低:建议批量注水一轮花费的 60%~80%(接近目标RTP),相当于提前垫付整轮返奖预算,保证高倍饰品全程可正常抽出。</li>
<li>日常运营小幅补水:比例 5%~20% 即可;某箱长期不出高倍时检查该箱水位,接近 0 或为负则单箱注水。</li>
<li>返奖偏高想收紧:批量或单箱抽水;水位为负时只会出最低倍,请谨慎大额抽水。</li>
</ul>
</div>
<div class="guide-col">
<h5><i class="el-icon-s-operation" /> 批量调整规则</h5>
<ul>
<li>基数 = 该箱抽干一轮的总花费 = 底注 × 模式倍数 × 总回合数;总回合数在箱子管理【编辑】中配置,修改后基数自动跟随。</li>
<li>每箱调整金额 = 基数 × 比例。例:普通旋转底注 1、1000 回合 → 基数 1000,注水 10% = +100。</li>
<li>比例支持 0~1000%;基数为 0 的箱子自动跳过;批量操作记一条汇总历史可审计(调整原因必填,会写进调整历史)。</li>
</ul>
</div>
<div class="guide-col">
<h5><i class="el-icon-data-line" /> 两阶段RTP</h5>
<ul>
<li>本轮实时RTP / 已抽数反映当前轮收敛进度;一轮抽满 N 回合后自动重置账本开新轮。</li>
<li>收敛占比越高,轮末纠偏能力越强(购买免费旋转类建议更高占比)。</li>
<li>总回合数 N 与收敛占比在「箱子配置 → 编辑」中配置。</li>
</ul>
</div>
</div>
</div>
<el-row :gutter="16" class="stat-row">
<el-col :span="6">
<div class="stat-card">
<div class="sc-icon sc-blue"><i class="el-icon-coin" /></div>
<div class="sc-body"><small>{{ audienceLabel(audience) }}·总下注额</small><h3>{{ fmt(pool.totalBet) }}</h3></div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card">
<div class="sc-icon sc-orange"><i class="el-icon-present" /></div>
<div class="sc-body"><small>{{ audienceLabel(audience) }}·总返奖额</small><h3>{{ fmt(pool.totalWin) }}</h3></div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card">
<div class="sc-icon sc-cyan"><i class="el-icon-wallet" /></div>
<div class="sc-body"><small>{{ audienceLabel(audience) }}·总池余额</small><h3 :style="{color: Number(pool.balance) < 0 ? '#f56c6c' : '#409eff'}">{{ fmt(pool.balance) }}</h3></div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card">
<div class="sc-icon sc-green"><i class="el-icon-data-line" /></div>
<div class="sc-body"><small>{{ audienceLabel(audience) }}·实时RTP</small><h3 :style="{color: rtpPct(pool.rtp) > 100 ? '#f56c6c' : '#67c23a'}">{{ rtpPct(pool.rtp).toFixed(1) }}%</h3></div>
</div>
</el-col>
</el-row>
<el-card shadow="never" class="batch-card">
<div slot="header">
<b>批量调整(按一轮花费比例)</b>
<span class="sub-tip">身份【{{ audienceLabel(audience) }}】:基数 = 底注 × 模式倍数 × 总回合数(随箱子配置动态变化)</span>
</div>
<el-form :inline="true" @submit.native.prevent>
<!-- 注:skin.scss 全局隐藏了 inline 表单的 label,所以字段名写成 span,别用 label 属性 -->
<el-form-item>
<span class="fld-label">调整比例</span>
<el-input-number v-model="batchForm.ratio" :min="0.01" :max="1000" :precision="2" :step="5" :controls="false" style="width:120px" placeholder="如 10" />
<span style="margin-left:4px">%</span>
</el-form-item>
<el-form-item>
<span class="fld-label">调整原因<span class="req">*</span></span>
<el-input v-model="batchForm.remark" maxlength="80" placeholder="必填,如:活动补水" style="width:200px" />
</el-form-item>
<el-form-item>
<el-popconfirm :title="'按【' + audienceLabel(audience) + '】各箱一轮花费 ' + (batchForm.ratio || 0) + '% 注水,预估共 +' + fmt(estBatchTotal) + ',确定?'" @confirm="submitBatch(1)">
<el-button slot="reference" type="success" plain icon="el-icon-top" :loading="batchLoading" :disabled="!batchForm.ratio">批量注水</el-button>
</el-popconfirm>
</el-form-item>
<el-form-item>
<el-popconfirm :title="'按【' + audienceLabel(audience) + '】各箱一轮花费 ' + (batchForm.ratio || 0) + '% 抽水,预估共 -' + fmt(estBatchTotal) + ',确定?'" @confirm="submitBatch(2)">
<el-button slot="reference" type="danger" plain icon="el-icon-bottom" :loading="batchLoading" :disabled="!batchForm.ratio">批量抽水</el-button>
</el-popconfirm>
</el-form-item>
<el-form-item>
<el-popconfirm :title="'重置将把身份【' + audienceLabel(audience) + '】所有箱子的注水/抽水累计值归 0,只保留真实水位(下注−返奖),确定?'" @confirm="submitReset">
<el-button slot="reference" type="warning" plain icon="el-icon-refresh-left" :loading="resetLoading">重置调整</el-button>
</el-popconfirm>
</el-form-item>
<el-form-item v-if="batchForm.ratio">
<span class="est-text">预估总额:{{ fmt(estBatchTotal) }}({{ boxes.length }} 个箱子)</span>
</el-form-item>
</el-form>
</el-card>
<div class="section-head">
<h4 style="margin:0">箱子奖池 · 身份【{{ audienceLabel(audience) }}】</h4>
<el-button size="mini" type="text" icon="el-icon-refresh" @click="loadConfig">刷新</el-button>
</div>
<el-table v-loading="boxLoading" :data="pagedBoxes" size="small" border stripe>
<el-table-column prop="boxId" label="ID" width="60" align="center" />
<el-table-column prop="name" label="箱子" min-width="140" align="center" show-overflow-tooltip />
<el-table-column prop="boxType" label="模式" min-width="110" align="center">
<template slot-scope="scope">{{ boxTypeLabels[scope.row.boxType] || scope.row.boxType }}</template>
</el-table-column>
<el-table-column label="底注" width="70" align="center">
<template slot-scope="scope">{{ fmt(scope.row.bet) }}</template>
</el-table-column>
<el-table-column label="一轮花费(基数)" width="130" align="center">
<template slot-scope="scope">
<div>{{ fmt(scope.row.cycleCost) }}</div>
<div style="font-size:11px;color:#909399">{{ fmt(scope.row.costPerRound) }} × {{ scope.row.cycleRounds || '—' }}回合</div>
</template>
</el-table-column>
<el-table-column label="箱内下注" min-width="100" align="center">
<template slot-scope="scope">{{ fmt(scope.row.totalBet) }}</template>
</el-table-column>
<el-table-column label="箱内返奖" min-width="100" align="center">
<template slot-scope="scope">{{ fmt(scope.row.totalWin) }}</template>
</el-table-column>
<el-table-column label="人工调整" min-width="90" align="center">
<template slot-scope="scope">
<span :style="{color: signColor(scope.row.adjust)}">{{ fmt(scope.row.adjust) }}</span>
</template>
</el-table-column>
<el-table-column label="水位余额" min-width="100" align="center">
<template slot-scope="scope">
<b :style="{color: Number(scope.row.balance) < 0 ? '#f56c6c' : '#409eff'}">{{ fmt(scope.row.balance) }}</b>
</template>
</el-table-column>
<el-table-column label="累计RTP" width="90" align="center">
<template slot-scope="scope">
<span :style="{color: rtpPct(scope.row.rtp) > 100 ? '#f56c6c' : '#67c23a'}">{{ scope.row.rtp == null ? '—' : rtpPct(scope.row.rtp).toFixed(1) + '%' }}</span>
</template>
</el-table-column>
<el-table-column label="本轮RTP/进度" min-width="120" align="center">
<template slot-scope="scope">
<div :style="{color: rtpPct(scope.row.cycleRtp) > 100 ? '#f56c6c' : '#67c23a'}">{{ scope.row.cycleRtp == null ? '—' : rtpPct(scope.row.cycleRtp).toFixed(1) + '%' }}</div>
<div style="font-size:11px;color:#909399">{{ scope.row.cycleDrawn || 0 }} / {{ scope.row.totalRounds || '—' }} 回合</div>
</template>
</el-table-column>
<el-table-column label="收敛占比" width="80" align="center">
<template slot-scope="scope">{{ scope.row.convergeRatio == null ? '—' : (Number(scope.row.convergeRatio) * 100).toFixed(0) + '%' }}</template>
</el-table-column>
<el-table-column label="操作" width="110" align="center">
<template slot-scope="scope">
<el-tooltip content="注水(增加水位)" placement="top">
<el-button circle size="mini" type="success" plain icon="el-icon-top" @click="openAdjust(scope.row, 1)" />
</el-tooltip>
<el-tooltip content="抽水(降低水位)" placement="top">
<el-button circle size="mini" type="danger" plain icon="el-icon-bottom" @click="openAdjust(scope.row, 2)" />
</el-tooltip>
</template>
</el-table-column>
</el-table>
<el-pagination
style="margin-top:12px;text-align:right"
background
layout="total, prev, pager, next, sizes"
:total="boxes.length"
:page-size.sync="boxSize"
:current-page.sync="boxPage"
:page-sizes="[10,20,30,50]"
/>
<el-dialog :title="'调整水位【' + audienceLabel(audience) + '】- ' + (form.boxName || '')" :visible.sync="dialogVisible" width="440px" append-to-body>
<el-form label-width="90px" @submit.native.prevent>
<el-form-item label="调整类型">
<el-radio-group v-model="form.type">
<el-radio-button :label="1">注水(增加)</el-radio-button>
<el-radio-button :label="2">抽水(减少)</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="当前水位">
<b :style="{color: Number(form.balance) < 0 ? '#f56c6c' : '#409eff'}">{{ fmt(form.balance) }}</b>
</el-form-item>
<el-form-item label="调整金额">
<el-input-number v-model="form.amount" :min="0.01" :precision="2" :step="100" :controls="false" style="width:100%" placeholder="请输入金额" />
</el-form-item>
<el-form-item v-if="form.amount" label="调整后水位">
<b :style="{color: afterBalance < 0 ? '#f56c6c' : '#67c23a'}">{{ fmt(afterBalance) }}</b>
</el-form-item>
<el-form-item label="调整原因" required>
<el-input v-model="form.remark" maxlength="100" placeholder="必填,如:活动补水" />
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button :type="form.type === 1 ? 'success' : 'danger'" :loading="submitting" @click="submit">确定{{ form.type === 1 ? '注水' : '抽水' }}</el-button>
</div>
</el-dialog>
<div class="section-head">
<h4 style="margin:0">调整历史 · 身份【{{ audienceLabel(audience) }}】</h4>
</div>
<el-table v-loading="loading" :data="list" border size="small">
<el-table-column prop="createTime" label="时间" min-width="150" align="center" />
<el-table-column label="身份" width="80" align="center">
<template slot-scope="scope"><el-tag size="mini">{{ audienceLabel(scope.row.audience) }}</el-tag></template>
</el-table-column>
<el-table-column prop="boxName" label="箱子" min-width="130" align="center" show-overflow-tooltip />
<el-table-column label="类型" width="80" align="center">
<template slot-scope="scope">
<el-tag :type="scope.row.type === 1 ? 'success' : (scope.row.type === 2 ? 'danger' : 'warning')" size="mini">
{{ scope.row.type === 1 ? '注水' : (scope.row.type === 2 ? '抽水' : '重置') }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="金额" min-width="110" align="center">
<template slot-scope="scope">
<span :style="{color: scope.row.type === 1 ? '#67c23a' : (scope.row.type === 2 ? '#f56c6c' : '#e6a23c')}">
{{ scope.row.type === 1 ? '+' : (scope.row.type === 2 ? '-' : '') }}{{ fmt(scope.row.amount) }}
</span>
</template>
</el-table-column>
<el-table-column label="调整前余额" min-width="110" align="center">
<template slot-scope="scope">{{ scope.row.balanceBefore == null ? '—' : fmt(scope.row.balanceBefore) }}</template>
</el-table-column>
<el-table-column label="调整后余额" min-width="110" align="center">
<template slot-scope="scope">{{ scope.row.balanceAfter == null ? '—' : fmt(scope.row.balanceAfter) }}</template>
</el-table-column>
<el-table-column label="总池余额" min-width="110" align="center">
<template slot-scope="scope">
<b :style="{color: Number(scope.row.poolBalance) < 0 ? '#f56c6c' : '#409eff'}">{{ scope.row.poolBalance == null ? '—' : fmt(scope.row.poolBalance) }}</b>
</template>
</el-table-column>
<el-table-column prop="operator" label="操作人" min-width="90" align="center" />
<el-table-column label="备注" min-width="160" align="center" show-overflow-tooltip>
<template slot-scope="scope">
<span :style="{color: scope.row.remark ? '#303133' : '#c0c4cc'}">{{ scope.row.remark || '—' }}</span>
</template>
</el-table-column>
</el-table>
<el-pagination
style="margin-top:16px;text-align:right"
background
layout="total, prev, pager, next, sizes"
:total="total"
:page-size.sync="queryParams.size"
:current-page.sync="queryParams.page"
:page-sizes="[10,20,50]"
@size-change="loadLogs"
@current-change="loadLogs"
/>
</div>
</template>
<script>
import { getPoolConfig, adjustPool, adjustPoolBatch, adjustPoolReset, getPoolLogs } from '@/api/game/olympus/rewardPool/api'
export default {
name: 'OlympusRewardPool',
data() {
return {
loading: false,
boxLoading: false,
submitting: false,
batchLoading: false,
resetLoading: false,
dialogVisible: false,
showGuide: false,
audience: 'common',
audiences: [{ key: 'common', name: '普通' }, { key: 'anchor', name: '主播' }, { key: 'robot', name: '机器人' }, { key: 'white', name: '白号' }],
pool: { totalBet: 0, totalWin: 0, adjust: 0, balance: 0, rtp: 0 },
boxes: [],
list: [],
total: 0,
boxPage: 1,
boxSize: 10,
boxTypeLabels: { normal: '普通旋转', ante: '底注', buy_fs: '购买免费旋转' },
form: { boxId: undefined, boxName: '', balance: 0, type: 1, amount: undefined, remark: '' },
batchForm: { ratio: undefined, remark: '' },
queryParams: { page: 1, size: 10, audience: 'common' }
}
},
computed: {
pagedBoxes() {
const start = (this.boxPage - 1) * this.boxSize
return this.boxes.slice(start, start + this.boxSize)
},
estBatchTotal() {
const r = Number(this.batchForm.ratio) || 0
if (r <= 0) return 0
return this.boxes.reduce((s, b) => s + (Number(b.cycleCost) > 0 ? Number(b.cycleCost) * r / 100 : 0), 0)
},
afterBalance() {
const amt = Number(this.form.amount) || 0
return Number(this.form.balance) + (this.form.type === 2 ? -amt : amt)
}
},
created() {
this.loadConfig()
this.loadLogs()
},
methods: {
fmt(v) {
const n = Number(v)
return isNaN(n) ? '0.00' : n.toFixed(2)
},
rtpPct(v) {
const n = Number(v)
return isNaN(n) ? 0 : n * 100
},
signColor(v) {
const n = Number(v)
return n < 0 ? '#f56c6c' : (n > 0 ? '#67c23a' : '#303133')
},
applyConfig(data) {
if (!data) return
this.pool = data.total || this.pool
this.boxes = data.boxes || []
},
audienceLabel(key) {
const a = this.audiences.find(x => x.key === key)
return a ? a.name : (key || '普通')
},
requiredRemark(v) {
if (v && v.trim()) return true
this.$message.warning('请填写调整原因(备注)')
return false
},
switchAudience() {
this.queryParams.audience = this.audience
this.queryParams.page = 1
this.loadConfig()
this.loadLogs()
},
loadConfig() {
this.boxLoading = true
getPoolConfig({ audience: this.audience }).then(res => {
this.applyConfig(res.data)
this.boxLoading = false
}).catch(() => { this.boxLoading = false })
},
loadLogs() {
this.loading = true
this.queryParams.audience = this.audience
getPoolLogs(this.queryParams).then(res => {
this.list = (res.data && res.data.rows) || []
this.total = (res.data && res.data.total) || 0
this.loading = false
}).catch(() => { this.loading = false })
},
openAdjust(row, type) {
this.form = { boxId: row.boxId, boxName: row.name, audience: this.audience, balance: row.balance, type, amount: undefined, remark: '' }
this.dialogVisible = true
},
submit() {
if (!this.form.amount || this.form.amount <= 0) {
this.$message.warning('请输入大于 0 的调整金额')
return
}
if (!this.requiredRemark(this.form.remark)) return
this.submitting = true
adjustPool({ boxId: this.form.boxId, audience: this.audience, type: this.form.type, amount: this.form.amount, remark: this.form.remark.trim() }).then(res => {
this.applyConfig(res.data)
this.$message.success((this.form.type === 1 ? '注水' : '抽水') + '成功')
this.dialogVisible = false
this.queryParams.page = 1
this.loadLogs()
this.submitting = false
}).catch(() => { this.submitting = false })
},
submitBatch(type) {
if (!this.batchForm.ratio || this.batchForm.ratio <= 0) {
this.$message.warning('请输入大于 0 的调整比例')
return
}
if (!this.requiredRemark(this.batchForm.remark)) return
this.batchLoading = true
adjustPoolBatch({ audience: this.audience, type, ratio: this.batchForm.ratio, remark: this.batchForm.remark.trim() }).then(res => {
this.applyConfig(res.data)
this.$message.success(`批量${type === 1 ? '注水' : '抽水'}完成:${res.data.adjusted} 个箱子,共 ${this.fmt(res.data.totalAmount)}`)
this.batchForm.ratio = undefined
this.batchForm.remark = ''
this.queryParams.page = 1
this.loadLogs()
this.batchLoading = false
}).catch(() => { this.batchLoading = false })
},
submitReset() {
if (!this.requiredRemark(this.batchForm.remark)) return
this.resetLoading = true
adjustPoolReset({ audience: this.audience, remark: this.batchForm.remark.trim() }).then(res => {
this.applyConfig(res.data)
this.$message.success(`重置完成:${res.data.cleared} 个箱子的人工调整已归零(清除净调整 ${this.fmt(res.data.totalCleared)})`)
this.batchForm.remark = ''
this.queryParams.page = 1
this.loadLogs()
this.resetLoading = false
}).catch(() => { this.resetLoading = false })
}
}
}
</script>
<style scoped>
.page-head { margin-bottom: 14px; }
.page-head-row { display: flex; align-items: center; justify-content: space-between; }
.head-tip { color: #909399; font-size: 12px; margin: 6px 0 0; }
.audience-row { display: flex; align-items: center; margin-top: 10px; }
.aud-label { color: #606266; font-size: 13px; margin-right: 6px; }
.aud-tip { color: #909399; font-size: 12px; margin-left: 10px; }
.guide-panel { display: flex; gap: 16px; background: #f7f9fc; border: 1px solid #e4e9f2; border-radius: 8px; padding: 14px 16px; margin-top: 10px; }
.guide-col { flex: 1; min-width: 0; }
.guide-col h5 { margin: 0 0 8px; font-size: 13px; color: #303133; }
.guide-col h5 i { color: #409eff; margin-right: 4px; }
.guide-col ul { margin: 0; padding-left: 16px; }
.guide-col li { font-size: 12px; color: #606266; line-height: 1.9; }
.stat-row { margin-bottom: 16px; }
.stat-card { display: flex; align-items: center; gap: 12px; background: #fff; border: 1px solid #ebeef5; border-radius: 8px; padding: 14px 16px; box-shadow: 0 1px 4px rgba(0,0,0,.04); }
.sc-icon { width: 42px; height: 42px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 20px; flex-shrink: 0; }
.sc-blue { background: #ecf5ff; color: #409eff; }
.sc-orange { background: #fdf6ec; color: #e6a23c; }
.sc-cyan { background: #e8f7f7; color: #13a8a8; }
.sc-green { background: #f0f9eb; color: #67c23a; }
.sc-body small { color: #909399; display: block; }
.sc-body h3 { margin: 2px 0 0; font-size: 18px; }
.batch-card { margin-bottom: 16px; border-radius: 8px; }
.batch-card .sub-tip { color: #909399; font-size: 12px; margin-left: 10px; }
.fld-label { color: #606266; font-size: 13px; margin-right: 6px; }
.fld-label .req { color: #f56c6c; margin-left: 2px; }
.batch-card >>> .el-form-item { margin-bottom: 0; }
.est-text { color: #e6a23c; font-size: 13px; }
.section-head { display: flex; align-items: center; justify-content: space-between; margin: 16px 0 10px; }
</style>
@@ -1,82 +0,0 @@
<template>
<div class="app-container">
<el-form :inline="true" :model="queryParams" class="demo-form-inline">
<el-form-item label="回合ID">
<el-input v-model="queryParams.roundId" placeholder="输入回合ID" clearable />
</el-form-item>
<el-form-item label="模式类型">
<el-select v-model="queryParams.boxType" placeholder="全部" clearable>
<el-option v-for="m in modeTypes" :key="m.code" :label="m.name" :value="m.code" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="roundList" border style="width:100%">
<el-table-column prop="roundId" label="回合ID" width="100" align="center" />
<el-table-column prop="boxId" label="箱子ID" width="80" align="center" />
<el-table-column label="模式" min-width="120" align="center">
<template slot-scope="scope">{{ getModeName(scope.row.boxType) }}</template>
</el-table-column>
<el-table-column prop="ornamentId" label="饰品桶" width="80" align="center" />
<el-table-column prop="actualMul" label="返奖倍率" width="100" align="center" />
<el-table-column prop="freeSpinCount" label="免费旋转" width="90" align="center" />
<el-table-column prop="totalCascades" label="翻转步数" width="90" align="center" />
</el-table>
<el-pagination
style="margin-top:16px;text-align:right"
background
layout="total, prev, pager, next, sizes"
:total="total"
:page-size.sync="queryParams.pageSize"
:current-page.sync="queryParams.pageNum"
:page-sizes="[10,20,50,100]"
@size-change="loadData"
@current-change="loadData"
/>
</div>
</template>
<script>
import { getRoundList } from '@/api/game/olympus/round/api'
export default {
name: 'OlympusRound',
data() {
return {
loading: false, roundList: [], total: 0,
queryParams: { pageNum: 1, pageSize: 10, roundId: null, boxType: null },
modeTypes: [
{ code: 'normal', name: '普通旋转' },
{ code: 'ante', name: '底注' },
{ code: 'buy_fs', name: '购买免费旋转' }
]
}
},
created() { this.loadData() },
methods: {
getModeName(boxType) {
if (!boxType) return ''
const m = this.modeTypes.find(t => t.code === boxType)
return m ? m.name : boxType
},
loadData() {
this.loading = true
const params = { pageNum: this.queryParams.pageNum, pageSize: this.queryParams.pageSize }
if (this.queryParams.roundId) params.roundId = this.queryParams.roundId
if (this.queryParams.boxType) params.boxType = this.queryParams.boxType
getRoundList(params).then(res => {
this.roundList = (res.data && res.data.rows) || []
this.total = (res.data && res.data.total) || 0
this.loading = false
}).catch(() => { this.loading = false })
},
handleQuery() { this.queryParams.pageNum = 1; this.loadData() },
resetQuery() { this.queryParams = { pageNum: 1, pageSize: 10, roundId: null, boxType: null }; this.loadData() }
}
}
</script>
-578
View File
@@ -1,578 +0,0 @@
<template>
<div class="app-container">
<el-row :gutter="10" style="margin-bottom:16px">
<el-select v-model="queryParams.boxType" placeholder="按模式筛选" clearable @change="loadData" style="width:180px;margin-right:10px">
<el-option v-for="t in boxTypes" :key="t" :label="boxTypeLabels[t]" :value="t" />
</el-select>
<el-input v-model="queryParams.name" placeholder="按名称搜索" clearable @clear="loadData" @keyup.enter.native="loadData" style="width:200px">
<el-button slot="append" icon="el-icon-search" @click="loadData" />
</el-input>
<el-button icon="el-icon-refresh" @click="loadData">刷新</el-button>
<el-button type="danger" icon="el-icon-refresh" @click="handleRecalc" :loading="recalcLoading" style="float:right">一键重算分布</el-button>
</el-row>
<!-- 奖池水位控制 -->
<el-card shadow="never" style="margin-bottom:16px">
<div slot="header"><i class="el-icon-coin" /> 奖池水位控制</div>
<div style="color:#909399;font-size:12px;line-height:1.7;margin-bottom:10px">
【水位】= 这个游戏当前可用来赔大奖的“资金池”(≈ 真实玩家累计下注 − 累计赔奖,白号不算)。水越多越容易开出大奖,水越少越压着不出。<br>
【注水】= 手动把水位调高(更容易出大奖);【抽水】= 手动调低(压制大奖)。注/抽水只动这个内部资金池,不是真给玩家发钱,也不会改动首页/每日RTP 等报表数字。<br>
提醒:水位被抽得太低时,大奖会被压住,实时RTP 也可能到不了目标。
</div>
<el-row :gutter="20">
<el-col :span="8">
<div class="pool-stat"><span>累计投注</span><b>{{ pool.totalBet || 0 }}</b></div>
</el-col>
<el-col :span="8">
<div class="pool-stat"><span>累计返奖</span><b>{{ pool.totalWin || 0 }}</b></div>
</el-col>
<el-col :span="8">
<div class="pool-stat"><span>当前水位</span><b :style="{color: pool.balance < 0 ? '#f56c6c' : '#67c23a'}">{{ pool.balance || 0 }}</b></div>
</el-col>
</el-row>
<el-row :gutter="10" style="margin-top:12px">
<el-col :span="8">
<el-input v-model="addBetAmount" type="number" placeholder="金额" style="width:120px;margin-right:8px" />
<el-button type="success" size="small" icon="el-icon-bottom" @click="addBet" :loading="poolLoading">注水</el-button>
</el-col>
<el-col :span="8">
<el-input v-model="addWinAmount" type="number" placeholder="金额" style="width:120px;margin-right:8px" />
<el-button type="warning" size="small" icon="el-icon-top" @click="addWin" :loading="poolLoading">抽水</el-button>
</el-col>
<el-col :span="8">
<el-button size="small" icon="el-icon-refresh" @click="loadPool(); loadData()" :loading="poolLoading">刷新</el-button>
</el-col>
</el-row>
</el-card>
<el-table v-loading="loading" :data="boxList" border>
<el-table-column prop="boxId" label="ID" min-width="50" align="center" />
<el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
<el-table-column label="模式" min-width="120" align="center">
<template slot-scope="scope">{{ boxTypeLabels[scope.row.boxType] || scope.row.boxType }}</template>
</el-table-column>
<el-table-column prop="bet" label="赌注" min-width="70" align="center" />
<el-table-column prop="price" label="价格" min-width="80" align="center" />
<el-table-column prop="totalOrnaments" label="总回合" min-width="80" align="center" />
<el-table-column label="目标RTP" min-width="70" align="center">
<template slot-scope="scope">{{ (scope.row.rtp * 100).toFixed(1) }}%</template>
</el-table-column>
<!-- 实时RTP=本轮口径(Redis轮次账本 mulSum/drawn/cm,抽满一轮/重算分布自动重置);
如需改回全时段累计口径,把 cycleRtp 换回 actualRtp(>100飘红)即可 -->
<el-table-column label="实时RTP" min-width="80" align="center">
<template slot-scope="scope">
<span v-if="scope.row.cycleRtp != null" :style="{color: scope.row.cycleRtp > (scope.row.rtp * 100) ? '#f56c6c' : '#67c23a'}">
{{ scope.row.cycleRtp }}%
</span>
<span v-else style="color:#c0c4cc">-</span>
</template>
</el-table-column>
<el-table-column label="状态" min-width="60" align="center">
<template slot-scope="scope">
<el-tag :type="scope.row.status === '1' ? 'success' : 'danger'" size="small">
{{ scope.row.status === '1' ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" min-width="260" align="center">
<template slot-scope="scope">
<el-button size="mini" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="mini" type="info" @click="handleOrnaments(scope.row)">饰品配置</el-button>
<el-button size="mini" type="success" @click="openTemplateManager(scope.row)">模板管理</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
style="margin-top:16px;text-align:right"
background
layout="total, prev, pager, next, sizes"
:total="total"
:page-size.sync="queryParams.pageSize"
:current-page.sync="queryParams.pageNum"
:page-sizes="[10,20,50,100]"
@size-change="loadData"
@current-change="loadData"
/>
<!-- 编辑弹窗 -->
<el-dialog :title="'编辑箱子 - ' + form.name" :visible.sync="dialogVisible" width="480px" append-to-body>
<el-form ref="form" :model="form" label-width="100px">
<el-form-item label="模式类型">
<el-input :value="boxTypeLabels[form.boxType] || form.boxType" disabled />
</el-form-item>
<el-form-item label="基本赌注">
<el-input-number v-model="form.bet" disabled style="width:100%" />
</el-form-item>
<el-form-item label="价格(自动)">
<el-input-number v-model="form.price" disabled style="width:100%" />
<span style="color:#909399;font-size:12px">价格 = 赌注 × 倍率系数</span>
</el-form-item>
<el-form-item label="总回合数">
<el-input-number v-model="form.totalRounds" :min="0" :step="50" style="width:100%" />
<span style="color:#909399;font-size:12px">修改后将重新计算饰品分布</span>
</el-form-item>
<el-form-item label="RTP">
<el-input-number v-model="form.rtp" :min="0" :max="1" :precision="3" :step="0.05" style="width:100%" />
</el-form-item>
<el-form-item label="固定/收敛回合%">
<el-input-number v-model="form.fixedPct" :min="0" :max="100" :step="5" @change="onFixedPctChange" style="width:46%" />
<span style="margin:0 2%">/</span>
<el-input-number v-model="form.convergePct" :min="0" :max="100" :step="5" @change="onConvergePctChange" style="width:46%" />
<div style="color:#909399;font-size:12px">固定% + 收敛% = 100;收敛回合用于一轮内把实时RTP动态拉回目标</div>
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio label="1">启用</el-radio>
<el-radio label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="submitForm">确 定</el-button>
</div>
</el-dialog>
<!-- 饰品配置弹窗 -->
<el-dialog :title="'饰品配置 - ' + (boxTypeLabels[currentBox.boxType] || currentBox.boxType) + ' (' + currentBox.price + ')'" :visible.sync="ornamentVisible" width="650px" append-to-body>
<div style="display:flex;align-items:center;margin-bottom:10px">
<el-alert type="info" :closable="false" style="flex:1">
计算回合(固定): <b>{{ ornamentTotal }}</b> &nbsp;|&nbsp;
收敛回合: <b>{{ convergeRoundsDerived }}</b> &nbsp;|&nbsp;
总回合: <b>{{ totalRoundsDerived }}</b> &nbsp;|&nbsp;
计算RTP: <b>{{ ornamentRTP }}%</b> &nbsp;|&nbsp;
目标RTP: <b>{{ (currentBox.rtp * 100).toFixed(1) }}%</b>
</el-alert>
<el-button type="warning" size="small" icon="el-icon-refresh" @click="handleOrnamentRecalc" style="margin-left:10px;flex-shrink:0">重算分布</el-button>
</div>
<el-table :data="ornamentList" border max-height="400">
<el-table-column prop="id" label="ID" width="50" align="center" />
<el-table-column label="饰品" min-width="100" align="center">
<template slot-scope="scope">{{ getOrnName(scope.row.name) }}</template>
</el-table-column>
<el-table-column label="倍率范围" min-width="110" align="center">
<template slot-scope="scope">{{ scope.row.rangeLow }} - {{ scope.row.rangeHigh }}</template>
</el-table-column>
<el-table-column label="回合数" min-width="130" align="center">
<template slot-scope="scope">
<el-input-number v-model="scope.row.count" :min="0" :step="10" size="small" controls-position="right" />
</template>
</el-table-column>
<el-table-column label="剩余" min-width="70" align="center">
<template slot-scope="scope">
<span :style="{color: (scope.row.remaining != null ? scope.row.remaining : scope.row.count) <= 0 ? '#f56c6c' : '#67c23a'}">{{ scope.row.remaining != null ? scope.row.remaining : scope.row.count }}</span>
</template>
</el-table-column>
<el-table-column label="动态收敛回合" min-width="90" align="center">
<template slot-scope="scope">
<span style="color:#e6a23c">{{ scope.row.convergeCount || 0 }}</span>
</template>
</el-table-column>
</el-table>
<div slot="footer">
<el-button @click="ornamentVisible = false">取 消</el-button>
<el-button type="success" icon="el-icon-document-add" @click="handleSaveAsTemplate">保存为模板</el-button>
<el-button type="primary" @click="saveOrnaments">保存</el-button>
</div>
</el-dialog>
<!-- 模板管理对话框(按箱子隔离) -->
<el-dialog :title="'模板管理 - ' + (boxTypeLabels[templateBox.boxType] || templateBox.boxType || '') + ' (ID:' + templateBox.boxId + ')'" :visible.sync="templateDialogVisible" width="680px" append-to-body>
<el-alert type="info" :closable="false" style="margin-bottom:12px">
模板保存了该箱子的饰品回合分布。选择模板后点击【应用】可将饰品数量恢复为模板保存时的值。
</el-alert>
<el-table :data="boxTemplates" border v-if="boxTemplates.length > 0" highlight-current-row @current-change="onTemplateRowChange">
<el-table-column label="模板名称" min-width="160">
<template slot-scope="scope">
{{ scope.row.name }}
<el-tag v-if="scope.row.isActive === 1" type="success" size="mini" style="margin-left:6px">生效中</el-tag>
</template>
</el-table-column>
<el-table-column label="饰品数" width="80" align="center">
<template slot-scope="scope">{{ scope.row.items ? scope.row.items.length : 0 }}</template>
</el-table-column>
<el-table-column label="固定回合" width="80" align="center">
<template slot-scope="scope">{{ templateTotalRounds(scope.row) }}</template>
</el-table-column>
<el-table-column label="总回合" width="90" align="center">
<template slot-scope="scope">{{ templateDerivedTotal(scope.row) }}</template>
</el-table-column>
<el-table-column label="保存时间" width="170" align="center">
<template slot-scope="scope">{{ scope.row.createTime }}</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center">
<template slot-scope="scope">
<el-button size="mini" type="danger" @click="handleDeleteTemplate(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-else description="该箱子暂无模板,请在饰品配置中修改数量后点击【保存为模板】。" />
<div slot="footer">
<el-button @click="templateDialogVisible = false">关 闭</el-button>
<el-button type="primary" :disabled="!selectedTemplateRow" @click="handleApplyTemplate">应用选中模板</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { getBoxList, saveBox, getBoxOrnaments, saveBoxOrnaments, calcDistribution, simulateRtp, getBoxTemplates, saveBoxTemplate, deleteBoxTemplate, applyBoxTemplate, getPoolStatus, addPoolBet, addPoolWin } from '@/api/game/sweet/box/api'
import { getOrnamentList } from '@/api/game/sweet/ornament/api'
export default {
name: 'SweetBox',
data() {
return {
loading: false, recalcLoading: false, boxList: [], total: 0,
queryParams: { pageNum: 1, pageSize: 10, name: null, boxType: null },
dialogVisible: false, ornamentVisible: false, isEdit: false, currentBox: {}, origTotalRounds: 0,
fixedRounds: 0, convergeRounds: 0,
form: { boxId: null, name: '', bet: 1, boxType: 'normal', rtp: 0.8, status: '1', price: 0, totalRounds: 0, fixedPct: 85, convergePct: 15 },
boxTypes: ['normal','ante','buy_fs','buy_super_fs'],
boxTypeLabels: {
'normal':'普通旋转', 'ante':'底注',
'buy_fs':'购买免费旋转', 'buy_super_fs':'购买超级免费旋转'
},
boxTypeCM: { 'normal':1, 'ante':2, 'buy_fs':50, 'buy_super_fs':250 },
modeOrnRange: {
'normal': [1,15], 'ante': [1,17], 'buy_fs': [5,24], 'buy_super_fs': [6,31]
},
ornamentList: [], _ornamentAll: [], _ornamentExpects: {},
estimatedRTP: '--', simRtpLoading: false,
pool: { totalBet: 0, totalWin: 0, balance: 0, histRtp: '0%' },
poolLoading: false, addBetAmount: null, addWinAmount: null,
// 模板管理
templateDialogVisible: false,
templateBox: {},
boxTemplates: [],
selectedTemplateRow: null
}
},
created() { this.loadData(); this.loadPool() },
computed: {
ornamentTotal() { return this.ornamentList.reduce((s, o) => s + (o.count || 0), 0) },
ornamentRTP() {
const total = this.ornamentTotal
if (!total || !this.currentBox.bet) return '0.0'
let contrib = 0
this.ornamentList.forEach(o => {
const mul = this._ornamentExpects[o.id]
if (mul != null) contrib += (o.count || 0) * mul
})
const cm = this.boxTypeCM[this.currentBox.boxType] || 1
return (contrib / total / cm * 100).toFixed(1)
},
// 收敛占比(当前箱子,空回退 0.15,兑底 [0,0.9])
convergeRatioVal() {
let cr = this.currentBox.rtpConvergeRatio != null ? Number(this.currentBox.rtpConvergeRatio) : 0.15
if (!(cr >= 0)) cr = 0.15
return Math.min(0.9, Math.max(0, cr))
},
// 由固定回合(计算回合)之和反推总回合 N = round(fixed/(1-cr))
totalRoundsDerived() {
const fixed = this.ornamentTotal
return fixed > 0 ? Math.round(fixed / (1 - this.convergeRatioVal)) : 0
},
// 收敛回合 = 总回合 - 固定回合
convergeRoundsDerived() {
return Math.max(0, this.totalRoundsDerived - this.ornamentTotal)
}
},
methods: {
// ==================== 模板加载(后端持久化) ====================
_loadBoxTemplates(boxId) {
getBoxTemplates(boxId).then(res => {
this.boxTemplates = (res.data && res.data.list) || []
}).catch(() => { this.boxTemplates = [] })
},
// ==================== 保存为模板(在饰品配置弹窗中) ====================
async handleSaveAsTemplate() {
const items = this.ornamentList.filter(o => o.count > 0).map(o => ({
ornamentId: o.id,
count: o.count
}))
if (items.length === 0) {
return this.$message.warning('当前没有配置任何饰品数量,无法保存模板')
}
const boxId = this.currentBox.boxId
// 拉取已有模板名,用于不区分大小写的去重校验
let existingNames = new Set()
try {
const res = await getBoxTemplates(boxId)
const list = (res.data && res.data.list) || []
existingNames = new Set(list.map(t => (t.name || '').toLowerCase().trim()))
} catch { /* ignore */ }
this.$prompt('请输入模板名称(同一箱子内名称不可重复)', '保存为模板', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /\S+/,
inputErrorMessage: '名称不能为空',
inputValidator: (value) => {
if (!value || !value.trim()) return '名称不能为空'
if (existingNames.has(value.trim().toLowerCase())) {
return '模板名称已存在,请使用不同的名称'
}
return true
}
}).then(async ({ value }) => {
const trimmedName = value.trim()
try {
await saveBoxTemplate({ boxId, name: trimmedName, items })
this.$message.success('模板【' + trimmedName + '】已保存')
} catch (e) {
this.$message.error('保存失败: ' + (e.message || e))
}
}).catch(() => {})
},
// ==================== 模板管理对话框 ====================
openTemplateManager(row) {
this.templateBox = row
this.boxTemplates = []
this.selectedTemplateRow = null
this.templateDialogVisible = true
this._loadBoxTemplates(row.boxId)
},
onTemplateRowChange(row) {
this.selectedTemplateRow = row
},
templateTotalRounds(tpl) {
if (!tpl.items) return 0
return tpl.items.reduce((s, i) => s + (i.count || 0), 0)
},
// 模板存的是固定池分布(固定回合),按当前箱子收敛占比反推总回合 N = round(fixed/(1-cr))
templateDerivedTotal(tpl) {
const fixed = this.templateTotalRounds(tpl)
let cr = this.templateBox.rtpConvergeRatio != null ? Number(this.templateBox.rtpConvergeRatio) : 0.15
if (!(cr >= 0)) cr = 0.15
cr = Math.min(0.9, Math.max(0, cr))
return fixed > 0 ? Math.round(fixed / (1 - cr)) : 0
},
handleDeleteTemplate(tpl) {
this.$confirm('确定删除模板【' + tpl.name + '】吗?', '提示', { type: 'warning' }).then(async () => {
try {
await deleteBoxTemplate(tpl.id)
this.selectedTemplateRow = null
this.$message.success('已删除')
this._loadBoxTemplates(this.templateBox.boxId)
} catch (e) {
this.$message.error('删除失败: ' + (e.message || e))
}
}).catch(() => {})
},
async handleApplyTemplate() {
if (!this.selectedTemplateRow) return
const tpl = this.selectedTemplateRow
const boxId = this.templateBox.boxId
try {
await this.$confirm('将模板【' + tpl.name + '】的饰品分布应用到该箱子,并设为抽干补桶依据,确认?', '应用模板', { type: 'warning' })
} catch { return }
// 1. 应用模板(写入分布 + 标记为该箱子生效模板,作为抽干补桶依据)
try {
await applyBoxTemplate({ templateId: tpl.id })
// 立即更新本地表格行中的总回合数(模板存固定回合,按收敛占比反推 N)
const newTotal = this.templateDerivedTotal(tpl)
this._updateBoxTotalOrnaments(boxId, newTotal)
this.$message.success('模板已应用,饰品分布已更新')
} catch (e) {
this.$message.error('应用失败: ' + (e.message || e))
return
}
// 2. 关闭模板对话框
this.templateDialogVisible = false
// 3. 如果饰品配置弹窗正打开着同一个箱子,立即刷新饰品列表
if (this.ornamentVisible && this.currentBox.boxId === boxId) {
this._refreshOrnamentList(boxId)
}
this.loadData()
},
// ==================== 原有功能 ====================
async handleRecalc() {
this.$confirm('确认重算所有箱子的饰品分布吗?将根据当前 RTP 和总回合数重新分配。', '提示', { confirmButtonText: '确定', type: 'warning' })
.then(async () => {
this.recalcLoading = true
try {
const res = await getBoxList({ pageNum: 1, pageSize: 100 })
const allBoxes = res.data && res.data.rows ? res.data.rows : []
for (const box of allBoxes) {
const rtp = box.rtp || 0.8
const rounds = box.totalOrnaments || 200
const convergeRatio = box.rtpConvergeRatio != null ? box.rtpConvergeRatio : 0.15
await calcDistribution({ boxId: box.boxId, totalRounds: rounds, rtp, convergeRatio })
}
this.$message.success('全部重算完成')
this.loadData()
} catch (e) { /* ignore */ }
this.recalcLoading = false
}).catch(() => {})
},
loadData() {
this.loading = true
const params = { pageNum: this.queryParams.pageNum, pageSize: this.queryParams.pageSize }
if (this.queryParams.name) params.name = this.queryParams.name
if (this.queryParams.boxType) params.boxType = this.queryParams.boxType
getBoxList(params).then(res => {
this.boxList = res.data.rows || []
this.total = res.data.total || 0
this.loading = false
})
},
async loadPool() {
this.poolLoading = true
try {
const res = await getPoolStatus()
if (res.data) this.pool = res.data
} catch (e) { /* ignore */ }
this.poolLoading = false
},
async addBet() {
if (!this.addBetAmount || this.addBetAmount <= 0) return this.$message.warning('请输入金额')
this.poolLoading = true
try {
await addPoolBet({ amount: this.addBetAmount })
this.$message.success('注水成功')
this.addBetAmount = null
await this.loadPool()
} catch (e) { this.$message.error('操作失败') }
this.poolLoading = false
},
async addWin() {
if (!this.addWinAmount || this.addWinAmount <= 0) return this.$message.warning('请输入金额')
this.poolLoading = true
try {
await addPoolWin({ amount: this.addWinAmount })
this.$message.success('抽水成功')
this.addWinAmount = null
await this.loadPool()
} catch (e) { this.$message.error('操作失败') }
this.poolLoading = false
},
handleEdit(row) {
this.isEdit = true
this.origTotalRounds = row.totalOrnaments || 0
const convergePct = row.rtpConvergeRatio != null ? Math.round(row.rtpConvergeRatio * 100) : 15
this.form = { ...row, totalRounds: row.totalOrnaments || 0, rtp: row.rtp || 0.8, convergePct, fixedPct: 100 - convergePct }
this.dialogVisible = true
},
onFixedPctChange(v) {
const val = Math.min(100, Math.max(0, v || 0))
this.form.fixedPct = val
this.form.convergePct = 100 - val
},
onConvergePctChange(v) {
const val = Math.min(100, Math.max(0, v || 0))
this.form.convergePct = val
this.form.fixedPct = 100 - val
},
submitForm() {
const convergeRatio = (this.form.convergePct != null ? this.form.convergePct : 15) / 100
const params = { boxId: this.form.boxId, name: this.form.name, bet: this.form.bet, boxType: this.form.boxType, rtp: this.form.rtp, status: this.form.status, rtpConvergeRatio: convergeRatio }
saveBox(params).then(res => {
const boxId = this.form.boxId || (res.data && res.data.boxId)
if (boxId) {
calcDistribution({ boxId, totalRounds: this.form.totalRounds, rtp: this.form.rtp, convergeRatio }).then(() => {
this.$message.success('修改成功,饰品分布已更新'); this.dialogVisible = false; this.loadData()
}).catch(() => { this.$message.success('修改成功(分布计算失败)'); this.dialogVisible = false; this.loadData() })
}
})
},
handleOrnaments(row) {
this.currentBox = row
getOrnamentList().then(ornRes => {
this._ornamentAll = (ornRes.data && ornRes.data.list) ? ornRes.data.list : []
this._ornamentExpects = {}
this._ornamentAll.forEach(o => { this._ornamentExpects[o.id] = Number(o.expectMul) })
this._refreshOrnamentList(row.boxId)
})
},
_refreshOrnamentList(boxId) {
getBoxOrnaments(boxId).then(res => {
const items = res.data && res.data.list ? res.data.list : []
this.fixedRounds = (res.data && res.data.fixedRounds) || 0
this.convergeRounds = (res.data && res.data.convergeRounds) || 0
const map = {}
items.forEach(it => { map[it.ornamentId] = { count: it.count, remaining: it.remaining, convergeCount: it.convergeCount } })
const [lo, hi] = this.modeOrnRange[this.currentBox.boxType] || [1, 17]
this.ornamentList = this._ornamentAll
.filter(o => o.id >= lo && o.id <= hi)
.map(o => ({ ...o, count: (map[o.id] && map[o.id].count) || 0, remaining: map[o.id] ? map[o.id].remaining : 0, convergeCount: map[o.id] ? map[o.id].convergeCount : 0 }))
if (!this.ornamentVisible) this.ornamentVisible = true
this.fetchEstimatedRtp()
})
},
/** 调用后端模拟API获取预估实际RTP */
fetchEstimatedRtp() {
const items = this.ornamentList.filter(o => o.count > 0).map(o => ({
ornamentId: o.id,
count: o.count
}))
if (items.length === 0) {
this.estimatedRTP = '--'
return
}
this.simRtpLoading = true
simulateRtp({
items: items,
boxType: this.currentBox.boxType,
rtp: this.currentBox.rtp || 0.8
}).then(res => {
this.estimatedRTP = (res.data && res.data.estimatedRtp) ? res.data.estimatedRtp.replace('%', '') : '--'
}).catch(() => {
this.estimatedRTP = '--'
}).finally(() => {
this.simRtpLoading = false
})
},
getOrnName(name) { return name ? name.replace('x', '倍饰品') : name },
handleOrnamentRecalc() {
const box = this.currentBox
const convergeRatio = box.rtpConvergeRatio != null ? box.rtpConvergeRatio : 0.15
calcDistribution({ boxId: box.boxId, totalRounds: box.totalOrnaments || 200, rtp: box.rtp || 0.8, convergeRatio }).then(r => {
if (r.data && r.data.items) {
const map = {}
r.data.items.forEach(it => { map[it.ornamentId] = it.count })
const [lo, hi] = this.modeOrnRange[box.boxType] || [1, 17]
this.ornamentList = this._ornamentAll
.filter(o => o.id >= lo && o.id <= hi)
.map(o => ({ ...o, count: map[o.id] || 0, remaining: map[o.id] || 0 }))
// 同步更新表格行的总回合数(后端已按收敛占比反推 N,优先用返回的 boxTotal)
const fixed = this.ornamentList.reduce((s, o) => s + (o.count || 0), 0)
const newTotal = (r.data && r.data.boxTotal) || (fixed > 0 ? Math.round(fixed / (1 - this.convergeRatioVal)) : 0)
this._updateBoxTotalOrnaments(box.boxId, newTotal)
this.$message.success('分布已重算并保存')
this.fetchEstimatedRtp()
}
}).catch(() => { this.$message.error('重算失败') })
},
saveOrnaments() {
const fixed = this.ornamentList.reduce((s, o) => s + (o.count || 0), 0)
const newTotal = fixed > 0 ? Math.round(fixed / (1 - this.convergeRatioVal)) : 0
saveBoxOrnaments({
boxId: this.currentBox.boxId,
items: this.ornamentList.filter(o => o.count > 0).map(o => ({ ornamentId: o.id, count: o.count }))
}).then(() => {
this.$message.success('保存成功')
// 立即更新本地表格行中的总回合数,避免显示旧值
this._updateBoxTotalOrnaments(this.currentBox.boxId, newTotal)
this.ornamentVisible = false
this.loadData()
})
},
/** 更新 boxList 中指定箱子的 totalOrnaments(本地立即生效,不等后端返回) */
_updateBoxTotalOrnaments(boxId, newTotal) {
const box = this.boxList.find(b => b.boxId === boxId)
if (box) {
this.$set(box, 'totalOrnaments', newTotal)
}
// 同步更新 currentBox 引用
if (this.currentBox.boxId === boxId) {
this.$set(this.currentBox, 'totalOrnaments', newTotal)
}
},
}
}
</script>
@@ -1,80 +0,0 @@
<template>
<div class="app-container">
<el-row :gutter="16" style="margin-bottom:16px">
<el-col :span="6"><el-card shadow="hover"><div style="text-align:center"><small>累计投注</small><h3>{{ summary.totalCost }}</h3></div></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><div style="text-align:center"><small>累计赢奖</small><h3>{{ summary.totalWin }}</h3></div></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><div style="text-align:center"><small>整体RTP</small><h3 :style="{color: rtp > 100 ? 'red' : 'green'}">{{ rtp }}%</h3></div></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><div style="text-align:center"><small>总局数</small><h3>{{ summary.totalSpins }}</h3></div></el-card></el-col>
</el-row>
<div style="margin-bottom:16px">
<el-date-picker v-model="queryParams.dateStart" type="date" placeholder="开始日期" value-format="yyyy-MM-dd" style="width:150px;margin-right:8px" />
<span style="margin:0 4px">至</span>
<el-date-picker v-model="queryParams.dateEnd" type="date" placeholder="结束日期" value-format="yyyy-MM-dd" style="width:150px;margin-right:8px" />
<el-button type="primary" icon="el-icon-search" @click="loadAll">查询</el-button>
<el-button icon="el-icon-refresh" @click="handleReset">重置</el-button>
</div>
<el-table v-loading="loading" :data="list" border>
<el-table-column prop="date" label="日期" min-width="120" align="center" />
<el-table-column prop="totalCost" label="总投注" min-width="120" align="center" />
<el-table-column prop="totalWin" label="总赢奖" min-width="120" align="center" />
<el-table-column prop="totalSpins" label="总局数" min-width="80" align="center" />
<el-table-column prop="playerCount" label="玩家数" min-width="80" align="center" />
<el-table-column label="RTP" min-width="90" align="center">
<template slot-scope="scope">
<span :style="{color: scope.row.totalCost > 0 && scope.row.totalWin / scope.row.totalCost > 1 ? 'red' : 'green'}">
{{ scope.row.totalCost > 0 ? (Math.round(scope.row.totalWin / scope.row.totalCost * 10000) / 100).toFixed(2) + '%' : '-' }}
</span>
</template>
</el-table-column>
</el-table>
<el-pagination
style="margin-top:16px;text-align:right"
background
layout="total, prev, pager, next, sizes"
:total="total"
:page-size.sync="queryParams.pageSize"
:current-page.sync="queryParams.pageNum"
:page-sizes="[10,20,50]"
@size-change="loadData"
@current-change="loadData"
/>
</div>
</template>
<script>
import { getWinList } from '@/api/game/sweet/dailywin/api'
import request from '@/utils/request'
export default {
name: 'SweetDailyWin',
data() {
return {
loading: false, list: [], total: 0, summary: {},
queryParams: { pageNum: 1, pageSize: 10, dateStart: new Date().toISOString().slice(0, 10), dateEnd: null }
}
},
computed: {
rtp() {
const c = Number(this.summary.totalCost) || 0, w = Number(this.summary.totalWin) || 0
return c > 0 ? (Math.round(w / c * 10000) / 100).toFixed(2) : '0.00'
}
},
created() { this.loadAll() },
methods: {
loadAll() { this.loadSummary(); this.loadData() },
loadSummary() {
request({ url: '/admin/sweet/win/summary', method: 'get', params: { dateStart: this.queryParams.dateStart, dateEnd: this.queryParams.dateEnd } }).then(res => {
this.summary = res.data || {}
})
},
loadData() {
this.loading = true
getWinList(this.queryParams).then(res => { this.list = res.data.rows || []; this.total = res.data.total || 0; this.loading = false })
},
handleReset() { this.queryParams = { pageNum: 1, pageSize: 10, dateStart: new Date().toISOString().slice(0, 10), dateEnd: null }; this.loadAll() }
}
}
</script>
@@ -1,22 +0,0 @@
<template>
<div class="app-container">
<el-table v-loading="loading" :data="list" border>
<el-table-column prop="id" label="ID" width="60" />
<el-table-column label="饰品名称">
<template slot-scope="scope">{{ scope.row.name.replace('x', '倍饰品') }}</template>
</el-table-column>
<el-table-column prop="expectMul" label="预期倍率" />
<el-table-column prop="rangeLow" label="倍率下限" />
<el-table-column prop="rangeHigh" label="倍率上限" />
</el-table>
</div>
</template>
<script>
import { getOrnamentList } from '@/api/game/sweet/ornament/api'
export default {
name: 'SweetOrnament',
data() { return { loading: false, list: [] } },
created() { this.loading = true; getOrnamentList().then(res => { this.list = res.data.list; this.loading = false }) }
}
</script>
@@ -1,125 +0,0 @@
<template>
<div class="app-container">
<el-table v-loading="loading" :data="list" border>
<el-table-column prop="id" label="ID" width="60" />
<el-table-column label="名称" min-width="180" align="center">
<template slot-scope="scope">
<span v-if="scope.row.id !== editingId" class="editable-name" @click="startEdit(scope.row)">{{ scope.row.name }}</span>
<el-input v-else v-model="editName" size="mini" @blur="finishEdit(scope.row)" @keyup.enter.native="finishEdit(scope.row)" ref="nameInput" />
</template>
</el-table-column>
<el-table-column prop="grade" label="品质" width="70" align="center">
<template slot-scope="scope">
<el-tag v-if="scope.row.grade === 1" type="warning" size="small">金</el-tag>
<el-tag v-else-if="scope.row.grade === 2" type="danger" size="small">红</el-tag>
<el-tag v-else-if="scope.row.grade === 3" size="small">紫</el-tag>
<el-tag v-else-if="scope.row.grade === 4" type="success" size="small">绿</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="饰品图片" width="80" align="center">
<template slot-scope="scope">
<el-image v-if="scope.row.imageUrl" :src="getImg(scope.row.imageUrl)" style="width:40px;height:40px" fit="cover" />
</template>
</el-table-column>
<el-table-column label="品质图片" width="80" align="center">
<template slot-scope="scope">
<el-image v-if="scope.row.levelImg" :src="getImg(scope.row.levelImg)" style="width:40px;height:40px" fit="cover" />
</template>
</el-table-column>
<el-table-column label="兜底" width="70" align="center">
<template slot-scope="scope">
<el-tag v-if="scope.row.isFallback === 1" type="danger" size="small">是</el-tag>
<el-tag v-else type="info" size="small">否</el-tag>
</template>
</el-table-column>
<el-table-column label="磨损" width="110" align="center">
<template slot-scope="scope">
<span>{{ scope.row.exteriorName || '崭新出厂' }}</span>
</template>
</el-table-column>
<el-table-column label="返奖区间" width="180" align="center">
<template slot-scope="scope">
<span v-if="scope.row.winLow != null">{{ scope.row.winLow }} ~ {{ scope.row.winHigh }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="操作" width="80">
<template slot-scope="scope">
<el-button size="mini" @click="handleEdit(scope.row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog title="编辑奖品" :visible.sync="dialogVisible" width="480px">
<el-form ref="form" :model="form" label-width="80px">
<el-form-item label="名称"><el-input v-model="form.name" /></el-form-item>
<el-form-item label="品质">
<el-select v-model="form.grade" style="width:100%">
<el-option label="1 - 金" :value="1" />
<el-option label="2 - 红" :value="2" />
<el-option label="3 - 紫" :value="3" />
<el-option label="4 - 绿" :value="4" />
</el-select>
</el-form-item>
<el-form-item label="饰品图片">
<ImageUpload v-model="form.imageUrl" :limit="1" />
</el-form-item>
<el-form-item label="磨损">
<el-select v-model="form.exteriorName" placeholder="请选择磨损度" clearable style="width:100%">
<el-option label="崭新出厂" value="崭新出厂" />
<el-option label="略有磨损" value="略有磨损" />
<el-option label="久经沙场" value="久经沙场" />
<el-option label="破损不堪" value="破损不堪" />
<el-option label="战痕累累" value="战痕累累" />
</el-select>
</el-form-item>
<el-form-item label="兜底饰品">
<el-switch v-model="form.isFallback" :active-value="1" :inactive-value="0" active-text="是" inactive-text="否" />
<div style="color:#909399;font-size:12px;margin-top:4px">可设置多个兜底饰品,未命中区间时随机选择一个兜底返奖</div>
</el-form-item>
<el-form-item label="返奖区间">
<div style="display:flex;align-items:center;gap:8px">
<el-input-number v-model="form.winLow" :min="0" :max="form.winHigh" :precision="2" :step="0.01" controls-position="right" placeholder="最小值" style="flex:1" />
<span>~</span>
<el-input-number v-model="form.winHigh" :min="form.winLow" :precision="2" :step="0.01" controls-position="right" placeholder="最大值" style="flex:1" />
</div>
<div style="color:#909399;font-size:12px;margin-top:4px">玩家返奖金额落在该区间即获得此饰品</div>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { prizeList, prizeSave } from "@/api/game/sweet/prize/api";
import ImageUpload from "@/components/ImageUpload";
export default {
name: "SweetPrize",
components: { ImageUpload },
data() {
return { loading: false, list: [], dialogVisible: false, form: {}, editingId: null, editName: '', rangeEditId: null, rangeLow: 0, rangeHigh: 0 };
},
computed: { fallbackExists() { return this.list.some(o => o.isFallback === 1); } },
created() { this.getList(); },
methods: {
getList() { this.loading = true; prizeList().then(r => { this.list = r.data.list || []; this.loading = false; }); },
getImg(u) { return u ? (u.startsWith('http') ? u : (process.env.VUE_APP_BASE_API || '') + u) : ''; },
startEdit(row) { this.editingId = row.id; this.editName = row.name; this.$nextTick(() => { this.$refs.nameInput && this.$refs.nameInput.focus(); }); },
finishEdit(row) { if (!this.editName || !this.editName.trim()) { this.$message.warning("名称不能为空"); this.editingId = null; return; } if (this.editName !== row.name) { row.name = this.editName.trim(); prizeSave(row).then(() => this.$message.success("已更新")); } this.editingId = null; },
startRangeEdit(row) { this.rangeEditId = row.id; this.rangeLow = row.winLow; this.rangeHigh = row.winHigh; },
finishRangeEdit(row) { if (this.rangeLow !== row.winLow || this.rangeHigh !== row.winHigh) { row.winLow = this.rangeLow; row.winHigh = this.rangeHigh; prizeSave(row).then(() => this.$message.success("区间已更新")); } this.rangeEditId = null; },
handleEdit(row) { this.form = { ...row }; this.dialogVisible = true; },
submitForm() { prizeSave(this.form).then(() => { this.$message.success("保存成功"); this.dialogVisible = false; this.getList(); }); },
}
};
</script>
<style scoped>
.editable-name { color: #409eff; cursor: pointer; border-bottom: 1px dashed #409eff; }
.editable-name:hover { color: #66b1ff; }
</style>
@@ -1,101 +0,0 @@
<template>
<div class="app-container">
<el-form :inline="true" class="demo-form-inline">
<el-form-item label="模式类型">
<el-select v-model="queryParams.boxType" placeholder="全部" clearable>
<el-option v-for="m in modeList" :key="m.key" :label="m.name" :value="m.key" />
</el-select>
</el-form-item>
<el-form-item label="玩家ID">
<el-input v-model="queryParams.userId" placeholder="输入玩家ID" clearable />
</el-form-item>
<el-form-item label="日期">
<el-date-picker v-model="dateRange" type="daterange" range-separator="~" start-placeholder="开始" end-placeholder="结束" value-format="yyyy-MM-dd" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadData">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
<el-button type="danger" @click="clearAll">清除所有数据</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="list" border>
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="userId" label="用户ID" align="center" />
<el-table-column prop="nickName" label="昵称" align="center" />
<el-table-column label="模式" width="140" align="center">
<template slot-scope="scope">{{ getModeName(scope.row.modeType) }}</template>
</el-table-column>
<el-table-column prop="prizeName" label="饰品名称" width="160" align="center" />
<el-table-column label="饰品图片" width="80" align="center">
<template slot-scope="scope">
<el-image v-if="scope.row.prizeImage" :src="getImg(scope.row.prizeImage)" style="width:40px;height:40px" fit="cover" />
</template>
</el-table-column>
<el-table-column prop="cost" label="花费" width="90" align="center" />
<el-table-column prop="totalWin" label="赢取" width="90" align="center" />
<el-table-column prop="createTime" label="时间" width="155" align="center" />
<el-table-column label="操作" width="70" align="center">
<template slot-scope="scope">
<el-button size="mini" type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
style="margin-top:16px;text-align:right"
background
layout="total, prev, pager, next, sizes"
:total="total"
:page-size.sync="queryParams.pageSize"
:current-page.sync="queryParams.pageNum"
:page-sizes="[10,20,50]"
@size-change="loadData"
@current-change="loadData"
/>
</div>
</template>
<script>
import { getRecordList } from '@/api/game/sweet/record/api'
import request from '@/utils/request'
export default {
name: 'SweetRecord',
data() {
return {
loading: false, list: [], total: 0,
queryParams: { pageNum: 1, pageSize: 10, userId: null, boxType: null }, dateRange: null,
modeList: [{key:'normal',name:'普通'},{key:'ante',name:'底注'},{key:'buy_fs',name:'购买免费旋转'},{key:'buy_super_fs',name:'购买超级免费旋转'}],
// 卡片顺序:普通 → 底注 → 购FS → 购SuperFS
cardOrder: [
{ key: 'normal', name: '普通' },
{ key: 'ante', name: '底注' },
{ key: 'buy_fs', name: '购买免费旋转' },
{ key: 'buy_super_fs', name: '购买超级免费旋转' }
]
}
},
created() { this.loadData() },
methods: {
getModeName(type) { const card = this.cardOrder.find(c => c.key === type); return card ? card.name : type },
loadData() {
this.loading = true
const params = { pageNum: this.queryParams.pageNum, pageSize: this.queryParams.pageSize }
if (this.queryParams.userId) params.userId = this.queryParams.userId
if (this.queryParams.boxType) params.boxType = this.queryParams.boxType
if (this.dateRange) { params.dateStart = this.dateRange[0]; params.dateEnd = this.dateRange[1] }
getRecordList(params).then(res => { this.list = res.data.rows; this.total = res.data.total; this.loading = false })
},
resetQuery() { this.queryParams = { pageNum: 1, pageSize: 10, userId: null, boxType: null }; this.dateRange = null; this.loadData(); },
handleDelete(row) { this.$confirm('确认删除此记录?').then(() => { request({ url: '/admin/sweet/record/' + row.id, method: 'delete' }).then(() => { this.$message.success('已删除'); this.loadData(); }) }) },
clearAll() { this.$confirm('确认清除所有数据?此操作不可恢复!', '危险操作', { type: 'warning' }).then(() => { request({ url: '/admin/sweet/record/clear', method: 'delete' }).then(() => { this.$message.success('已清空'); this.loadData(); }) }) },
handleSizeChange(v) { this.queryParams.pageSize = v; this.loadData() },
handleCurrentChange(v) { this.queryParams.pageNum = v; this.loadData() },
getImg(u) { return u ? (u.startsWith('http') ? u : (process.env.VUE_APP_BASE_API || '') + u) : '' }
}
}
</script>
<style scoped>
.active-card { box-shadow: 0 0 0 2px #409eff; }
</style>
@@ -1,85 +0,0 @@
<template>
<div class="app-container">
<el-form :inline="true" :model="queryParams" class="demo-form-inline">
<el-form-item label="回合ID">
<el-input v-model="queryParams.roundId" placeholder="输入回合ID" clearable />
</el-form-item>
<el-form-item label="模式类型">
<el-select v-model="queryParams.boxType" placeholder="全部" clearable>
<el-option v-for="m in modeTypes" :key="m.code" :label="m.name" :value="m.code" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadData">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="roundList" border style="width:100%">
<el-table-column prop="roundId" label="回合ID" width="90" align="center" />
<el-table-column prop="boxId" label="箱子ID" width="70" align="center" />
<el-table-column label="模式" min-width="120" align="center">
<template slot-scope="scope">{{ getModeName(scope.row.boxType) }}</template>
</el-table-column>
<el-table-column prop="ornamentId" label="饰品ID" width="70" align="center" />
<el-table-column prop="actualMul" label="返奖倍率" width="90" align="center" />
<el-table-column prop="freeSpinCount" label="免费旋转" width="80" align="center" />
<el-table-column prop="totalCascades" label="翻转次数" width="80" align="center" />
</el-table>
<el-pagination
style="margin-top:16px;text-align:right"
background
layout="total, prev, pager, next, sizes"
:total="total"
:page-size.sync="queryParams.pageSize"
:current-page.sync="queryParams.pageNum"
:page-sizes="[10,20,50,100]"
@size-change="loadData"
@current-change="loadData"
/>
</div>
</template>
<script>
import { getRoundList } from '@/api/game/sweet/round/api'
export default {
name: 'SweetRound',
data() {
return {
loading: false, roundList: [], total: 0,
queryParams: { pageNum: 1, pageSize: 10, roundId: null, boxType: null },
modeTypes: [
{ code: 'normal', name: '普通旋转' }, { code: 'ante', name: '底注' },
{ code: 'buy_fs', name: '购买免费旋转' }, { code: 'buy_super_fs', name: '购买超级免费旋转' }
],
}
},
created() {
this.loadData()
},
methods: {
getModeName(boxType) {
if (!boxType) return ''
const m = this.modeTypes.find(t => t.code === boxType)
return m ? m.name : boxType
},
loadData() {
this.loading = true
const params = { ...this.queryParams }
if (!params.roundId) delete params.roundId
if (!params.boxType) delete params.boxType
getRoundList(params).then(res => {
this.roundList = res.data.rows || []
this.total = res.data.total || 0
this.loading = false
})
},
resetQuery() {
this.queryParams = { pageNum: 1, pageSize: 10, roundId: null, boxType: null }
this.loadData()
}
}
}
</script>
+20 -20
View File
@@ -5,7 +5,7 @@
<div slot="header">
<span>奖池统计</span>
<el-button style="float: right" type="text" icon="el-icon-refresh" @click="refreshAll">刷新</el-button>
<el-button style="float: right; margin-right: 16px; color: #F56C6C" type="text" @click="handleResetPool">重置奖池</el-button>
<el-button style="float: right; margin-right: 16px; color: #a4485a" type="text" @click="handleResetPool">重置奖池</el-button>
</div>
<el-table :data="poolStatsList" border size="small">
<el-table-column prop="pool" label="决策池" width="120" align="center">
@@ -18,19 +18,19 @@
<el-table-column prop="profit" label="系统盈利(毛口径)" align="right" min-width="150">
<template slot-scope="scope">
<el-tooltip content="毛口径:累计下注 − 累计返奖(按饰品标价记账,含每注 0.01 安慰奖),不含人工调整;真实成本请看奖池余额" placement="top">
<span :style="{ color: Number(scope.row.profit) > 0 ? '#67C23A' : '#F56C6C' }">{{ scope.row.profit }}</span>
<span :style="{ color: Number(scope.row.profit) > 0 ? '#457450' : '#a4485a' }">{{ scope.row.profit }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="adjustment" label="人工调整" align="right">
<template slot-scope="scope">
<span :style="{ color: Number(scope.row.adjustment) < 0 ? '#F56C6C' : '' }">{{ scope.row.adjustment }}</span>
<span :style="{ color: Number(scope.row.adjustment) < 0 ? '#a4485a' : '' }">{{ scope.row.adjustment }}</span>
</template>
</el-table-column>
<el-table-column prop="poolBalance" label="奖池余额" align="right" />
<el-table-column prop="rtp" label="RTP" width="100" align="center">
<template slot-scope="scope">
<span :style="{ color: scope.row.rtp > 0.85 ? '#F56C6C' : '' }">{{ (scope.row.rtp * 100).toFixed(2) }}%</span>
<span :style="{ color: scope.row.rtp > 0.85 ? '#a4485a' : '' }">{{ (scope.row.rtp * 100).toFixed(2) }}%</span>
</template>
</el-table-column>
</el-table>
@@ -47,7 +47,7 @@
</div>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="当前奖池余额">
<span style="font-size: 16px; font-weight: bold; color: #409EFF;">{{ poolBalance }}</span> 元
<span style="font-size: 16px; font-weight: bold; color: #9a5c35;">{{ poolBalance }}</span> 元
<el-button type="text" size="mini" icon="el-icon-refresh" @click="fetchPoolBalance" style="margin-left: 8px">刷新</el-button>
</el-descriptions-item>
<el-descriptions-item label="说明">
@@ -67,7 +67,7 @@
</el-form-item>
<el-form-item label="调整金额" prop="amount">
<el-input-number v-model="adjustForm.amount" :min="0.01" :precision="2" :step="100" style="width: 100%;" />
<div style="color: #909399; font-size: 12px; margin-top: 4px;">
<div style="color: #7a664e; font-size: 12px; margin-top: 4px;">
当前余额:{{ poolBalance }} 元
</div>
</el-form-item>
@@ -92,7 +92,7 @@
</el-table-column>
<el-table-column prop="adjustmentAmount" label="调整金额" width="120" align="right">
<template slot-scope="scope">
<span :style="{ color: Number(scope.row.adjustmentAmount) > 0 ? '#67C23A' : '#F56C6C', fontWeight: 'bold' }">
<span :style="{ color: Number(scope.row.adjustmentAmount) > 0 ? '#457450' : '#a4485a', fontWeight: 'bold' }">
{{ Number(scope.row.adjustmentAmount) > 0 ? '+' : '' }}{{ scope.row.adjustmentAmount }}
</span>
</template>
@@ -117,13 +117,13 @@
<el-form-item label="游戏关闭">
<el-switch v-model="config.switchOff" />
<span style="margin-left: 8px; color: #E6A23C">切换后<strong>必须点下方「保存」才生效</strong>(只拨开关不会保存)</span>
<span style="margin-left: 8px; color: #8f6418">切换后<strong>必须点下方「保存」才生效</strong>(只拨开关不会保存)</span>
</el-form-item>
<el-form-item label="下注额">
<div>
<el-tag v-for="b in [5,10,20,50,100,200,500,1000,2000,5000]" :key="b" style="margin-right: 6px; margin-bottom: 4px;">{{ b }} 元</el-tag>
<span style="color: #909399; font-size: 12px; margin-left: 8px">固定10档,对应奖励矩阵120条</span>
<span style="color: #7a664e; font-size: 12px; margin-left: 8px">固定10档,对应奖励矩阵120条</span>
</div>
</el-form-item>
@@ -131,12 +131,12 @@
<el-form-item label="转盘总格数">
<el-input-number v-model="config.segmentCount" :min="8" :max="48" disabled />
<span style="margin-left: 8px; color: #909399">内外圈各 N 格,索引一一对应</span>
<span style="margin-left: 8px; color: #7a664e">内外圈各 N 格,索引一一对应</span>
</el-form-item>
<el-form-item label="外圈符号布局">
<el-input v-model="config.outerSymbols" placeholder="24个字符,由 A/B/C/D 组成" style="width: 500px" @change="onLayoutChanged" />
<span style="margin-left: 8px; color: #909399">{{ (config.outerSymbols || '').length }} 格</span>
<span style="margin-left: 8px; color: #7a664e">{{ (config.outerSymbols || '').length }} 格</span>
</el-form-item>
<el-form-item label="内圈金色格数">
@@ -147,14 +147,14 @@
</el-form-item>
<el-form-item label="内圈紫色格数">
<el-input-number v-model="config.innerPurpleCount" :min="0" :max="48" @change="onLayoutChanged" />
<span v-if="innerSumError" style="color: #F56C6C; margin-left: 8px">三色合计必须 = {{ config.segmentCount }},当前 {{ innerSum }}</span>
<span v-if="innerSumError" style="color: #a4485a; margin-left: 8px">三色合计必须 = {{ config.segmentCount }},当前 {{ innerSum }}</span>
</el-form-item>
<el-divider content-position="left">倍率矩阵(内圈3色 × 外圈4符号 = 12个组合)</el-divider>
<el-form-item label="倍率矩阵">
<div style="width: 640px; margin-bottom: 8px; line-height: 20px;">
<span style="color: #909399; font-size: 12px">
<span style="color: #7a664e; font-size: 12px">
这 12 格由下方「目标RTP(累计口径)」= <strong>{{ config.rtp }}</strong> 自动生成:倍率 = 目标 × {{ cellSquare }} ÷ (色格数 × 符号格数);改目标 → 12 格全变,手改某一格 → 目标RTP 跟着变成新的均衡返奖率;保存时一起写入 | 当前均衡返奖率 <strong>{{ matrixRtp }}%</strong><br>
注意:「累计上限」此时等于倍率期望,上限会去削高倍格(实测押 38 倍那格只剩 ~65%)。需要让上限只当保险(比期望高 5 个点)就说一声,我加个固定余量
</span>
@@ -189,12 +189,12 @@
<el-form-item label="单局奖池投放比例">
<el-input-number v-model="config.bufferRatio" :min="0" :max="1" :step="0.05" :precision="2" style="width: 200px" />
<span style="margin-left: 8px; color: #909399">0 ~ 1,一次旋转最多可动用奖池余额的比例(填 0.6 = 最多拿出 60%)</span>
<span style="margin-left: 8px; color: #7a664e">0 ~ 1,一次旋转最多可动用奖池余额的比例(填 0.6 = 最多拿出 60%)</span>
</el-form-item>
<el-form-item label="目标RTP(累计口径)">
<el-input-number v-model="config.rtp" :min="0" :max="1" :step="0.05" :precision="2" style="width: 200px" @change="applyRecalc" />
<span style="margin-left: 8px; color: #909399">0 ~ 1,累计返奖 ≤ 累计投注 × 该值;本局可发额度 = 累计投注×该值 − 累计返奖;改动后上方倍率矩阵会自动重算。<strong>注意这是"上限不是目标"</strong>:实际返奖率通常低于它,详见下方说明</span>
<span style="margin-left: 8px; color: #7a664e">0 ~ 1,累计返奖 ≤ 累计投注 × 该值;本局可发额度 = 累计投注×该值 − 累计返奖;改动后上方倍率矩阵会自动重算。<strong>注意这是"上限不是目标"</strong>:实际返奖率通常低于它,详见下方说明</span>
</el-form-item>
<!-- 盈利配置说明 -->
@@ -203,7 +203,7 @@
<div style="margin-bottom: 8px; font-weight: 600;">💡 配置说明(下一局立即生效)</div>
<div style="margin-bottom: 6px;">
<strong>单局奖池投放比例 (bufferRatio = {{ config.bufferRatio }})</strong>:一局最多从奖池拿出 <span style="color: #E6A23C;">{{ bufferRatioPct }} 的余额</span> 来发奖
<strong>单局奖池投放比例 (bufferRatio = {{ config.bufferRatio }})</strong>:一局最多从奖池拿出 <span style="color: #8f6418;">{{ bufferRatioPct }} 的余额</span> 来发奖
</div>
<div style="padding-left: 16px; color: #606266; font-size: 12px; margin-bottom: 8px;">
• 例:奖池余额 10000 元 → 本项上限 10000 × {{ config.bufferRatio }} = <strong>{{ poolCapDemo }} 元</strong><br>
@@ -212,19 +212,19 @@
</div>
<div style="margin-bottom: 6px;">
<strong>目标RTP (rtp = {{ config.rtp }},累计口径)</strong>:按"总共该发多少 − 已经发了多少"算出<span style="color: #E6A23C;">还能发多少</span>,这就是本局的返奖预算(是发奖上限,不是平台欠玩家钱)
<strong>目标RTP (rtp = {{ config.rtp }},累计口径)</strong>:按"总共该发多少 − 已经发了多少"算出<span style="color: #8f6418;">还能发多少</span>,这就是本局的返奖预算(是发奖上限,不是平台欠玩家钱)
</div>
<div style="padding-left: 16px; color: #606266; font-size: 12px; margin-bottom: 8px;">
• 本局可发额度 = 累计真实投注 × {{ rtpPct }} − 累计真实返奖;<br>
• 例:累计投注 10 万、累计返奖 7 万 → 该发 {{ rtpDemo.should }} 万、已发 7 万 → 本局额度 <strong>{{ rtpDemo.room }} 万</strong><br>
• 它只是"这局最多能发多少":实际发多少仍由转盘概率决定,没发完的额度继续攒到以后(所以池子厚了能一局爆大奖)。本局投注在封盘时已计入累计,历史刚好发到 {{ rtpPct }} 时,额度正好 = 本局投注 × {{ rtpPct }}<br>
• <strong style="color: #F56C6C;">它是"上限",不是"恒温器"</strong>:只保证长期返奖率<strong>不超过</strong> {{ rtpPct }},不保证一定发到 {{ rtpPct }}。只要还有组合没被真实玩家押上,系统就优先开"没人押的那一格"(全体只拿安慰奖),实际返奖率会明显低于该值<br>
• <strong style="color: #a4485a;">它是"上限",不是"恒温器"</strong>:只保证长期返奖率<strong>不超过</strong> {{ rtpPct }},不保证一定发到 {{ rtpPct }}。只要还有组合没被真实玩家押上,系统就优先开"没人押的那一格"(全体只拿安慰奖),实际返奖率会明显低于该值<br>
• 实际返奖率请以首页「实时RTP」为准,不要拿本配置值当预期
</div>
<div style="padding: 8px; background: #F4F4F5; border-radius: 4px; margin-top: 8px;">
<strong style="color: #303133;">协同机制</strong>:发奖上限 = <span style="color: #409EFF;">min(奖池余额×{{ config.bufferRatio }}, 本局可发额度)</span>,两条都要满足<br>
<span style="color: #909399; font-size: 12px;">
<strong style="color: #303133;">协同机制</strong>:发奖上限 = <span style="color: #9a5c35;">min(奖池余额×{{ config.bufferRatio }}, 本局可发额度)</span>,两条都要满足<br>
<span style="color: #7a664e; font-size: 12px;">
• 谁小听谁的:池子不够厚 → 按奖池的 {{ bufferRatioPct }} 卡;池子够厚 → 按返奖额度卡<br>
• 额度够时,单局返奖可以远超本局投注("池子厚就能爆大奖")<br>
• 后台注水/抽水只改奖池余额,只抬高上面第一条线,不会凭空增加返奖额度<br>
@@ -4,7 +4,7 @@
<el-table v-loading="loading" :data="betAmountList" border>
<el-table-column prop="betAmount" label="下注额" width="120" align="center">
<template slot-scope="scope">
<span style="font-size: 16px; font-weight: bold; color: #409eff">{{ scope.row.betAmount }} 元</span>
<span style="font-size: 16px; font-weight: bold; color: #9a5c35">{{ scope.row.betAmount }} 元</span>
</template>
</el-table-column>
<el-table-column label="倍率预览">
@@ -50,7 +50,7 @@
<el-card shadow="hover" :body-style="{ padding: '14px' }">
<div slot="header" style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-weight: bold; font-size: 15px;">符号 {{ item.symbol }}</span>
<span style="color: #67C23A; font-weight: bold; font-size: 14px;">{{ item.prizeValue }}</span>
<span style="color: #457450; font-weight: bold; font-size: 14px;">{{ item.prizeValue }}</span>
</div>
<el-form label-width="56px" size="mini" label-position="left">
<el-form-item label="倍率">
@@ -27,7 +27,7 @@
<el-tag v-if="scope.row.result != null" type="primary" size="medium" style="font-size: 16px; font-weight: bold;">
{{ scope.row.result }}
</el-tag>
<span v-if="scope.row.result != null && scope.row.symbols && scope.row.innerColors" style="margin-left: 8px; font-size: 16px; font-weight: bold; color: #409EFF;">
<span v-if="scope.row.result != null && scope.row.symbols && scope.row.innerColors" style="margin-left: 8px; font-size: 16px; font-weight: bold; color: #9a5c35;">
({{ getResultLabel(scope.row.result, scope.row.symbols, scope.row.innerColors) }})
</span>
</div>
@@ -44,11 +44,11 @@
textAlign: 'center',
fontWeight: idx == scope.row.result ? 'bold' : 'normal',
fontSize: idx == scope.row.result ? '16px' : '14px',
color: idx == scope.row.result ? '#E6A23C' : '#909399',
color: idx == scope.row.result ? '#8f6418' : '#7a664e',
backgroundColor: idx == scope.row.result ? '#FDF6EC' : 'transparent',
padding: idx == scope.row.result ? '3px 0' : '0',
borderRadius: '3px',
border: idx == scope.row.result ? '2px solid #E6A23C' : 'none',
border: idx == scope.row.result ? '2px solid #8f6418' : 'none',
marginRight: '2px'
}"
>{{ sym }}</span>
@@ -90,7 +90,7 @@
<el-table-column prop="bet" label="下注金额" width="100" align="right" />
<el-table-column prop="win" label="单注赢得" width="100" align="right">
<template slot-scope="inner">
<span :style="{ color: Number(inner.row.win) > 0 ? '#67C23A' : '', fontWeight: Number(inner.row.win) > 0.01 ? 'bold' : 'normal' }">{{ inner.row.win }}</span>
<span :style="{ color: Number(inner.row.win) > 0 ? '#457450' : '', fontWeight: Number(inner.row.win) > 0.01 ? 'bold' : 'normal' }">{{ inner.row.win }}</span>
</template>
</el-table-column>
</el-table>
@@ -110,7 +110,7 @@
<el-table-column prop="totalBetAmount" label="总下注" width="100" align="right" />
<el-table-column prop="userRoundPayout" label="回合实际赔付" width="120" align="right">
<template slot-scope="scope">
<span :style="{ color: scope.row.userRoundPayout > 0 ? '#E6A23C' : '' }">{{ scope.row.userRoundPayout }}</span>
<span :style="{ color: scope.row.userRoundPayout > 0 ? '#8f6418' : '' }">{{ scope.row.userRoundPayout }}</span>
</template>
</el-table-column>
<el-table-column prop="settled" label="结算状态" width="90" align="center">
@@ -217,13 +217,13 @@ export default {
},
/** 高亮颜色(用于中奖格文字) */
getHighlightColor(color) {
const map = { '金': '#E6A23C', '红': '#F56C6C', '紫': '#909399' };
return map[color] || '#409EFF';
const map = { '金': '#8f6418', '红': '#a4485a', '紫': '#7a664e' };
return map[color] || '#9a5c35';
},
/** 高亮背景色(用于中奖格背景) */
getHighlightBgColor(color) {
const map = { '金': '#FDF6EC', '红': '#FEF0F0', '紫': '#F4F4F5' };
return map[color] || '#ECF5FF';
return map[color] || '#fbf5ea';
}
}
}
+147 -101
View File
@@ -343,34 +343,41 @@ export default {
this.lineChartInstance.dispose();
}
this.lineChartInstance = echarts.init(chartDom);
/* 6 条线的颜色:按「色相均匀分布」取(约每 60° 一个),并统一压低饱和度。
★ 这里刻意放宽了 Cottagecore 的配色纪律(出现了灰青 #4a7f8c、灰紫 #7a6a9c):
上一轮为了收敛配色把 6 条线压成「两个绿 + 两个棕」,结果用户反馈"区分不明显"。
数据可视化里「6 条线能分辨」是功能需求,优先于风格约束;饱和度已压低,不会显得刺眼。
若哪天要严格贴风格,正解是拆成两张图(每张 ≤3 条线),而不是继续挤这 6 个色。 */
var seriesConfig = [
{ name: "充值", data: rechargeList, color: "#3b82f6" },
{ name: "开箱流水", data: boxOpenList, color: "#10b981" },
{ name: "对战开箱流水", data: fightBoxOpenList, color: "#f59e0b" },
{ name: "幸运升级流水", data: luckyUpgradeList, color: "#8b5cf6" },
{ name: "疯狂转盘流水", data: wheelList, color: "#14b8a6" },
{ name: "玫瑰女郎流水", data: roseladyList, color: "#ec4899" }
{ name: "充值", data: rechargeList, color: "#b05730" },
{ name: "开箱流水", data: boxOpenList, color: "#9aa334" },
{ name: "对战开箱流水", data: fightBoxOpenList, color: "#4a9163" },
{ name: "幸运升级流水", data: luckyUpgradeList, color: "#4a7f8c" },
{ name: "疯狂转盘流水", data: wheelList, color: "#7a6a9c" },
{ name: "玫瑰女郎流水", data: roseladyList, color: "#a35790" }
];
var option = {
tooltip: {
trigger: "axis",
backgroundColor: "rgba(255, 255, 255, 0.96)",
borderColor: "#e5e7eb",
backgroundColor: "rgba(255, 253, 250, 0.97)",
borderColor: "#eadfd0",
borderWidth: 1,
textStyle: { color: "#374151", fontSize: 12 },
textStyle: { color: "#5f4f3d", fontSize: 12 },
axisPointer: {
type: "line",
lineStyle: { color: "#9ca3af", type: "dashed" }
lineStyle: { color: "#7a664e", type: "dashed" }
}
},
legend: {
data: seriesConfig.map(s => s.name),
top: 0,
icon: "roundRect",
itemWidth: 12,
itemHeight: 4,
itemGap: 18,
textStyle: { color: "#6b7280", fontSize: 12 },
/* 原来色块只有 12x4px,像一条细线,6 个色块挤在一起根本看不出差别;
加宽加厚成 16x8 的小色条 */
itemWidth: 16,
itemHeight: 8,
itemGap: 16,
textStyle: { color: "#7a664e", fontSize: 12 },
selectedMode: true
},
grid: {
@@ -385,17 +392,17 @@ export default {
type: "category",
boundaryGap: false,
data: dateList,
axisLine: { lineStyle: { color: "#e5e7eb" } },
axisLine: { lineStyle: { color: "#eadfd0" } },
axisTick: { show: false },
axisLabel: {
color: "#9ca3af",
color: "#7a664e",
rotate: dateList.length > 12 ? 45 : 0
}
},
yAxis: {
type: "value",
axisLabel: {
color: "#9ca3af",
color: "#7a664e",
formatter: function(value) {
if (value >= 10000) return (value / 10000).toFixed(1) + '万';
if (value >= 1000) return (value / 1000).toFixed(1) + 'k';
@@ -403,7 +410,7 @@ export default {
}
},
splitLine: {
lineStyle: { type: "dashed", color: "#f0f1f5" }
lineStyle: { type: "dashed", color: "#eadfd0" }
}
},
series: seriesConfig.map(s => ({
@@ -412,9 +419,10 @@ export default {
smooth: true,
data: s.data,
itemStyle: { color: s.color },
lineStyle: { width: 2.5 },
/* 线加粗到 3:6 条线叠在同一张图上时,细线更容易糊成一片 */
lineStyle: { width: 3 },
symbol: "circle",
symbolSize: 6,
symbolSize: 5,
showSymbol: false,
emphasis: { focus: "series" },
areaStyle: {
@@ -456,13 +464,13 @@ export default {
margin: 0;
font-size: 22px;
font-weight: 700;
color: #1f2937;
color: #4f4130;
letter-spacing: 0.5px;
}
p {
margin: 4px 0 0;
font-size: 13px;
color: #9ca3af;
color: #7a664e;
}
}
}
@@ -489,20 +497,25 @@ export default {
position: relative;
display: flex;
align-items: center;
background: #fff;
border-radius: 14px;
background: #fffdfa;
border-radius: 16px;
padding: 20px;
box-shadow: 0 1px 3px rgba(17, 24, 39, 0.06);
border: 1px solid rgba(229, 231, 235, 0.7);
transition: all 0.25s ease;
box-shadow: 0 4px 14px rgba(139, 115, 85, 0.10);
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 {
transform: translateY(-3px);
box-shadow: 0 10px 24px rgba(17, 24, 39, 0.1);
/* 规范:悬停用「小幅放大 + ≤1deg 微旋」,避免机械式直上直下 */
transform: scale(1.015) rotate(-0.3deg);
border-color: #c08a8a;
box-shadow: 0 10px 26px rgba(139, 115, 85, 0.16);
.stat-icon {
transform: scale(1.08) rotate(-4deg);
transform: rotate(-1deg) scale(1.04);
}
.stat-arrow {
opacity: 1;
@@ -519,16 +532,19 @@ export default {
flex-shrink: 0;
width: 54px;
height: 54px;
border-radius: 14px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
transition: transform 0.3s ease;
/* 底色与图标色由 .theme-* 提供:浅底 + 同色深图标(水彩感),
不再是一格一色的高饱和实心块 */
background: rgba(154, 92, 53, 0.22);
transition: transform 0.5s ease-in-out;
i {
font-size: 26px;
color: #fff;
color: #9a5c35;
}
}
@@ -538,7 +554,7 @@ export default {
.stat-value {
font-size: 24px;
font-weight: 700;
color: #1f2937;
color: #4f4130;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
@@ -547,11 +563,11 @@ export default {
.stat-label {
margin-top: 4px;
font-size: 13px;
color: #9ca3af;
color: #7a664e;
.stat-note-icon {
margin-left: 4px;
color: #c0c4cc;
color: #7a664e;
cursor: help;
}
}
@@ -563,32 +579,56 @@ export default {
top: 50%;
margin-top: -7px;
font-size: 14px;
color: #c0c4cc;
color: #7a664e;
opacity: 0;
transform: translateX(-6px);
transition: all 0.25s ease;
}
/* 图标渐变主题 */
.theme-blue { background: linear-gradient(135deg, #4e9bff, #2f6bff); box-shadow: 0 6px 14px rgba(47, 107, 255, 0.3); }
.theme-orange { background: linear-gradient(135deg, #ffb64d, #ff8a00); box-shadow: 0 6px 14px rgba(255, 138, 0, 0.3); }
.theme-green { background: linear-gradient(135deg, #3ddc84, #14b866); box-shadow: 0 6px 14px rgba(20, 184, 102, 0.3); }
.theme-red { background: linear-gradient(135deg, #ff7a6e, #f5424e); box-shadow: 0 6px 14px rgba(245, 66, 78, 0.3); }
.theme-purple { background: linear-gradient(135deg, #a78bfa, #7c5cfc); box-shadow: 0 6px 14px rgba(124, 92, 252, 0.3); }
.theme-cyan { background: linear-gradient(135deg, #38d1e0, #00a8c6); box-shadow: 0 6px 14px rgba(0, 168, 198, 0.3); }
.theme-pink { background: linear-gradient(135deg, #ff85b3, #f43f7f); box-shadow: 0 6px 14px rgba(244, 63, 127, 0.3); }
.theme-amber { background: linear-gradient(135deg, #ffd166, #f4a300); box-shadow: 0 6px 14px rgba(244, 163, 0, 0.3); }
.theme-indigo { background: linear-gradient(135deg, #7f9cf5, #4c62e0); box-shadow: 0 6px 14px rgba(76, 98, 224, 0.3); }
.theme-teal { background: linear-gradient(135deg, #45e0c0, #0fb894); box-shadow: 0 6px 14px rgba(15, 184, 148, 0.3); }
.theme-sky { background: linear-gradient(135deg, #6ec1ff, #3c8ce7); box-shadow: 0 6px 14px rgba(60, 140, 231, 0.3); }
/* 统计卡图标配色
11 个卡片只用 5 个田园色(陶土 / 草绿 / 苔绿 / 雏菊金 / 玫瑰粉),循环使用。
浅底 + 同色深图标。
tint 从 .14~.18 提到 .22~.26:原来太淡,小图标几乎"看不见"(用户反馈对比度不够)。
类名(blue/orange/...)只是 card.theme 的旧键名,沿用不改,免得动模板。 */
.theme-blue,
.theme-indigo {
background: rgba(154, 92, 53, 0.22) !important;
box-shadow: inset 0 0 0 1px rgba(154, 92, 53, 0.28);
i { color: #8a4f2b !important; }
}
.theme-green,
.theme-teal {
background: rgba(69, 116, 80, 0.22) !important;
box-shadow: inset 0 0 0 1px rgba(69, 116, 80, 0.28);
i { color: #3a6342 !important; }
}
.theme-purple,
.theme-cyan {
background: rgba(127, 154, 95, 0.26) !important;
box-shadow: inset 0 0 0 1px rgba(127, 154, 95, 0.32);
i { color: #56682f !important; }
}
.theme-orange,
.theme-amber {
background: rgba(168, 121, 31, 0.24) !important;
box-shadow: inset 0 0 0 1px rgba(168, 121, 31, 0.30);
i { color: #7d5512 !important; }
}
.theme-red,
.theme-pink,
.theme-sky {
background: rgba(181, 116, 116, 0.24) !important;
box-shadow: inset 0 0 0 1px rgba(181, 116, 116, 0.30);
i { color: #8f3f50 !important; }
}
/* ========== 区块卡片(RTP / 图表) ========== */
.section-card {
background: #fff;
border-radius: 14px;
background: #faf6f0;
border-radius: 16px;
padding: 20px 24px;
box-shadow: 0 1px 3px rgba(17, 24, 39, 0.06);
border: 1px solid rgba(229, 231, 235, 0.7);
box-shadow: 0 6px 18px rgba(139, 115, 85, 0.11);
border: 1px solid rgba(212, 160, 160, 0.55);
margin-bottom: 20px;
}
@@ -604,26 +644,27 @@ export default {
align-items: center;
font-size: 16px;
font-weight: 600;
color: #1f2937;
color: #4f4130;
.title-bar {
/* 原为 4px 竖细条(单侧色带式装饰,规范禁止),改成草绿「莓果点」 */
display: inline-block;
width: 4px;
height: 16px;
border-radius: 2px;
background: linear-gradient(180deg, #4e9bff, #2f6bff);
width: 10px;
height: 10px;
border-radius: 50%;
background: #5a8f5a;
margin-right: 10px;
}
}
.section-sub {
font-size: 12px;
color: #9ca3af;
color: #7a664e;
&.chart-sub {
margin-left: 12px;
font-weight: 400;
background: #f3f4f6;
background: #f3ece0;
padding: 2px 10px;
border-radius: 10px;
}
@@ -647,16 +688,19 @@ export default {
}
.rtp-card {
border-radius: 12px;
border-radius: 14px;
padding: 16px 18px;
background: #fafbfd;
border: 1px solid #eef0f4;
transition: all 0.25s ease;
background: #f6efe3;
border: 1px solid rgba(212, 160, 160, 0.48);
transition:
transform 0.5s ease-in-out,
box-shadow 0.5s ease-in-out,
background-color 0.5s ease-in-out;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 18px rgba(17, 24, 39, 0.08);
background: #fff;
transform: scale(1.015) rotate(-0.3deg);
box-shadow: 0 8px 20px rgba(139, 115, 85, 0.14);
background: #fbf5ea;
}
}
@@ -665,12 +709,13 @@ export default {
align-items: center;
font-size: 13px;
font-weight: 600;
color: #4b5563;
color: #5f4f3d;
margin-bottom: 10px;
.rtp-dot {
width: 8px;
height: 8px;
/* 8px 在奶油底上偏小,色相看不出差别;放到 10px */
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 8px;
}
@@ -681,12 +726,14 @@ export default {
font-weight: 700;
line-height: 1.2;
margin-bottom: 10px;
/* 数值统一用正文墨色:颜色交给圆点与进度条,避免 5 个百分比各染一色 */
color: #4f4130;
}
.rtp-progress {
height: 6px;
border-radius: 3px;
background: #eef0f4;
background: #eadfd0;
overflow: hidden;
margin-bottom: 12px;
@@ -702,11 +749,11 @@ export default {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #9ca3af;
color: #7a664e;
line-height: 1.9;
span:last-child {
color: #4b5563;
color: #5f4f3d;
font-weight: 500;
}
}
@@ -715,42 +762,41 @@ export default {
.rtp-empty {
grid-column: 1 / -1;
text-align: center;
color: #c0c4cc;
color: #7a664e;
font-size: 13px;
padding: 30px 0;
}
/* RTP 主题色 */
.rtp-theme-0 {
.rtp-dot { background: #ec4899; }
.rtp-value { color: #ec4899; }
.rtp-progress-inner { background: linear-gradient(90deg, #ff85b3, #ec4899); }
/* RTP 主题色(只给圆点 + 进度条上色)
★ 与上方折线图的颜色一一对应:同一游戏在哪儿都是同一个色,
这样「颜色」本身就带信息,不用每次重新认。
(原来是品红 #ec4899 / 青 #14b8a6 / 紫 #8b5cf6 / 翠绿 #10b981 / 琥珀 #f59e0b,
属于规范禁止的高饱和荧光色,已换掉。) */
.rtp-theme-0 { /* 疯狂转盘 */
.rtp-dot { background: #7a6a9c; }
.rtp-progress-inner { background: linear-gradient(90deg, #a89cc0, #7a6a9c); }
}
.rtp-theme-1 {
.rtp-dot { background: #14b8a6; }
.rtp-value { color: #14b8a6; }
.rtp-progress-inner { background: linear-gradient(90deg, #45e0c0, #14b8a6); }
.rtp-theme-1 { /* 幸运升级 */
.rtp-dot { background: #4a7f8c; }
.rtp-progress-inner { background: linear-gradient(90deg, #8fb0b8, #4a7f8c); }
}
.rtp-theme-2 {
.rtp-dot { background: #8b5cf6; }
.rtp-value { color: #8b5cf6; }
.rtp-progress-inner { background: linear-gradient(90deg, #a78bfa, #8b5cf6); }
.rtp-theme-2 { /* 经典开箱 */
.rtp-dot { background: #9aa334; }
.rtp-progress-inner { background: linear-gradient(90deg, #c0c677, #9aa334); }
}
.rtp-theme-3 {
.rtp-dot { background: #10b981; }
.rtp-value { color: #10b981; }
.rtp-progress-inner { background: linear-gradient(90deg, #3ddc84, #10b981); }
.rtp-theme-3 { /* 对战 */
.rtp-dot { background: #4a9163; }
.rtp-progress-inner { background: linear-gradient(90deg, #8fb8a0, #4a9163); }
}
.rtp-theme-4 {
.rtp-dot { background: #f59e0b; }
.rtp-value { color: #f59e0b; }
.rtp-progress-inner { background: linear-gradient(90deg, #ffd166, #f59e0b); }
.rtp-theme-4 { /* 玫瑰女郎 */
.rtp-dot { background: #a35790; }
.rtp-progress-inner { background: linear-gradient(90deg, #c69bb8, #a35790); }
}
/* ========== 图表切换标签 ========== */
.chart-tabs {
display: flex;
background: #f3f4f6;
background: #f3ece0;
border-radius: 10px;
padding: 3px;
@@ -758,19 +804,19 @@ export default {
padding: 5px 18px;
cursor: pointer;
font-size: 13px;
color: #6b7280;
color: #7a664e;
border-radius: 8px;
transition: all 0.2s;
user-select: none;
&:hover {
color: #2f6bff;
color: #9a5c35;
}
&.active {
background: #fff;
color: #2f6bff;
background: #fffdfa;
color: #9a5c35;
font-weight: 600;
box-shadow: 0 1px 4px rgba(17, 24, 39, 0.1);
box-shadow: 0 2px 8px rgba(139, 115, 85, 0.14);
}
}
}
+4 -4
View File
@@ -123,7 +123,7 @@
<el-table-column label="操作内容" align="left" min-width="320" :show-overflow-tooltip="true">
<template slot-scope="scope">
<span>{{ actionOf(scope.row) }}</span>
<span v-if="briefOf(scope.row)" style="color:#909399;"> {{ briefOf(scope.row) }}</span>
<span v-if="briefOf(scope.row)" style="color:#7a664e;"> {{ briefOf(scope.row) }}</span>
</template>
</el-table-column>
<el-table-column label="类型" align="center" prop="businessType" width="90">
@@ -171,7 +171,7 @@
<el-descriptions-item label="结果">
<el-tag v-if="form.status === 0" type="success" size="mini">成功</el-tag>
<el-tag v-else type="danger" size="mini">失败</el-tag>
<span style="color:#909399;margin-left:6px;">{{ form.costTime }}毫秒</span>
<span style="color:#7a664e;margin-left:6px;">{{ form.costTime }}毫秒</span>
</el-descriptions-item>
<el-descriptions-item label="操作内容" :span="2">
<b>{{ actionOf(form) }}</b>
@@ -192,12 +192,12 @@
<el-table-column label="项目" prop="label" width="160" />
<el-table-column label="改之前" prop="before" :show-overflow-tooltip="true">
<template slot-scope="scope">
<span style="color:#F56C6C;">{{ scope.row.before }}</span>
<span style="color:#a4485a;">{{ scope.row.before }}</span>
</template>
</el-table-column>
<el-table-column label="改之后" prop="after" :show-overflow-tooltip="true">
<template slot-scope="scope">
<span style="color:#67C23A;">{{ scope.row.after }}</span>
<span style="color:#457450;">{{ scope.row.after }}</span>
</template>
</el-table-column>
</el-table>
@@ -31,7 +31,7 @@
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="openBlankDialog">生成白号用户</el-button>
</el-col>
<el-col :span="12">
<span style="color: #909399; font-size: 12px; line-height: 32px;">
<span style="color: #7a664e; font-size: 12px; line-height: 32px;">
<i class="el-icon-info"></i> 提示:如需删除白号,请前往“用户管理 / 用户列表”操作。
</span>
</el-col>
@@ -91,7 +91,7 @@
<el-form label-width="90px">
<el-form-item label="请选择数量:" style="margin-bottom: 12px;">
<el-input-number size="mini" v-model="newNumber" :min="1" :max="10" @change="onBlankNumberChange"></el-input-number>
<span style="font-size:12px;color:#909399;margin-left:10px;">
<span style="font-size:12px;color:#7a664e;margin-left:10px;">
<i class="el-icon-info"></i> 可生成 1-10 个白号用户
</span>
</el-form-item>
@@ -108,7 +108,7 @@
placeholder="留空则自动生成随机手机号"
/>
</el-form-item>
<div style="font-size:12px;color:#909399;padding-left:90px;">
<div style="font-size:12px;color:#7a664e;padding-left:90px;">
<i class="el-icon-info"></i> 手机号选填:填写后作为登录账号,留空则系统随机生成
</div>
</el-form>
@@ -26,7 +26,7 @@
<el-table-column prop="id" label="ID" width="70" align="center" />
<el-table-column prop="cardCode" label="卡密编码" width="200" align="center">
<template slot-scope="scope">
<span style="font-family: 'Courier New', monospace; color: #409eff; font-weight: 600; margin-right: 6px;">{{ scope.row.cardCode }}</span>
<span style="font-family: 'Courier New', monospace; color: #9a5c35; font-weight: 600; margin-right: 6px;">{{ scope.row.cardCode }}</span>
<el-button type="text" size="mini" @click="copyText(scope.row.cardCode)">复制</el-button>
</template>
</el-table-column>
+1 -1
View File
@@ -154,7 +154,7 @@ export default {
methods: {
getPriceColor(row) {
if (!row.cs340Price) return '#606266'; // 无三方价格时默认色
return row.ornamentsPrice > row.cs340Price ? '#F56C6C' : '#67C23A'; // 溢价红色,未溢价绿色
return row.ornamentsPrice > row.cs340Price ? '#a4485a' : '#457450'; // 溢价红色,未溢价绿色
},
handleSubmit(formName) {
this.$refs[formName].validate(valid => {
@@ -99,7 +99,7 @@
已用:{{ scope.row.dailyUsedCount }}<br/>
审核中:{{ scope.row.dailyPendingCount }}
</div>
<i class="el-icon-question" style="color: #909399; margin-left: 4px;"></i>
<i class="el-icon-question" style="color: #7a664e; margin-left: 4px;"></i>
</el-tooltip>
</span>
</template>
@@ -122,8 +122,8 @@
<el-table-column align="center" class-name="operation-column" label="操作" width="120">
<template slot-scope="scope">
<div v-if="scope.row.status === 0" style="display: flex; justify-content: center; align-items: center;">
<el-button type="text" style="color: #67C23A; margin: 0 5px;" @click="handleApprove(scope.row)">同意</el-button>
<el-button type="text" style="color: #F56C6C; margin: 0 5px;" @click="handleReject(scope.row)">驳回</el-button>
<el-button type="text" style="color: #457450; margin: 0 5px;" @click="handleApprove(scope.row)">同意</el-button>
<el-button type="text" style="color: #a4485a; margin: 0 5px;" @click="handleReject(scope.row)">驳回</el-button>
</div>
<div v-else style="text-align: center; color: #999;">已处理</div>
</template>
@@ -105,7 +105,7 @@
已用:{{ scope.row.dailyUsedCount }}<br/>
审核中:{{ scope.row.dailyPendingCount }}
</div>
<i class="el-icon-question" style="color: #909399; margin-left: 4px;"></i>
<i class="el-icon-question" style="color: #7a664e; margin-left: 4px;"></i>
</el-tooltip>
</span>
</template>
@@ -265,7 +265,7 @@ export default {
align-items: center;
justify-content: space-between;
&:hover {
background-color: #409eff;
background-color: #9a5c35;
transition: all ease 0.3s;
}
}
+5 -5
View File
@@ -414,7 +414,7 @@
</div>
</div>
</div>
<div v-else style="text-align:center; color:#909399; padding:20px;">暂无饰品</div>
<div v-else style="text-align:center; color:#7a664e; padding:20px;">暂无饰品</div>
<!-- 分页:默认每页 10 条 -->
<div style="margin-top: 8px; display: flex; justify-content: flex-end;" v-if="orTotal > 0">
<el-pagination
@@ -1105,7 +1105,7 @@ export default {
padding: 2px 5px;
line-height: 32px;
&:hover {
background-color: #409eff;
background-color: #9a5c35;
transition: all ease 0.3s;
}
}
@@ -1137,8 +1137,8 @@ export default {
box-sizing: border-box;
}
.ornament-card:hover {
border-color: #409eff;
background: #f0f9ff;
border-color: #9a5c35;
background: #f6efe3;
}
.ornament-img {
width: 50px;
@@ -1164,7 +1164,7 @@ export default {
}
.ornament-price {
font-size: 12px;
color: #f56c6c;
color: #a4485a;
font-weight: bold;
margin-top: 2px;
}
+2 -2
View File
@@ -917,7 +917,7 @@ export default {
"普通": "#b0c3d9",
"普通级": "#b0c3d9"
};
return map[rarityName] || "#909399";
return map[rarityName] || "#7a664e";
},
// 图表展示当前 tab 奖池当日的出货金额/开启次数/利润
initEcharts(day, pool) {
@@ -1455,7 +1455,7 @@ export default {
padding: 2px 5px;
line-height: 32px;
&:hover {
background-color: #409eff;
background-color: #9a5c35;
transition: all ease 0.3s;
}
}
@@ -21,7 +21,7 @@
<el-table-column align="center" prop="phoneNumber" label="手机号" width="140" />
<el-table-column align="center" label="卡商余额" width="140">
<template slot-scope="scope">
<span style="color: #E6A23C; font-weight: bold; font-size: 16px;">{{ scope.row.merchantBalance }}</span>
<span style="color: #8f6418; font-weight: bold; font-size: 16px;">{{ scope.row.merchantBalance }}</span>
</template>
</el-table-column>
@@ -70,7 +70,7 @@
</el-table-column>
<el-table-column align="center" label="变动金额" width="140">
<template slot-scope="scope">
<span :style="{ color: scope.row.credits > 0 ? '#67C23A' : '#F56C6C', fontWeight: 'bold' }">
<span :style="{ color: scope.row.credits > 0 ? '#457450' : '#a4485a', fontWeight: 'bold' }">
{{ scope.row.credits > 0 ? '+' : '' }}{{ scope.row.credits }}
</span>
</template>
@@ -99,7 +99,7 @@
<el-tag type="info" size="medium">{{ currentMerchant.nickName }}(ID: {{ currentMerchant.userId }})</el-tag>
<br />
<span style="margin-top: 8px; display: inline-block;">
当前卡商余额:<strong style="color: #E6A23C; font-size: 18px;">{{ currentMerchant.merchantBalance }}</strong>
当前卡商余额:<strong style="color: #8f6418; font-size: 18px;">{{ currentMerchant.merchantBalance }}</strong>
</span>
</div>
<el-form :model="rechargeForm" :rules="rechargeRules" ref="rechargeForm" label-width="100px">
@@ -134,7 +134,7 @@
<el-tag type="info" size="medium">{{ currentMerchant.nickName }}(ID: {{ currentMerchant.userId }})</el-tag>
<br />
<span style="margin-top: 8px; display: inline-block;">
当前卡商余额:<strong style="color: #E6A23C; font-size: 18px;">{{ currentMerchant.merchantBalance }}</strong>
当前卡商余额:<strong style="color: #8f6418; font-size: 18px;">{{ currentMerchant.merchantBalance }}</strong>
</span>
</div>
<el-form :model="editBalanceForm" :rules="editBalanceRules" ref="editBalanceForm" label-width="100px">
@@ -149,10 +149,10 @@
/>
</el-form-item>
<el-form-item label="变动金额">
<span :style="{ color: balanceDelta > 0 ? '#67C23A' : (balanceDelta < 0 ? '#F56C6C' : '#909399'), fontWeight: 'bold' }">
<span :style="{ color: balanceDelta > 0 ? '#457450' : (balanceDelta < 0 ? '#a4485a' : '#7a664e'), fontWeight: 'bold' }">
{{ balanceDelta > 0 ? '+' : '' }}{{ balanceDelta }}
</span>
<span style="color: #909399; margin-left: 8px;">(按差额记入充值 / 消费记录)</span>
<span style="color: #7a664e; margin-left: 8px;">(按差额记入充值 / 消费记录)</span>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
@@ -419,12 +419,15 @@ export default {
</script>
<style scoped lang="scss">
/* 原来是 border-left: 4px solid #9a5c35 的单侧粗边框(规范列为禁止项),
换成雏菊黄浅底的圆角标题条,仍是整块标题但不靠一侧色带表达层级 */
.section-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
padding-left: 10px;
border-left: 4px solid #409EFF;
color: #303133;
padding: 6px 14px;
border-radius: 12px;
background: rgba(245, 215, 95, 0.22);
color: #4f4130;
}
</style>
@@ -325,7 +325,7 @@ export default {
align-items: center;
justify-content: space-between;
&:hover {
background-color: #409eff;
background-color: #9a5c35;
transition: all ease 0.3s;
}
}
@@ -393,7 +393,7 @@ export default {
padding: 2px 5px;
line-height: 32px;
&:hover {
background-color: #409eff;
background-color: #9a5c35;
transition: all ease 0.3s;
}
}
@@ -122,7 +122,7 @@
>发放饰品</el-button>
<el-button icon="el-icon-edit" size="mini" type="text" @click="handleUpdate(scope.row)">编辑</el-button>
<el-button icon="el-icon-date" size="mini" type="text" @click="handleDetail(scope.row)">详情</el-button>
<el-button icon="el-icon-delete" size="mini" type="text" style="color: #f56c6c;" @click="handleDelete(scope.row)">删除</el-button>
<el-button icon="el-icon-delete" size="mini" type="text" style="color: #a4485a;" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -545,7 +545,7 @@ export default {
"普通": "#b0c3d9",
"普通级": "#b0c3d9"
};
return map[rarityName] || "#909399";
return map[rarityName] || "#7a664e";
},
// 饰品品质颜色映射:1黄/2红/3紫/4蓝/5蓝(4与5均为蓝色)
getQualityColor(quality) {
@@ -553,10 +553,10 @@ export default {
"1": "#ffd700",
"2": "#ff4d4f",
"3": "#a16ee8",
"4": "#409eff",
"5": "#409eff"
"4": "#9a5c35",
"5": "#9a5c35"
};
return map[quality] || "#909399";
return map[quality] || "#7a664e";
},
getQualityName(quality) {
if (quality == '4' || quality == '5') return '蓝色';
@@ -792,7 +792,7 @@ export default {
align-items: center;
justify-content: space-between;
&:hover {
background-color: #409eff;
background-color: #9a5c35;
transition: all ease 0.3s;
}
}
@@ -491,7 +491,7 @@ export default {
align-items: center;
justify-content: space-between;
&:hover {
background-color: #409eff;
background-color: #9a5c35;
transition: all ease 0.3s;
}
}
@@ -234,7 +234,7 @@ export default {
align-items: center;
justify-content: space-between;
&:hover {
background-color: #409eff;
background-color: #9a5c35;
transition: all ease 0.3s;
}
}
+5 -5
View File
@@ -172,10 +172,10 @@
></el-option>
</el-select>
<div v-if="form.rollType == 1 && form.userId && form.jackpotId" style="font-size: 13px; line-height: 1.6; margin-top: 4px;">
当前金币余额:<span :style="{ color: balanceInsufficient ? '#F56C6C' : '#67C23A' }">{{ anchorBalance }}</span> 金币
&nbsp;|&nbsp; 奖池总价值:<span style="color:#E6A23C;">{{ selectedJackpotTotal }}</span> 金币
<span v-if="balanceInsufficient" style="color:#F56C6C; font-weight:bold;">(余额不足)</span>
<span v-if="selectedJackpotTotal > 0 && selectedJackpotTotal < 200" style="color:#F56C6C; font-weight:bold;">(主播房奖池总价值不能低于200金币)</span>
当前金币余额:<span :style="{ color: balanceInsufficient ? '#a4485a' : '#457450' }">{{ anchorBalance }}</span> 金币
&nbsp;|&nbsp; 奖池总价值:<span style="color:#8f6418;">{{ selectedJackpotTotal }}</span> 金币
<span v-if="balanceInsufficient" style="color:#a4485a; font-weight:bold;">(余额不足)</span>
<span v-if="selectedJackpotTotal > 0 && selectedJackpotTotal < 200" style="color:#a4485a; font-weight:bold;">(主播房奖池总价值不能低于200金币)</span>
</div>
</el-form-item>
<el-form-item label="开奖时间" prop="endTime">
@@ -225,7 +225,7 @@
</el-dialog>
<!-- 中奖详情 -->
<el-dialog title="中奖详情" :visible.sync="winningVisible" width="700px">
<div style="margin-bottom: 10px; color: #909399; font-size: 13px;">
<div style="margin-bottom: 10px; color: #7a664e; font-size: 13px;">
共 <b>{{ winningList.length }}</b> 件奖品,总价值 <b>{{ winningTotalPrice }}</b> 金币
</div>
<el-table v-loading="winningLoading" :data="winningPagedList" border stripe max-height="400">
+2 -2
View File
@@ -412,7 +412,7 @@ export default {
"普通": "#b0c3d9",
"普通级": "#b0c3d9"
};
return map[rarityName] || "#909399";
return map[rarityName] || "#7a664e";
},
serchprop() {
this.loadings = true;
@@ -620,7 +620,7 @@ export default {
padding: 2px 5px;
line-height: 32px;
&:hover {
background-color: #409eff;
background-color: #9a5c35;
transition: all ease 0.3s;
}
}
@@ -434,7 +434,7 @@ export default {
padding: 2px 5px;
line-height: 32px;
&:hover {
background-color: #409eff;
background-color: #9a5c35;
transition: all ease 0.3s;
}
}
+1 -1
View File
@@ -522,7 +522,7 @@ export default {
"普通": "#b0c3d9",
"普通级": "#b0c3d9"
};
return map[rarityName] || "#909399";
return map[rarityName] || "#7a664e";
},
initEcharts(res) {
var chartDom = document.getElementById("main");
@@ -331,7 +331,7 @@ export default {
"普通": "#b0c3d9",
"普通级": "#b0c3d9"
};
return map[rarityName] || "#909399";
return map[rarityName] || "#7a664e";
},
handleR() {
document.body.click();
@@ -34,7 +34,7 @@
<div style="padding-left: 18px; color: #606266;">
玩家选的概率 ≤ 75% 时,真实计算金额 = 投入金额;<br>
玩家选的概率 > 75% 时,最高只按 75% 计算。<br>
<span style="color: #909399;">例如:饰品价格 1000,选 90% 概率,实际扣 900,但真实计算金额只算 750。</span>
<span style="color: #7a664e;">例如:饰品价格 1000,选 90% 概率,实际扣 900,但真实计算金额只算 750。</span>
</div>
<div><strong>3. 起始金额:</strong>本轮抽奖之前,真实计算口径下已经累计了多少钱。如果上一轮多投了,多出的部分会带到这里。</div>
<div><strong>4. 本轮目标金额:</strong>= 饰品价格 × 幸运区间随机值 ÷ 100。幸运区间是后台配置的(如 [100,160]),每轮开始时在区间内随机取一个值,算出本轮累计到多少钱就会出货。</div>
@@ -336,7 +336,7 @@ export default {
align-items: center;
justify-content: space-between;
&:hover {
background-color: #409eff;
background-color: #9a5c35;
transition: all ease 0.3s;
}
}
@@ -501,7 +501,7 @@ export default {
"普通": "#b0c3d9",
"普通级": "#b0c3d9"
};
return map[rarityName] || "#909399";
return map[rarityName] || "#7a664e";
},
getList() {
this.loading = true;
@@ -938,7 +938,7 @@ export default {
padding: 2px 5px;
line-height: 32px;
&:hover {
background-color: #409eff;
background-color: #9a5c35;
transition: all ease 0.3s;
}
}
@@ -23,7 +23,7 @@
controls-position="right"
style="width:150px"
/>
<span style="margin-left:8px; color:#909399;">
<span style="margin-left:8px; color:#7a664e;">
元 返水超过该金额不自动发放,需在下方「操作」列点「发放CDK」人工发放;填 <b>0</b> 表示不设上限
</span>
</el-form-item>

Some files were not shown because too many files have changed in this diff Show More