feat: ai
报表
This commit is contained in:
-328
@@ -1,328 +0,0 @@
|
|||||||
# AI 平台运营对接接口清单(给前端)
|
|
||||||
|
|
||||||
> 版本:v1.0(2026-10-15) 后端模块:cpms-operations-platform(运营管理端)
|
|
||||||
> 后端路由前缀:`admin/manager/ai` 鉴权:角色 `coo` + `ai:*` 权限(Shiro)
|
|
||||||
> 状态说明:✅=后端已有、前端待对接;🚧=后端缺口(需后端开发后再给前端对接)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 已有接口(✅ 后端已实现,前端可直接对接)
|
|
||||||
|
|
||||||
### 1.1 AI 场景配置(ai_agent_config)
|
|
||||||
|
|
||||||
运营实时配置:场景开关 / 计费开关 / 倍率 / 每日免费额。改动**实时生效**(无缓存)。
|
|
||||||
|
|
||||||
#### ① 场景配置列表
|
|
||||||
|
|
||||||
- `GET /admin/manager/ai/agent/list`
|
|
||||||
- 权限:`ai.agent.view`
|
|
||||||
- 参数:无
|
|
||||||
- 返回:`Result<List<AiAgentConfigVO>>`
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"bizType": "matchImport", // 业务类型(唯一键)
|
|
||||||
"name": "对阵导入助手", // 展示名
|
|
||||||
"enabled": 1, // 业务开关 1/0(0=前端隐藏入口、会话创建被拒)
|
|
||||||
"billingEnabled": 1, // 计费开关 1/0(0=该场景免费)
|
|
||||||
"billingMultiplier": 1.0, // 计费倍率(>0,促销可调)
|
|
||||||
"dailyFreePoints": 10, // 每日免费额度(口径=每日免费结算条数;0=无免费额)
|
|
||||||
"descr": "运营备注"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
#### ② 更新场景配置
|
|
||||||
|
|
||||||
- `POST /admin/manager/ai/agent/update`
|
|
||||||
- 权限:`ai.agent.edit`(操作留痕 LogOperation)
|
|
||||||
- Body(JSON,id 必填,其余传了才更新):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"enabled": 1,
|
|
||||||
"billingEnabled": 1,
|
|
||||||
"billingMultiplier": 1.5,
|
|
||||||
"dailyFreePoints": 20,
|
|
||||||
"descr": "双11促销"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- 校验:倍率必须 >0;免费额 ≥0
|
|
||||||
- 返回:`Result<String>`("配置已更新")
|
|
||||||
- 🚧 注意:**目前没有"新增/删除场景配置"接口**(bizType 由代码注册,见 §3.2)
|
|
||||||
|
|
||||||
### 1.2 积分账户与流水(ai_points_account / ai_points_transaction)
|
|
||||||
|
|
||||||
#### ③ 账户分页
|
|
||||||
|
|
||||||
- `GET /admin/manager/ai/points/accounts`
|
|
||||||
- 权限:`ai.points.view`
|
|
||||||
- Query:`page`(默认1)、`size`(默认10)、`userId`(可选)
|
|
||||||
- 返回:`Result<PageData<AiPointsAccountVO>>`,行含 `userId / points(余额,可为负=欠款) / totalRecharged / totalConsumed`
|
|
||||||
- 口径说明:**筛选目前仅支持 userId**(接口注解里的"手机号筛选"未实现,勿按手机号出筛选项;如需后端补关联查询另行排期)
|
|
||||||
- 账户产生时机:初始表为空属正常态——用户**首次对话预检/结算或首次被调点**时自动建户(余额 0 起户)
|
|
||||||
|
|
||||||
#### ④ 手工调点(充值/纠错)
|
|
||||||
|
|
||||||
- `POST /admin/manager/ai/points/adjust`
|
|
||||||
- 权限:`ai.points.adjust`(留痕)
|
|
||||||
- **自动建户**:目标用户无账户时自动创建(0 余额起户后再原子加点,并发安全)——无需先引导用户消费
|
|
||||||
- ⚠️ **userId 不校验真实性**:后端不校验 userId 是否存在于用户表,输错会静默创建孤儿账户——
|
|
||||||
前端调点弹窗务必做成**用户选择器**(按 userId/昵称搜索选中回填),不要裸文本输入
|
|
||||||
- Body:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"userId": 123, // 必填
|
|
||||||
"points": 100, // 必填,非0;正=充/赠,负=扣
|
|
||||||
"type": 1, // 可选 1=充值/赠送 2=调账;缺省按正负推导
|
|
||||||
"descr": "客诉补偿" // 可选,审计备注
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- 返回:`Result<String>`("调点成功")
|
|
||||||
|
|
||||||
#### ⑤ 流水分页
|
|
||||||
|
|
||||||
- `GET /admin/manager/ai/points/transactions`
|
|
||||||
- 权限:`ai.points.view`
|
|
||||||
- Query:`page`、`size`、`userId`、`bizType`(场景)、`type`(1充值/2调账/3消费/4欠款)、`date`(yyyy-MM-dd)
|
|
||||||
- 返回:`Result<PageData<AiPointsTxnVO>>`,行含 `userId / bizType / type / changePoints / inputTokens / outputTokens / model / messageId / freeFlag / descr / createDate`
|
|
||||||
|
|
||||||
### 1.3 AI 使用黑名单(✅ 后端已实现,前端可直接对接)
|
|
||||||
|
|
||||||
拉黑期间禁止用户使用 AI 平台能力(对话三助手 + 对阵导入)。生效口径:`status=1` 且(未设到期时间或未到期);**解除不物理删**(status 置 0 保留历史),再次拉黑原地置回。拉黑**即时生效**(入口逐请求查库,无缓存)。
|
|
||||||
|
|
||||||
#### ⑪ 黑名单分页
|
|
||||||
|
|
||||||
- `GET /admin/manager/ai/blacklist/list`
|
|
||||||
- 权限:`ai.blacklist.view`
|
|
||||||
- Query:`page`(默认1)、`size`(默认10)、`userId`(可选)、`status`(可选 1/0)
|
|
||||||
- 返回:`Result<PageData<AiBlacklistVO>>`
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"userId": 123,
|
|
||||||
"reason": "恶意刷 AI(敏感信息预警 3 次)",
|
|
||||||
"status": 1,
|
|
||||||
"expireDate": "2026-11-15 00:00:00", // null=永久
|
|
||||||
"operatorId": 88,
|
|
||||||
"createDate": "...",
|
|
||||||
"updateDate": "..."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
#### ⑫ 拉黑
|
|
||||||
|
|
||||||
- `POST /admin/manager/ai/blacklist/add`
|
|
||||||
- 权限:`ai.blacklist.edit`(留痕)
|
|
||||||
- Body:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"userId": 123, // 必填
|
|
||||||
"reason": "恶意刷 AI", // 必填(审计要求)
|
|
||||||
"expireDate": "2026-11-15 00:00:00" // 可选(yyyy-MM-dd[ HH:mm:ss]);空=永久;必须晚于当前
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- 返回:`Result<String>`("已拉黑(对话与导入入口即时拦截)")
|
|
||||||
- 前端建议同调点:userId 用**用户选择器**选中回填,不要裸文本输入
|
|
||||||
|
|
||||||
#### ⑬ 解除拉黑
|
|
||||||
|
|
||||||
- `POST /admin/manager/ai/blacklist/remove`
|
|
||||||
- 权限:`ai.blacklist.edit`(留痕)
|
|
||||||
- Body:`{"userId": 123}`;幂等(无拉黑记录也返回成功)
|
|
||||||
- 返回:`Result<String>`("已解除拉黑")
|
|
||||||
|
|
||||||
**被拦截用户看到的表现**(前端可据此定制文案):
|
|
||||||
|
|
||||||
- 创建/续接会话:HTTP 403 JSON(`Result.error`,msg="您已被限制使用 AI 功能,如有疑问请联系管理员")
|
|
||||||
- 对话 SSE:`error` 事件 code=403,同文案
|
|
||||||
- 导入(建任务/接管/提取/矩阵学习/提交):HTTP 500 JSON 业务异常,同文案
|
|
||||||
|
|
||||||
### 1.4 报表
|
|
||||||
|
|
||||||
#### ⑥ 计费报表(按日按场景汇总)
|
|
||||||
|
|
||||||
- `GET /admin/manager/ai/billing/report`
|
|
||||||
- 权限:`ai.points.view`
|
|
||||||
- Query:`startDate`、`endDate`(均可选,yyyy-MM-dd)
|
|
||||||
- 返回行:`{ date, bizType, messageCount, totalTokens, totalPoints }`(按日期降序;消费+欠款流水聚合)
|
|
||||||
|
|
||||||
#### ⑦ 直解格式统计报表
|
|
||||||
|
|
||||||
- `GET /admin/manager/ai/freeform/report?top=50`
|
|
||||||
- 权限:`ai.points.view`
|
|
||||||
- 返回行:`{ fingerprint, docNature, signalSet, headerSignature, useCount, lastUsedAt, highFrequency }`
|
|
||||||
(`highFrequency`=月频次≥5 的文档指纹,确定性解析器需求评估依据)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 数据字典 ai_agent_model(🚧 运营平台**未对接**,当前直接改库)
|
|
||||||
|
|
||||||
### 2.1 现状(重要)
|
|
||||||
|
|
||||||
模型/网关/Key 的运行时热切换依赖**系统数据字典** `sys_dict_type` / `sys_dict_data`:
|
|
||||||
|
|
||||||
- `sys_dict_type.dict_type = 'ai_agent_model'`(取第一条该类型的 id)
|
|
||||||
- `sys_dict_data`:`dict_type_id` 关联上表,`dict_label → dict_value` 键值对,同 label 按 `sort` 升序取第一条
|
|
||||||
- **后端无任何字典维护接口**(全仓库无 SysDictType/DictData Controller),运营平台也没维护页——目前只能 DBA 直接改表
|
|
||||||
- 生效机制:后端本地 **30s TTL 缓存**,改库后最长 30s 生效,无需重启
|
|
||||||
|
|
||||||
### 2.2 字典键值契约(维护页按此渲染表单)
|
|
||||||
|
|
||||||
label 即配置键,value 即配置值。三个用途组,组内未配置的维度按「用途组 → 全局默认字典 → 配置文件 cpms.ai.*」回落:
|
|
||||||
|
|
||||||
| label(dict_label) | value(dict_value) | 说明 |
|
|
||||||
| ------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| `model` | 如 `deepseek-flash` | **全局默认模型**(对话/排赛/导入助手+流式:未单独配置用途组的助手均回落此项) |
|
|
||||||
| `baseUrl` | OpenAI 兼容网关地址 | 全局默认网关;缺省时按模型取内置映射(deepseek-flash→api.deepseek.com、qwen3.7-flash→maas.qianwenaiapi.com),再回落配置文件 |
|
|
||||||
| `apiKey` | sk-xxx | 全局默认 API Key(**敏感**,前端展示需脱敏/改写场景) |
|
|
||||||
| `thinking` | on / off / default | 全局思考模式(按厂商自动映射参数,见模型配置指南 §5);default=跟随网关默认 |
|
|
||||||
| `steward.model` | 同上 | 赛事管家模型(2026-10-16 分离:管家以查询问答为主,可配轻量高并发模型);整组未配置时回落全局默认 |
|
|
||||||
| `steward.baseUrl` | 同上 | 赛事管家网关 |
|
|
||||||
| `steward.apiKey` | 同上 | 赛事管家 Key |
|
|
||||||
| `steward.thinking` | 同 thinking | 管家思考模式;未配置回落全局 `thinking` |
|
|
||||||
| `extract.model` | 同上 | 提取组模型(导入直解提取+诊断);整组未配置时回落全局默认 |
|
|
||||||
| `extract.baseUrl` | 同上 | 提取组网关 |
|
|
||||||
| `extract.apiKey` | 同上 | 提取组 Key |
|
|
||||||
| `extract.thinking` | 同 thinking | 提取组思考模式;未配置回落全局 `thinking` |
|
|
||||||
| `failover.model` | 同上 | 备用网关模型;**未配置=故障转移功能整体关闭** |
|
|
||||||
| `failover.baseUrl` | 同上 | 备用网关地址 |
|
|
||||||
| `failover.apiKey` | 同上 | 备用网关 Key |
|
|
||||||
| `failover.thinking` | 同 thinking | 备用网关思考模式;未配置回落全局 `thinking` |
|
|
||||||
|
|
||||||
其余运行参数(temperature/超时/maxTokens/maxToolHops/流式开关等)在配置文件 `cpms.ai.*`,不在字典范围。
|
|
||||||
|
|
||||||
### 2.3 接口(✅ 后端已实现,前端可直接对接)
|
|
||||||
|
|
||||||
#### ⑧ 模型字典-读当前配置
|
|
||||||
|
|
||||||
- `GET /admin/manager/ai/model/dict`
|
|
||||||
- 权限:`ai.model.view`
|
|
||||||
- 参数:无。白名单 16 键**全量返回**(未配置的键 value=null,前端按此渲染完整表单)
|
|
||||||
- 返回:`Result<List<AiModelDictVO>>`
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"label": "extract.apiKey",
|
|
||||||
"value": "sk-****ab3f", // 敏感键脱敏(前3+****+后4);未配置 null
|
|
||||||
"sensitive": true, // true=apiKey 类:编辑时"留空=保留原值",禁止回传掩码
|
|
||||||
"purpose": "extract", // chat=对话/工具(全局默认) extract=导入提取+诊断 failover=备用网关
|
|
||||||
"remark": "提取组 API Key(敏感)" // 表单提示文案
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
#### ⑨ 模型字典-整组保存(upsert)
|
|
||||||
|
|
||||||
- `POST /admin/manager/ai/model/dict`
|
|
||||||
- 权限:`ai.model.edit`(留痕;事务性)
|
|
||||||
- Body:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"items": [
|
|
||||||
{ "label": "model", "value": "deepseek-flash" },
|
|
||||||
{ "label": "extract.apiKey", "value": "" } // 敏感键传空=保留原值
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- 校验:label 必须在白名单 16 键内;`*model` 必填(要回落请用重置);`*baseUrl` 须 http(s) 开头;`*thinking` 取值 on/off/default;**`*apiKey` 留空或回传脱敏掩码(含 `****`)均视为"未修改、保留原值"**——前端可整表单原样提交,无需清空未编辑的 Key 字段
|
|
||||||
- 返回:`Result<String>`("已保存(最长 30 秒生效)")
|
|
||||||
|
|
||||||
#### ⑩ 模型字典-重置指定键
|
|
||||||
|
|
||||||
- `POST /admin/manager/ai/model/dict/reset`
|
|
||||||
- 权限:`ai.model.edit`(留痕)
|
|
||||||
- Body:`{"labels": ["extract.model", "failover.apiKey"]}`(须在白名单内)
|
|
||||||
- 语义:删除字典行,该键回落后一级(用途组 → 全局默认字典 → 配置文件);`failover.model` 重置=故障转移整体关闭
|
|
||||||
- 返回:`Result<String>`("已重置 N 项(最长 30 秒生效)")
|
|
||||||
|
|
||||||
**生效机制**:后端本地 30s TTL 缓存,保存/重置后最长 30 秒生效、无需重启。页面保存成功文案建议提示这一点。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 其它后端缺口(🚧 排期后补充对接)
|
|
||||||
|
|
||||||
### 3.1 AI 审计查询(数据已落库、无运营查询接口)
|
|
||||||
|
|
||||||
运营排查客诉/审计需要,建议新增(只读,权限 `ai.audit.view`):
|
|
||||||
|
|
||||||
| 数据表 | 内容 | 建议接口 |
|
|
||||||
| -------------------- | ---------------------------------------------------------------------- | ------------------------------------------------- |
|
|
||||||
| `ai_chat_session` | 会话:userId/bizType/activeAgent/tournamentId/status/model/totalTokens | `GET /admin/manager/ai/chat/sessions`(分页筛选) |
|
|
||||||
| `ai_chat_message` | 消息审计:sessionId/role/agentCode/content(已脱敏)/tokens | `GET /admin/manager/ai/chat/messages?sessionId=` |
|
|
||||||
| `ai_chat_card_event` | 卡片事件:cardType/refId/payload/version/delivered | 可并入消息详情或独立查询 |
|
|
||||||
|
|
||||||
### 3.2 场景配置新增/下线
|
|
||||||
|
|
||||||
`ai_agent_config` 的 bizType 由代码注册(matchImport / tournamentSteward / scheduleAgent 等),目前运营只能改不能增删。若要页面化新增,需后端补 `agent/add` + 启动期注册校验。
|
|
||||||
|
|
||||||
### 3.3 模型单价维护(✅ 已上线:进数据字典 + 通用字典页维护)
|
|
||||||
|
|
||||||
单价已从配置文件升级为**数据字典热切换**(与模型配置同模式):
|
|
||||||
|
|
||||||
- 字典类型 `ai_billing_price`,label 契约:`{model}.input` / `{model}.output`(单价=点/千 token),以及兜底默认价 `default.input` / `default.output`
|
|
||||||
- **生效价回落链**:字典 → 配置文件 `cpms.ai.billing.models.{model}.*` → 内置默认(0.8/2.0);改字典最长 30s 生效
|
|
||||||
- 读接口:`GET /admin/manager/ai/billing/prices`(权限 `ai.points.view`)——模型集合=字典已配 ∪ 计费流水出现过的模型,返回 `[{model, inputPrice, outputPrice}]`(生效价);漏配模型也在列,便于发现
|
|
||||||
- 维护入口:**通用字典管理页**(§2.4)编辑 `ai_billing_price` 类型的数据行即可,无需 AI 专用页面
|
|
||||||
|
|
||||||
### 2.4 通用字典管理(✅ 已上线:全部 sys_dict 类型可维护,不限 AI)
|
|
||||||
|
|
||||||
独立控制器 `admin/manager/dict/*`,权限 `sys.dict.view` / `sys.dict.edit`(coo 角色):
|
|
||||||
|
|
||||||
| 接口 | 说明 |
|
|
||||||
| --------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| `GET types` | 类型全量列表(含每类型数据行数,sort 升序) |
|
|
||||||
| `POST type/save` | `{id?, dictType, dictName, remark, sort}`;id 空=新建(dictType 必填且唯一);**dictType 建后不可改** |
|
|
||||||
| `POST type/delete` | `{id}`;级联删除该类型全部数据行(删 AI 字典=回落配置文件语义) |
|
|
||||||
| `GET data/list?dictTypeId=` | 该类型数据行(sort 升序) |
|
|
||||||
| `POST data/save` | `{id?, dictTypeId, dictLabel, dictValue, sort, remark}`;同 type 下 label 唯一(更新排除自身);更新不换 type |
|
|
||||||
| `POST data/delete` | `{id}`;删除后该键回落消费方下一级配置 |
|
|
||||||
|
|
||||||
返回行结构(VO):类型 `{id, dictType, dictName, remark, sort, dataCount}`;数据 `{id, dictTypeId, dictLabel, dictValue, sort, remark}`。
|
|
||||||
|
|
||||||
注意事项:
|
|
||||||
|
|
||||||
- 消费方(模型字典/单价/系统其它字典)均有 30s TTL 缓存,保存/删除后最长 30s 生效,页面提示沿用
|
|
||||||
- `ai_agent_model` 的 apiKey 类行在本页**明文展示**——敏感场景建议运营走 AI 专用页(`/admin/manager/ai/model/dict`,有脱敏与"留空=保留"保护);通用页保留明文是通用字典的语义(其他系统字典值无敏感问题)
|
|
||||||
|
|
||||||
### 3.4 调试工具(低优先级)
|
|
||||||
|
|
||||||
`/ai/debug/*`(赛程快照/校验/文件下载),仅在 `cpms.ai.debug-tools.enabled=true` 的环境暴露,带 secret 校验,暂无需前端页面。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 前端菜单与权限配置
|
|
||||||
|
|
||||||
| 菜单建议 | 权限标识 | 说明 |
|
|
||||||
| -------------------------- | ----------------------------------------- | -------------------------------------------------------- |
|
|
||||||
| AI 场景配置 | `ai.agent.view` / `ai.agent.edit` | 已有后端权限点 |
|
|
||||||
| 积分管理 | `ai.points.view` / `ai.points.adjust` | 已有后端权限点 |
|
|
||||||
| 计费报表 | `ai.points.view` | 复用 |
|
|
||||||
| 模型单价(✅ 已上线) | `ai.points.view` | `GET billing/prices`;编辑走通用字典页 |
|
|
||||||
| 直解报表 | `ai.points.view` | 复用 |
|
|
||||||
| AI 使用黑名单(✅ 已上线) | `ai.blacklist.view` / `ai.blacklist.edit` | 权限点需在运营平台权限表配置 |
|
|
||||||
| 通用字典管理(✅ 已上线) | `sys.dict.view` / `sys.dict.edit` | 覆盖全部 sys_dict 类型(含 AI 模型/单价字典) |
|
|
||||||
| 模型字典(✅ 已上线) | `ai.model.view` / `ai.model.edit` | AI 专用聚合视图(白名单校验/掩码);权限点需在权限表配置 |
|
|
||||||
| AI 审计(待后端) | `ai.audit.view` | 需后端随接口一并加 |
|
|
||||||
|
|
||||||
角色:以上接口全部要求 `coo` 角色。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 参考:用户端 AI 接口(非运营平台,前端勿混用)
|
|
||||||
|
|
||||||
用户端(小程序/管理后台用户侧)AI 能力走 `cpms-admin` 的 `/ai/*`:会话 `/ai/session`、对话 SSE `/ai/chat`、历史回放 `/ai/session/messages`、点数 `/ai/points/balance|transactions`、拍板卡 `/ai/draft/*`、转接 `/ai/session/transfer`、对阵导入 `/ai/import/*`。运营平台页面不调用这组接口。
|
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import { createApp } from 'vue';
|
import { createApp } from 'vue';
|
||||||
import Antd from 'ant-design-vue';
|
import Antd from 'ant-design-vue';
|
||||||
import 'ant-design-vue/dist/reset.css';
|
import 'ant-design-vue/dist/reset.css';
|
||||||
|
import './styles.less';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import router from './router';
|
import router from './router';
|
||||||
import './router/guard'; // 路由守卫:登录鉴权 + 动态菜单加载
|
import './router/guard'; // 路由守卫:登录鉴权 + 动态菜单加载
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// ===== 高频标记 =====
|
||||||
|
.highFreqTag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
background: #fff1f0;
|
||||||
|
color: #ff4d4f;
|
||||||
|
border: 1px solid #ffccc7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.normalTag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
}
|
||||||
@@ -0,0 +1,368 @@
|
|||||||
|
import { defineComponent, reactive, ref, computed } from 'vue';
|
||||||
|
import { Button, Table, Space, DatePicker, InputNumber, Radio, Pagination } from 'ant-design-vue';
|
||||||
|
import { type Dayjs } from 'dayjs';
|
||||||
|
import { useEffect } from '@/hooks/useEffect';
|
||||||
|
import { useContainerSize } from '@/hooks';
|
||||||
|
import {
|
||||||
|
getAiBillingReport,
|
||||||
|
getAiFreeformReport,
|
||||||
|
getAiAgentList,
|
||||||
|
type AiBillingReportVO,
|
||||||
|
type AiFreeformReportVO,
|
||||||
|
type AiAgentConfigVO,
|
||||||
|
} from '../model/services';
|
||||||
|
import styles from './BillingReportTab.module.less';
|
||||||
|
import pageStyles from '@/assets/styles/pageLayout.module.less';
|
||||||
|
|
||||||
|
// ===== 工具函数 =====
|
||||||
|
|
||||||
|
/** 格式化数字(千分位),兼容后端返回字符串类型 */
|
||||||
|
const formatNum = (val: unknown): string => {
|
||||||
|
const n = Number(val);
|
||||||
|
if (val == null || Number.isNaN(n)) return '-';
|
||||||
|
return n.toLocaleString('zh-CN');
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 安全的文本渲染:null/undefined/空 → '-' */
|
||||||
|
const renderText = (text: unknown): string => (text != null && text !== '' ? String(text) : '-');
|
||||||
|
|
||||||
|
/** 等宽字体渲染(用于指纹、签名等哈希值) */
|
||||||
|
const renderMono = (text: string) => (
|
||||||
|
<span style={{ fontFamily: "'SF Mono', 'Courier New', Consolas, monospace", fontSize: '12px' }}>
|
||||||
|
{renderText(text)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== 场景名称映射 =====
|
||||||
|
|
||||||
|
const useBizTypeMap = () => {
|
||||||
|
const map = ref<Record<string, string>>({});
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const res = await getAiAgentList();
|
||||||
|
if (res.code == 200 && res.data) {
|
||||||
|
const m: Record<string, string> = {};
|
||||||
|
res.data.forEach((item: AiAgentConfigVO) => {
|
||||||
|
m[item.bizType] = item.name;
|
||||||
|
});
|
||||||
|
map.value = m;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
return map;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== 组件 =====
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'BillingReportTab',
|
||||||
|
setup() {
|
||||||
|
type SubView = 'billing' | 'freeform';
|
||||||
|
const subView = ref<SubView>('billing');
|
||||||
|
const bizTypeMap = useBizTypeMap();
|
||||||
|
const { containerRef, height } = useContainerSize({ headerOffset: 55 });
|
||||||
|
|
||||||
|
// ===== 计费报表(前端分页) =====
|
||||||
|
const billingLoading = ref(false);
|
||||||
|
const billingData = ref<AiBillingReportVO[]>([]);
|
||||||
|
const billingFilter = reactive({
|
||||||
|
startDate: null as Dayjs | null,
|
||||||
|
endDate: null as Dayjs | null,
|
||||||
|
});
|
||||||
|
const billingPage = reactive({ current: 1, pageSize: 20 });
|
||||||
|
const billingPagedData = computed(() => {
|
||||||
|
const start = (billingPage.current - 1) * billingPage.pageSize;
|
||||||
|
return billingData.value.slice(start, start + billingPage.pageSize);
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchBilling = async () => {
|
||||||
|
billingLoading.value = true;
|
||||||
|
try {
|
||||||
|
const params: Record<string, string> = {};
|
||||||
|
if (billingFilter.startDate)
|
||||||
|
params.startDate = billingFilter.startDate.format('YYYY-MM-DD');
|
||||||
|
if (billingFilter.endDate) params.endDate = billingFilter.endDate.format('YYYY-MM-DD');
|
||||||
|
const res = await getAiBillingReport(params);
|
||||||
|
billingData.value = res.code == 200 && res.data ? res.data : [];
|
||||||
|
} catch {
|
||||||
|
billingData.value = [];
|
||||||
|
} finally {
|
||||||
|
billingLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件初始化时立即加载
|
||||||
|
fetchBilling();
|
||||||
|
|
||||||
|
const onBillingReset = () => {
|
||||||
|
billingFilter.startDate = null;
|
||||||
|
billingFilter.endDate = null;
|
||||||
|
billingPage.current = 1;
|
||||||
|
fetchBilling();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBillingPageChange = (page: number, pageSize: number) => {
|
||||||
|
billingPage.current = page;
|
||||||
|
billingPage.pageSize = pageSize;
|
||||||
|
};
|
||||||
|
|
||||||
|
const billingColumns = [
|
||||||
|
{
|
||||||
|
title: '日期',
|
||||||
|
dataIndex: 'date',
|
||||||
|
key: 'date',
|
||||||
|
width: 130,
|
||||||
|
align: 'center' as const,
|
||||||
|
sorter: (a: AiBillingReportVO, b: AiBillingReportVO) =>
|
||||||
|
(a.date || '').localeCompare(b.date || ''),
|
||||||
|
defaultSortOrder: 'descend' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '业务场景',
|
||||||
|
dataIndex: 'bizType',
|
||||||
|
key: 'bizType',
|
||||||
|
width: 160,
|
||||||
|
align: 'center' as const,
|
||||||
|
customRender: ({ text }: { text: string }) => bizTypeMap.value[text] || text || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '消息数',
|
||||||
|
dataIndex: 'messageCount',
|
||||||
|
key: 'messageCount',
|
||||||
|
width: 120,
|
||||||
|
align: 'center' as const,
|
||||||
|
sorter: (a: AiBillingReportVO, b: AiBillingReportVO) =>
|
||||||
|
Number(a.messageCount) - Number(b.messageCount),
|
||||||
|
customRender: ({ text }: { text: number }) => formatNum(text),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '总 Token',
|
||||||
|
dataIndex: 'totalTokens',
|
||||||
|
key: 'totalTokens',
|
||||||
|
width: 140,
|
||||||
|
align: 'center' as const,
|
||||||
|
sorter: (a: AiBillingReportVO, b: AiBillingReportVO) =>
|
||||||
|
Number(a.totalTokens) - Number(b.totalTokens),
|
||||||
|
customRender: ({ text }: { text: number }) => formatNum(text),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '消耗积分',
|
||||||
|
dataIndex: 'totalPoints',
|
||||||
|
key: 'totalPoints',
|
||||||
|
width: 140,
|
||||||
|
align: 'center' as const,
|
||||||
|
sorter: (a: AiBillingReportVO, b: AiBillingReportVO) =>
|
||||||
|
Number(a.totalPoints) - Number(b.totalPoints),
|
||||||
|
customRender: ({ text }: { text: number }) => (
|
||||||
|
<span style={{ color: '#ff4d4f', fontWeight: 600 }}>{formatNum(text)}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ===== 直解报表 =====
|
||||||
|
const freeformLoading = ref(false);
|
||||||
|
const freeformData = ref<AiFreeformReportVO[]>([]);
|
||||||
|
const freeformTop = ref(50);
|
||||||
|
const freeformLoaded = ref(false);
|
||||||
|
|
||||||
|
const fetchFreeform = async () => {
|
||||||
|
freeformLoading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getAiFreeformReport({ top: freeformTop.value });
|
||||||
|
freeformData.value = res.code == 200 && res.data ? res.data : [];
|
||||||
|
} catch {
|
||||||
|
freeformData.value = [];
|
||||||
|
} finally {
|
||||||
|
freeformLoading.value = false;
|
||||||
|
freeformLoaded.value = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换到直解格式统计时自动加载(首次)
|
||||||
|
useEffect(() => {
|
||||||
|
if (subView.value === 'freeform' && !freeformLoaded.value) {
|
||||||
|
fetchFreeform();
|
||||||
|
}
|
||||||
|
}, [subView]);
|
||||||
|
|
||||||
|
const onFreeformReset = () => {
|
||||||
|
freeformTop.value = 50;
|
||||||
|
fetchFreeform();
|
||||||
|
};
|
||||||
|
|
||||||
|
const freeformColumns = [
|
||||||
|
{
|
||||||
|
title: '文档指纹',
|
||||||
|
dataIndex: 'fingerprint',
|
||||||
|
key: 'fingerprint',
|
||||||
|
width: 200,
|
||||||
|
align: 'left' as const,
|
||||||
|
ellipsis: true,
|
||||||
|
customRender: ({ text }: { text: string }) => renderMono(text),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '文档性质',
|
||||||
|
dataIndex: 'docNature',
|
||||||
|
key: 'docNature',
|
||||||
|
width: 120,
|
||||||
|
align: 'center' as const,
|
||||||
|
customRender: ({ text }: { text: string }) => renderText(text),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '信号集',
|
||||||
|
dataIndex: 'signalSet',
|
||||||
|
key: 'signalSet',
|
||||||
|
width: 120,
|
||||||
|
align: 'center' as const,
|
||||||
|
customRender: ({ text }: { text: string }) => renderText(text),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '表头签名',
|
||||||
|
dataIndex: 'headerSignature',
|
||||||
|
key: 'headerSignature',
|
||||||
|
width: 180,
|
||||||
|
align: 'left' as const,
|
||||||
|
ellipsis: true,
|
||||||
|
customRender: ({ text }: { text: string }) => renderMono(text),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '使用次数',
|
||||||
|
dataIndex: 'useCount',
|
||||||
|
key: 'useCount',
|
||||||
|
width: 100,
|
||||||
|
align: 'center' as const,
|
||||||
|
sorter: (a: AiFreeformReportVO, b: AiFreeformReportVO) =>
|
||||||
|
Number(a.useCount) - Number(b.useCount),
|
||||||
|
defaultSortOrder: 'descend' as const,
|
||||||
|
customRender: ({ text }: { text: number }) => formatNum(text),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '最后使用',
|
||||||
|
dataIndex: 'lastUsedAt',
|
||||||
|
key: 'lastUsedAt',
|
||||||
|
width: 180,
|
||||||
|
align: 'center' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '频次',
|
||||||
|
dataIndex: 'highFrequency',
|
||||||
|
key: 'highFrequency',
|
||||||
|
width: 100,
|
||||||
|
align: 'center' as const,
|
||||||
|
sorter: (a: AiFreeformReportVO, b: AiFreeformReportVO) =>
|
||||||
|
Number(a.highFrequency) - Number(b.highFrequency),
|
||||||
|
customRender: ({ text }: { text: boolean }) =>
|
||||||
|
text ? (
|
||||||
|
<span class={styles.highFreqTag}>高频</span>
|
||||||
|
) : (
|
||||||
|
<span class={styles.normalTag}>普通</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
<>
|
||||||
|
{/* 筛选栏 */}
|
||||||
|
<div class={pageStyles.filter}>
|
||||||
|
<Radio.Group
|
||||||
|
value={subView.value}
|
||||||
|
onUpdate:value={(val: SubView) => (subView.value = val)}
|
||||||
|
optionType="button"
|
||||||
|
buttonStyle="solid"
|
||||||
|
>
|
||||||
|
<Radio.Button value="billing">计费报表</Radio.Button>
|
||||||
|
<Radio.Button value="freeform">直解格式统计</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
|
||||||
|
{subView.value === 'billing' ? (
|
||||||
|
<Space>
|
||||||
|
<DatePicker
|
||||||
|
value={billingFilter.startDate ?? undefined}
|
||||||
|
onUpdate:value={(val: any) => (billingFilter.startDate = val ?? null)}
|
||||||
|
format="YYYY-MM-DD"
|
||||||
|
placeholder="开始日期"
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
<DatePicker
|
||||||
|
value={billingFilter.endDate ?? undefined}
|
||||||
|
onUpdate:value={(val: any) => (billingFilter.endDate = val ?? null)}
|
||||||
|
format="YYYY-MM-DD"
|
||||||
|
placeholder="结束日期"
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
<Button type="primary" onClick={fetchBilling} loading={billingLoading.value}>
|
||||||
|
查询
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onBillingReset}>重置</Button>
|
||||||
|
</Space>
|
||||||
|
) : (
|
||||||
|
<Space>
|
||||||
|
<span style={{ fontSize: '13px', color: 'rgba(0,0,0,0.65)' }}>返回行数:</span>
|
||||||
|
<InputNumber
|
||||||
|
value={freeformTop.value}
|
||||||
|
onUpdate:value={(val: any) =>
|
||||||
|
(freeformTop.value = typeof val === 'number' ? val : 50)
|
||||||
|
}
|
||||||
|
min={1}
|
||||||
|
max={500}
|
||||||
|
step={10}
|
||||||
|
precision={0}
|
||||||
|
placeholder="50"
|
||||||
|
style={{ width: 100 }}
|
||||||
|
/>
|
||||||
|
<Button type="primary" onClick={fetchFreeform} loading={freeformLoading.value}>
|
||||||
|
查询
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onFreeformReset}>重置</Button>
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 表格区 */}
|
||||||
|
<div class={pageStyles.table}>
|
||||||
|
<div ref={containerRef} class={pageStyles.tableBody}>
|
||||||
|
{subView.value === 'billing' ? (
|
||||||
|
<Table
|
||||||
|
columns={billingColumns as any}
|
||||||
|
dataSource={billingPagedData.value}
|
||||||
|
loading={billingLoading.value}
|
||||||
|
rowKey={(record: AiBillingReportVO) => `${record.date}_${record.bizType}`}
|
||||||
|
scroll={{ x: 'max-content', y: height.value }}
|
||||||
|
pagination={false}
|
||||||
|
size="middle"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Table
|
||||||
|
columns={freeformColumns as any}
|
||||||
|
dataSource={freeformData.value}
|
||||||
|
loading={freeformLoading.value}
|
||||||
|
rowKey="fingerprint"
|
||||||
|
scroll={{ x: 'max-content', y: height.value }}
|
||||||
|
pagination={false}
|
||||||
|
size="middle"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{subView.value === 'billing' && (
|
||||||
|
<div class={pageStyles.pagination}>
|
||||||
|
<Pagination
|
||||||
|
current={billingPage.current}
|
||||||
|
pageSize={billingPage.pageSize}
|
||||||
|
total={billingData.value.length}
|
||||||
|
showSizeChanger
|
||||||
|
showQuickJumper
|
||||||
|
showTotal={(total: number) => `共 ${total} 条`}
|
||||||
|
pageSizeOptions={['10', '20', '50', '100']}
|
||||||
|
onChange={onBillingPageChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -8,6 +8,7 @@ import AiAgentEditModal from './components/AiAgentEditModal';
|
|||||||
import AiModelDict from './components/AiModelDict';
|
import AiModelDict from './components/AiModelDict';
|
||||||
import BlacklistTab from './components/BlacklistTab';
|
import BlacklistTab from './components/BlacklistTab';
|
||||||
import PointsTab from './components/PointsTab';
|
import PointsTab from './components/PointsTab';
|
||||||
|
import BillingReportTab from './components/BillingReportTab';
|
||||||
import type { AiAgentConfigVO } from './model/services';
|
import type { AiAgentConfigVO } from './model/services';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@@ -100,6 +101,10 @@ export default defineComponent({
|
|||||||
<TabPane key="blacklist" tab="黑名单管理">
|
<TabPane key="blacklist" tab="黑名单管理">
|
||||||
<BlacklistTab />
|
<BlacklistTab />
|
||||||
</TabPane>
|
</TabPane>
|
||||||
|
|
||||||
|
<TabPane key="report" tab="报表">
|
||||||
|
<BillingReportTab />
|
||||||
|
</TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
{editModalVisible.value && (
|
{editModalVisible.value && (
|
||||||
|
|||||||
@@ -245,6 +245,54 @@ export interface DictDataDeleteParams {
|
|||||||
id: number;
|
id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== 报表相关(文档 §1.4) =====
|
||||||
|
|
||||||
|
/** 计费报表行 VO(§1.4 ⑥ GET /admin/manager/ai/billing/report) */
|
||||||
|
export interface AiBillingReportVO {
|
||||||
|
/** 日期 yyyy-MM-dd */
|
||||||
|
date: string;
|
||||||
|
/** 业务场景 */
|
||||||
|
bizType: string;
|
||||||
|
/** 消息数 */
|
||||||
|
messageCount: number;
|
||||||
|
/** 总 token 数 */
|
||||||
|
totalTokens: number;
|
||||||
|
/** 总消耗积分 */
|
||||||
|
totalPoints: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 直解格式统计报表行 VO(§1.4 ⑦ GET /admin/manager/ai/freeform/report) */
|
||||||
|
export interface AiFreeformReportVO {
|
||||||
|
/** 文档指纹 */
|
||||||
|
fingerprint: string;
|
||||||
|
/** 文档性质 */
|
||||||
|
docNature: string;
|
||||||
|
/** 信号集 */
|
||||||
|
signalSet: string;
|
||||||
|
/** 表头签名 */
|
||||||
|
headerSignature: string;
|
||||||
|
/** 使用次数 */
|
||||||
|
useCount: number;
|
||||||
|
/** 最后使用时间 */
|
||||||
|
lastUsedAt: string;
|
||||||
|
/** 是否高频(月频次≥5) */
|
||||||
|
highFrequency: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 计费报表查询参数(§1.4 ⑥) */
|
||||||
|
export interface AiBillingReportQueryParams {
|
||||||
|
/** 可选:开始日期 yyyy-MM-dd */
|
||||||
|
startDate?: string;
|
||||||
|
/** 可选:结束日期 yyyy-MM-dd */
|
||||||
|
endDate?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 直解报表查询参数(§1.4 ⑦) */
|
||||||
|
export interface AiFreeformReportQueryParams {
|
||||||
|
/** 返回行数,默认 50 */
|
||||||
|
top?: number;
|
||||||
|
}
|
||||||
|
|
||||||
const AI_AGENT_LIST = '/admin/manager/ai/agent/list';
|
const AI_AGENT_LIST = '/admin/manager/ai/agent/list';
|
||||||
const AI_AGENT_UPDATE = '/admin/manager/ai/agent/update';
|
const AI_AGENT_UPDATE = '/admin/manager/ai/agent/update';
|
||||||
const AI_MODEL_DICT_GET = '/admin/manager/ai/model/dict';
|
const AI_MODEL_DICT_GET = '/admin/manager/ai/model/dict';
|
||||||
@@ -261,6 +309,8 @@ const DICT_TYPES = '/admin/manager/dict/types';
|
|||||||
const DICT_DATA_LIST = '/admin/manager/dict/data/list';
|
const DICT_DATA_LIST = '/admin/manager/dict/data/list';
|
||||||
const DICT_DATA_SAVE = '/admin/manager/dict/data/save';
|
const DICT_DATA_SAVE = '/admin/manager/dict/data/save';
|
||||||
const DICT_DATA_DELETE = '/admin/manager/dict/data/delete';
|
const DICT_DATA_DELETE = '/admin/manager/dict/data/delete';
|
||||||
|
const AI_BILLING_REPORT = '/admin/manager/ai/billing/report';
|
||||||
|
const AI_FREEFORM_REPORT = '/admin/manager/ai/freeform/report';
|
||||||
|
|
||||||
/** 单价字典类型标识 */
|
/** 单价字典类型标识 */
|
||||||
export const AI_BILLING_PRICE_DICT_TYPE = 'ai_billing_price';
|
export const AI_BILLING_PRICE_DICT_TYPE = 'ai_billing_price';
|
||||||
@@ -350,3 +400,17 @@ export function saveDictData(params: DictDataSaveParams): Promise<ApiResult<stri
|
|||||||
export function deleteDictData(id: number): Promise<ApiResult<string>> {
|
export function deleteDictData(id: number): Promise<ApiResult<string>> {
|
||||||
return post(DICT_DATA_DELETE, { id });
|
return post(DICT_DATA_DELETE, { id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 计费报表(§1.4 ⑥,按日按场景汇总) */
|
||||||
|
export function getAiBillingReport(
|
||||||
|
params: AiBillingReportQueryParams,
|
||||||
|
): Promise<ApiResult<AiBillingReportVO[]>> {
|
||||||
|
return get(AI_BILLING_REPORT, params as Record<string, unknown>);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 直解格式统计报表(§1.4 ⑦) */
|
||||||
|
export function getAiFreeformReport(
|
||||||
|
params: AiFreeformReportQueryParams,
|
||||||
|
): Promise<ApiResult<AiFreeformReportVO[]>> {
|
||||||
|
return get(AI_FREEFORM_REPORT, params as Record<string, unknown>);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
@import url('//at.alicdn.com/t/c/font_5142813_uugy4s5x1d.css');
|
||||||
|
|
||||||
|
/* 全局样式 */
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
overflow-x: hidden;
|
||||||
|
max-width: 100%;
|
||||||
|
overscroll-behavior-y: none;
|
||||||
|
touch-action: pan-x;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family:
|
||||||
|
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
|
||||||
|
'Microsoft YaHei', Arial, sans-serif;
|
||||||
|
background-color: #f0f2f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
user-drag: none;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user