160 Commits
Author SHA1 Message Date
Zhou Weidong 88db96c374 feat(commerce): 新增打赏任务管理页
web-ci / build (push) Failing after 8s
2026-11-09 10:27:50 +08:00
Zhou Weidong 96b213354b fix(layout): 扩大顶栏图标与用户菜单点击热区 2026-11-09 10:27:49 +08:00
Zhou Weidong 0d862dfe1d fix(router): 按账号菜单落地避免创作者进入 404 2026-11-09 10:27:48 +08:00
Zhou Weidong 8535305185 style(creator-center): 数据看板改用管理端卡片布局 2026-11-09 10:27:47 +08:00
Zhou Weidong 837266ce7e feat(creator): 内容审核展示创作者与分集信息 2026-11-09 10:27:42 +08:00
Zhou Weidong 71ee469563 feat(creator-center): 分集支持视频预览与表格横向滚动 2026-11-09 10:27:41 +08:00
Zhou Weidong 3c730c7d33 feat(creator-center): 新增两步发布作品并支持删除作品 2026-11-09 10:27:40 +08:00
Vicky 4055b7a2a9 fix(layout): 修复顶栏通知角标脱离铃铛右上角
web-ci / build (push) Failing after 10s
角标改为锚定图标右上角(right 9px + 右移 50%),
位数变化时位置不漂移。
2026-11-05 10:56:09 +08:00
Vicky bbff52f324 feat(drama): 短剧审核合并为审核通过并上线一键操作
审核列草稿/已下线/审核中统一为「审核通过并上线」,
审核中保留驳回,待发布与已上线维持原按钮。
2026-11-05 10:56:09 +08:00
Zhou Weidong 9ce014e7b3 feat(classification): 分类支持榜单布局与顺序维护
web-ci / build (push) Failing after 12s
2026-11-02 14:47:59 +08:00
Vicky 23026dc371 style(setting): 按 eslint 规则规范系统设置模板格式
web-ci / build (push) Canceled after 0s
2026-10-30 17:17:39 +08:00
Vicky 2f52011491 perf(build): 图标改为离线子集并精简图表依赖
web-ci / build (push) Canceled after 0s
2026-10-30 17:14:51 +08:00
Vicky dd4be08d49 feat(setting): 系统设置支持上传 App 登录页背景 2026-10-30 17:14:50 +08:00
Vicky a94018d5d3 feat(prototype): 新增印尼市场短剧 App 静态原型
web-ci / build (push) Canceled after 0s
- public/indonesia/ 下独立静态原型,不影响后台构建
- 覆盖首页、发现、详情、播放、收藏、个人中心、金币解锁等核心流程
2026-10-29 17:33:07 +08:00
Vicky 923b7dce1e feat(upload): 封面上传支持横版与前端压缩
- 新增 imageCompress 与 imagePresets,上传前按规格高质量等比压缩
- 短剧封面拆分为竖版/横版两个上传位,带建议尺寸提示
- 轮播图、创作者中心封面与分集封面接入压缩与提示
2026-10-29 17:32:56 +08:00
Vicky dea414739e feat(tag): 标签管理支持配置语义码
web-ci / build (push) Canceled after 0s
客户端按语义码判断板块归属(如推荐页「上新」横滑),标签改名或换语言
都不再影响;新增/编辑弹窗增加语义码输入并做格式校验,留空则按名称兜底。
2026-10-29 15:44:42 +08:00
Zhou Weidong d13bdd5dc8 ci: 新增 Gitea Actions 流水线(测试/分析/构建门禁)
web-ci / build (push) Failing after 36s
2026-10-29 10:47:47 +08:00
Zhou Weidong 040ec3f382 fix(login): 退出登录不清除记住我的账号密码 2026-10-29 10:08:21 +08:00
Zhou Weidong 9a58b98f12 fix(login): 记住我同时记住密码并回填 2026-10-29 09:58:57 +08:00
Zhou Weidong e2618735ba chore(merge): 合并 main 的样式排序到 develop 2026-10-29 09:21:32 +08:00
Zhou Weidong b68361b037 chore(merge): 合并 phase1 UI prep 后台用户系统筛选与列表展示
合并后新增收藏/点赞列表页、用户单元格组件与系统筛选,
并统一观看时间格式与标签文案。
2026-10-27 20:20:31 +08:00
Vicky bedae02946 feat: phase1 UI prep — 后台 App 用户列表支持系统筛选 + 国家中文名 + 标签语义优化
新增: 系统下拉 (ANDROID/IOS)、国家中文名映射、用户列昵称/UID 冒号标签
详情抽屉: 同步系统/国家/标签展示
2026-10-27 15:45:23 +08:00
Vicky daf681ae1a fix(admin): 观看时间统一格式化为 yyyy-MM-dd HH:mm:ss
后端改输出格式后仍保留前端兜底,同时修掉用户详情里的原始 ISO 展示。
2026-10-27 14:39:18 +08:00
Vicky 111bbdaeb7 fix(admin): 用户列标签改用半角冒号 2026-10-27 14:29:56 +08:00
Vicky 5da72d1aa2 fix(admin): 用户列补全「昵称 / UID / 设备」标签与冒号
两行都以「字段名:值」呈现,避免只看到一串随机字母。
2026-10-27 14:28:59 +08:00
Vicky e1eb58d4c4 feat(admin): 列表支持按系统筛选,用户列补语义标签
- 收藏/点赞/观看增加「全部系统 / Android / iOS」下拉
- 用户列第二行标注 UID,游客行标注设备,避免一串字母看不出含义
2026-10-27 14:27:16 +08:00
Vicky 5dbc752798 feat(admin): 观看/收藏/点赞列表单独增加系统列
用户单元格只保留身份信息,Android/iOS 抽成独立「系统」列,三页列结构一致。
2026-10-27 14:17:37 +08:00
Vicky c89c5668e1 refactor(admin): 用户单元格只保留平台标识
机型与系统版本号属于多余信息,Android/iOS 加游客标签已足够区分。
2026-10-27 14:00:07 +08:00
Vicky 9ea22d5951 chore(merge): 合并后台用户单元格统一 2026-10-27 13:53:54 +08:00
Vicky f3fbc57c14 feat(admin): 统一用户单元格,展示游客设备号与客户端系统
- 新增 AppUserCell,游客显示标签+deviceId,第二行统一展示系统与机型
- 收藏、点赞、观看记录三页复用,观看记录去掉重复的系统列
2026-10-27 13:53:53 +08:00
Zhou Weidong c45820258e chore(merge): 合并游客标签样式修正 2026-10-27 13:11:10 +08:00
Zhou Weidong 12d63e9670 fix(admin): 修正游客标签拉伸 2026-10-27 13:10:19 +08:00
Zhou Weidong 7aa1571c15 chore(merge): 合并后台互动列表展示调整 2026-10-27 13:05:14 +08:00
Zhou Weidong 8cd4569a20 fix(admin): 收紧收藏与点赞列表展示 2026-10-27 13:03:53 +08:00
Zhou Weidong 83bb87eeb3 chore(merge): 合并后台 props lint 修复 2026-10-27 12:47:46 +08:00
Zhou Weidong 3409a2b427 fix(admin): 修复组件 props 变异的 lint 基线 2026-10-27 12:47:45 +08:00
Zhou Weidong a21fe6f189 chore(merge): 合并后台收藏与点赞列表页 2026-10-27 12:38:40 +08:00
Zhou Weidong 5580f9b99b feat(admin): 新增收藏与点赞列表页
全量 lint 仍有 5 处既有 vue/no-mutating-props 基线错误,
位于 LocaleTextFields.vue 与 ClassificationDramaDialogs.vue,本次未处理。
2026-10-27 12:38:31 +08:00
Zhou Weidong d846d2d864 Merge branch fix/icons-vue-deps 2026-10-26 13:06:45 +08:00
Zhou Weidong e6d1d6997b fix(deps): 补充 icons-vue 依赖声明 2026-10-26 13:06:38 +08:00
Zhou Weidong d661b808d1 chore(merge): 合并后台多语言工作台和登录页 2026-10-24 17:32:51 +08:00
Zhou Weidong d42a09fa36 feat(admin): 后台页面显示版权标识 2026-10-24 17:32:32 +08:00
Zhou Weidong 33bf5bd3bb feat(admin): 登录页补充水果装饰 2026-10-24 17:32:12 +08:00
Zhou Weidong a266d44483 feat(admin): 增加工作台大屏和转码通知 2026-10-24 17:31:16 +08:00
Zhou Weidong 2b2c6bd708 feat(admin): 内容管理支持多语言和标签 2026-10-24 17:30:52 +08:00
Zhou Weidong eaee765239 fix(app-version): 修复版本页类型检查 2026-10-24 17:30:29 +08:00
Zhou Weidong 18234ebd5b Merge branch fix/app-version-batch 2026-10-23 16:46:29 +08:00
Zhou Weidong 93d6d8c59f fix(app-version): 修复弹窗禁用并改批量提交 2026-10-23 16:46:22 +08:00
Zhou Weidong 9fc57e5c1a chore(merge): 合并 App 版本管理页面 2026-10-23 15:27:46 +08:00
Zhou Weidong 0a28eb0237 feat(admin): 新增 App 版本管理页面 2026-10-23 15:27:45 +08:00
Zhou Weidong 73a9b078ee chore(merge): 合并后台默认主题色调整 2026-10-23 15:13:00 +08:00
Zhou Weidong bb48f14ba7 feat(admin): 头部与菜单主题默认改为白色 2026-10-23 15:12:59 +08:00
Zhou Weidong ada74efc3a chore(merge): 合并运营大盘 ECharts 图表 2026-10-23 14:58:56 +08:00
Zhou Weidong 10372524ba feat(admin): 运营大盘接入 ECharts 图表 2026-10-23 14:58:55 +08:00
Zhou Weidong fd9a9c50c4 chore(merge): 合并立即补齐转码按钮 2026-10-23 14:14:48 +08:00
Zhou Weidong e51d371e07 feat(admin): 转码设置页新增立即补齐转码按钮 2026-10-23 14:14:46 +08:00
Zhou Weidong ada3b56d14 chore(merge): 将 release/v1.0.0 同步到 main 2026-10-23 13:08:30 +08:00
Zhou Weidong 7e8ae86d9f chore(merge): 对齐远程 main 2026-10-23 13:08:28 +08:00
Zhou Weidong 430ba59613 chore(merge): 将 develop 同步到 release/v1.0.0 2026-10-23 13:08:09 +08:00
Zhou Weidong d7d43fed44 chore(merge): 合并转码设置菜单去重修复 2026-10-23 12:55:57 +08:00
Zhou Weidong aa892fe7a8 fix(router): 修正转码设置菜单重复注入 2026-10-23 12:55:56 +08:00
Zhou Weidong 7c10e057a5 chore(merge): 合并协议设置与用户系统展示 2026-10-23 12:35:26 +08:00
Zhou Weidong 8bd6aa35cb style(admin): 修正页面格式以通过检查 2026-10-23 12:35:09 +08:00
Zhou Weidong 0578052d6e feat(admin): 用户与观看记录展示系统 2026-10-23 12:34:52 +08:00
Zhou Weidong c1fb6cc21c feat(admin): 新增三语协议设置页 2026-10-23 12:34:33 +08:00
Zhou Weidong 91f1f7d793 fix(admin): 短剧简介长度上限调整为 1000 2026-10-22 22:18:06 +08:00
Zhou Weidong a47700a980 feat(admin): 新增分类管理与剧单管理页面 2026-10-22 22:18:05 +08:00
Zhou Weidong 1d18b7b52b feat(admin): 清晰度产物支持 HLS 预览播放 2026-10-22 18:12:30 +08:00
Zhou Weidong 208b9f29d8 chore(merge): 合并转码实时进度展示 2026-10-22 15:13:57 +08:00
Zhou Weidong 9b2ab1a425 fix(admin): 转码面板展示实时进度与更新时间 2026-10-22 15:12:12 +08:00
Zhou Weidong 0d5d0d841f chore(merge): 合并转码媒资 id 精度修复 2026-10-22 14:59:54 +08:00
Zhou Weidong fecc3b4817 fix(admin): 转码媒资 id 按字符串传递并处理空态 2026-10-22 14:59:22 +08:00
Zhou Weidong a4893c31a2 chore(merge): 合并转码面板组件拆分 2026-10-22 14:48:43 +08:00
Zhou Weidong e2ca3bfa81 refactor(admin): 拆分剧集转码产物面板组件 2026-10-22 14:47:01 +08:00
Zhou Weidong 52b10d3040 chore(merge): 合并剧集转码产物面板 2026-10-22 14:26:05 +08:00
Zhou Weidong 41151a7f17 feat(admin): 剧集编辑页展示转码产物与帧图 2026-10-22 14:23:47 +08:00
Zhou Weidong 75da3ea0ba chore(merge): 合并后台转码设置页面 2026-10-22 11:30:05 +08:00
Zhou Weidong 218196ee9f feat(admin): 新增转码设置页面 2026-10-22 11:29:48 +08:00
Zhou Weidong 723d056fc7 chore(merge): 批量上传改为手动点击开始 2026-10-22 09:31:43 +08:00
Zhou Weidong f174ba46ae feat(admin): 批量上传改为手动点击开始 2026-10-22 09:31:35 +08:00
Zhou Weidong 2e89f8460c chore(merge): 剧集列表分页条始终显示 2026-10-22 09:23:10 +08:00
Zhou Weidong 632bf13990 fix(admin): 剧集列表分页条始终显示 2026-10-22 09:23:04 +08:00
Zhou Weidong becbe18d76 chore(merge): 剧集列表改用服务端分页修复列表为空 2026-10-22 09:19:33 +08:00
Zhou Weidong 6bcabea261 fix(admin): 剧集列表改用服务端分页修复列表为空 2026-10-22 09:18:59 +08:00
Vicky 99bb666e47 chore(merge): 剧集列表改为服务端分页 2026-10-21 23:20:56 +08:00
Zhou Weidong 092fe18ae7 feat(admin): 剧集列表改为服务端分页 2026-10-21 23:20:38 +08:00
Zhou Weidong c2a76a593a chore(merge): 剧集列表分页默认每页100条 2026-10-21 23:00:13 +08:00
Zhou Weidong a28eaa4514 feat(admin): 剧集列表分页默认每页100条 2026-10-21 23:00:08 +08:00
Zhou Weidong 189a7f689a chore(merge): 批量上传创建的分集默认已上线 2026-10-21 22:53:22 +08:00
Zhou Weidong 93e1c1b8c9 feat(admin): 批量上传创建的分集默认已上线 2026-10-21 22:53:07 +08:00
Zhou Weidong 05b977cc33 fix(admin): 批量建分集是否免费改由短剧免费集数决定 2026-10-21 22:25:50 +08:00
Zhou Weidong 5741ebbf17 fix(admin): 简化批量建分集表单并将移除操作置右 2026-10-21 22:20:10 +08:00
Zhou Weidong 836600b811 chore(build): commit-msg 拦截 AI 署名 2026-10-21 22:10:35 +08:00
Zhou Weidong 4841b0bc24 fix(admin): 移除上传提示中的并发表述
上传为异步队列,文件数量不设上限,提示只保留格式与单文件大小限制
2026-10-21 22:08:47 +08:00
Vicky e1e0055915 chore(admin): 合并剧集上传文案统一与组件拆分 2026-10-21 22:07:07 +08:00
Zhou WeidongandCursor cff864d582 refactor(admin): 拆分剧集管理页超长组件
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-21 22:07:04 +08:00
Zhou WeidongandCursor c71a12f6f4 fix(admin): 统一剧集上传格式与提示文案
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-21 22:04:36 +08:00
Zhou WeidongandCursor 7d39e3c684 chore(admin): 合并剧集多选分片上传与断点续传
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-21 21:58:40 +08:00
Zhou WeidongandCursor 6b3620c3c4 feat(admin): 剧集多选分片上传与断点续传
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-21 21:58:28 +08:00
Zhou Weidong 8e1a02ace0 chore(web): 忽略本机启动脚本与运行日志 2026-10-21 18:55:24 +08:00
Zhou Weidong 982346045d Merge branch 'develop' of http://git.reelshort2.com/jianghaifeng/ReelShortWeb into develop 2026-10-21 18:52:34 +08:00
Zhou Weidong 48b09ee8cf feat(admin): 短剧编辑支持排序权重 2026-10-21 18:52:15 +08:00
Zhou Weidong ceb7905697 Merge branch 'develop' of http://git.reelshort2.com/jianghaifeng/ReelShortWeb into develop 2026-10-21 18:10:21 +08:00
Zhou Weidong 979a502373 merge: 合并 K93.1 多选完整展示 2026-10-21 17:16:54 +08:00
Zhou Weidong 3e779e4efd fix(admin): 多选类型与内容类型完整展示 2026-10-21 17:16:40 +08:00
Zhou Weidong 481c231dd9 merge: 合并 K93 短剧多类型管理 2026-10-21 17:01:37 +08:00
Zhou Weidong 9e427d485f feat(admin): 短剧类型与内容类型多选 2026-10-21 17:01:22 +08:00
Zhou Weidong 58380170b0 merge: 合并 K92 举报列表短剧与分集展示 2026-10-21 16:45:08 +08:00
Zhou Weidong e583bfed5f feat(admin): 举报列表展示短剧与分集 2026-10-21 16:44:55 +08:00
Zhou Weidong 923a350cde merge: 合并 K91 观看记录页面与治理筛选 2026-10-21 16:36:09 +08:00
Zhou Weidong 0710ea55f3 feat(admin): 观看记录独立菜单与治理状态全部筛选 2026-10-21 16:35:41 +08:00
Zhou Weidong 50f2b177b1 merge: 合并 K89 分集互动数据管理 2026-10-21 15:59:13 +08:00
Zhou Weidong 0e814840b1 feat(admin): 剧集列表展示分集互动数据 2026-10-21 15:59:08 +08:00
Zhou Weidong c1e03f94be merge: 合并 K88 短剧内容类型与互动数据管理 2026-10-21 15:44:12 +08:00
Zhou Weidong c8fe571be1 feat(admin): 短剧列表展示并编辑互动数据 2026-10-21 15:43:51 +08:00
Zhou Weidong 45507dee8d perf(admin): 降低首屏体积并修复 Windows 访问白屏
1) Editor 改 defineAsyncComponent,IEditorConfig 改 import type
2) dev server 绑定 :: 双栈,避免 Windows 下 localhost 走 ::1 超时
3) dev 脚本去掉 POSIX 环境变量前缀,Windows 可直接 pnpm dev
2026-10-21 13:35:55 +08:00
Zhou Weidong 1737f37d82 fix(login): 忘记密码链接加提示,避免点击无响应 2026-10-20 09:15:19 +08:00
Zhou Weidong d283d223c0 chore: 忽略构建产物 apk 2026-10-20 09:12:11 +08:00
Zhou Weidong e9a956f16c chore(web): 忽略运行时产物(日志/启动脚本) 2026-10-19 22:43:29 +08:00
Zhou Weidong f78960fcce chore(web): 合并 release 分片上传修复 2026-10-19 18:46:27 +08:00
Zhou Weidong 8e9435ee7d fix(web): 修复分片上传 chunkSize 字符串拼接导致大文件超限 2026-10-19 18:38:06 +08:00
Zhou Weidong 83e535d12a chore(web): 合并剧集编辑弹窗 2026-10-19 13:55:29 +08:00
Zhou Weidong 2b5dda7c71 feat(web): 重做剧集编辑弹窗 2026-10-19 13:53:57 +08:00
Zhou Weidong 685d4fd1bd chore(web): 合并轮播图编辑弹窗 2026-10-19 13:49:09 +08:00
Zhou Weidong f281dcf1b3 feat(web): 重做轮播图编辑弹窗 2026-10-19 13:47:42 +08:00
Zhou Weidong 5db565b15d chore(web): 合并列表图片预览 2026-10-19 13:42:57 +08:00
Zhou Weidong 8a84f24baf feat(web): 支持后台列表图片大图预览 2026-10-19 13:42:28 +08:00
Zhou Weidong 046400ae4a chore(web): 合并剧集管理布局修复 2026-10-19 13:32:02 +08:00
Zhou Weidong 122aa21323 fix(web): 重做剧集管理顶部布局 2026-10-19 13:30:29 +08:00
Zhou Weidong 9f92d113f2 chore(web): 合并二级菜单背景修复 2026-10-19 13:28:07 +08:00
Zhou Weidong f6f4d264ee fix(web): 调整二级菜单背景样式 2026-10-19 13:27:44 +08:00
Zhou Weidong f023c7d416 chore(web): 合并工作台菜单图标修复 2026-10-19 13:22:44 +08:00
Zhou Weidong bb03a5ad84 fix(web): 修复工作台菜单图标继承 2026-10-19 13:21:09 +08:00
Zhou Weidong 73da0bbd85 chore(web): 合并 K23.1 布局修复 2026-10-19 12:58:02 +08:00
Zhou Weidong c8bf4a8fd4 fix(web): 修复禁用按钮与弹窗布局 2026-10-19 12:56:07 +08:00
Zhou Weidong 6f522303a0 chore(web): 合并 K23.1 后台修正 2026-10-19 12:34:21 +08:00
Zhou Weidong f5b399a52f fix(web): 统一危险操作按钮样式 2026-10-19 12:32:23 +08:00
Zhou Weidong e77db70278 fix(web): 修正浅色品牌文字颜色 2026-10-19 12:32:01 +08:00
Zhou Weidong fe1ae954e3 chore(web): 合并菜单对齐修复 2026-10-19 12:04:22 +08:00
Zhou Weidong 40fbd6ec7b fix(web): 对齐父子菜单文字基线 2026-10-19 12:04:18 +08:00
Zhou Weidong ee3bb1c286 chore(web): 合并 K23 后台视觉深化 2026-10-19 12:00:41 +08:00
Zhou Weidong 898e439b84 fix(web): 优化锁屏弹窗布局与交互 2026-10-19 12:00:30 +08:00
Zhou Weidong 805d7ddc52 feat(web): 重做短剧编辑弹窗 2026-10-19 12:00:26 +08:00
Zhou Weidong ba90bf9bfb style(web): 统一辣椒红主色与菜单层级 2026-10-19 12:00:21 +08:00
Zhou Weidong 3e8312ea58 chore(web): 合并首屏 loading 文字修复 2026-10-19 11:34:19 +08:00
Zhou Weidong a97b8cc026 fix(web): 修正首屏 loading 文字颜色 2026-10-19 11:34:15 +08:00
Zhou Weidong e5c5101450 chore(web): 合并 K22.1 后台收尾 2026-10-19 11:32:09 +08:00
Zhou Weidong 87970487af chore(web): 清理 mock 与旧品牌资源 2026-10-19 11:32:00 +08:00
Zhou Weidong ada741f53d style(web): 精简登录品牌与注册入口 2026-10-19 11:31:55 +08:00
Zhou Weidong 5494190330 chore(web): 合并 K22 路由图标收尾 2026-10-19 11:18:15 +08:00
Zhou Weidong 29465ab4ed fix(web): 统一静态路由图标 2026-10-19 11:18:11 +08:00
Zhou Weidong 8f3447e528 chore(web): 合并 K22 后台视觉统一 2026-10-19 11:12:52 +08:00
Zhou Weidong 97fbeda508 style(web): 统一后台图标与全局视觉 2026-10-19 11:12:38 +08:00
Zhou Weidong 5c087f4db9 chore(admin): 合并注册限流配置项 2026-10-17 23:29:04 +08:00
Zhou Weidong 54db082ffb feat(admin): 增加注册限流配置项 2026-10-17 23:28:40 +08:00
Vicky 3ec9ea6717 fix(web): 适配后端 ID 字符串化(雪花 id 不能过 Number)
后端 J1 起所有 Long id 以字符串下发,审计出 4 处把 id 当数字用的地方:

- utils/creatorImpersonation.ts:代看创作者 id 曾 Number(raw) 存 localStorage,
  19 位 id 会被四舍五入导致 X-Creator-Id 头带错 id;改为全程字符串
- contentManagement/EpisodeManagementView.vue:Number(route.query.drama) 与 item.id 比较,
  从短剧管理带 ?drama= 跳过来会选不中;改为字符串比较
- creator-center/composables/useDramaEdit.js:Number(route.params.id) 同上,作品编辑打不开
- permission/composables/useMenuManagement.js:顶级菜单哨兵 0 与接口的 "0" 类型不一致,
  下拉回显会变裸 0;统一成字符串 '0'

核过不需要改:分页 total 后端保持数字;role.id === selectedRoleId 等两侧都来自接口;
formatCount 内部 Number() 吃字符串;全局无 id 算术/下标/a.id-b.id。
2026-10-16 16:44:19 +08:00
Vicky db313f4aa6 feat(admin): 新增商品配置与系统公告两个后台页面
- src/api/admin/product.ts、src/api/admin/notice.ts 对齐后端新接口
- ProductManagementView:列表/筛选/新建编辑/上下架/删除,新建默认下架有提示
- NoticeManagementView:列表/筛选/新建编辑/发布下架/删除
- 状态列一律用接口返回的 statusText,不在前端写死中文
- id 按字符串处理(雪花 id 超过 JS 安全整数范围,数字会被四舍五入)
2026-10-16 16:33:22 +08:00
Vicky 1148458ca1 chore(merge): 合入远端 main(全量作者标识)到 develop
为什么:远端 main 领先一个提交(1fd0b8b 全量补充作者标识),本地 main 领先一个(4564aa8
el-radio 修复);按条目要求先把远端合进来,再在 develop 上做 I1,main 本身不动(合并走 PR)。
2026-10-16 16:14:15 +08:00
Vicky 4564aa894d chore(web): 修掉 eslint 全量检查的 2 个 error,门禁口径改跑 eslint + pnpm build:pro
为什么:红线第 8 条要求 Web 门禁是 `pnpm build` + eslint 零错误,而此前只跑过 ts:check,
全量 eslint 存量有 2 个 error。Web 已冻结,本次只做"去噪"式修复,不改任何行为。

- BannerManagementView: 表格 slot 解构里去掉未被使用的 row($index 仍在用)
- CreatorCenterPage: 自闭合 <slot /> 改为 <slot></slot>(等价写法)
- 实测:eslint 0 error / 0 warning;pnpm build:pro = Build successful
2026-10-16 09:41:19 +08:00
Zhou Weidong 1fd0b8b100 style: 全量补充作者标识 // Author: fruit.com.cn 2026-10-16 00:02:03 +08:00
338 changed files with 18183 additions and 2205 deletions

No files matched your search

+1 -1
View File
@@ -17,4 +17,4 @@ VITE_USE_ALL_ELEMENT_PLUS_STYLE=true
VITE_USE_MOCK=false VITE_USE_MOCK=false
# 是否使用在线图标 # 是否使用在线图标
VITE_USE_ONLINE_ICON=true VITE_USE_ONLINE_ICON=false
+2 -2
View File
@@ -29,10 +29,10 @@ VITE_USE_BUNDLE_ANALYZER=false
VITE_USE_ALL_ELEMENT_PLUS_STYLE=false VITE_USE_ALL_ELEMENT_PLUS_STYLE=false
# 是否开启mock # 是否开启mock
VITE_USE_MOCK=true VITE_USE_MOCK=false
# 是否切割css # 是否切割css
VITE_USE_CSS_SPLIT=true VITE_USE_CSS_SPLIT=true
# 是否使用在线图标 # 是否使用在线图标
VITE_USE_ONLINE_ICON=true VITE_USE_ONLINE_ICON=false
+2 -2
View File
@@ -29,10 +29,10 @@ VITE_USE_BUNDLE_ANALYZER=true
VITE_USE_ALL_ELEMENT_PLUS_STYLE=false VITE_USE_ALL_ELEMENT_PLUS_STYLE=false
# 是否开启mock # 是否开启mock
VITE_USE_MOCK=true VITE_USE_MOCK=false
# 是否切割css # 是否切割css
VITE_USE_CSS_SPLIT=true VITE_USE_CSS_SPLIT=true
# 是否使用在线图标 # 是否使用在线图标
VITE_USE_ONLINE_ICON=true VITE_USE_ONLINE_ICON=false
+2 -2
View File
@@ -29,10 +29,10 @@ VITE_USE_BUNDLE_ANALYZER=false
VITE_USE_ALL_ELEMENT_PLUS_STYLE=false VITE_USE_ALL_ELEMENT_PLUS_STYLE=false
# 是否开启mock # 是否开启mock
VITE_USE_MOCK=true VITE_USE_MOCK=false
# 是否切割css # 是否切割css
VITE_USE_CSS_SPLIT=false VITE_USE_CSS_SPLIT=false
# 是否使用在线图标 # 是否使用在线图标
VITE_USE_ONLINE_ICON=true VITE_USE_ONLINE_ICON=false
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
// @ts-check // @ts-check
const { defineConfig } = require('eslint-define-config') const { defineConfig } = require('eslint-define-config')
module.exports = defineConfig({ module.exports = defineConfig({
+28
View File
@@ -0,0 +1,28 @@
# Author: fruit.com.cn
# ReelShortWeb 后台 CI:lint + 构建门禁
name: web-ci
on:
push:
branches: [develop, main]
pull_request:
branches: [develop, main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: pnpm
- name: 依赖
run: pnpm install --frozen-lockfile
- name: 类型检查
run: pnpm ts:check
- name: 构建
run: pnpm build:pro
+6
View File
@@ -7,3 +7,9 @@ dist-ssr
*-lock.* *-lock.*
pnpm-debug pnpm-debug
stats.html stats.html
*.apk
# 本机运行时产物(start-dev.bat / vite 日志)
start-dev.bat
dev.out.log
dev.err.log
+5
View File
@@ -1,4 +1,9 @@
#!/bin/sh #!/bin/sh
. "$(dirname "$0")/_/husky.sh" . "$(dirname "$0")/_/husky.sh"
if grep -qiE "(co-authored-by|generated with|assisted by).*(cursor|claude|codex|gpt|copilot|ai)" "$1"; then
echo "commit message contains AI attribution, remove it before committing" >&2
exit 1
fi
pnpm commitlint --edit "$1" pnpm commitlint --edit "$1"
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
module.exports = { module.exports = {
'*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'], '*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'],
'{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': ['prettier --write--parser json'], '{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': ['prettier --write--parser json'],
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
module.exports = { module.exports = {
extends: ['@commitlint/config-conventional'], extends: ['@commitlint/config-conventional'],
rules: { rules: {
@@ -0,0 +1,18 @@
# 转码媒资 ID 前端精度修复
## 根因
聚合接口把 Snowflake 媒资 ID 以字符串返回,但面板使用
`Number(detail.mediaAssetId)`,超过 `2^53` 后发生精度丢失,手动转码请求了错误 ID。
## 修复
- 聚合响应中的 episodeId/mediaAssetId/taskId 类型改为字符串。
- 父面板不再 Number 归一化,媒体 ID 校验改为字符串 `/\d+/`。
- 手动转码、帧图分页 URL 原样传递字符串。
- mediaAssetId 为空时禁用重新转码并展示明确空态。
## 验证
- 超 2^53 ID 字符串保持逐位一致。
- `pnpm lint:eslint`、`pnpm build:dev`。
@@ -0,0 +1,26 @@
# 剧集转码面板组件拆分
## 目标
- 将 `EpisodeTranscodePanel.vue` 从 816 行降到 500 行以内。
- 状态轮询、分页请求与切集重置继续由父面板统一管理。
- UI 按职责拆为状态徽标、状态操作区、清晰度表格、帧图预览。
## 组件
1. `EpisodeTranscodeStatusBadge.vue`:折叠标题中的状态与进度。
2. `EpisodeTranscodeStatusPanel.vue`:状态、进度、重试次数、错误与重转按钮。
3. `EpisodeTranscodeRenditions.vue`:清晰度表格、复制地址、新窗口打开。
4. `EpisodeTranscodeFrames.vue`:逐帧网格、雪碧图、分页与放大预览。
公共状态映射与格式化放入 `episodeTranscodeUi.ts`。
## 风险
- 父组件仍负责全部接口、轮询、请求序号和分页状态,子组件只通过 props/emits 交互。
- 不新增依赖、不改接口和 locales 文案。
## 验证
- `wc -l` 确认所有相关文件不超过 500 行。
- `pnpm lint:eslint`、`pnpm build:dev`。
@@ -0,0 +1,39 @@
# 后台转码设置页面
## 目标
1. 在内容管理下新增「转码设置」页面,权限沿用 `episode:upload`。
2. 页面字段与 FruitServer `MediaTranscodeProfile` 完全一致。
3. 对接 `GET/PUT /admin/media/transcode-profile`,保存后回填服务端响应。
4. 前端校验规则与后端一致,并在失败时定位到首个错误字段。
## 方案
- API:在 `src/api/admin/media.ts` 增加读取、保存函数,类型定义放
`src/api/admin/types.ts`;响应继续遵循现有 `Result {code,message,data}`。
- 路由:静态路由表增加 `/content/media-transcode`;默认服务端动态路由模式下,
根据 `episode:upload` 权限把同一条本地路由补充进服务端 `/content` 菜单,
已存在同名菜单时不重复添加,仅统一 i18n 标题。
- 页面:`views/systemManagement/TranscodeSettingsView.vue`,使用 Element Plus 表单、
表格、数字输入和开关;保存按钮受 `episode:upload` 控制。
- 校验:
- quality 非空且忽略首尾空格后唯一;
- width/height 为正偶数;
- videoBitrateKbps/audioBitrateKbps 为正整数;
- segmentSeconds 2–10;
- sprite.intervalSeconds 1–10、columns 5–20、thumbWidth 120–240;
- ladder 至少一档 enabled。
- 校验失败由 Element Plus 定位到首个错误 prop,动态列表错误会定位到对应行。
- 所有可见文案、提示和校验信息进入 `zh-CN.ts` / `en.ts`。
## 非目标
- 不修改 FruitServer、ReelShort 或接口契约。
- 不新增权限码、不新增依赖、不启动服务。
## 测试与门禁
- 当前工程没有单元/组件测试框架,不引入新依赖;使用 TS 类型检查和构建门禁。
- `pnpm lint:eslint`
- `pnpm ts:check`
- `pnpm build:dev`
@@ -0,0 +1,12 @@
# 转码面板实时进度与更新时间
## 目标
- 展示后端 task progress、状态文案与 updatedAt。
- RUNNING/QUEUED 且 progress=0 时显示“处理中(等待进度上报)”。
- FAILED 保持错误信息与重新转码入口。
- 保持现有轮询停止、关闭清理与字符串 ID 逻辑。
## 验证
- `pnpm lint:eslint`、`pnpm build:dev`。
@@ -0,0 +1,29 @@
# 印尼市场短剧 App UI / UX 原型
## 范围
- 新增独立静态原型 `public/indonesia/`,不修改管理后台、Flutter 或接口契约。
- 沿用现有 FruitShorts / DramaPulse 设计方向:深色、橙红主色、印尼语、三个主导航。
- 覆盖首页、发现/搜索、详情/选集、播放交互、收藏/历史、个人中心、金币解锁。
- 原型数据及钱包均仅保存在浏览器;不调用真实支付,不伪造支付成功。
## 步骤
1. 阅读项目规范、PRD、契约与设计基线,保留用户现有修改。
2. 独立实现响应式界面、可访问交互与本地状态。
3. 检查桌面/移动布局、搜索空态、收藏持久化与解锁幂等。
4. 验证原后台构建,记录结果并提供可直接打开的入口。
## 风险 / 待确认
- 当前目录为管理后台,故交付独立设计原型,不将 C 端覆盖到后台。
- 原型不代表已实现 Flutter App;虚构剧名、热度、价格只用于界面设计。
- 生图工具没有返回可用图片,采用示意照片;正式剧集图需替换为已授权内容。
- 播放器为分镜交互演示,不将静态图片伪装成已接入的剧集视频。
- 金币不足仅展示充值方案与商店结算说明,不接外部钱包或真实扣款。
- 原有 `TagManagementView.vue` 修改不属于本任务,保持原样。
- INBOX 内旧任务不属于本次用户明确要求,不执行、不标记完成。
## 验收
- 待执行。
+189
View File
@@ -0,0 +1,189 @@
## 2026-10-22 后台转码设置页面
### 开工确认与计划
- 仓库:ReelShortWeb
- 基线:`develop` / `origin/develop` = `723d056`
- 工作区:开工前干净
- 分支:`feature/transcode-profile-settings`
- 计划:`docs/plans/2026-10-22-transcode-profile-settings.md`
```text
$ git status --short --branch && git rev-parse --short HEAD
## develop...origin/develop
723d056
```
解读:基于指定 develop 基线创建隔离工作树。后端契约已确认字段为
`enabled / segmentSeconds / ladder / sprite`,权限为 `episode:upload`。
默认服务端动态菜单只读取 `sys_menu`,本单按“只改 Web”约束在不改后端的前提下,
对具备 `episode:upload` 的账号补充本地受控菜单,后端接口仍执行最终鉴权。
### 实现与门禁
- 新增 `/content/media-transcode` 静态路由和服务端动态菜单补充;
菜单标题走 `router.transcodeSettings`,只在账号具备 `episode:upload` 时加入。
- 新增转码设置页面,字段严格为 `enabled / segmentSeconds / ladder / sprite`;
支持档位增删、动态行校验、首个错误字段定位及服务端返回值回填。
- API 使用现有 axios Result 包装,GET 初始化、PUT 保存;成功/失败/校验提示全部走中英 i18n。
- 当前工程没有单元/组件测试框架,未新增依赖;使用 ESLint、TypeScript 与构建门禁。
```text
$ pnpm lint:eslint
> fruitshorts-admin@1.0.0 lint:eslint /private/tmp/reelshortweb-transcode-settings
> eslint --fix --ext .js,.ts,.vue ./src
$ pnpm ts:check
> fruitshorts-admin@1.0.0 ts:check /private/tmp/reelshortweb-transcode-settings
> pnpm vue-tsc --noEmit --skipLibCheck
```
解读:ESLint 零告警,TypeScript 类型检查通过。
```text
$ pnpm build:dev
> fruitshorts-admin@1.0.0 build:dev /private/tmp/reelshortweb-transcode-settings
> VITE_CJS_IGNORE_WARNING=1 vite build --mode dev
WARN Circular chunk: element-plus -> vue-chunks -> element-plus. Please adjust the manual chunk logic for these chunks.
Build successful. Please see dist-dev directory
```
解读:开发构建成功;circular chunk 为仓库既有 Element Plus 分包警告,本次未改构建配置。
## 2026-10-22 剧集转码面板组件拆分
- 分支:`feature/transcode-panel-split`
- 基线:`develop` / `origin/develop` = `52b10d3`
- 计划:`docs/plans/2026-10-22-transcode-panel-split.md`
已读取 `AGENTS.md` 与 `CODING_STANDARDS.md`。当前
`EpisodeTranscodePanel.vue` 为 816 行,超过单文件 500 行红线。
拆分将保持父组件的数据获取、轮询清理、分页请求与切集重置逻辑不变。
### 拆分结果
```text
$ wc -l src/views/contentManagement/upload/EpisodeTranscodePanel.vue \
src/views/contentManagement/upload/EpisodeTranscodeStatusBadge.vue \
src/views/contentManagement/upload/EpisodeTranscodeStatusPanel.vue \
src/views/contentManagement/upload/EpisodeTranscodeRenditions.vue \
src/views/contentManagement/upload/EpisodeTranscodeFrames.vue \
src/views/contentManagement/upload/episodeTranscodeUi.ts
266 EpisodeTranscodePanel.vue
45 EpisodeTranscodeStatusBadge.vue
155 EpisodeTranscodeStatusPanel.vue
119 EpisodeTranscodeRenditions.vue
315 EpisodeTranscodeFrames.vue
93 episodeTranscodeUi.ts
```
解读:父面板只保留接口调用、轮询、请求序号、分页状态与组件组合;
状态、清晰度、帧图分别独立,全部文件均低于 500 行。
### 门禁
```text
$ pnpm ts:check
> vue-tsc --noEmit --skipLibCheck
(通过)
$ pnpm lint:eslint
> eslint --fix --ext .js,.ts,.vue ./src
(0 error / 0 warning)
$ pnpm build:dev
WARN Circular chunk: element-plus -> vue-chunks -> element-plus.
Build successful. Please see dist-dev directory
```
解读:组件 props/emits、类型检查、ESLint 和开发构建全部通过;
循环 chunk 为项目既有警告。轮询启停、接口路径、分页参数和 locales 文案未改变。
## 2026-10-22 转码媒资 ID 前端精度修复
- 分支:`feature/transcode-media-id-fix-ui`
- 基线:`develop` / `origin/develop` = `a4893c3`
已复现 `Number('2113150398670966786')` 变为 `2113150398670966800`。
本次将媒资、任务 id 全链路按字符串处理,禁止 Number 转换;没有媒资时
禁用重新转码并显示空态。
```text
$ node -e "const id='2113150398670966786'; console.log(String(Number(id)))"
2113150398670966800
```
解读:Snowflake id 超过 JS 安全整数,字符串化后的 Number 已失真。
### 实现
- `EpisodeTranscodeItem` 的 episodeId/mediaAssetId/taskId,以及 rendition/thumbnail
相关 id 类型改为字符串。
- 父面板使用 `detail.mediaAssetId` 原始字符串和正则校验,不再 Number 转换。
- 手动转码与帧图分页接口原样传递字符串 id。
- `mediaAssetId` 为空时按钮保持禁用,空态优先展示后端 errorMsg 原因。
静态检查确认:
```text
$ rg -n "Number\([^\n]*mediaAssetId|mediaAssetId[^\n]*Number\(" \
src/views/contentManagement/upload src/api/admin
(无匹配)
```
### 门禁
```text
$ pnpm lint:eslint
> eslint --fix --ext .js,.ts,.vue ./src
(0 error / 0 warning)
$ pnpm ts:check
> vue-tsc --noEmit --skipLibCheck
(通过)
$ pnpm build:dev
WARN Circular chunk: element-plus -> vue-chunks -> element-plus.
Build successful. Please see dist-dev directory
```
解读:字符串 id 链路、类型检查、ESLint 与开发构建均通过;
循环 chunk 为项目既有警告。
## 2026-10-22 转码面板实时进度与更新时间
- 分支:`feature/transcode-progress-ui`
- 基线:`develop` / `origin/develop` = `0d5d0d8`
- 计划:`docs/plans/2026-10-22-transcode-progress-ui.md`
后端将补充 task.updatedAt;前端在现有轮询面板中展示进度、状态、
最近更新时间,并在 RUNNING/QUEUED progress=0 时显示等待进度上报提示。
### 实现
- `EpisodeTranscodeItem` 增加 `updatedAt`。
- 状态区展示“最近更新”,格式沿用 `formatDateTime`。
- RUNNING/QUEUED 且 progress=0 时显示“处理中(等待进度上报)”。
- SUCCESS/FAILED 继续停止轮询;关闭面板继续清理定时器。
- 所有 id 保持字符串传递,不引入 Number 转换。
### 门禁
```text
$ pnpm lint:eslint
> eslint --fix --ext .js,.ts,.vue ./src
(0 error / 0 warning)
$ pnpm ts:check
> vue-tsc --noEmit --skipLibCheck
(通过)
$ pnpm build:dev
WARN Circular chunk: element-plus -> vue-chunks -> element-plus.
Build successful. Please see dist-dev directory
```
解读:进度与更新时间类型、ESLint、TypeScript 和开发构建全部通过;
循环 chunk 为项目既有警告。
+8 -25
View File
@@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" type="image/png" href="/brand-mark.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%= title %></title> <title><%= title %></title>
</head> </head>
@@ -16,7 +16,8 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
flex-direction: column; flex-direction: column;
background: #f0f2f5; color: #f8f1ea;
background: #100c0a;
} }
.app-loading .app-loading-wrap { .app-loading .app-loading-wrap {
@@ -38,8 +39,9 @@
} }
.app-loading .app-loading-logo { .app-loading .app-loading-logo {
width: 100px; width: 72px;
margin: 0 auto 15px; margin: 0 auto 15px;
border-radius: 12px;
} }
.app-loading .app-loading-item { .app-loading .app-loading-item {
@@ -55,7 +57,7 @@
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
border: 4px solid #2d8cf0; border: 4px solid #c4322a;
border-bottom: 0; border-bottom: 0;
border-left-color: transparent; border-left-color: transparent;
border-radius: 50%; border-radius: 50%;
@@ -68,7 +70,7 @@
left: calc(50% - 20px); left: calc(50% - 20px);
width: 40px; width: 40px;
height: 40px; height: 40px;
border: 4px solid #87bdff; border: 4px solid #ff8a72;
border-right: 0; border-right: 0;
border-top-color: transparent; border-top-color: transparent;
border-radius: 50%; border-radius: 50%;
@@ -85,16 +87,6 @@
} }
} }
@keyframes loader-outter {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes loader-inner { @keyframes loader-inner {
0% { 0% {
transform: rotate(0deg); transform: rotate(0deg);
@@ -105,20 +97,11 @@
} }
} }
@keyframes loader-inner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(-360deg);
}
}
</style> </style>
<div class="app-loading"> <div class="app-loading">
<div class="app-loading-wrap"> <div class="app-loading-wrap">
<div class="app-loading-title"> <div class="app-loading-title">
<img src="/logo.png" class="app-loading-logo" alt="Logo" /> <img src="/brand-mark.png" class="app-loading-logo" alt="Logo" />
<div class="app-loading-title"><%= title %></div> <div class="app-loading-title"><%= title %></div>
</div> </div>
<div class="app-loading-item"> <div class="app-loading-item">
-18
View File
@@ -1,18 +0,0 @@
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'
const modules = import.meta.glob('./**/*.mock.ts', {
import: 'default',
eager: true
})
const mockModules: any[] = []
Object.keys(modules).forEach(async (key) => {
if (key.includes('_')) {
return
}
mockModules.push(...(modules[key] as any))
})
export function setupProdMockServer() {
createProdMockServer(mockModules)
}
-129
View File
@@ -1,129 +0,0 @@
import { MockMethod } from 'vite-plugin-mock'
import { SUCCESS_CODE } from '@/constants'
const timeout = 1000
const adminList = [
{
path: '/dashboard',
component: '#',
redirect: '/dashboard/index',
name: 'Dashboard',
meta: {
title: '工作台',
icon: 'ant-design:dashboard-filled'
},
children: [
{
path: 'index',
name: 'dashboard',
component: 'views/common/DashboardView',
meta: {
title: '工作台',
icon: 'ant-design:dashboard-filled',
affix: true
}
}
]
},
{
path: '/content',
component: '#',
redirect: '/content/dramas',
name: 'Content',
meta: {
title: '内容管理',
icon: 'ep:video-camera',
alwaysShow: true
},
children: [
{
path: 'dramas',
name: 'dramas',
component: 'views/contentManagement/DramaManagementView',
meta: {
title: '短剧管理'
}
},
{
path: 'episodes',
name: 'episodes',
component: 'views/contentManagement/EpisodeManagementView',
meta: {
title: '剧集管理'
}
}
]
},
{
path: '/system',
component: '#',
redirect: '/system/access',
name: 'System',
meta: {
title: '后台管理',
icon: 'ep:setting',
alwaysShow: true
},
children: [
{
path: 'access',
name: 'access',
component: 'views/permission/RoleManagementView',
meta: {
title: '角色权限配置'
}
},
{
path: 'admin-users',
name: 'admin-users',
component: 'views/userManagement/AdminUsersView',
meta: {
title: '管理员用户列表'
}
},
{
path: 'menus',
name: 'menus',
component: 'views/permission/MenuManagementView',
meta: {
title: '菜单管理'
}
},
{
path: 'role-categories',
name: 'role-categories',
component: 'views/permission/RoleCategoriesView',
meta: {
title: '角色分类'
}
},
{
path: 'settings',
name: 'settings',
component: 'views/systemManagement/SettingsView',
meta: {
title: '系统设置'
}
}
]
}
]
const testList = adminList.filter((route) => route.path !== '/system')
export default [
// 列表接口
{
url: '/mock/role/list',
method: 'get',
timeout,
response: ({ query }) => {
const { roleName } = query
return {
code: SUCCESS_CODE,
data: roleName === 'admin' ? adminList : testList
}
}
}
] as MockMethod[]
-90
View File
@@ -1,90 +0,0 @@
import { SUCCESS_CODE } from '@/constants'
const timeout = 1000
const List: {
username: string
password: string
role: string
roleId: string
permissions: string | string[]
}[] = [
{
username: 'admin',
password: 'admin123',
role: 'admin',
roleId: '1',
permissions: ['*.*.*']
},
{
username: 'test',
password: 'test',
role: 'test',
roleId: '2',
permissions: ['example:dialog:create', 'example:dialog:delete']
}
]
export default [
// 列表接口
{
url: '/mock/user/list',
method: 'get',
response: ({ query }) => {
const { username, pageIndex, pageSize } = query
const mockList = List.filter((item) => {
if (username && item.username.indexOf(username) < 0) return false
return true
})
const pageList = mockList.filter(
(_, index) => index < pageSize * pageIndex && index >= pageSize * (pageIndex - 1)
)
return {
code: SUCCESS_CODE,
data: {
total: mockList.length,
list: pageList
}
}
}
},
// 登录接口
{
url: '/mock/user/login',
method: 'post',
timeout,
response: ({ body }) => {
const data = body
let hasUser = false
for (const user of List) {
if (user.username === data.username && user.password === data.password) {
hasUser = true
return {
code: SUCCESS_CODE,
data: user
}
}
}
if (!hasUser) {
return {
code: 500,
message: '账号或密码错误'
}
}
}
},
// 退出接口
{
url: '/mock/user/loginOut',
method: 'get',
timeout,
response: () => {
return {
code: SUCCESS_CODE,
data: null
}
}
}
]
+3 -4
View File
@@ -6,7 +6,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"i": "pnpm install", "i": "pnpm install",
"dev": "VITE_CJS_IGNORE_WARNING=1 vite --mode base", "dev": "vite --mode base",
"ts:check": "pnpm vue-tsc --noEmit --skipLibCheck", "ts:check": "pnpm vue-tsc --noEmit --skipLibCheck",
"build:pro": "VITE_CJS_IGNORE_WARNING=1 vite build --mode pro", "build:pro": "VITE_CJS_IGNORE_WARNING=1 vite build --mode pro",
"build:dev": "VITE_CJS_IGNORE_WARNING=1 vite build --mode dev", "build:dev": "VITE_CJS_IGNORE_WARNING=1 vite build --mode dev",
@@ -24,8 +24,7 @@
"prepare": "husky install" "prepare": "husky install"
}, },
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.2", "@element-plus/icons-vue": "2.3.2",
"@faker-js/faker": "^8.4.0",
"@iconify/iconify": "^3.1.1", "@iconify/iconify": "^3.1.1",
"@iconify/vue": "^4.1.1", "@iconify/vue": "^4.1.1",
"@vueuse/core": "^10.7.2", "@vueuse/core": "^10.7.2",
@@ -40,6 +39,7 @@
"echarts": "^5.4.3", "echarts": "^5.4.3",
"echarts-wordcloud": "^2.1.0", "echarts-wordcloud": "^2.1.0",
"element-plus": "2.5.5", "element-plus": "2.5.5",
"hls.js": "^1.7.3",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
@@ -103,7 +103,6 @@
"vite": "5.0.12", "vite": "5.0.12",
"vite-plugin-ejs": "^1.7.0", "vite-plugin-ejs": "^1.7.0",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vite-plugin-mock": "2.9.6",
"vite-plugin-progress": "^0.0.7", "vite-plugin-progress": "^0.0.7",
"vite-plugin-purge-icons": "^0.10.0", "vite-plugin-purge-icons": "^0.10.0",
"vite-plugin-style-import": "2.0.0", "vite-plugin-style-import": "2.0.0",
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
module.exports = { module.exports = {
plugins: { plugins: {
autoprefixer: {} autoprefixer: {}
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
module.exports = { module.exports = {
printWidth: 100, printWidth: 100,
tabWidth: 2, tabWidth: 2,
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

+93
View File
@@ -0,0 +1,93 @@
Copyright 2021 The Outfit Project Authors (https://github.com/Outfitio/Outfit-Fonts)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,93 @@
Copyright 2020 The Plus Jakarta Sans Project Authors (https://github.com/tokotype/PlusJakartaSans)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

+12
View File
@@ -0,0 +1,12 @@
/**
* @license lucide v0.468.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
(function(a,n){typeof exports=="object"&&typeof module<"u"?n(exports):typeof define=="function"&&define.amd?define(["exports"],n):(a=typeof globalThis<"u"?globalThis:a||self,n(a.lucide={}))})(this,function(a){"use strict";const n=(t,d,c=[])=>{const p=document.createElementNS("http://www.w3.org/2000/svg",t);return Object.keys(d).forEach(M=>{p.setAttribute(M,String(d[M]))}),c.length&&c.forEach(M=>{const v=n(...M);p.appendChild(v)}),p};var I0=([t,d,c])=>n(t,d,c);const Q$=t=>Array.from(t.attributes).reduce((d,c)=>(d[c.name]=c.value,d),{}),j$=t=>typeof t=="string"?t:!t||!t.class?"":t.class&&typeof t.class=="string"?t.class.split(" "):t.class&&Array.isArray(t.class)?t.class:"",Y$=t=>t.flatMap(j$).map(d=>d.trim()).filter(Boolean).filter((d,c,p)=>p.indexOf(d)===c).join(" "),_$=t=>t.replace(/(\w)(\w*)(_|-|\s*)/g,(d,c,p)=>c.toUpperCase()+p.toLowerCase()),E0=(t,{nameAttr:d,icons:c,attrs:p})=>{const M=t.getAttribute(d);if(M==null)return;const v=_$(M),X$=c[v];if(!X$)return console.warn(`${t.outerHTML} icon name was not found in the provided icons object.`);const N$=Q$(t),[hm,tm,dm]=X$,K$={...tm,"data-lucide":M,...p,...N$},J$=Y$(["lucide",`lucide-${M}`,N$,p]);J$&&Object.assign(K$,{class:J$});const cm=I0([hm,K$,dm]);return t.parentNode?.replaceChild(cm,t)},h={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":2,"stroke-linecap":"round","stroke-linejoin":"round"},x0=["svg",h,[["path",{d:"M3.5 13h6"}],["path",{d:"m2 16 4.5-9 4.5 9"}],["path",{d:"M18 7v9"}],["path",{d:"m14 12 4 4 4-4"}]]],W0=["svg",h,[["path",{d:"M3.5 13h6"}],["path",{d:"m2 16 4.5-9 4.5 9"}],["path",{d:"M18 16V7"}],["path",{d:"m14 11 4-4 4 4"}]]],X0=["svg",h,[["path",{d:"M21 14h-5"}],["path",{d:"M16 16v-3.5a2.5 2.5 0 0 1 5 0V16"}],["path",{d:"M4.5 13h6"}],["path",{d:"m3 16 4.5-9 4.5 9"}]]],N0=["svg",h,[["circle",{cx:"16",cy:"4",r:"1"}],["path",{d:"m18 19 1-7-6 1"}],["path",{d:"m5 8 3-3 5.5 3-2.36 3.5"}],["path",{d:"M4.24 14.5a5 5 0 0 0 6.88 6"}],["path",{d:"M13.76 17.5a5 5 0 0 0-6.88-6"}]]],K0=["svg",h,[["path",{d:"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2"}]]],J0=["svg",h,[["path",{d:"M6 12H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"}],["path",{d:"M6 8h12"}],["path",{d:"M18.3 17.7a2.5 2.5 0 0 1-3.16 3.83 2.53 2.53 0 0 1-1.14-2V12"}],["path",{d:"M6.6 15.6A2 2 0 1 0 10 17v-5"}]]],Q0=["svg",h,[["path",{d:"M5 17H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-1"}],["path",{d:"m12 15 5 6H7Z"}]]],o=["svg",h,[["circle",{cx:"12",cy:"13",r:"8"}],["path",{d:"M5 3 2 6"}],["path",{d:"m22 6-3-3"}],["path",{d:"M6.38 18.7 4 21"}],["path",{d:"M17.64 18.67 20 21"}],["path",{d:"m9 13 2 2 4-4"}]]],s=["svg",h,[["circle",{cx:"12",cy:"13",r:"8"}],["path",{d:"M5 3 2 6"}],["path",{d:"m22 6-3-3"}],["path",{d:"M6.38 18.7 4 21"}],["path",{d:"M17.64 18.67 20 21"}],["path",{d:"M9 13h6"}]]],j0=["svg",h,[["path",{d:"M6.87 6.87a8 8 0 1 0 11.26 11.26"}],["path",{d:"M19.9 14.25a8 8 0 0 0-9.15-9.15"}],["path",{d:"m22 6-3-3"}],["path",{d:"M6.26 18.67 4 21"}],["path",{d:"m2 2 20 20"}],["path",{d:"M4 4 2 6"}]]],r=["svg",h,[["circle",{cx:"12",cy:"13",r:"8"}],["path",{d:"M5 3 2 6"}],["path",{d:"m22 6-3-3"}],["path",{d:"M6.38 18.7 4 21"}],["path",{d:"M17.64 18.67 20 21"}],["path",{d:"M12 10v6"}],["path",{d:"M9 13h6"}]]],Y0=["svg",h,[["circle",{cx:"12",cy:"13",r:"8"}],["path",{d:"M12 9v4l2 2"}],["path",{d:"M5 3 2 6"}],["path",{d:"m22 6-3-3"}],["path",{d:"M6.38 18.7 4 21"}],["path",{d:"M17.64 18.67 20 21"}]]],_0=["svg",h,[["path",{d:"M11 21c0-2.5 2-2.5 2-5"}],["path",{d:"M16 21c0-2.5 2-2.5 2-5"}],["path",{d:"m19 8-.8 3a1.25 1.25 0 0 1-1.2 1H7a1.25 1.25 0 0 1-1.2-1L5 8"}],["path",{d:"M21 3a1 1 0 0 1 1 1v2a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4a1 1 0 0 1 1-1z"}],["path",{d:"M6 21c0-2.5 2-2.5 2-5"}]]],aa=["svg",h,[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["polyline",{points:"11 3 11 11 14 8 17 11 17 3"}]]],ha=["svg",h,[["path",{d:"M2 12h20"}],["path",{d:"M10 16v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-4"}],["path",{d:"M10 8V4a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v4"}],["path",{d:"M20 16v1a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2v-1"}],["path",{d:"M14 8V7c0-1.1.9-2 2-2h2a2 2 0 0 1 2 2v1"}]]],ta=["svg",h,[["path",{d:"M12 2v20"}],["path",{d:"M8 10H4a2 2 0 0 1-2-2V6c0-1.1.9-2 2-2h4"}],["path",{d:"M16 10h4a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-4"}],["path",{d:"M8 20H7a2 2 0 0 1-2-2v-2c0-1.1.9-2 2-2h1"}],["path",{d:"M16 14h1a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2h-1"}]]],da=["svg",h,[["path",{d:"M17 12H7"}],["path",{d:"M19 18H5"}],["path",{d:"M21 6H3"}]]],ca=["svg",h,[["rect",{width:"6",height:"16",x:"4",y:"2",rx:"2"}],["rect",{width:"6",height:"9",x:"14",y:"9",rx:"2"}],["path",{d:"M22 22H2"}]]],Ma=["svg",h,[["rect",{width:"16",height:"6",x:"2",y:"4",rx:"2"}],["rect",{width:"9",height:"6",x:"9",y:"14",rx:"2"}],["path",{d:"M22 22V2"}]]],pa=["svg",h,[["rect",{width:"6",height:"14",x:"4",y:"5",rx:"2"}],["rect",{width:"6",height:"10",x:"14",y:"7",Line truncated
If you want to use all the icons you can import it like:
\`import { createIcons, icons } from 'lucide';
lucide.createIcons({icons});\``);if(typeof document>"u")throw new Error("`createIcons()` only works in a browser environment.");const p=document.querySelectorAll(`[${d}]`);if(Array.from(p).forEach(M=>E0(M,{nameAttr:d,icons:t,attrs:c})),d==="data-lucide"){const M=document.querySelectorAll("[icon-name]");M.length>0&&(console.warn("[Lucide] Some icons were found with the now deprecated icon-name attribute. These will still be replaced for backwards compatibility, but will no longer be supported in v1.0 and you should switch to data-lucide"),Array.from(M).forEach(v=>E0(v,{nameAttr:"icon-name",icons:t,attrs:c})))}};a.AArrowDown=x0,a.AArrowUp=W0,a.ALargeSmall=X0,a.Accessibility=N0,a.Activity=K0,a.ActivitySquare=L2,a.AirVent=J0,a.Airplay=Q0,a.AlarmCheck=o,a.AlarmClock=Y0,a.AlarmClockCheck=o,a.AlarmClockMinus=s,a.AlarmClockOff=j0,a.AlarmClockPlus=r,a.AlarmMinus=s,a.AlarmPlus=r,a.AlarmSmoke=_0,a.Album=aa,a.AlertCircle=G,a.AlertOctagon=t2,a.AlertTriangle=P0,a.AlignCenter=da,a.AlignCenterHorizontal=ha,a.AlignCenterVertical=ta,a.AlignEndHorizontal=ca,a.AlignEndVertical=Ma,a.AlignHorizontalDistributeCenter=pa,a.AlignHorizontalDistributeEnd=ea,a.AlignHorizontalDistributeStart=na,a.AlignHorizontalJustifyCenter=ia,a.AlignHorizontalJustifyEnd=la,a.AlignHorizontalJustifyStart=va,a.AlignHorizontalSpaceAround=oa,a.AlignHorizontalSpaceBetween=sa,a.AlignJustify=ra,a.AlignLeft=ga,a.AlignRight=ya,a.AlignStartHorizontal=$a,a.AlignStartVertical=ma,a.AlignVerticalDistributeCenter=Ca,a.AlignVerticalDistributeEnd=ua,a.AlignVerticalDistributeStart=Ha,a.AlignVerticalJustifyCenter=wa,a.AlignVerticalJustifyEnd=Va,a.AlignVerticalJustifyStart=Aa,a.AlignVerticalSpaceAround=Sa,a.AlignVerticalSpaceBetween=La,a.Ambulance=fa,a.Ampersand=Pa,a.Ampersands=ka,a.Amphora=Ba,a.Anchor=Fa,a.Angry=Da,a.Annoyed=Ra,a.Antenna=za,a.Anvil=qa,a.Aperture=Ta,a.AppWindow=ba,a.AppWindowMac=Za,a.Apple=Ua,a.Archive=Ia,a.ArchiveRestore=Oa,a.ArchiveX=Ga,a.AreaChart=P,a.Armchair=Ea,a.ArrowBigDown=Wa,a.ArrowBigDownDash=xa,a.ArrowBigLeft=Na,a.ArrowBigLeftDash=Xa,a.ArrowBigRight=Ja,a.ArrowBigRightDash=Ka,a.ArrowBigUp=ja,a.ArrowBigUpDash=Qa,a.ArrowDown=eh,a.ArrowDown01=Ya,a.ArrowDown10=_a,a.ArrowDownAZ=g,a.ArrowDownAz=g,a.ArrowDownCircle=I,a.ArrowDownFromLine=ah,a.ArrowDownLeft=hh,a.ArrowDownLeftFromCircle=x,a.ArrowDownLeftFromSquare=F2,a.ArrowDownLeftSquare=f2,a.ArrowDownNarrowWide=th,a.ArrowDownRight=dh,a.ArrowDownRightFromCircle=W,a.ArrowDownRightFromSquare=D2,a.ArrowDownRightSquare=P2,a.ArrowDownSquare=k2,a.ArrowDownToDot=ch,a.ArrowDownToLine=Mh,a.ArrowDownUp=ph,a.ArrowDownWideNarrow=y,a.ArrowDownZA=$,a.ArrowDownZa=$,a.ArrowLeft=vh,a.ArrowLeftCircle=E,a.ArrowLeftFromLine=nh,a.ArrowLeftRight=ih,a.ArrowLeftSquare=B2,a.ArrowLeftToLine=lh,a.ArrowRight=gh,a.ArrowRightCircle=K,a.ArrowRightFromLine=oh,a.ArrowRightLeft=sh,a.ArrowRightSquare=q2,a.ArrowRightToLine=rh,a.ArrowUp=Sh,a.ArrowUp01=yh,a.ArrowUp10=$h,a.ArrowUpAZ=m,a.ArrowUpAz=m,a.ArrowUpCircle=J,a.ArrowUpDown=mh,a.ArrowUpFromDot=Ch,a.ArrowUpFromLine=uh,a.ArrowUpLeft=Hh,a.ArrowUpLeftFromCircle=X,a.ArrowUpLeftFromSquare=R2,a.ArrowUpLeftSquare=T2,a.ArrowUpNarrowWide=C,a.ArrowUpRight=wh,a.ArrowUpRightFromCircle=N,a.ArrowUpRightFromSquare=z2,a.ArrowUpRightSquare=Z2,a.ArrowUpSquare=b2,a.ArrowUpToLine=Vh,a.ArrowUpWideNarrow=Ah,a.ArrowUpZA=u,a.ArrowUpZa=u,a.ArrowsUpFromLine=Lh,a.Asterisk=fh,a.AsteriskSquare=U2,a.AtSign=Ph,a.Atom=kh,a.AudioLines=Bh,a.AudioWaveform=Fh,a.Award=Dh,a.Axe=Rh,a.Axis3D=H,a.Axis3d=H,a.Baby=zh,a.Backpack=qh,a.Badge=jh,a.BadgeAlert=Th,a.BadgeCent=Zh,a.BadgeCheck=w,a.BadgeDollarSign=bh,a.BadgeEuro=Uh,a.BadgeHelp=Oh,a.BadgeIndianRupee=Gh,a.BadgeInfo=Ih,a.BadgeJapaneseYen=Eh,a.BadgeMinus=xh,a.BadgePercent=Wh,a.BadgePlus=Xh,a.BadgePoundSterling=Nh,a.BadgeRussianRuble=Kh,a.BadgeSwissFranc=Jh,a.BadgeX=Qh,a.BaggageClaim=Yh,a.Ban=_h,a.Banana=at,a.Bandage=ht,a.Banknote=tt,a.BarChart=T,a.BarChart2=Z,a.BarChart3=z,a.BarChart4=R,a.BarChartBig=D,a.BarChartHorizontal=B,a.BarChartHorizontalBig=k,a.Barcode=dt,a.Baseline=ct,a.Bath=Mt,a.Battery=vt,a.BatteryCharging=pt,a.BatteryFull=et,a.BatteryLow=nt,a.BatteryMedium=it,a.BatteryWarning=lt,a.Beaker=ot,a.Bean=rt,a.BeanOff=st,a.Bed=$t,a.BedDouble=gt,a.BedSingle=yt,a.Beef=mt,a.Beer=ut,a.BeerOff=Ct,a.Bell=ft,a.BellDot=Ht,a.BellElectric=wt,a.BellMinus=Vt,a.BellOff=At,a.BellPlus=St,a.BellRing=Lt,a.BetweenHorizonalEnd=V,a.BetweenHorizonalStart=A,a.BetweenHorizontalEnd=V,a.BetweenHorizontalStart=A,a.BetweenVerticalEnd=Pt,a.BetweenVerticalStart=kt,a.BicepsFlexed=Bt,a.Bike=Ft,a.Binary=Dt,a.Binoculars=Rt,a.Biohazard=zt,a.Bird=qt,a.Bitcoin=Tt,a.Blend=Zt,a.Blinds=bt,a.Blocks=Ut,a.Bluetooth=Et,a.BluetoothConnected=Ot,a.BluetoothOff=Gt,a.BluetoothSearching=It,a.Bold=xt,a.Bolt=Wt,a.Bomb=Xt,a.Bone=Nt,a.Book=y4,a.BookA=Kt,a.BookAudio=Jt,a.BookCheck=Qt,a.BookCopy=jt,a.BookDashed=S,a.BookDown=Yt,a.BookHeadphones=_t,a.BookHeart=a4,a.BookImage=h4,a.BookKey=t4,a.BookLock=d4,a.BookMarked=c4,a.BookMinus=M4,a.BookOpen=n4,a.BookOpenCheck=p4,a.BookOpenText=e4,a.BookPlus=i4,a.BookTemplate=S,a.BookText=lLine truncated
//# sourceMappingURL=lucide.min.js.map
Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 KiB

+72
View File
@@ -0,0 +1,72 @@
<!-- Author: fruit.com.cn -->
<!-- 印尼市场短剧客户端交互设计入口。 -->
<!doctype html>
<html lang="id">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#0b0c10" />
<meta name="description" content="FruitShorts — drama pendek, cerita yang dekat." />
<title>FruitShorts · Drama pendek pilihanmu</title>
<link rel="icon" href="../brand-mark.png" />
<link rel="stylesheet" href="styles.css" />
<link rel="stylesheet" href="screens.css" />
<script defer src="assets/lucide.min.js"></script>
<script defer src="data.js"></script>
<script defer src="state.js"></script>
<script defer src="views.js"></script>
<script defer src="dialogs.js"></script>
<script defer src="app.js"></script>
</head>
<body>
<a class="skip-link" href="#main">Langsung ke konten</a>
<aside class="sidebar" aria-label="Navigasi utama">
<a class="brand" href="#home" aria-label="FruitShorts Beranda">
<span class="brand-mark"><i data-lucide="play" aria-hidden="true"></i></span>
<span>Fruit<span class="brand-accent">Shorts</span></span>
</a>
<span class="nav-caption">MENU UTAMA</span>
<nav class="main-nav">
<a href="#home" data-nav="home"><i data-lucide="house"></i><span>Beranda</span></a>
<a href="#discover" data-nav="discover"><i data-lucide="compass"></i><span>Jelajahi</span></a>
<a href="#me" data-nav="me"><i data-lucide="circle-user-round"></i><span>Saya</span></a>
</nav>
<div class="sidebar-divider"></div>
<span class="nav-caption">KOLEKSI SAYA</span>
<nav class="collection-nav">
<a href="#library"><i data-lucide="bookmark"></i>Daftar saya<span id="saved-count">0</span></a>
<a href="#history"><i data-lucide="history"></i>Riwayat tontonan</a>
</nav>
<div class="sidebar-bottom">
<div class="local-note"><i data-lucide="clapperboard"></i><span>Cerita singkat.<br /><strong>Rasa yang melekat.</strong></span></div>
<button class="help-link" data-action="help"><i data-lucide="circle-help"></i>Pusat bantuan<i data-lucide="arrow-up-right"></i></button>
<span class="copyright">© 2026 FruitShorts</span>
</div>
</aside>
<div class="app-shell">
<header class="topbar">
<a class="brand mobile-brand" href="#home"><span class="brand-mark"><i data-lucide="play"></i></span><span>Fruit<span class="brand-accent">Shorts</span></span></a>
<form class="search-form" role="search">
<i data-lucide="search" aria-hidden="true"></i>
<input id="global-search" type="search" placeholder="Cari drama, temukan cerita..." aria-label="Cari drama" autocomplete="off" />
<kbd aria-hidden="true">/</kbd>
</form>
<div class="topbar-actions">
<button class="language-button" data-action="language" title="Bahasa"><i data-lucide="globe-2"></i><span>ID</span><i data-lucide="chevron-down"></i></button>
<button class="wallet-button" data-action="wallet" title="Dompet koin"><i data-lucide="coins"></i><span data-balance>120</span><span class="wallet-add">+</span></button>
<button class="icon-button notification-button" data-action="notifications" aria-label="Notifikasi" title="Notifikasi"><i data-lucide="bell"></i><span></span></button>
<a class="avatar" href="#me" aria-label="Profil saya">N</a>
</div>
</header>
<main id="main"></main>
<footer class="page-footer"><a class="brand" href="#home">Fruit<span class="brand-accent">Shorts</span></a><span>Drama pendek. Momen besar.</span><button data-action="privacy">Privasi</button><button data-action="terms">Ketentuan</button><span>Indonesia · Bahasa Indonesia</span></footer>
</div>
<nav class="bottom-nav" aria-label="Navigasi seluler">
<a href="#home" data-nav="home"><i data-lucide="house"></i><span>Beranda</span></a>
<a href="#discover" data-nav="discover"><i data-lucide="compass"></i><span>Jelajahi</span></a>
<a href="#me" data-nav="me"><i data-lucide="circle-user-round"></i><span>Saya</span></a>
</nav>
<dialog id="app-dialog" aria-labelledby="dialog-title"></dialog>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
</body>
</html>
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

+14
View File
@@ -1,3 +1,5 @@
<!-- Author: fruit.com.cn -->
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { useAppStore } from '@/store/modules/app' import { useAppStore } from '@/store/modules/app'
@@ -20,6 +22,7 @@ appStore.initTheme()
<template> <template>
<ConfigGlobal :size="currentSize"> <ConfigGlobal :size="currentSize">
<RouterView :class="greyMode ? `${prefixCls}-grey-mode` : ''" /> <RouterView :class="greyMode ? `${prefixCls}-grey-mode` : ''" />
<footer class="app-copyright">© FruitShorts</footer>
</ConfigGlobal> </ConfigGlobal>
</template> </template>
@@ -46,4 +49,15 @@ body {
.@{prefix-cls}-grey-mode { .@{prefix-cls}-grey-mode {
filter: grayscale(100%); filter: grayscale(100%);
} }
.app-copyright {
position: fixed;
right: 20px;
bottom: 10px;
z-index: 5;
font-size: 12px;
line-height: 1;
color: var(--el-text-color-secondary, #909399);
pointer-events: none;
}
</style> </style>
+27
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import request from '@/axios' import request from '@/axios'
import type { AppUserBanRecord, AppUserItem, PageResult, WatchHistoryItem } from './types' import type { AppUserBanRecord, AppUserItem, PageResult, WatchHistoryItem } from './types'
@@ -9,6 +11,24 @@ export interface AppUserPageParams {
status?: string status?: string
countryCode?: string countryCode?: string
registerSource?: string registerSource?: string
/** 客户端平台:ANDROID / IOS / WEB */
platform?: string
}
export interface WatchHistoryPageParams {
pageIndex?: number
pageSize?: number
keyword?: string
userId?: number
dramaTitle?: string
dramaId?: number
episodeId?: number
/** 客户端平台:ANDROID / IOS / WEB */
platform?: string
source?: string
finished?: boolean
startTime?: string
endTime?: string
} }
export const pageAppUsersApi = (params: AppUserPageParams) => { export const pageAppUsersApi = (params: AppUserPageParams) => {
@@ -30,6 +50,13 @@ export const listAppUserWatchHistoryApi = (id: number, limit = 50) => {
}) })
} }
export const pageWatchHistoryApi = (params: WatchHistoryPageParams) => {
return request.get<PageResult<WatchHistoryItem>>({
url: '/admin/watch-history/page',
params
})
}
/** normal / limited / banned,封禁与限制会立即吊销该用户全部会话 */ /** normal / limited / banned,封禁与限制会立即吊销该用户全部会话 */
export const updateAppUserStatusApi = (id: number, status: string, reason?: string) => { export const updateAppUserStatusApi = (id: number, status: string, reason?: string) => {
return request.put<void>({ return request.put<void>({
+69
View File
@@ -0,0 +1,69 @@
// Author: fruit.com.cn
import request from '@/axios'
export interface AppVersionItem {
id: string
platform: 'ANDROID' | 'IOS'
platformText: string
versionCode: number
versionName: string
downloadUrl: string
updateContent?: string
forceUpdate: number
minSupportedCode: number
status: number
statusText: string
releasedAt?: string
createdAt?: string
}
export interface AppVersionSaveData {
platform: 'ANDROID' | 'IOS'
versionCode: number
versionName: string
downloadUrl: string
updateContent?: string
forceUpdate?: 0 | 1
minSupportedCode?: number
}
export const listAppVersionsApi = (params: {
pageIndex?: number
pageSize?: number
platform?: string
status?: number
keyword?: string
}) => {
return request.get<{ total: number; list: AppVersionItem[] }>({
url: '/admin/app-versions/page',
params
})
}
export const getAppVersionApi = (id: string | number) => {
return request.get<AppVersionItem>({ url: `/admin/app-versions/${id}` })
}
export const createAppVersionApi = (data: AppVersionSaveData) => {
return request.post<string>({ url: '/admin/app-versions', data })
}
export const batchCreateAppVersionsApi = (data: AppVersionSaveData[]) => {
return request.post<string[]>({ url: '/admin/app-versions/batch', data })
}
export const updateAppVersionApi = (id: string | number, data: AppVersionSaveData) => {
return request.post<void>({ url: `/admin/app-versions/${id}`, data })
}
export const updateAppVersionStatusApi = (id: string | number, status: 0 | 1) => {
return request.post<void>({
url: `/admin/app-versions/${id}/status`,
params: { status }
})
}
export const deleteAppVersionApi = (id: string | number) => {
return request.delete<void>({ url: `/admin/app-versions/${id}` })
}
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import request from '@/axios' import request from '@/axios'
import type { BannerItem, BannerSaveData, PageResult } from './types' import type { BannerItem, BannerSaveData, PageResult } from './types'
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import request from '@/axios' import request from '@/axios'
import type { PageResult } from './types' import type { PageResult } from './types'
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import request from '@/axios' import request from '@/axios'
import type { ConfigItem } from './types' import type { ConfigItem } from './types'
+41
View File
@@ -0,0 +1,41 @@
// Author: fruit.com.cn
import request from '@/axios'
export interface ContentTagItem {
id: number
name: string
nameEn: string
nameId: string
locales?: Record<string, string>
bgColor?: string
sort: number
status: number
}
export interface ContentTagSaveData {
name: string
locales: Record<string, string>
bgColor?: string
sort?: number
status?: number
}
export const listContentTagsApi = (enabledOnly = false) => {
return request.get<ContentTagItem[]>({
url: '/admin/content-tags',
params: enabledOnly ? { enabledOnly: true } : undefined
})
}
export const createContentTagApi = (data: ContentTagSaveData) => {
return request.post<number>({ url: '/admin/content-tags', data })
}
export const updateContentTagApi = (id: number, data: ContentTagSaveData) => {
return request.put<void>({ url: `/admin/content-tags/${id}`, data })
}
export const deleteContentTagApi = (id: number) => {
return request.delete<void>({ url: `/admin/content-tags/${id}` })
}
+7
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import request from '@/axios' import request from '@/axios'
import type { PageResult } from './types' import type { PageResult } from './types'
@@ -96,6 +98,11 @@ export const pageSubmissionsApi = (params: Record<string, unknown>) => {
return request.get<PageResult<SubmissionItem>>({ url: '/admin/submissions/page', params }) return request.get<PageResult<SubmissionItem>>({ url: '/admin/submissions/page', params })
} }
/** 内容提交详情:附带短剧预览 + 分集列表,审核弹窗展示用 */
export const detailSubmissionApi = (id: number) => {
return request.get<SubmissionItem>({ url: `/admin/submissions/${id}` })
}
/** action: APPROVE / REJECT / WITHDRAW */ /** action: APPROVE / REJECT / WITHDRAW */
export const auditSubmissionApi = (id: number, data: Record<string, unknown>) => { export const auditSubmissionApi = (id: number, data: Record<string, unknown>) => {
return request.put<SubmissionItem>({ url: `/admin/submissions/${id}/audit`, data }) return request.put<SubmissionItem>({ url: `/admin/submissions/${id}/audit`, data })
+41
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import request from '@/axios' import request from '@/axios'
import type { PageResult } from './types' import type { PageResult } from './types'
@@ -107,6 +109,35 @@ export interface CreatorDramaEditData {
language?: string language?: string
} }
/** 一键发布的分集入参:assetId 来自已完成的分片上传 */
export interface PublishEpisodeItem {
assetId: number
title?: string
durationSec?: number
cover?: string
}
export interface PublishDramaPayload {
title: string
subtitle?: string
/** 短剧分类 id(下拉选择) */
categoryId?: number
/** 题材标签(自由文本,可选) */
genre?: string
description?: string
cover?: string
language?: string
/** 默认 true:创建后立即提交审核 */
submit?: boolean
episodes: PublishEpisodeItem[]
}
export interface PublishDramaResult {
dramaId: number
submissionId?: number | null
status: string
}
export interface PriceEpisodeConfig { export interface PriceEpisodeConfig {
episodeId: number episodeId: number
number: number number: number
@@ -174,6 +205,11 @@ export const createDramaApi = (data: CreatorDramaEditData) => {
return request.post<number>({ url: '/admin/creator-center/dramas', data }) return request.post<number>({ url: '/admin/creator-center/dramas', data })
} }
/** 一键发布:一个事务里建作品 + 建分集 + 绑定视频(可选提交审核) */
export const publishDramaApi = (data: PublishDramaPayload) => {
return request.post<PublishDramaResult>({ url: '/admin/creator-center/publish', data })
}
export const updateDramaApi = (id: number, data: CreatorDramaEditData) => { export const updateDramaApi = (id: number, data: CreatorDramaEditData) => {
return request.put<void>({ url: `/admin/creator-center/dramas/${id}`, data }) return request.put<void>({ url: `/admin/creator-center/dramas/${id}`, data })
} }
@@ -203,6 +239,11 @@ export const deleteEpisodeApi = (dramaId: number, episodeId: number) => {
}) })
} }
/** 删除作品(草稿 / 已下线 / 审核中可删,已上线需先下线) */
export const deleteDramaApi = (dramaId: number) => {
return request.delete<void>({ url: `/admin/creator-center/dramas/${dramaId}` })
}
export const sortEpisodesApi = (dramaId: number, ids: number[]) => { export const sortEpisodesApi = (dramaId: number, ids: number[]) => {
return request.put<void>({ return request.put<void>({
url: `/admin/creator-center/dramas/${dramaId}/episodes/sort`, url: `/admin/creator-center/dramas/${dramaId}/episodes/sort`,
+32
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import request from '@/axios' import request from '@/axios'
/** 指标口径见后端 DashboardService 注释与 docs/api-phase1.md「2.8 数据看板」 */ /** 指标口径见后端 DashboardService 注释与 docs/api-phase1.md「2.8 数据看板」 */
@@ -25,6 +27,28 @@ export interface TranscodeBoard {
notes: string[] notes: string[]
} }
/** 近 7 天每日趋势(给折线图用)。 */
export interface DashboardTrend {
dates: string[]
newUsers: number[]
playEvents: number[]
newTranscodes: number[]
transcodeSuccess: number[]
}
/** 分布项(给饼图用)。 */
export interface DistributionItem {
label: string
value: number
}
export interface DashboardDistribution {
dramaStatus: Record<string, number>
transcodeStatus: Record<string, number>
episodeMedia: Record<string, number>
pendingModeration: DistributionItem[]
}
export const getDashboardOverviewApi = () => { export const getDashboardOverviewApi = () => {
return request.get<DashboardOverview>({ url: '/admin/dashboard/overview' }) return request.get<DashboardOverview>({ url: '/admin/dashboard/overview' })
} }
@@ -32,3 +56,11 @@ export const getDashboardOverviewApi = () => {
export const getTranscodeBoardApi = () => { export const getTranscodeBoardApi = () => {
return request.get<TranscodeBoard>({ url: '/admin/dashboard/transcode' }) return request.get<TranscodeBoard>({ url: '/admin/dashboard/transcode' })
} }
export const getDashboardTrendApi = () => {
return request.get<DashboardTrend>({ url: '/admin/dashboard/trend' })
}
export const getDashboardDistributionApi = () => {
return request.get<DashboardDistribution>({ url: '/admin/dashboard/distribution' })
}
+8 -2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import request from '@/axios' import request from '@/axios'
import type { import type {
DramaCategory, DramaCategory,
@@ -13,7 +15,9 @@ export interface DramaPageParams {
keyword?: string keyword?: string
status?: string status?: string
categoryId?: number categoryId?: number
categoryIds?: number[]
isHot?: boolean isHot?: boolean
dramaTypes?: Array<'ANIME' | 'LIVE'>
} }
export const pageDramasApi = (params: DramaPageParams) => { export const pageDramasApi = (params: DramaPageParams) => {
@@ -36,9 +40,10 @@ export const deleteDramaApi = (id: number) => {
return request.delete<void>({ url: `/admin/dramas/${id}` }) return request.delete<void>({ url: `/admin/dramas/${id}` })
} }
/** 审核与发布:submit / approve / reject / publish / offline */ /** 审核与发布:submit / approve / reject / publish / offline / approvePublish */
export const changeDramaStatusApi = (id: number, action: DramaReviewAction, reason?: string) => { export const changeDramaStatusApi = (id: number, action: DramaReviewAction, reason?: string) => {
return request.post<void>({ url: `/admin/dramas/${id}/${action}`, data: { reason } }) const path = action === 'approvePublish' ? 'approve-publish' : action
return request.post<void>({ url: `/admin/dramas/${id}/${path}`, data: { reason } })
} }
/** 下拉选项(短剧表单用),只返回 id/name */ /** 下拉选项(短剧表单用),只返回 id/name */
@@ -48,6 +53,7 @@ export const listDramaCategoryOptionsApi = () => {
export interface DramaCategorySaveData { export interface DramaCategorySaveData {
name: string name: string
locales: Record<string, string>
sort?: number sort?: number
status?: number status?: number
} }
+103
View File
@@ -0,0 +1,103 @@
// Author: fruit.com.cn
import request from '@/axios'
import type { PageResult } from './types'
export interface DramaClassificationItem {
id: string
name: string
code: string
nameEn: string
nameId: string
locales?: Record<string, string>
layoutType: 'GRID' | 'RANKING'
revision: string
sort: number
status: number
dramaCount: number
updatedAt?: string
}
export interface DramaClassificationSaveData {
name: string
locales: Record<string, string>
code?: string
layoutType?: 'GRID' | 'RANKING'
sort?: number
status?: number
}
export interface ClassificationDramaItem {
id: string
title: string
cover: string
status: string
totalEpisodes: number
sort: number
position?: number
updatedAt: string
}
export interface ClassificationDramaPageParams {
keyword?: string
pageIndex?: number
pageSize?: number
}
export const listClassificationsApi = () => {
return request.get<DramaClassificationItem[]>({ url: '/admin/drama-classifications' })
}
export const createClassificationApi = (data: DramaClassificationSaveData) => {
return request.post<string>({ url: '/admin/drama-classifications', data })
}
export const updateClassificationApi = (id: string, data: DramaClassificationSaveData) => {
return request.put<void>({ url: `/admin/drama-classifications/${id}`, data })
}
export const deleteClassificationApi = (id: string) => {
return request.delete<void>({ url: `/admin/drama-classifications/${id}` })
}
/** 分类下已关联的短剧 */
export const listClassificationDramasApi = (id: string, params: ClassificationDramaPageParams) => {
return request.get<PageResult<ClassificationDramaItem>>({
url: `/admin/drama-classifications/${id}/dramas`,
params
})
}
/** 尚未关联到该分类的短剧(添加弹窗用) */
export const listClassificationDramaCandidatesApi = (
id: string,
params: ClassificationDramaPageParams
) => {
return request.get<PageResult<ClassificationDramaItem>>({
url: `/admin/drama-classifications/${id}/dramas/candidates`,
params
})
}
/** 批量关联短剧,返回实际新增数量 */
export const bindClassificationDramasApi = (id: string, dramaIds: string[]) => {
return request.post<number>({
url: `/admin/drama-classifications/${id}/dramas`,
data: { dramaIds }
})
}
/** 从分类移除短剧 */
export const unbindClassificationDramaApi = (id: string, dramaId: string) => {
return request.delete<void>({
url: `/admin/drama-classifications/${id}/dramas/${dramaId}`
})
}
/** 原子保存推荐榜完整内容与顺序 */
export const saveRankingItemsApi = (id: string, dramaIds: string[]) => {
return request.put<void>({
url: `/admin/drama-classifications/${id}/ranking-items`,
data: { dramaIds }
})
}
+87
View File
@@ -0,0 +1,87 @@
// Author: fruit.com.cn
import request from '@/axios'
import type { PageResult } from './types'
import type { ClassificationDramaItem } from './dramaClassification'
export interface DramaPlaylistCategoryItem {
id: number
name: string
nameEn: string
nameId: string
description: string
descriptionEn: string
descriptionId: string
locales?: Record<string, string>
descriptionLocales?: Record<string, string>
sort: number
status: number
dramaCount: number
}
export interface DramaPlaylistCategorySaveData {
name: string
locales: Record<string, string>
descriptionLocales?: Record<string, string>
sort?: number
status?: number
}
export interface PlaylistCategoryDramaPageParams {
keyword?: string
pageIndex?: number
pageSize?: number
}
export const listPlaylistCategoriesApi = () => {
return request.get<DramaPlaylistCategoryItem[]>({ url: '/admin/drama-playlist-categories' })
}
export const createPlaylistCategoryApi = (data: DramaPlaylistCategorySaveData) => {
return request.post<number>({ url: '/admin/drama-playlist-categories', data })
}
export const updatePlaylistCategoryApi = (id: number, data: DramaPlaylistCategorySaveData) => {
return request.put<void>({ url: `/admin/drama-playlist-categories/${id}`, data })
}
export const deletePlaylistCategoryApi = (id: number) => {
return request.delete<void>({ url: `/admin/drama-playlist-categories/${id}` })
}
/** 分类下已关联的短剧 */
export const listPlaylistCategoryDramasApi = (
id: number,
params: PlaylistCategoryDramaPageParams
) => {
return request.get<PageResult<ClassificationDramaItem>>({
url: `/admin/drama-playlist-categories/${id}/dramas`,
params
})
}
/** 尚未关联到该分类的短剧(选择弹窗用) */
export const listPlaylistCategoryDramaCandidatesApi = (
id: number,
params: PlaylistCategoryDramaPageParams
) => {
return request.get<PageResult<ClassificationDramaItem>>({
url: `/admin/drama-playlist-categories/${id}/dramas/candidates`,
params
})
}
/** 批量关联短剧,返回实际新增数量 */
export const bindPlaylistCategoryDramasApi = (id: number, dramaIds: number[]) => {
return request.post<number>({
url: `/admin/drama-playlist-categories/${id}/dramas`,
data: { dramaIds }
})
}
/** 从分类移除短剧 */
export const unbindPlaylistCategoryDramaApi = (id: number, dramaId: number) => {
return request.delete<void>({
url: `/admin/drama-playlist-categories/${id}/dramas/${dramaId}`
})
}
+54
View File
@@ -0,0 +1,54 @@
// Author: fruit.com.cn
import request from '@/axios'
import type { PageResult } from './types'
/** 收藏与点赞只读分页筛选条件。 */
export interface EngagementPageParams {
pageIndex?: number
pageSize?: number
keyword?: string
userId?: string | number
deviceId?: string
/** 客户端平台:ANDROID / IOS / WEB */
platform?: string
dramaId?: string | number
dramaTitle?: string
targetType?: string
targetId?: string | number
episodeId?: string | number
startTime?: string
endTime?: string
}
/** 后台互动列表行。 */
export interface EngagementItem {
id: string
userId: string | null
uid: string | null
nickname: string | null
deviceId: string | null
/** 客户端平台 ANDROID / IOS / WEB;老客户端未上报时为空。 */
platform: string | null
anonymous: boolean
dramaId: string | null
dramaTitle: string | null
dramaCover: string | null
dramaStatus: string | null
dramaPublished: boolean | null
targetType: string
targetId: string
episodeId: string | null
episodeNo: number | null
episodeTitle: string | null
commentSummary: string | null
createdAt: string
}
/** 查询收藏分页。 */
export const pageFavoritesApi = (params: EngagementPageParams) =>
request.get<PageResult<EngagementItem>>({ url: '/admin/favorites/page', params })
/** 查询点赞分页。 */
export const pageLikesApi = (params: EngagementPageParams) =>
request.get<PageResult<EngagementItem>>({ url: '/admin/likes/page', params })
+31 -4
View File
@@ -1,8 +1,35 @@
import request from '@/axios' // Author: fruit.com.cn
import type { EpisodeItem, EpisodeSaveData } from './types'
export const listEpisodesApi = (dramaId: number, status?: string) => { import request from '@/axios'
return request.get<EpisodeItem[]>({ url: '/admin/episodes', params: { dramaId, status } }) import type { EpisodeItem, EpisodeSaveData, PageResult } from './types'
export interface EpisodePageParams {
dramaId: number | string
status?: string
pageIndex?: number
pageSize?: number
}
/**
* 解包剧集列表分页。
* 契约(OpenAPI PageResultEpisodeRespVO):data = { total, list }。
* 兼容误把 data 当数组的旧调用,避免列表空白。
*/
export function parseEpisodePage(payload: unknown): PageResult<EpisodeItem> {
if (Array.isArray(payload)) {
return { total: payload.length, list: payload as EpisodeItem[] }
}
if (payload && typeof payload === 'object') {
const page = payload as { total?: unknown; list?: unknown }
const list = Array.isArray(page.list) ? (page.list as EpisodeItem[]) : []
const total = Number(page.total)
return { total: Number.isFinite(total) ? total : list.length, list }
}
return { total: 0, list: [] }
}
export const listEpisodesApi = (params: EpisodePageParams) => {
return request.get<PageResult<EpisodeItem>>({ url: '/admin/episodes', params })
} }
export const createEpisodeApi = (data: EpisodeSaveData) => { export const createEpisodeApi = (data: EpisodeSaveData) => {
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import request from '@/axios' import request from '@/axios'
import type { PageResult, CommentItem, ReportItem, SensitiveWordItem } from './types' import type { PageResult, CommentItem, ReportItem, SensitiveWordItem } from './types'
+18
View File
@@ -0,0 +1,18 @@
// Author: fruit.com.cn
import request from '@/axios'
export interface LegalDocument {
docCode: 'terms' | 'privacy' | string
lang: 'zh' | 'en' | 'id' | string
title: string
content: string
}
export const listLegalDocumentsApi = () => {
return request.get<LegalDocument[]>({ url: '/admin/legal-documents' })
}
export const saveLegalDocumentsApi = (items: LegalDocument[]) => {
return request.put<void>({ url: '/admin/legal-documents', data: items })
}
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import request from '@/axios' import request from '@/axios'
import type { OperationLogItem, PageResult } from './types' import type { OperationLogItem, PageResult } from './types'
+20 -1
View File
@@ -1,5 +1,7 @@
// Author: fruit.com.cn
import request from '@/axios' import request from '@/axios'
import type { MediaAsset } from './types' import type { MediaAsset, MediaTranscodeProfile } from './types'
export interface UploadInitData { export interface UploadInitData {
fileName: string fileName: string
@@ -59,3 +61,20 @@ export const getUploadProgressApi = (uploadId: string) => {
export const getMediaApi = (id: number) => { export const getMediaApi = (id: number) => {
return request.get<MediaAsset>({ url: `/admin/media/${id}` }) return request.get<MediaAsset>({ url: `/admin/media/${id}` })
} }
/** 读取当前转码档位配置;缺省时后端返回四档默认值。 */
export const getMediaTranscodeProfileApi = () => {
return request.get<MediaTranscodeProfile>({
url: '/admin/media/transcode-profile',
silent: true
})
}
/** 保存转码档位配置,并返回服务端最终生效值。 */
export const saveMediaTranscodeProfileApi = (data: MediaTranscodeProfile) => {
return request.put<MediaTranscodeProfile>({
url: '/admin/media/transcode-profile',
data,
silent: true
})
}
+66
View File
@@ -0,0 +1,66 @@
// Author: fruit.com.cn
import request from '@/axios'
import type { PageResult } from './types'
/** 系统公告:字段与 /admin/notices 的响应一一对应 */
export interface NoticeItem {
/** 雪花 id,后端以字符串下发(超过 JS 安全整数范围),原样回传即可 */
id: string
title: string
content: string
/** ALL / USER / CREATOR */
target: string
targetText: string
/** SYSTEM / AUDIT / PAYMENT / ACTIVITY */
type: string
typeText: string
/** 1 已发布 / 0 已下架 */
status: number
statusText: string
publishAt?: string
expireAt?: string
createdAt?: string
}
export interface NoticePageParams {
pageIndex?: number
pageSize?: number
status?: number
target?: string
type?: string
keyword?: string
}
export interface NoticeSaveData {
title: string
content: string
target: string
type: string
publishAt?: string | null
expireAt?: string | null
}
export const pageNoticesApi = (params: NoticePageParams) => {
return request.get<PageResult<NoticeItem>>({ url: '/admin/notices/page', params })
}
export const getNoticeApi = (id: string | number) => {
return request.get<NoticeItem>({ url: `/admin/notices/${id}` })
}
export const createNoticeApi = (data: NoticeSaveData) => {
return request.post<string>({ url: '/admin/notices', data })
}
export const updateNoticeApi = (id: string | number, data: NoticeSaveData) => {
return request.put<void>({ url: `/admin/notices/${id}`, data })
}
export const updateNoticeStatusApi = (id: string | number, status: number) => {
return request.put<void>({ url: `/admin/notices/${id}/status`, params: { status } })
}
export const deleteNoticeApi = (id: string | number) => {
return request.delete<void>({ url: `/admin/notices/${id}` })
}
+85
View File
@@ -0,0 +1,85 @@
// Author: fruit.com.cn
import request from '@/axios'
import type { PageResult } from './types'
/** 商品(金币包):字段与 /admin/products 的响应一一对应 */
export interface ProductItem {
/** 雪花 id,后端以字符串下发(超过 JS 安全整数范围),原样回传即可 */
id: string
code: string
name: string
/** COIN_PACK / VIP */
type: string
typeText: string
/** ALL / GOOGLE / APPLE */
platform: string
storeProductId: string
coinAmount: number
bonusCoin: number
/** coinAmount + bonusCoin,服务端算好,前端不再自己加 */
totalCoin: number
priceAmount: number
currency: string
displayPrice: string
regionCodes: string
vipDays: number
sort: number
/** 1 已上架 / 0 已下架 */
status: number
statusText: string
effectiveAt?: string
expireAt?: string
createdAt?: string
}
export interface ProductPageParams {
pageIndex?: number
pageSize?: number
type?: string
status?: number
platform?: string
keyword?: string
}
export interface ProductSaveData {
code: string
name: string
type: string
platform: string
storeProductId: string
coinAmount: number
bonusCoin: number
priceAmount: number
currency: string
displayPrice: string
regionCodes: string
vipDays: number
sort: number
effectiveAt?: string | null
expireAt?: string | null
}
export const pageProductsApi = (params: ProductPageParams) => {
return request.get<PageResult<ProductItem>>({ url: '/admin/products/page', params })
}
export const getProductApi = (id: string | number) => {
return request.get<ProductItem>({ url: `/admin/products/${id}` })
}
export const createProductApi = (data: ProductSaveData) => {
return request.post<string>({ url: '/admin/products', data })
}
export const updateProductApi = (id: string | number, data: ProductSaveData) => {
return request.put<void>({ url: `/admin/products/${id}`, data })
}
export const updateProductStatusApi = (id: string | number, status: number) => {
return request.put<void>({ url: `/admin/products/${id}/status`, params: { status } })
}
export const deleteProductApi = (id: string | number) => {
return request.delete<void>({ url: `/admin/products/${id}` })
}
+93
View File
@@ -0,0 +1,93 @@
// Author: fruit.com.cn
import request from '@/axios'
import type { PageResult } from './types'
export interface RewardTaskItem {
id: string
code: string
sectionCode: string
type: string
verifyMode: string
targetValue: number
rewardCoin: number
cycleType: string
cycleLimit: number
actionType: string
actionUrl: string
iconUrl: string
titleId: string
titleEn: string
titleZh: string
subtitleId: string
subtitleEn: string
subtitleZh: string
ctaId: string
ctaEn: string
ctaZh: string
sectionTitleId: string
sectionTitleEn: string
sectionTitleZh: string
configJson: string
sort: number
status: number
statusText: string
startAt?: string
endAt?: string
}
export interface RewardTaskPageParams {
pageIndex?: number
pageSize?: number
keyword?: string
type?: string
sectionCode?: string
status?: number
}
export type RewardTaskSaveData = Omit<RewardTaskItem, 'id' | 'status' | 'statusText'>
export interface RewardRuleDay {
dayIndex: number
rewardCoin: number
}
export interface RewardGrantItem {
id: string
userId: string
taskId: string
cycleKey: string
amount: number
status: string
idempotentKey: string
coinLedgerId: string
source: string
operatorId: string
failureReason: string
grantedAt?: string
createdAt?: string
}
export const pageRewardTasksApi = (params: RewardTaskPageParams) =>
request.get<PageResult<RewardTaskItem>>({ url: '/admin/rewards/tasks/page', params })
export const createRewardTaskApi = (data: RewardTaskSaveData) =>
request.post<string>({ url: '/admin/rewards/tasks', data })
export const updateRewardTaskApi = (id: string, data: RewardTaskSaveData) =>
request.put<void>({ url: `/admin/rewards/tasks/${id}`, data })
export const updateRewardTaskStatusApi = (id: string, status: number) =>
request.put<void>({ url: `/admin/rewards/tasks/${id}/status`, params: { status } })
export const getRewardRulesApi = () =>
request.get<RewardRuleDay[]>({ url: '/admin/rewards/check-in/rules' })
export const saveRewardRulesApi = (days: RewardRuleDay[]) =>
request.put<void>({ url: '/admin/rewards/check-in/rules', data: { days } })
export const pageRewardGrantsApi = (params: Record<string, unknown>) =>
request.get<PageResult<RewardGrantItem>>({ url: '/admin/rewards/grants/page', params })
export const manualRewardGrantApi = (data: Record<string, unknown>) =>
request.post<RewardGrantItem>({ url: '/admin/rewards/grants/manual', data })
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import request from '@/axios' import request from '@/axios'
import type { import type {
MenuNode, MenuNode,
+43
View File
@@ -0,0 +1,43 @@
// Author: fruit.com.cn
import request from '@/axios'
import type {
EpisodeTranscodeItem,
MediaThumbnailResult,
TranscodeBackfillResult,
TranscodeTriggerResult
} from './types'
/** 分集编辑页聚合查询转码任务、清晰度与帧图。 */
export const getEpisodeTranscodeApi = (episodeId: number | string) => {
return request.get<EpisodeTranscodeItem>({
url: `/admin/episodes/${episodeId}/transcode`
})
}
/** 按媒体分页读取帧图;页面切换时复用该接口。 */
export const getMediaThumbnailsApi = (
mediaAssetId: string,
pageIndex: number,
pageSize: number
) => {
return request.get<MediaThumbnailResult>({
url: `/admin/media/${mediaAssetId}/thumbnails`,
params: { pageIndex, pageSize }
})
}
/** 手动触发或重转当前分集绑定的媒体。 */
export const triggerMediaTranscodeApi = (mediaAssetId: string) => {
return request.post<TranscodeTriggerResult>({
url: `/admin/media/${mediaAssetId}/transcode`
})
}
/** 手动补齐转码:扫描缺码率产物/帧图的分集并入队。 */
export const runTranscodeBackfillApi = (batchSize?: number) => {
return request.post<TranscodeBackfillResult>({
url: '/admin/media/transcode/backfill',
params: batchSize ? { batchSize } : undefined
})
}
+27
View File
@@ -0,0 +1,27 @@
// Author: fruit.com.cn
import request from '@/axios'
export interface TranscodeNoticeItem {
id: number
status: string
dramaTitle: string
episodeNo?: number
episodeTitle?: string
progress?: number
errorMsg?: string
updatedAt?: string
}
export interface TranscodeNotice {
running: number
success: number
failed: number
runningList: TranscodeNoticeItem[]
successList: TranscodeNoticeItem[]
failedList: TranscodeNoticeItem[]
}
export const getTranscodeNoticesApi = () => {
return request.get<TranscodeNotice>({ url: '/admin/notifications/transcode' })
}
+162 -1
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
export interface PageResult<T> { export interface PageResult<T> {
total: number total: number
list: T[] list: T[]
@@ -6,6 +8,9 @@ export interface PageResult<T> {
export interface DramaCategory { export interface DramaCategory {
id: number id: number
name: string name: string
nameEn?: string
nameId?: string
locales?: Record<string, string>
sort?: number sort?: number
status?: number status?: number
/** 该类型下的短剧数量,管理列表才有 */ /** 该类型下的短剧数量,管理列表才有 */
@@ -18,11 +23,17 @@ export interface DramaItem {
title: string title: string
subtitle?: string subtitle?: string
categoryId?: number categoryId?: number
categoryIds?: number[]
categoryNames?: string[]
category?: string category?: string
genre?: string genre?: string
description?: string description?: string
cover?: string cover?: string
/** 横版封面,用于继续观看横幅等横向展示位 */
horizontalCover?: string
language?: string language?: string
dramaType?: 'ANIME' | 'LIVE'
dramaTypes?: Array<'ANIME' | 'LIVE'>
episodes?: number episodes?: number
freeEpisodes?: number freeEpisodes?: number
unlockCoin?: number unlockCoin?: number
@@ -31,6 +42,9 @@ export interface DramaItem {
status: string status: string
statusText: string statusText: string
views?: number views?: number
favoriteCount?: number
likeCount?: number
commentCount?: number
sort?: number sort?: number
publishAt?: string publishAt?: string
createdAt?: string createdAt?: string
@@ -41,17 +55,26 @@ export interface DramaSaveData {
title: string title: string
subtitle?: string subtitle?: string
categoryId?: number categoryId?: number
categoryIds?: number[]
genre?: string genre?: string
description?: string description?: string
cover?: string cover?: string
/** 横版封面,用于继续观看横幅等横向展示位 */
horizontalCover?: string
language?: string language?: string
dramaType?: 'ANIME' | 'LIVE'
dramaTypes?: Array<'ANIME' | 'LIVE'>
freeEpisodes?: number freeEpisodes?: number
unlockCoin?: number unlockCoin?: number
favoriteCount?: number
likeCount?: number
heat?: number
isHot?: boolean isHot?: boolean
sort?: number sort?: number
} }
export type DramaReviewAction = 'submit' | 'approve' | 'reject' | 'publish' | 'offline' export type DramaReviewAction =
'submit' | 'approve' | 'reject' | 'publish' | 'offline' | 'approvePublish'
export interface EpisodeItem { export interface EpisodeItem {
id: number id: number
@@ -67,6 +90,10 @@ export interface EpisodeItem {
status: string status: string
statusText: string statusText: string
views?: number views?: number
likeCount?: number
commentCount?: number
favoriteCount?: number
heat?: number
sort?: number sort?: number
} }
@@ -79,6 +106,7 @@ export interface EpisodeSaveData {
videoUrl?: string videoUrl?: string
isFree?: boolean isFree?: boolean
unlockCoin?: number unlockCoin?: number
likeCount?: number
status?: string status?: string
sort?: number sort?: number
} }
@@ -202,6 +230,115 @@ export interface MediaAsset {
createdAt?: string createdAt?: string
} }
/** 单档清晰度配置,字段与 FruitServer MediaTranscodeProfile.Rung 一致。 */
export interface MediaTranscodeLadderRung {
quality: string
width: number
height: number
videoBitrateKbps: number
audioBitrateKbps: number
enabled: boolean
}
/** 雪碧图采样配置,字段与 FruitServer MediaTranscodeProfile.Sprite 一致。 */
export interface MediaTranscodeSprite {
enabled: boolean
intervalSeconds: number
columns: number
thumbWidth: number
}
/** 转码档位配置,字段与 FruitServer MediaTranscodeProfile 一致。 */
export interface MediaTranscodeProfile {
enabled: boolean
segmentSeconds: number
ladder: MediaTranscodeLadderRung[]
sprite: MediaTranscodeSprite
}
export interface MediaRenditionItem {
id: string
quality: string
qualityName: string
width: number
height: number
resolution: string
videoBitrateKbps: number
audioBitrateKbps: number
videoCodec: string
audioCodec?: string
container: string
hlsUrl: string
sizeBytes: number
durationSec: number
segmentCount: number
status: string
}
export interface MediaThumbnailFrameItem {
frameIndex: number
timeMs: number
endTimeMs?: number
imageUrl: string
spriteUrl: string
x: number
y: number
width: number
height: number
}
export interface MediaThumbnailSpriteInfo {
spriteUrl?: string
spriteUrls: string[]
vttUrl?: string
columns: number
rows: number
totalFrames: number
thumbWidth: number
thumbHeight: number
intervalSeconds: number
}
export interface MediaThumbnailResult {
mediaAssetId?: string | null
episodeId?: string | null
/** READY / FAILED / EMPTY / INVALID / NO_EPISODE */
status: string
errorMsg?: string
pageIndex: number
pageSize: number
frames: PageResult<MediaThumbnailFrameItem>
sprite?: MediaThumbnailSpriteInfo | null
}
export interface EpisodeTranscodeItem {
episodeId: string
mediaAssetId?: string | null
taskId?: string | null
status: string
progress?: number
retryCount?: number
errorMsg?: string
updatedAt?: string
renditions: MediaRenditionItem[]
thumbnails?: MediaThumbnailResult | null
}
export interface TranscodeTriggerResult {
taskId: string
status: string
}
export interface TranscodeBackfillItem {
episodeId: string
taskId: string
}
export interface TranscodeBackfillResult {
triggered: number
items: TranscodeBackfillItem[]
}
export interface BannerItem { export interface BannerItem {
id: number id: number
imageUrl: string imageUrl: string
@@ -267,6 +404,10 @@ export interface AppUserItem {
maskedTarget: string maskedTarget: string
createdAt?: string createdAt?: string
lastLoginAt?: string lastLoginAt?: string
/** ANDROID / IOS / WEB */
platform?: string
/** 系统版本,例如 Android 16 */
osVersion?: string
/** 预留:会员等级与到期(第二阶段) */ /** 预留:会员等级与到期(第二阶段) */
vipLevel?: number vipLevel?: number
vipExpireAt?: string vipExpireAt?: string
@@ -292,16 +433,22 @@ export interface AppUserBanRecord {
export interface WatchHistoryItem { export interface WatchHistoryItem {
id: number id: number
userId?: number
uid?: string
nickname?: string
dramaId: number dramaId: number
dramaTitle?: string dramaTitle?: string
dramaPublished?: boolean dramaPublished?: boolean
episodeId: number episodeId: number
episodeNo?: number episodeNo?: number
episodeTitle?: string
positionMs: number positionMs: number
durationMs: number durationMs: number
finished?: boolean finished?: boolean
source: string source: string
deviceId: string deviceId: string
platform?: string
osVersion?: string
watchedAt?: string watchedAt?: string
} }
@@ -309,7 +456,10 @@ export interface WatchHistoryItem {
export interface CommentItem { export interface CommentItem {
id: number id: number
dramaId: number dramaId: number
dramaTitle?: string
episodeId: number episodeId: number
episodeNo?: number
episodeTitle?: string
parentId: number parentId: number
rootId: number rootId: number
userId: number userId: number
@@ -328,9 +478,20 @@ export interface CommentItem {
export interface ReportItem { export interface ReportItem {
id: number id: number
reporterId: number reporterId: number
reporterNickname?: string
reporterUid?: string
/** COMMENT / USER / DRAMA */ /** COMMENT / USER / DRAMA */
targetType: string targetType: string
targetId: number targetId: number
targetSummary?: string
dramaId?: number
dramaTitle?: string
episodeId?: number
episodeNo?: number
episodeTitle?: string
commentContent?: string
targetNickname?: string
targetUid?: string
reasonCode: string reasonCode: string
description: string description: string
/** pending / handled / rejected */ /** pending / handled / rejected */
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import request from '@/axios' import request from '@/axios'
import type { AdminUserItem, AdminUserSaveData, PageResult } from './types' import type { AdminUserItem, AdminUserSaveData, PageResult } from './types'
+2 -4
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import request from '@/axios' import request from '@/axios'
import type { UserType } from './types' import type { UserType } from './types'
@@ -24,10 +26,6 @@ export const getAdminRoleApi = (
return request.get({ url: '/admin/auth/menus', params }) return request.get({ url: '/admin/auth/menus', params })
} }
export const getTestRoleApi = (params: RoleParams): Promise<IResponse<string[]>> => {
return request.get({ url: '/mock/role/list2', params })
}
/** 自助改密(首登强制改密也走这个接口) */ /** 自助改密(首登强制改密也走这个接口) */
export const changePasswordApi = (data: { oldPassword: string; newPassword: string }) => { export const changePasswordApi = (data: { oldPassword: string; newPassword: string }) => {
return request.post<void>({ url: '/admin/auth/password', data }) return request.post<void>({ url: '/admin/auth/password', data })
+3 -1
View File
@@ -1,6 +1,8 @@
// Author: fruit.com.cn
export type UserLoginType = { export type UserLoginType = {
username: string username: string
password: string password?: string
} }
export type UserType = { export type UserType = {
@@ -0,0 +1 @@
{"prefix":"material-symbols","info":{"name":"Material Symbols","total":15728,"author":{"name":"Google","url":"https://github.com/google/material-design-icons"},"license":{"title":"Apache 2.0","spdx":"Apache-2.0","url":"https://github.com/google/material-design-icons/blob/master/LICENSE"},"samples":["downloading","privacy-tip","filter-drama-outline","assignment-ind","monitoring","desktop-access-disabled-outline-sharp"],"height":24,"category":"Material","tags":["Has Padding"],"palette":false},"lastModified":1791524209,"width":24,"height":24,"icons":{"menu-rounded":{"body":"<path fill=\"currentColor\" d=\"M4 18q-.425 0-.712-.288T3 17t.288-.712T4 16h16q.425 0 .713.288T21 17t-.288.713T20 18zm0-5q-.425 0-.712-.288T3 12t.288-.712T4 11h16q.425 0 .713.288T21 12t-.288.713T20 13zm0-5q-.425 0-.712-.288T3 7t.288-.712T4 6h16q.425 0 .713.288T21 7t-.288.713T20 8z\"/>"},"menu-open-rounded":{"body":"<path fill=\"currentColor\" d=\"M4 18q-.425 0-.712-.288T3 17t.288-.712T4 16h11q.425 0 .713.288T16 17t-.288.713T15 18zm14.9-1.7l-3.6-3.6q-.3-.3-.3-.7t.3-.7l3.6-3.6q.275-.275.7-.275t.7.275t.275.7t-.275.7L17.4 12l2.9 2.9q.275.275.275.7t-.275.7t-.7.275t-.7-.275M4 13q-.425 0-.712-.288T3 12t.288-.712T4 11h8q.425 0 .713.288T13 12t-.288.713T12 13zm0-5q-.425 0-.712-.288T3 7t.288-.712T4 6h11q.425 0 .713.288T16 7t-.288.713T15 8z\"/>"},"dashboard-rounded":{"body":"<path fill=\"currentColor\" d=\"M14 9q-.425 0-.712-.288T13 8V4q0-.425.288-.712T14 3h6q.425 0 .713.288T21 4v4q0 .425-.288.713T20 9zM4 13q-.425 0-.712-.288T3 12V4q0-.425.288-.712T4 3h6q.425 0 .713.288T11 4v8q0 .425-.288.713T10 13zm10 8q-.425 0-.712-.288T13 20v-8q0-.425.288-.712T14 11h6q.425 0 .713.288T21 12v8q0 .425-.288.713T20 21zM4 21q-.425 0-.712-.288T3 20v-4q0-.425.288-.712T4 15h6q.425 0 .713.288T11 16v4q0 .425-.288.713T10 21z\"/>"},"video-library-rounded":{"body":"<path fill=\"currentColor\" d=\"M17.525 10.625q.35-.225.35-.625t-.35-.625L12.65 6.25q-.375-.25-.763-.038t-.387.663v6.25q0 .45.388.663t.762-.038zM8 18q-.825 0-1.412-.587T6 16V4q0-.825.588-1.412T8 2h12q.825 0 1.413.588T22 4v12q0 .825-.587 1.413T20 18zm-4 4q-.825 0-1.412-.587T2 20V7q0-.425.288-.712T3 6t.713.288T4 7v13h13q.425 0 .713.288T18 21t-.288.713T17 22z\"/>"},"settings-rounded":{"body":"<path fill=\"currentColor\" d=\"M10.825 22q-.675 0-1.162-.45t-.588-1.1L8.85 18.8q-.325-.125-.612-.3t-.563-.375l-1.55.65q-.625.275-1.25.05t-.975-.8l-1.175-2.05q-.35-.575-.2-1.225t.675-1.075l1.325-1Q4.5 12.5 4.5 12.337v-.675q0-.162.025-.337l-1.325-1Q2.675 9.9 2.525 9.25t.2-1.225L3.9 5.975q.35-.575.975-.8t1.25.05l1.55.65q.275-.2.575-.375t.6-.3l.225-1.65q.1-.65.588-1.1T10.825 2h2.35q.675 0 1.163.45t.587 1.1l.225 1.65q.325.125.613.3t.562.375l1.55-.65q.625-.275 1.25-.05t.975.8l1.175 2.05q.35.575.2 1.225t-.675 1.075l-1.325 1q.025.175.025.338v.674q0 .163-.05.338l1.325 1q.525.425.675 1.075t-.2 1.225l-1.2 2.05q-.35.575-.975.8t-1.25-.05l-1.5-.65q-.275.2-.575.375t-.6.3l-.225 1.65q-.1.65-.587 1.1t-1.163.45zm1.225-6.5q1.45 0 2.475-1.025T15.55 12t-1.025-2.475T12.05 8.5q-1.475 0-2.488 1.025T8.55 12t1.013 2.475T12.05 15.5\"/>"},"group-rounded":{"body":"<path fill=\"currentColor\" d=\"M1 17.2q0-.85.438-1.562T2.6 14.55q1.55-.775 3.15-1.162T9 13t3.25.388t3.15 1.162q.725.375 1.163 1.088T17 17.2v.8q0 .825-.587 1.413T15 20H3q-.825 0-1.412-.587T1 18zM18.45 20q.275-.45.413-.962T19 18v-1q0-1.1-.612-2.113T16.65 13.15q1.275.15 2.4.513t2.1.887q.9.5 1.375 1.112T23 17v1q0 .825-.587 1.413T21 20zM6.175 10.825Q5 9.65 5 8t1.175-2.825T9 4t2.825 1.175T13 8t-1.175 2.825T9 12t-2.825-1.175m11.65 0Q16.65 12 15 12q-.275 0-.7-.062t-.7-.138q.675-.8 1.038-1.775T15 8t-.362-2.025T13.6 4.2q.35-.125.7-.163T15 4q1.65 0 2.825 1.175T19 8t-1.175 2.825\"/>"},"movie-edit-rounded":{"body":"<path fill=\"currentColor\" d=\"M4 20q-.825 0-1.412-.587T2 18V6q0-.825.588-1.412T4 4l1.625 3.25q.175.35.5.55t.7.2q.75 0 1.15-.638t.05-1.312L7 4h2l1.625 3.25q.175.35.5.55t.7.2q.75 0 1.15-.638t.05-1.312L12 4h2l1.625 3.25q.175.35.5.55t.7.2q.75 0 1.15-.638t.05-1.312L17 4h3q.825 0 1.413.587T22 6v3.45q0 .3-.25.475t-.55.1q-.825-.15-1.638.087T18.1 11l-5.525 5.525q-.275.275-.425.638t-.15.762V19q0 .425-.288.713T11 20zm10 0v-1.65q0-.2.075-.387t.225-.338l5.225-5.2q.225-.225.5-.325t.55-.1q.3 0 .575.113t.5.337l.925.925q.2.225.313.5t.112.55t-.1.563t-.325.512l-5.2 5.2q-.15.15-.337.225T16.65 21H15q-.425 0-.712-.287T14 20m6.575-4.6l.925-.975l-.925-.925l-.95.95z\"/>"},"policy-rounded":{"body":"<path fill=\"currentColor\" d=\"M11.675 21.875q-.15-.025-.3-.075Q8 20.675 6 17.637T4 11.1V6.375q0-.625.363-1.125t.937-.725l6-2.25q.35-.125.7-.125t.7.125l6 2.25q.575.225.938.725T20 6.375V11.1q0 1.575-.413 3.063T18.4 17l-2.95-2.95q.275-.475.413-.987T16 12q0-1.65-1.175-2.825T12 8T9.175 9.175T8 12t1.175 2.825T12 16q.525 0 1.038-.137T14 15.45l3.225 3.2q-.95 1.125-2.087 1.913T12.625 21.8q-.15.05-.3.075T12 21.9t-.325-.025m-1.088-8.462Q10 12.825 10 12t.588-1.412T12 10t1.413.588T14 12t-.587 1.413T12 14t-1.412-.587\"/>"},"payments-rounded":{"body":"<path fill=\"currentColor\" d=\"M3 20q-.825 0-1.412-.587T1 18V8q0-.425.288-.712T2 7t.713.288T3Line truncated
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import { AxiosResponse, InternalAxiosRequestConfig } from './types' import { AxiosResponse, InternalAxiosRequestConfig } from './types'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import qs from 'qs' import qs from 'qs'
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import service from './service' import service from './service'
import { CONTENT_TYPE } from '@/constants' import { CONTENT_TYPE } from '@/constants'
import { useUserStoreWithOut } from '@/store/modules/user' import { useUserStoreWithOut } from '@/store/modules/user'
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import axios, { AxiosError } from 'axios' import axios, { AxiosError } from 'axios'
import { defaultRequestInterceptors, defaultResponseInterceptors } from './config' import { defaultRequestInterceptors, defaultResponseInterceptors } from './config'
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import type { import type {
InternalAxiosRequestConfig, InternalAxiosRequestConfig,
AxiosResponse, AxiosResponse,
+5
View File
@@ -0,0 +1,5 @@
// Author: fruit.com.cn
import AppIcon from './src/AppIcon.vue'
export { AppIcon }
+40
View File
@@ -0,0 +1,40 @@
<!-- Author: fruit.com.cn -->
<script setup lang="ts">
import { computed } from 'vue'
import { Icon } from '@/components/Icon'
import { normalizeAppIcon } from '@/constants/icons'
import { propTypes } from '@/utils/propTypes'
/**
* 后台统一图标入口:默认使用 Material Symbols Rounded。
*/
const props = defineProps({
icon: propTypes.string,
color: propTypes.string,
size: propTypes.number.def(18),
hoverColor: propTypes.string
})
const resolvedIcon = computed(() => normalizeAppIcon(props.icon))
</script>
<template>
<Icon
:icon="resolvedIcon"
:color="color"
:size="size"
:hover-color="hoverColor"
class="app-icon"
/>
</template>
<style scoped>
.app-icon {
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
vertical-align: middle;
}
</style>
+5
View File
@@ -0,0 +1,5 @@
// Author: fruit.com.cn
import AppImage from './src/AppImage.vue'
export { AppImage }
+94
View File
@@ -0,0 +1,94 @@
<!-- Author: fruit.com.cn -->
<script setup lang="ts">
import { computed } from 'vue'
import type { PropType } from 'vue'
import { ElImage } from 'element-plus'
import { AppIcon } from '@/components/AppIcon'
import { AppIcons } from '@/constants/icons'
defineOptions({ inheritAttrs: false })
const props = defineProps({
src: {
type: String,
required: true
},
previewList: {
type: Array as () => string[],
default: () => []
},
initialIndex: {
type: Number,
default: 0
},
fit: {
type: String as PropType<'fill' | 'contain' | 'cover' | 'none' | 'scale-down'>,
default: 'cover'
},
class: {
type: [String, Array, Object],
default: ''
},
style: {
type: [String, Array, Object],
default: undefined
},
alt: {
type: String,
default: ''
}
})
const resolvedPreviewList = computed(() => {
const images = (props.previewList || []).filter(Boolean)
return images.length ? images : [props.src]
})
const resolvedInitialIndex = computed(() =>
Math.max(0, Math.min(props.initialIndex, resolvedPreviewList.value.length - 1))
)
</script>
<template>
<ElImage
v-bind="$attrs"
:src="src"
:alt="alt"
:fit="fit"
:preview-src-list="resolvedPreviewList"
:initial-index="resolvedInitialIndex"
:preview-teleported="true"
:hide-on-click-modal="true"
:class="['app-image', props.class]"
:style="style"
>
<template #error>
<div class="app-image-error">
<AppIcon :icon="AppIcons.error" :size="20" />
</div>
</template>
</ElImage>
</template>
<style scoped>
.app-image {
cursor: zoom-in;
}
.app-image :deep(.el-image__inner) {
display: block;
width: 100%;
height: 100%;
}
.app-image-error {
display: flex;
width: 100%;
height: 100%;
color: var(--el-text-color-placeholder);
background: var(--el-fill-color-lighter);
align-items: center;
justify-content: center;
}
</style>
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import Backtop from './src/Backtop.vue' import Backtop from './src/Backtop.vue'
export { Backtop } export { Backtop }
+2
View File
@@ -1,3 +1,5 @@
<!-- Author: fruit.com.cn -->
<script setup lang="ts"> <script setup lang="ts">
import { ElBacktop } from 'element-plus' import { ElBacktop } from 'element-plus'
import { useDesign } from '@/hooks/web/useDesign' import { useDesign } from '@/hooks/web/useDesign'
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import Breadcrumb from './src/Breadcrumb.vue' import Breadcrumb from './src/Breadcrumb.vue'
export { Breadcrumb } export { Breadcrumb }
+6 -4
View File
@@ -1,3 +1,5 @@
<!-- Author: fruit.com.cn -->
<script lang="tsx"> <script lang="tsx">
import { ElBreadcrumb, ElBreadcrumbItem } from 'element-plus' import { ElBreadcrumb, ElBreadcrumbItem } from 'element-plus'
import { ref, watch, computed, unref, defineComponent, TransitionGroup } from 'vue' import { ref, watch, computed, unref, defineComponent, TransitionGroup } from 'vue'
@@ -7,7 +9,7 @@ import { filterBreadcrumb } from './helper'
import { filter, treeToList } from '@/utils/tree' import { filter, treeToList } from '@/utils/tree'
import type { RouteLocationNormalizedLoaded, RouteMeta } from 'vue-router' import type { RouteLocationNormalizedLoaded, RouteMeta } from 'vue-router'
import { useI18n } from '@/hooks/web/useI18n' import { useI18n } from '@/hooks/web/useI18n'
import { Icon } from '@/components/Icon' import { AppIcon } from '@/components/AppIcon'
import { useAppStore } from '@/store/modules/app' import { useAppStore } from '@/store/modules/app'
import { useDesign } from '@/hooks/web/useDesign' import { useDesign } from '@/hooks/web/useDesign'
@@ -52,7 +54,7 @@ export default defineComponent({
<ElBreadcrumbItem to={{ path: disabled ? '' : v.path }} key={v.name}> <ElBreadcrumbItem to={{ path: disabled ? '' : v.path }} key={v.name}>
{meta?.icon && breadcrumbIcon.value ? ( {meta?.icon && breadcrumbIcon.value ? (
<> <>
<Icon icon={meta.icon} class="mr-[5px]"></Icon> {t(v?.meta?.title)} <AppIcon icon={meta.icon} class="mr-[5px]"></AppIcon> {t(v?.meta?.title)}
</> </>
) : ( ) : (
t(v?.meta?.title) t(v?.meta?.title)
@@ -115,10 +117,10 @@ export default defineComponent({
:deep(&__item):last-child { :deep(&__item):last-child {
.@{prefix-cls}__inner { .@{prefix-cls}__inner {
color: var(--el-text-color-placeholder); color: var(--el-text-color-regular);
&:hover { &:hover {
color: var(--el-text-color-placeholder); color: var(--el-color-primary);
} }
} }
} }
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import { pathResolve } from '@/utils/routerHelper' import { pathResolve } from '@/utils/routerHelper'
import type { RouteMeta } from 'vue-router' import type { RouteMeta } from 'vue-router'
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import BaseButton from './src/Button.vue' import BaseButton from './src/Button.vue'
export { BaseButton } export { BaseButton }
+2
View File
@@ -1,3 +1,5 @@
<!-- Author: fruit.com.cn -->
<script setup lang="ts"> <script setup lang="ts">
import { useDesign } from '@/hooks/web/useDesign' import { useDesign } from '@/hooks/web/useDesign'
import { ElButton, ComponentSize, ButtonType } from 'element-plus' import { ElButton, ComponentSize, ButtonType } from 'element-plus'
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import Collapse from './src/Collapse.vue' import Collapse from './src/Collapse.vue'
export { Collapse } export { Collapse }
+5 -2
View File
@@ -1,8 +1,11 @@
<!-- Author: fruit.com.cn -->
<script setup lang="ts"> <script setup lang="ts">
import { computed, unref } from 'vue' import { computed, unref } from 'vue'
import { useAppStore } from '@/store/modules/app' import { useAppStore } from '@/store/modules/app'
import { propTypes } from '@/utils/propTypes' import { propTypes } from '@/utils/propTypes'
import { useDesign } from '@/hooks/web/useDesign' import { useDesign } from '@/hooks/web/useDesign'
import { AppIcons } from '@/constants/icons'
const { getPrefixCls } = useDesign() const { getPrefixCls } = useDesign()
@@ -24,9 +27,9 @@ const toggleCollapse = () => {
<template> <template>
<div :class="prefixCls" @click="toggleCollapse"> <div :class="prefixCls" @click="toggleCollapse">
<Icon <AppIcon
:size="18" :size="18"
:icon="collapse ? 'ant-design:menu-unfold-outlined' : 'ant-design:menu-fold-outlined'" :icon="collapse ? AppIcons.menuOpen : AppIcons.menu"
:color="color" :color="color"
class="cursor-pointer" class="cursor-pointer"
/> />
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import ConfigGlobal from './src/ConfigGlobal.vue' import ConfigGlobal from './src/ConfigGlobal.vue'
export type { ConfigGlobalTypes } from './src/types' export type { ConfigGlobalTypes } from './src/types'
@@ -1,3 +1,5 @@
<!-- Author: fruit.com.cn -->
<script setup lang="ts"> <script setup lang="ts">
import { provide, computed, watch, onMounted } from 'vue' import { provide, computed, watch, onMounted } from 'vue'
import { propTypes } from '@/utils/propTypes' import { propTypes } from '@/utils/propTypes'
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import { ComponentSize } from 'element-plus' import { ComponentSize } from 'element-plus'
export interface ConfigGlobalTypes { export interface ConfigGlobalTypes {
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import ContentDetailWrap from './src/ContentDetailWrap.vue' import ContentDetailWrap from './src/ContentDetailWrap.vue'
export { ContentDetailWrap } export { ContentDetailWrap }
@@ -1,3 +1,5 @@
<!-- Author: fruit.com.cn -->
<script setup lang="ts"> <script setup lang="ts">
import { ElCard } from 'element-plus' import { ElCard } from 'element-plus'
import { propTypes } from '@/utils/propTypes' import { propTypes } from '@/utils/propTypes'
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import ContentWrap from './src/ContentWrap.vue' import ContentWrap from './src/ContentWrap.vue'
export { ContentWrap } export { ContentWrap }
@@ -1,7 +1,10 @@
<!-- Author: fruit.com.cn -->
<script setup lang="ts"> <script setup lang="ts">
import { ElCard, ElTooltip } from 'element-plus' import { ElCard, ElTooltip } from 'element-plus'
import { propTypes } from '@/utils/propTypes' import { propTypes } from '@/utils/propTypes'
import { useDesign } from '@/hooks/web/useDesign' import { useDesign } from '@/hooks/web/useDesign'
import { AppIcons } from '@/constants/icons'
const { getPrefixCls } = useDesign() const { getPrefixCls } = useDesign()
@@ -22,7 +25,7 @@ defineProps({
<template #content> <template #content>
<div class="max-w-200px">{{ message }}</div> <div class="max-w-200px">{{ message }}</div>
</template> </template>
<Icon class="ml-5px" icon="bi:question-circle-fill" :size="14" /> <AppIcon class="ml-5px" :icon="AppIcons.help" :size="14" />
</ElTooltip> </ElTooltip>
</div> </div>
</template> </template>
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import ContextMenu from './src/ContextMenu.vue' import ContextMenu from './src/ContextMenu.vue'
import { ElDropdown } from 'element-plus' import { ElDropdown } from 'element-plus'
import type { RouteLocationNormalizedLoaded } from 'vue-router' import type { RouteLocationNormalizedLoaded } from 'vue-router'
@@ -1,3 +1,5 @@
<!-- Author: fruit.com.cn -->
<script setup lang="ts"> <script setup lang="ts">
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus' import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
import { PropType, ref } from 'vue' import { PropType, ref } from 'vue'
@@ -64,7 +66,7 @@ defineExpose({
:disabled="item.disabled" :disabled="item.disabled"
:command="item" :command="item"
> >
<Icon :icon="item.icon" /> {{ t(item.label) }} <AppIcon :icon="item.icon" /> {{ t(item.label) }}
</ElDropdownItem> </ElDropdownItem>
</ElDropdownMenu> </ElDropdownMenu>
</template> </template>
Loaded 100 of 338 files, more files were not shown because too many files have changed in this diff. Show more