Compare commits

...

170 Commits

Author SHA1 Message Date
小肥羊 338a27ead7 1 2026-04-30 11:26:36 +08:00
小肥羊 60e5900541 新增 视频切片查询 返回vod视频id 2026-04-27 10:25:33 +08:00
小肥羊 65134e9c9e 注释 无效的判断 2026-04-23 18:22:53 +08:00
小肥羊 e3c97ef0b2 优化 直接使用 现有媒体资源url 如果没有则报错 2026-04-22 16:38:14 +08:00
小肥羊 ada94793cb 优化 直接使用 现有媒体资源url 而不是从阿里云获取 2026-04-20 18:09:18 +08:00
小肥羊 f4c90e8606 新增 阿里云vod配置 2026-04-20 17:46:08 +08:00
小肥羊 999cef2073 完善 接口修复 未记录 阶段类型字段的情况
优化 查询切片接口
2026-04-20 17:13:51 +08:00
小肥羊 04881ef3b8 更新 401鉴权失败情况 2026-04-20 14:30:59 +08:00
小肥羊 36f719370b 优化 查询切片的接口返回参数 2026-04-17 18:15:50 +08:00
小肥羊 63041cd019 修复 作业识别模块识别成功后没有写入成功 2026-04-17 14:59:10 +08:00
小肥羊 de3bd90822 新增 全人物流程GradeYear 2026-04-17 09:50:10 +08:00
小肥羊 00e0664940 优化 88分分析视频结果即合格
修复 部分模型失效导致的死循环
修复 课程未能匹配知识点的极端情况重试
2026-04-14 17:57:22 +08:00
小肥羊 270395e939 修复 更改模型的引用 2026-04-13 16:43:54 +08:00
小肥羊 b161d83aac 输出错误模型 2026-04-10 18:38:20 +08:00
小肥羊 8abe276c9d 优化 提示词 加上分析的改进意见 2026-04-10 18:19:19 +08:00
小肥羊 3e2627c55e 修复 部分日志写入错误工作流问题 2026-04-10 15:22:22 +08:00
小肥羊 ed8387dea3 更新 新版模型 2026-04-10 15:07:13 +08:00
小肥羊 ea949f0825 修复 新的工作流未能获取到导致NodePackage接口失效问题 2026-03-26 10:55:37 +08:00
小肥羊 a2b3c53898 修复 PPT清洗工作流的异常退出 2026-03-25 16:17:05 +08:00
小肥羊 3df0615dc4 更新 替换掉阿里云过时的配置
优化 过时的RedisJoinQueue流程迁移到WorkFlow
2026-03-11 11:18:04 +08:00
小肥羊 7dd5054060 新增 接入Baset的deepSeek
修改 oss配置
修复 错误的文件名
2026-03-09 16:17:41 +08:00
小肥羊 3b431b978a 修复 Redis 将 KEYS 命令替换为更安全的 SCAN 命令 2026-03-06 18:17:25 +08:00
小肥羊 7188f8ab71 接入 bset的Deepseek 2026-03-06 18:00:22 +08:00
小肥羊 a8ac40d6fb 优化 拆分任务与工作流的执行进度
新增 PPT清洗流程
2026-03-05 18:15:18 +08:00
小肥羊 a2200c0296 优化 独立工作流为单独配置 2026-02-28 14:15:05 +08:00
小肥羊 43599fea1d 新增 设备id字段 2026-02-26 17:41:20 +08:00
小肥羊 407e93c208 新增 强制清理文件缓存目录办法 2026-02-26 15:01:42 +08:00
小肥羊 4361e7fa0f 新增 强制清理文件缓存目录办法 2026-02-26 15:01:12 +08:00
小肥羊 d49550807b 修复 生产者端的 请求日志无法读取request问题 2026-02-24 12:14:53 +08:00
小肥羊 bbad3da13a 修复 生产者端的 请求日志无法读取request问题 2026-02-24 12:14:44 +08:00
小肥羊 2acc0d4239 优化 AI模型使用的方法 2026-02-12 17:28:39 +08:00
小肥羊 0de07e733a 新增 本地运行流程 2026-02-12 15:53:14 +08:00
小肥羊 abca6480a1 删除 过时的打包配置AI模型 2026-02-12 11:38:59 +08:00
小肥羊 18e60d4a9b 修复 API日志未能读取 httpRequest的内容问题 2026-02-12 10:57:05 +08:00
小肥羊 7025590d3b 修改 正式环境redis使用的分库 2026-02-12 10:31:37 +08:00
小肥羊 9d7edad80a 修复 API服务的部分流程问题
新增 API服务的日志请求写入
2026-02-12 10:30:34 +08:00
小肥羊 dac6eee091 修复 部分单例应用写入数据库时未能COPY NEW的问题导致的写入失败 2026-02-09 17:13:50 +08:00
小肥羊 53951aa870 修复 过时的AI请求temperature 字段 2026-02-06 17:15:29 +08:00
小肥羊 d948f854fb 调试 FunASR的STT,修复流程上的bug 2026-01-30 17:23:40 +08:00
小肥羊 eed63794b8 修复 Ai字幕展示的方式
新增 页面持久化
2026-01-21 15:19:54 +08:00
小肥羊 43a59dd593 优化 字幕清洗方案 2026-01-21 13:08:08 +08:00
小肥羊 f8a758ff40 优化 字幕清洗方案 2026-01-21 11:33:38 +08:00
小肥羊 aecfa4ac0d 完善 0122AI视频分析工作流的调试 2026-01-21 10:30:38 +08:00
小肥羊 a2d14487cb 优化 调试工作流1的AI分析流程(知识点权重,来源,AI复查复检...) 2026-01-19 18:17:58 +08:00
小肥羊 b4b02cfbd3 新增 评鉴结果优化功能 2026-01-15 18:14:15 +08:00
小肥羊 1adeba007c 新增 语音转录的测试 2026-01-15 10:26:58 +08:00
小肥羊 a8ec291497 修复 ioc注入异常 2026-01-13 18:16:30 +08:00
小肥羊 cf44785ae9 优化 补全部分注入的注释 2026-01-13 17:49:37 +08:00
小肥羊 d52504a3a0 新增 封装vad,接入 FunASRNano 2026-01-13 17:42:07 +08:00
小肥羊 de1cdcf32c 新增 视频所属云校id 2026-01-13 15:11:50 +08:00
小肥羊 ba00c75a36 新增 视频批量处理 2026-01-07 17:27:58 +08:00
小肥羊 1f5ecaa604 优化 AI分析流程的注释 2026-01-06 14:08:28 +08:00
小肥羊 89432c681f 优化 字符串提取JSON函数
优化 AI分析流程
2026-01-06 10:02:10 +08:00
小肥羊 ce5f549407 优化 字幕优化流程 2025-12-23 18:15:35 +08:00
小肥羊 fddcdbc1d9 优化 AI分析流程文件命名
新增 gemini3的AI分析
2025-12-23 16:14:07 +08:00
小肥羊 deb7f80bce 调整 部分文件代码命名 2025-12-19 17:41:24 +08:00
小肥羊 929d6f8a98 新增 使用独立的gpt聚合平台token 2025-11-20 11:23:59 +08:00
小肥羊 363bf5325e 修复 请求AI超时后不记录日志问题 2025-11-11 18:22:08 +08:00
小肥羊 3f113a9bc5 修复 预览任务时候的复习课试题展示 2025-11-11 14:28:50 +08:00
小肥羊 a6c19d5d97 降低 视频关键帧阈值 2025-11-11 11:36:00 +08:00
小肥羊 844e122d2b 添加 日志展示模块 2025-11-10 15:56:05 +08:00
小肥羊 42becf154a 优化 AI分析超时情况 2025-11-07 15:39:54 +08:00
小肥羊 94aa7572bf 修复 优化后导致的CHATGPT分析异常 2025-11-07 11:21:37 +08:00
小肥羊 836ba81a73 优化 gpt的注入方式 2025-11-07 11:02:37 +08:00
小肥羊 d437ff0a46 新增 异常任务界面接口 2025-11-07 09:39:27 +08:00
小肥羊 037ec9dc3c 新增 异常任务界面 2025-11-06 18:14:30 +08:00
小肥羊 a31352b8f5 修改 ui打包配置 2025-11-06 16:24:31 +08:00
小肥羊 54ba342153 完善 web 执行中任务页面 2025-11-06 15:11:11 +08:00
小肥羊 bcd6f63bb3 完善 预览功能 2025-11-05 11:39:58 +08:00
小肥羊 24502a526d 完善 任务列表页面 2025-11-04 18:45:19 +08:00
小肥羊 963448382d 完善 新版UI 2025-11-04 16:08:36 +08:00
小肥羊 16d058d2b9 新增 忽略项 2025-11-03 16:46:08 +08:00
小肥羊 99f7601184 修复 分析知识点bug 2025-11-03 16:29:53 +08:00
小肥羊 964f97c115 优化 任务日志打印功能 2025-10-29 18:11:21 +08:00
小肥羊 193289437b 新增 redis任务缓存过期时间 2025-10-28 17:15:13 +08:00
小肥羊 d5a1da1040 新增 批量插入队列接口 2025-10-27 10:23:47 +08:00
小肥羊 5a6dc49615 修复 执行部分任务时 没使用await等待导致的数据库连接异常 2025-10-27 10:01:11 +08:00
小肥羊 0a654c72ed 新增 任务完成后立马删除 视频文件 2025-10-23 17:07:48 +08:00
小肥羊 1151068185 修复 尝试解决多线程sql异常问题 2025-10-22 14:18:00 +08:00
小肥羊 1cb53fe405 优化 课程类型分析流程 2025-10-21 18:02:33 +08:00
小肥羊 2609f13ea5 优化 新版AI分析流程 2025-10-21 15:02:17 +08:00
小肥羊 383d0b4c32 优化 尝试优化分析字幕的AI提示词 2025-09-30 17:09:55 +08:00
小肥羊 0edf143f8a 优化 各个模块的注入方式
修复 初始化时 之前未执行完的任务 导致的死循环
2025-09-29 17:32:08 +08:00
小肥羊 94cde3af70 优化 当前消费中的队列缓存
修改 sql仓储层的注入
2025-09-26 15:11:39 +08:00
小肥羊 6015245d78 修复 视频知识点片段名称错误问题 2025-09-24 10:43:37 +08:00
小肥羊 a0e00c8b5d 修复 测试完善后的新流程 2025-09-14 15:17:06 +08:00
小肥羊 776c2b9b52 修复 重新打包dockerBase镜像 learnvideo-net8.0:v0.1 2025-09-14 10:06:00 +08:00
小肥羊 7f98327020 优化 任务失败也列入清理队列 2025-08-28 17:10:25 +08:00
小肥羊 ecca0751ef 屏蔽 无用日志 2025-08-28 15:21:29 +08:00
小肥羊 4f48553afd 取消 ai优化字幕的 死循环情况
新增 接收的任务学科类型校验
2025-08-28 10:29:57 +08:00
小肥羊 2ddbe98a1b 优化 蓝鲸字库数据库切换为rds 2025-08-17 19:04:26 +08:00
小肥羊 eca3e9278e 新增 课程id主动获取 2025-08-17 18:32:52 +08:00
小肥羊 d3764e1d33 修改 启用正式环境的任务清理 2025-08-12 10:37:42 +08:00
小肥羊 c88001c40c 修复 蓝鲸字库回调流程异常问题 2025-07-15 14:44:55 +08:00
小肥羊 13080a8f63 修复 InsertChannel字符串异常 2025-07-11 17:40:37 +08:00
小肥羊 2050d06ba9 优化 文件包订阅时候额外的异常处理 2025-07-11 17:33:14 +08:00
小肥羊 de72788ca8 修改 文件包订阅时没有老师授课视频的提示 2025-07-11 17:20:29 +08:00
小肥羊 f0b668ae4a 修改 优化字幕步骤的并发线程为20个 2025-07-11 11:33:31 +08:00
小肥羊 eab1d9e653 新增 swaggerui 开发环境条件限制 2025-07-08 10:11:04 +08:00
小肥羊 8c4ecac249 新增 swagger 登录 2025-07-04 17:48:49 +08:00
小肥羊 0bd93c14bb 优化 计算帧差异算法
修复 复习课试题匹配错误情况
2025-07-02 16:30:56 +08:00
小肥羊 3d112773c9 新增 AI分析视频课程类型流程 2025-06-30 18:01:49 +08:00
小肥羊 d36fb2fc9f 更新 蓝鲸字库接口 批量视频文件入参模型 2025-06-24 16:18:23 +08:00
小肥羊 d12839be20 修复 课程类型无效时导致的异常
新增 ToJson函数
2025-06-23 17:13:45 +08:00
小肥羊 73d4fbd8c1 优化 提取试题判断界限 2025-06-20 18:18:51 +08:00
小肥羊 4ab527c388 优化任务调度流程 2025-06-18 10:33:36 +08:00
小肥羊 19855abb6f 优化任务调度流程和异常捕获流程 2025-06-17 11:35:06 +08:00
小肥羊 f7c787cdf7 修改 数据结构后调整管理端UI
优化 任务接收辞池[进行中]
2025-06-13 18:23:30 +08:00
小肥羊 f943c4fec4 优化 视频试题AI分析试题识别精准度
修改 蓝鲸字库 请求流程
2025-06-12 16:12:01 +08:00
小肥羊 d433d84d46 优化 ai分析试题提示词
优化 视频预览ui
2025-06-05 18:19:26 +08:00
小肥羊 02518a1c4f 优化 复习课切题提示词 2025-06-04 17:44:33 +08:00
小肥羊 dd7e217bb2 优化 AI字幕分析流程[deepseek升级] 2025-05-30 16:51:11 +08:00
小肥羊 2bdd77380c 优化 复习课切题流程/预览UI 2025-05-29 17:42:09 +08:00
小肥羊 ad0f415ef6 接入 oss
调试 simpLetex API
2025-05-27 17:59:53 +08:00
小肥羊 2d1c3de69b 新增 视频任务[视频类型]字段 2025-05-26 17:59:32 +08:00
小肥羊 8fd3d4ef4b 优化 AI字幕优化提示词 2025-05-22 17:16:08 +08:00
小肥羊 22330627a8 修复 TaskKnowInfo 接口异常
新增 复习课流程[未完成]
2025-05-22 15:54:57 +08:00
小肥羊 0f0a24aeb3 新增 simpletex公式分析 API 2025-05-20 14:52:01 +08:00
小肥羊 7ca52f3c97 修复 ppt类型文件分析完成回调失败 2025-05-16 10:11:09 +08:00
小肥羊 8ed02b9a79 提升 FFMPEG片段帧精确度180p->540p 2025-05-14 14:36:49 +08:00
小肥羊 7da037806f 新增 AI优化字幕流程
优化 AI分析流程
2025-05-14 09:53:07 +08:00
小肥羊 61af4e9827 新增 AI优化字幕
优化 获取片段流程
2025-05-12 18:14:39 +08:00
小肥羊 7cbd4588f5 优化 ai分析流程 2025-05-09 18:14:33 +08:00
小肥羊 c616581918 新增 支持蓝鲸智库多系统接入 2025-05-06 09:30:01 +08:00
小肥羊 8523c8ee8e 修复 文件包api失效问题 2025-04-22 16:04:03 +08:00
小肥羊 25f9dc0ba7 修复 未使用的IBserGPT 2025-04-22 11:42:52 +08:00
小肥羊 9558aab6ce 修复 未使用的IBserGPT 2025-04-22 11:17:14 +08:00
小肥羊 d1bbce8baf 删除 过时的自动任务
优化 ppt流程
2025-04-22 10:41:42 +08:00
小肥羊 ac7ec56997 优化 刷新进度显示文本 2025-04-18 18:07:23 +08:00
小肥羊 56853c2243 优化 迁移控制器到Core层复用
新增 视频关键帧切片流程
2025-04-17 18:13:12 +08:00
小肥羊 7ef238e4e2 拆分项目 为 API+消费端
修改 配置 定时任务 dockerFIle
2025-04-08 18:16:32 +08:00
小肥羊 972fba9959 新增 系统配置项[处理任务 下载速度...] 2025-04-03 18:30:24 +08:00
小肥羊 6b9fc167ee 修改 文件包回调执行时间 2025-03-24 18:12:52 +08:00
小肥羊 655ee7ea5f 新增 NodeSubscriptionJob添加任务时去重 2025-03-21 17:11:39 +08:00
小肥羊 8847ae987b 新增 文件包未处理的视频
优化 下载视频时基于任务类型实时获取资源地址
2025-03-21 16:49:39 +08:00
小肥羊 374c582cd1 修复 sv语言转录函数最后截断漏话问题 2025-03-18 17:27:35 +08:00
小肥羊 18af52484a 优化 sv语言转录函数[待测试] 2025-03-18 12:01:03 +08:00
小肥羊 b5e174e683 修改 暂时屏蔽 正式环境文件缓存清理 2025-03-17 11:30:45 +08:00
小肥羊 29f9b44b93 修复 线上redis连接失效问题 2025-03-14 12:27:14 +08:00
小肥羊 1881f2b39e 修复 线上redis连接失效问题 2025-03-14 11:27:17 +08:00
小肥羊 6f312821df 修改 发布正式环境配置
修复 正式环境数据库切换至RDS
2025-03-14 11:11:45 +08:00
小肥羊 7c0e39a43e 修复 注入仓储仓时导致数据库连接被占用问题 2025-03-13 15:48:33 +08:00
小肥羊 d25fa4bcff 优化 文件订阅Page 2025-03-13 09:48:21 +08:00
小肥羊 3d7abaecd5 修复 任务EndTime结束时未赋值问题 2025-03-12 17:03:19 +08:00
小肥羊 17f75c975e 优化 视频AI分析允许异步
新增 文件订阅管理后台页面
2025-03-12 17:01:29 +08:00
小肥羊 c39fbb7501 优化 AI分析流程 2025-03-11 14:22:56 +08:00
小肥羊 ae6e5ce836 优化文件结构 2025-03-06 18:21:33 +08:00
小肥羊 27b4b1f57a 修复 Core项目的枚举路径后导致的异常 2025-03-05 17:29:25 +08:00
小肥羊 1e4d7ac5f6 新增 蓝鲸字库 订阅节点接口
调整 Core项目的枚举路径
2025-03-05 16:31:28 +08:00
小肥羊 977faaef75 引入 蓝鲸智库类
优化 其他数据库引入配置
2025-03-04 15:41:05 +08:00
小肥羊 1b239201a2 优化 数据存储同步到数据库 2025-02-27 18:28:12 +08:00
小肥羊 c12c8ebfcd 优化 部署线上流程
优化 ds提示词
2025-02-25 18:26:47 +08:00
小肥羊 8ee1db05ed 优化 校验分段提示词
优化 任务预览UI
2025-02-21 11:33:12 +08:00
小肥羊 49dcdc9fcb 优化 dk 连接方式 优化提示词 2025-02-14 18:28:02 +08:00
小肥羊 613019da82 修复 deepseek 流式传入失败问题
新增 阿里云api
2025-02-13 18:07:52 +08:00
小肥羊 67f3c4fb57 新增 绑定时间片段知识点 2025-02-11 15:18:44 +08:00
小肥羊 6c5c7db4f5 新增 deepseek_api
新增 fst语音转文本后处理流程
2025-02-07 17:35:18 +08:00
小肥羊 c14842a99d 优化 完善gpt请求参数,gpt版本 2025-01-23 10:34:19 +08:00
小肥羊 b48c86936b 优化 新版ai内容分段关键词 2025-01-17 18:22:20 +08:00
小肥羊 cbb6315fac 优化 任务预览流程 2025-01-13 18:22:25 +08:00
小肥羊 eb45f0fd41 完善 任务预览功能 2025-01-10 18:25:28 +08:00
小肥羊 59c8171ce8 同步代码
引入 知识点数据库
重写 AI分析视频片段功能
2025-01-09 11:53:48 +08:00
小肥羊 6981314257 新增 接入ChatGPT-4.o 2024-12-12 11:22:54 +08:00
小肥羊 2a955b5b43 新增 chatGPT 模型 2024-12-11 11:54:24 +08:00
小肥羊 99cc6d3e42 优化 kimi提示词 2024-12-05 17:01:06 +08:00
小肥羊 58a25c5c08 优化 kimi模型提示词,优化程序执行看流程 2024-12-05 15:36:16 +08:00
小肥羊 936423dc22 修复 发布后 docker ffmpge安装缓慢问题 2024-12-05 13:49:50 +08:00
小肥羊 4d146d718b 优化 重新开始任务 允许修改学科 2024-12-04 12:27:43 +08:00
小肥羊 559c7fba21 优化 KIMI 提示词 2024-12-04 10:54:11 +08:00
小肥羊 3b12e463fd 优化 课堂指标 2024-12-04 09:57:14 +08:00
356 changed files with 37157 additions and 3503 deletions

5
.gitignore vendored
View File

@ -365,3 +365,8 @@ FodyWeavers.xsd
VideoAnalysis/AICore/_Static/
VideoAnalysisCore/AICore/_Static/
VideoAnalysis/WebUI/node_modules/
VideoAnalysis/WebUI/dist/
VideoAnalysis/WebUI/.vscode/
/VideoAnalysis/device_id.config
/Learn.VideoAnalysis.API/device_id.config

View File

@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "10.0.6",
"commands": [
"dotnet-ef"
],
"rollForward": false
}
}
}

View File

@ -0,0 +1,172 @@
{
"Env": [
{
"Name": "10楼刀片机",
"ServerList": [],
"LinuxServerList": [
{
"UserName": "heyang",
"Pwd": "9718CB3C9A0760CA326767D677ADEC1C",
"Host": "10.127.127.107",
"NickName": "10楼刀片机",
"IIsFireUrl": null,
"DockerFireUrl": "",
"WindowsServiceFireUrl": null,
"LinuxServiceFireUrl": null
}
],
"IgnoreList": [
"ffmpeg.exe"
],
"WindowsBackUpIgnoreList": []
},
{
"Name": "阿里云_代理",
"ServerList": [],
"LinuxServerList": [
{
"UserName": "heyang",
"Pwd": "AAC53130AF118B652BCED77C39B959F9",
"Host": "10.127.127.77:10022",
"NickName": "",
"IIsFireUrl": null,
"DockerFireUrl": "",
"WindowsServiceFireUrl": null,
"LinuxServiceFireUrl": null
}
],
"IgnoreList": [],
"WindowsBackUpIgnoreList": []
}
],
"IIsConfig": {
"SdkType": "netcore",
"WebSiteName": "",
"LastEnvName": "10楼刀片机",
"EnvPairList": [
{
"EnvName": "10楼刀片机",
"ConfigName": "",
"LinuxEnvParam": null,
"DockerPort": null,
"DockerEnvName": null,
"DockerVolume": null,
"DockerOther": null
},
{
"EnvName": "阿里云_代理",
"ConfigName": "",
"LinuxEnvParam": null,
"DockerPort": null,
"DockerEnvName": null,
"DockerVolume": null,
"DockerOther": null
}
]
},
"WindowsServiveConfig": {
"ServiceName": "",
"SdkType": null,
"LastEnvName": null,
"EnvPairList": [
{
"EnvName": "10楼刀片机",
"ConfigName": "",
"LinuxEnvParam": null,
"DockerPort": null,
"DockerEnvName": null,
"DockerVolume": null,
"DockerOther": null
},
{
"EnvName": "阿里云_代理",
"ConfigName": "",
"LinuxEnvParam": null,
"DockerPort": null,
"DockerEnvName": null,
"DockerVolume": null,
"DockerOther": null
}
]
},
"LinuxServiveConfig": {
"ServiceName": "",
"EnvParam": "",
"LastEnvName": null,
"EnvPairList": [
{
"EnvName": "10楼刀片机",
"ConfigName": "",
"LinuxEnvParam": null,
"DockerPort": null,
"DockerEnvName": null,
"DockerVolume": null,
"DockerOther": null
},
{
"EnvName": "阿里云_代理",
"ConfigName": "",
"LinuxEnvParam": null,
"DockerPort": null,
"DockerEnvName": null,
"DockerVolume": null,
"DockerOther": null
}
]
},
"DockerConfig": {
"Prot": "9040",
"AspNetCoreEnv": "",
"LastEnvName": "阿里云_代理",
"RemoveDaysFromPublished": "10",
"WorkDir": "/home/heyang/",
"Volume": "/home/hy/VideoAnalysis/AICore:/app/AICore/_Static;/home/hy/VideoAnalysis/TaskCachedFile:/app/TaskCachedFile",
"Other": "-e va_args=\"\" --name videoanalysis",
"EnvPairList": [
{
"EnvName": "10楼刀片机",
"ConfigName": null,
"LinuxEnvParam": null,
"DockerPort": "9040",
"DockerEnvName": "",
"DockerVolume": "/home/hy/VideoAnalysis/AICore:/app/AICore/_Static;/home/hy/VideoAnalysis/TaskCachedFile:/app/TaskCachedFile",
"DockerOther": "-e va_args=\"\""
},
{
"EnvName": "阿里云_代理",
"ConfigName": null,
"LinuxEnvParam": null,
"DockerPort": "9040",
"DockerEnvName": "",
"DockerVolume": "/home/hy/VideoAnalysis/AICore:/app/AICore/_Static;/home/hy/VideoAnalysis/TaskCachedFile:/app/TaskCachedFile",
"DockerOther": "-e va_args=\"\" --name videoanalysis"
}
]
},
"DockerImageConfig": {
"BaseHttpProxy": "",
"BaseImage": "",
"BaseImageCredential": {
"UserName": "",
"Password": ""
},
"TargetImage": "",
"TargetHttpProxy": "",
"TargetTags": [
""
],
"TargetImageCredential": {
"UserName": "",
"Password": ""
},
"ImageFormat": "Docker",
"Entrypoint": [
""
],
"Cmd": [
""
],
"IgnoreList": [],
"SkipExistingImages": false
}
}

View File

@ -0,0 +1,15 @@
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 9040
COPY . .
#设置时间为中国上海 环境为开发环境
ENV TZ=Asia/Shanghai
# 给我们要传的参数一个初始值
ENV va_args=
ENV ASPNETCORE_URLS=http://+:9040
ENTRYPOINT dotnet Learn.VideoAnalysis.API.dll $va_args

View File

@ -0,0 +1,36 @@
using Coravel;
using Coravel.Scheduling.Schedule;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VideoAnalysisCore.Job;
namespace Learn.VideoAnalysis.API.Expand
{
public static class CoravelExpand
{
public static void AddCoravel(this IServiceCollection service)
{
Console.WriteLine($"{DateTime.Now}=>初始化 Coravel");
service.AddScheduler();
//service.AddTransient<TaskFileClearJob>();
//service.AddTransient<ClearAllCacheJob>();
service.AddTransient<NodePackageJob>();
}
public static void UseCoravelExpand(this IApplicationBuilder provider)
{
provider.ApplicationServices.UseScheduler(scheduler =>
{
//文件包分析
//scheduler.Schedule<NodePackageJob>().EveryMinute(); //每分钟执行一次
scheduler.Schedule<NodePackageJob>().EveryThirtyMinutes(); //每30分钟执行一次
});
}
}
}

View File

@ -0,0 +1,288 @@
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System;
using System.Linq;
using Microsoft.AspNetCore.Http;
using SqlSugar;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.Text.Json;
using System.Collections.Generic;
using System.Data;
using Microsoft.Extensions.Hosting;
using System.IO;
using Microsoft.AspNetCore.Hosting;
using SqlSugar.IOC;
using Microsoft.AspNetCore.Authorization;
using VideoAnalysisCore.Common.Dto;
using VideoAnalysisCore.Common;
using VideoAnalysisCore.Model;
namespace Learn.VideoAnalysis.API.Expand
{
/// <summary>
/// 使用该属性,接口对结果原样输出,不做包装
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)]
public class ResultIgnore : Attribute { }
/// <summary>
/// http接口日志启用
/// </summary>
public class HttpLogEnable : Attribute { }
/// <summary>
/// Http请求过滤器
/// </summary>
public class HttpLogAttribute : ActionFilterAttribute, IAsyncExceptionFilter
{
readonly Stopwatch _stopwatch;//统计程序耗时
public HttpLogAttribute()
{
_stopwatch = Stopwatch.StartNew();
}
/// <summary>
/// 执行接口前文件做缓存处理
/// </summary>
/// <param name="context"></param>
/// <exception cref="CustomException"></exception>
public void ExecutingCached(ActionExecutingContext context)
{
//特殊处理ResultIgnore不进行返回结果包装原样输出
var endpoint = context.HttpContext.GetEndpoint();
var request = context.HttpContext.Request;
// 1. 只有非 GET 请求且不是文件上传时才读取 Body
if (!request.Method.Equals("GET", StringComparison.OrdinalIgnoreCase)
&& !request.HasFormContentType)
{
// 确保从头开始读
request.Body.Position = 0;
// 使用 leaveOpen: true 防止 StreamReader 关闭底层的 Request.Body
using (var reader = new System.IO.StreamReader(request.Body, System.Text.Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 1024, leaveOpen: true))
{
var body = reader.ReadToEnd();
context.HttpContext.Items["RequestBodyRaw"] = body;
request.Body.Position = 0;
}
}
// 直接返回原始结果,不封装
if (endpoint?.Metadata.GetMetadata<HttpLogEnable>() == null) return;
if (context.HttpContext.Request.HasFormContentType &&
context.HttpContext.Request.Form.Files != null &&
context.HttpContext.Request.Form.Files.Count() > 0)
{
context.HttpContext.Items["FileCached"]=
context.HttpContext.Request.Form.Files.Select(s =>
{
var stream = new MemoryStream();
s.CopyTo(stream);
stream.Position = 0;
return (s, stream);
}).ToArray();
}
}
/// <summary>
/// 执行接口前400 处理
/// </summary>
/// <param name="context"></param>
/// <exception cref="CustomException"></exception>
public void Executing400(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
var errMsg = string.Join(',', context.ModelState.Values.SelectMany(s => s.Errors.Select(e => e.ErrorMessage)));
Oh.ModelError(errMsg);
}
}
private bool HasAttribute<T>(ActionExecutedContext context) where T : Attribute
{
if (context.ActionDescriptor is ControllerActionDescriptor descriptor)
{
// 检查方法上是否有 SkipApiResultAttribute
if (descriptor.MethodInfo.GetCustomAttributes(typeof(T), false).Any())
return true;
// 检查控制器上是否有 SkipApiResultAttribute
if (descriptor.ControllerTypeInfo.GetCustomAttributes(typeof(T), false).Any())
return true;
}
return false;
}
/// <summary>
/// 接口结果格式化
/// </summary>
/// <param name="context"></param>
public BaseReturn<object>? ApiResultFormatting(ActionExecutedContext context)
{
//特殊处理ResultIgnore不进行返回结果包装原样输出
if (HasAttribute<ResultIgnore>(context))
{
base.OnActionExecuted(context);
return null;
}
// 返回结果为JsonResult的请求进行Result包装
if (context.Exception != null)
throw context.Exception;
if (context.Result != null)
{
object? resData = null;
if (context.Result is ObjectResult objectResult)
resData = objectResult.Value;
else if (context.Result is ContentResult contentRes)
resData = contentRes.Content;
else if (context.Result is JsonResult resJ)
resData = resJ.Value;
else if (context.Result is FileResult)
return null;
var code = (context?.Result as IStatusCodeActionResult)?.StatusCode ?? 200;
var res = new BaseReturn<object>()
{
Code = code,
Data = resData,
Message = "SUCCESS"
};
//不对返回结果结果做修改
//context.Result = new JsonResult(res);
return res;
}
return null;
}
/// <summary>
/// 添加http日志信息
/// </summary>
/// <param name="context"></param>
/// <param name="result"></param>
/// <param name="e"></param>
/// <returns></returns>
public async Task AddHttpLogAsync(HttpContext context, BaseReturn<object>? result = null, Exception? e = null)
{
string request = null;
var logId = Yitter.IdGenerator.YitIdHelper.NextId();
if (!context.Request.Method.Equals("GET", StringComparison.InvariantCultureIgnoreCase))
{
var fileArr = context.Items.ContainsKey("FileCached") ? context.Items["FileCached"] as (IFormFile file, MemoryStream stream)[] : null;
if (context.Request.HasFormContentType && fileArr != null)
{
string uploadsFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "UploadLogs", logId.ToString());
if (!Directory.Exists(uploadsFolder))
Directory.CreateDirectory(uploadsFolder);
foreach (var fileInfo in fileArr)
{
string uniqueFileName = Guid.NewGuid().ToString().Substring(0, 5) + "_" + Path.GetFileName(fileInfo.file.FileName);
using var stream = new FileStream(Path.Combine(uploadsFolder, uniqueFileName), FileMode.Create, FileAccess.Write);
fileInfo.stream.Position = 0;
await fileInfo.stream.CopyToAsync(stream);
fileInfo.stream.Dispose();
}
request = $"请求体包含{context.Request.Form.Files.Count()}个文件 目录 {uploadsFolder}";
}
}
if (context.Items.ContainsKey("RequestBodyRaw"))
{
request = context.Items["RequestBodyRaw"] as string;
}
//写入队列
await DbScoped.Sugar.CopyNew()
.Insertable<HttpLog>(new HttpLog
{
Id = logId,
Url = context.Request.Path + context.Request.QueryString,
Method = context.Request.Method,
Request = request,
IP = GetClientIp(context),
ResponseCode = result?.Code ?? -1,
Response = (result != null ? JsonSerializer.Serialize(result) : null) ,
Authorization = context.Request.Headers.ContainsKey("Authorization")
? context.Request.Headers["Authorization"].ToString()
: string.Empty,
Exception = e?.ToString(),
ExceptionMessage = e?.Message,
AdminId = 0,
TotalMilliseconds = (double)_stopwatch.Elapsed.TotalMilliseconds
}).ExecuteCommandAsync();
}
private string GetClientIp(HttpContext context)
{
var headers = context.Request.Headers;
var ip = headers["X-Forwarded-For"].FirstOrDefault();
if (!string.IsNullOrWhiteSpace(ip))
{
ip = ip.Split(',', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault()?.Trim();
}
if (string.IsNullOrWhiteSpace(ip))
{
ip = headers["X-Real-IP"].FirstOrDefault();
}
if (string.IsNullOrWhiteSpace(ip) && context.Connection.RemoteIpAddress != null)
{
ip = context.Connection.RemoteIpAddress.ToString();
}
return ip ?? string.Empty;
}
public override async void OnActionExecuting(ActionExecutingContext context)
{
Executing400(context);
ExecutingCached(context);
}
/// <summary>
/// 在Controller的Action执行后执行
/// </summary>
/// <param name="context"></param>
public override async void OnActionExecuted(ActionExecutedContext context)
{
try
{
BaseReturn<object>? res = ApiResultFormatting(context);
await AddHttpLogAsync(context.HttpContext, res);
}
catch (Exception ex)
{
Console.WriteLine($"{DateTime.Now}=>接口出现异常");
Console.WriteLine(ex.Message);
Console.WriteLine(ex.StackTrace);
}
base.OnActionExecuted(context);
}
/// <summary>
/// 执行错误时
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task OnExceptionAsync(ExceptionContext context)
{
var code = -1;
var msg = context.Exception.Message;
if (context.Exception is OhException exception)
code = exception.Code;
var result = new BaseReturn()
{
Code = code,
Message = context.Exception.Message
};
context.Result = new JsonResult(result);
await AddHttpLogAsync(context.HttpContext, null, context.Exception);
if (code == 401 || code == 403)
context.HttpContext.Response.StatusCode = code;
context.ExceptionHandled = true;
}
}
}

View File

@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<None Remove="Dockerfile" />
</ItemGroup>
<ItemGroup>
<Content Include="Dockerfile">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Mapster.DependencyInjection" Version="1.0.2-pre01" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\VideoAnalysisCore\VideoAnalysisCore.csproj" />
</ItemGroup>
<ItemGroup>
<Content Update="AntDeploy.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="appsettings.Production.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@ -0,0 +1,6 @@
@Learn.VideoAnalysis.API_HostAddress = http://localhost:5245
GET {{Learn.VideoAnalysis.API_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@ -0,0 +1,92 @@

using Learn.VideoAnalysis.API.Expand;
using Mapster;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.OpenApi.Models;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Unicode;
using VideoAnalysisCore.AICore.FFMPGE;
using VideoAnalysisCore.AICore.GPT.DeepSeek;
using VideoAnalysisCore.AICore.SherpaOnnx;
using VideoAnalysisCore.Common;
using VideoAnalysisCore.Common.Expand;
namespace Learn.VideoAnalysis.API
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers(options =>
{
// 全局模型赋值默认值 和 统一返回格式处理
options.Filters.Add<HttpLogAttribute>();
}).AddJsonOptions(options =>
{
options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All);//中文转换时不使用Unicode
//options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;// 默认小驼峰 null 大驼峰
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
var file = Path.Combine(AppContext.BaseDirectory, "Learn.VideoAnalysis.API.xml"); // xml文档绝对路径
var file1 = Path.Combine(AppContext.BaseDirectory, "VideoAnalysisCore.xml"); // xml文档绝对路径
c.IncludeXmlComments(file, true); // true : 显示控制器层注释
c.IncludeXmlComments(file1, true); // true : 显示控制器层注释
c.OrderActionsBy(o => o.RelativePath); // 对action的名称进行排序如果有多个就可以看见效果了。
});
builder.Services.AddMapster();
//初始化 插件
builder.AddAppConfig(args);
builder.Services.AddHttpClient();
builder.Services.AddSqlSugarExpand();
builder.Services.AddRedisExpand(false);
builder.Services.AddAlibabaCloudVod();
builder.Services.AddCoravel();
builder.Services.AddCorsExpand();
builder.Services.AddControllersWithViews(options =>
{
options.Filters.Add(typeof(ExceptionFilter));
});
var app = builder.Build();
AppCommon.Services = app.Services;
app.UseMiddleware<BasicAuthMiddleware>("Swagger");
// Configure the HTTP request pipeline.
app.UseSwagger();
app.UseSwaggerUI();
app.UseHttpsRedirection();
app.MapControllers();
//自定义 应用
app.UseCorsExpand();
app.UseSqlSugarExpand();
app.UseCoravelExpand();
// 注册启动后的回调
app.UseServiceSystem(null,false);
app.Run();
}
}
}

View File

@ -0,0 +1,15 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"http:5238": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5238",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,44 @@
{
"Logging": {
"LogLevel": {
"Default": "Error",
"Microsoft.AspNetCore": "Error"
}
},
"AppConfig": {
"TaskSetting": {
"DownloadSpeed": 8,
"IS_Server": true //?
},
"Subsystem": {
"蓝鲸智库": {
"APIUrl": "https://zyapi.23544.com",
"Token": ""
}
},
"Redis": {
"ConnectionString": "redis-external.23544.com:16379,password=poiuyt)(*&^%,defaultDatabase=5"
},
"FFmpeg": {
" TimeSlice": 600
},
"DB": {
//"ConnectionString": "AllowLoadLocalInfile=true;Server=10.255.255.3;Port=3306;Database=learn.videoanalysis;User ID=marking;Password=qwe123!@#;CharSet=utf8mb4;pooling=true;SslMode=None",
"ConnectionString": "AllowLoadLocalInfile=true;Server=rm-2vc20nd3d11g0oh6g2o.rwlb.cn-chengdu.rds.aliyuncs.com;User ID=marking;Password=poiuytPOIUYT098765)(*&^%;Port=3306;Database=learn.videoanalysis;CharSet=utf8mb4;pooling=true;SslMode=None",
"SqlType": "MySql",
"UpdateTable": false
},
"OtherDBArr": [
{ // 线
"ConfigId": 1001, //ResourceBank
"ConnectionString": "AllowLoadLocalInfile=true;Server=rm-2vc20nd3d11g0oh6g2o.rwlb.cn-chengdu.rds.aliyuncs.com;User ID=marking;Password=poiuytPOIUYT098765)(*&^%;Port=3306;Database=resourcebank;CharSet=utf8mb4;pooling=true;SslMode=None",
"SqlType": "MySql"
},
{
"ConfigId": 1002, //App.public.live
"ConnectionString": "AllowLoadLocalInfile=true;Server=rm-2vc20nd3d11g0oh6g2o.rwlb.cn-chengdu.rds.aliyuncs.com;User ID=marking;Password=poiuytPOIUYT098765)(*&^%;Port=3306;Database=app.public.live;CharSet=utf8mb4;pooling=true;SslMode=None",
"SqlType": "MySql"
}
]
}
}

View File

@ -0,0 +1,89 @@
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"AppConfig": {
"ID": 1, //
"TaskSetting": {
"DownloadSpeed": 2,
"IS_Server": true //
},
"Admin": {
"Account": "admin",
"Password": "q1w2e3!@#"
},
"Subsystem": {
"蓝鲸智库": {
"APIUrl": "http://192.168.2.117:6400",
"Token": ""
}
},
"SimpLetex": {
"Host": "https://server.simpletex.cn/api/",
"AppSecret": "05ZbPfCFZgTmfd4uIqHHc9pHgYR2V8bk",
"AppId": "GH2OXwuxSZEH5W28H61bdSzD"
},
"Redis": {
"ConnectionString": "127.0.0.1:6379,password=Woshiren123,defaultDatabase=10"
},
"Whisper": {
"ModelName": "ggml-small.bin"
},
"FFmpeg": {
" TimeSlice": 600
},
"ChatGpt": {
"KIMI": {
"Host": "https://api.moonshot.cn",
"ApiKey": "sk-8BvvhESZIkgUbiaaJhglPxFa4o2X9H3xEv9lXELrWWwGxHWY"
},
"ChatGpt": {
"Host": "https://api.oaibest.com/",
"ApiKey": "sk-D15tBln31N7dI9Fi7lds7OySFv5tOEK7DMNsG5rY2E6DCr4s"
},
"DeepSeek": {
"Host": "https://api.deepseek.com/chat/completions",
"ApiKey": "sk-88d3d2bc3dae4d50854b2569b281cf76"
},
"aliyun": {
"Host": "https://dashscope.aliyuncs.com/compatible-mode/",
"ApiKey": "sk-1742c2bf7b9846ae835de598dc6c427b"
}
},
"DB": {
"ConnectionString": "AllowLoadLocalInfile=true;Server=192.168.2.9;User ID=root;Password=qwe123!@#;Port=3306;Database=learn.videoanalysis;CharSet=utf8mb4;pooling=true;SslMode=None",
//"ConnectionString": "AllowLoadLocalInfile=true;Server=rm-2vc20nd3d11g0oh6g2o.rwlb.cn-chengdu.rds.aliyuncs.com;User ID=marking;Password=poiuytPOIUYT098765)(*&^%;Port=3306;Database=learn.videoanalysis;CharSet=utf8mb4;pooling=true;SslMode=None",
"SqlType": "MySql",
"UpdateTable": false
},
"AlibabaCloudVod": {
"AccessKeyId": "LTAI5tFLXyC3ixVdxhxLih8K",
"AccessKeySecret": "dlGu3WMoW0XQaoAYxiCPpnxry6qLhB",
"EndPoint": "vod.cn-shanghai.aliyuncs.com" //
},
"AliyunOSS": {
"AccessKeyId": "LTAI5tFLXyC3ixVdxhxLih8K",
"AccessKeySecret": "dlGu3WMoW0XQaoAYxiCPpnxry6qLhB",
"BucketDomain": "https://learn-videoanalysis.oss-cn-chengdu.aliyuncs.com",
"Region": "cn-chengdu",
"BucketName": "learn-videoanalysis",
"EndPoint": "oss-cn-chengdu.aliyuncs.com" //
},
"OtherDBArr": [
{ // 线
"ConfigId": 1001, //ResourceBank
"ConnectionString": "AllowLoadLocalInfile=true;Server=rm-2vc20nd3d11g0oh6g2o.rwlb.cn-chengdu.rds.aliyuncs.com;User ID=marking;Password=poiuytPOIUYT098765)(*&^%;Port=3306;Database=resourcebank;CharSet=utf8mb4;pooling=true;SslMode=None",
"SqlType": "MySql"
},
{
"ConfigId": 1002, //App.public.live
"ConnectionString": "AllowLoadLocalInfile=true;Server=rm-2vc20nd3d11g0oh6g2o.rwlb.cn-chengdu.rds.aliyuncs.com;User ID=marking;Password=poiuytPOIUYT098765)(*&^%;Port=3306;Database=app.public.live;CharSet=utf8mb4;pooling=true;SslMode=None",
"SqlType": "MySql"
}
]
}
}

View File

@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Learn.VideoAnalysis", "Vide
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VideoAnalysisCore", "VideoAnalysisCore\VideoAnalysisCore.csproj", "{69F4243A-B22E-431B-8F0B-ECD8729B8665}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Learn.VideoAnalysis.API", "Learn.VideoAnalysis.API\Learn.VideoAnalysis.API.csproj", "{D31BA4AB-73FC-47B1-A10A-34FD5E921F4A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -21,6 +23,10 @@ Global
{69F4243A-B22E-431B-8F0B-ECD8729B8665}.Debug|Any CPU.Build.0 = Debug|Any CPU
{69F4243A-B22E-431B-8F0B-ECD8729B8665}.Release|Any CPU.ActiveCfg = Release|Any CPU
{69F4243A-B22E-431B-8F0B-ECD8729B8665}.Release|Any CPU.Build.0 = Release|Any CPU
{D31BA4AB-73FC-47B1-A10A-34FD5E921F4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D31BA4AB-73FC-47B1-A10A-34FD5E921F4A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D31BA4AB-73FC-47B1-A10A-34FD5E921F4A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D31BA4AB-73FC-47B1-A10A-34FD5E921F4A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "10.0.3",
"commands": [
"dotnet-ef"
],
"rollForward": false
}
}
}

View File

@ -5,12 +5,12 @@
"ServerList": [],
"LinuxServerList": [
{
"UserName": "hy",
"Pwd": "E9D1AC136FDD59740A9595ABD0EB953A",
"Host": "192.168.2.21:10107",
"NickName": "",
"UserName": "heyang",
"Pwd": "9718CB3C9A0760CA326767D677ADEC1C",
"Host": "10.127.127.107",
"NickName": "10楼刀片机",
"IIsFireUrl": null,
"DockerFireUrl": "https://videoanalysis.w.23544.com:8843/",
"DockerFireUrl": "",
"WindowsServiceFireUrl": null,
"LinuxServiceFireUrl": null
}
@ -19,6 +19,24 @@
"ffmpeg.exe"
],
"WindowsBackUpIgnoreList": []
},
{
"Name": "阿里云_代理",
"ServerList": [],
"LinuxServerList": [
{
"UserName": "heyang",
"Pwd": "AAC53130AF118B652BCED77C39B959F9",
"Host": "10.127.127.77:10022",
"NickName": "",
"IIsFireUrl": null,
"DockerFireUrl": "",
"WindowsServiceFireUrl": null,
"LinuxServiceFireUrl": null
}
],
"IgnoreList": [],
"WindowsBackUpIgnoreList": []
}
],
"IIsConfig": {
@ -34,6 +52,15 @@
"DockerEnvName": null,
"DockerVolume": null,
"DockerOther": null
},
{
"EnvName": "阿里云_代理",
"ConfigName": "",
"LinuxEnvParam": null,
"DockerPort": null,
"DockerEnvName": null,
"DockerVolume": null,
"DockerOther": null
}
]
},
@ -50,6 +77,15 @@
"DockerEnvName": null,
"DockerVolume": null,
"DockerOther": null
},
{
"EnvName": "阿里云_代理",
"ConfigName": "",
"LinuxEnvParam": null,
"DockerPort": null,
"DockerEnvName": null,
"DockerVolume": null,
"DockerOther": null
}
]
},
@ -66,6 +102,15 @@
"DockerEnvName": null,
"DockerVolume": null,
"DockerOther": null
},
{
"EnvName": "阿里云_代理",
"ConfigName": "",
"LinuxEnvParam": null,
"DockerPort": null,
"DockerEnvName": null,
"DockerVolume": null,
"DockerOther": null
}
]
},
@ -74,9 +119,9 @@
"AspNetCoreEnv": "",
"LastEnvName": "10楼刀片机",
"RemoveDaysFromPublished": "10",
"WorkDir": "",
"Volume": "/home/hy/VideoAnalysis/AICore:/app/AICore/_Static;/home/hy//VideoAnalysis/TaskCachedFile:/app/TaskCachedFile",
"Other": "",
"WorkDir": "/home/heyang/",
"Volume": "/home/hy/VideoAnalysis/AICore:/app/AICore/_Static;/mnt/2tb/VideoAnalysis/TaskCachedFile:/app/TaskCachedFile",
"Other": "-e va_args=\"\"",
"EnvPairList": [
{
"EnvName": "10楼刀片机",
@ -84,8 +129,17 @@
"LinuxEnvParam": null,
"DockerPort": "9040",
"DockerEnvName": "",
"DockerVolume": "/home/hy/VideoAnalysis/AICore:/app/AICore/_Static;/home/hy//VideoAnalysis/TaskCachedFile:/app/TaskCachedFile",
"DockerOther": ""
"DockerVolume": "/home/hy/VideoAnalysis/AICore:/app/AICore/_Static;/mnt/2tb/VideoAnalysis/TaskCachedFile:/app/TaskCachedFile",
"DockerOther": "-e va_args=\"\""
},
{
"EnvName": "阿里云_代理",
"ConfigName": null,
"LinuxEnvParam": null,
"DockerPort": "9040",
"DockerEnvName": "",
"DockerVolume": "/home/hy/VideoAnalysis/AICore:/app/AICore/_Static;/home/hy/VideoAnalysis/TaskCachedFile:/app/TaskCachedFile",
"DockerOther": "-e va_args=\"IS_Server\" --name videoanalysis"
}
]
},

View File

@ -1,25 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link href="_content/AntDesign/css/ant-design-blazor.css" rel="stylesheet" />
<link href="_content/AntDesign.ProLayout/css/ant-design-pro-layout-blazor.css" rel="stylesheet" />
<link href="Learn.VideoAnalysis.styles.css" rel="stylesheet" />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet @rendermode="InteractiveServer" />
</head>
<body>
<Routes @rendermode="InteractiveServer" />
<script type="text/javascript" src="@("https://unpkg.com/@antv/g2plot@2.4.17/dist/g2plot.min.js")"></script>
<script src="_content/AntDesign/js/ant-design-blazor.js"></script>
<script src="_content/AntDesign.Charts/ant-design-charts-blazor.js"></script>
<script src="_framework/blazor.web.js"></script>
</body>
</html>

View File

@ -1,36 +0,0 @@
@page "/Error"
@using System.Diagnostics
<PageTitle>Error</PageTitle>
<h1 class="text-danger">错误页面.</h1>
<h2 class="text-danger">处理您的请求时出错。</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
</p>
}
<h3>开发模式</h3>
<p>
切换到<strong>Development</strong>环境将显示有关发生的错误的更多详细信息。
</p>
<p>
<strong> 不应为已部署的应用程序启用开发环境。</strong>
它可能导致向最终用户显示来自异常的敏感信息。
对于本地调试,通过将 <strong>ASPNETCORE_ENVIRONMENT</strong> 环境变量设置为 <strong>Development</strong> 来启用 <strong> 开发 </strong> 环境
并重新启动应用程序。
</p>
@code{
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
private string? RequestId { get; set; }
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
protected override void OnInitialized() =>
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}

View File

@ -1,35 +0,0 @@
@namespace VideoAnalysisRazor.Layouts
@inherits LayoutComponentBase
<AntDesign.ProLayout.BasicLayout
Logo="@("https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg")"
MenuData="_menuData"
Context="学习视频分析"
MenuAccordion
Title="学习视频分析"
@bind-Collapsed="collapsed">
<HeaderContentRender>
<Space Size="@("24")">
<SpaceItem>
<Icon Class="action" Type="@(collapsed?"menu-unfold":"menu-fold")" OnClick="Toggle" />
</SpaceItem>
<SpaceItem>
<Icon Class="action" Type="reload" Theme="outline" OnClick="Reload" />
</SpaceItem>
<SpaceItem>
<Icon Type="api" Theme="outline" OnClick="ToSwagger" />
</SpaceItem>
</Space>
</HeaderContentRender>
<RightContentRender>
</RightContentRender>
<ChildContent>
<ReuseTabs></ReuseTabs>
</ChildContent>
<FooterRender>
<FooterView Copyright="2024 重庆远轩教育科技有限公司" Links="new LinkItem[0]"></FooterView>
</FooterRender>
</AntDesign.ProLayout.BasicLayout>
<SettingDrawer />

View File

@ -1,89 +0,0 @@
using AntDesign.Extensions.Localization;
using AntDesign.ProLayout;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
using Microsoft.Identity.Client.Extensions.Msal;
using Microsoft.JSInterop;
using System.Globalization;
using System.Net.Http.Json;
namespace VideoAnalysisRazor.Layouts
{
public partial class BasicLayout : LayoutComponentBase, IDisposable
{
private MenuDataItem[] _menuData;
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
[Inject] IHttpContextAccessor HttpContextAccessor { get; set; } = default!;
[Inject] private ReuseTabsService TabService { get; set; }
[Inject] private IJSRuntime JSRuntime { get; set; }
[Inject] private ProtectedSessionStorage session { get; set; } = default!;
bool collapsed;
void Toggle()
{
collapsed = !collapsed;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!await CheckLogin())
{
NavigationManager.NavigateTo("/Login");
}
}
protected override async Task OnInitializedAsync()
{
_menuData = [
new MenuDataItem
{
Path = "/",
Name = "任务队列",
Key = "VideoTaskPage",
Icon = "unordered-list",
},
new MenuDataItem
{
Path = "/Project",
Name = "课堂指标",
Key = "EvaluationProject",
Icon = "question-circle",
},
new MenuDataItem
{
Path = "/Login",
Name = "登录页",
Key = "Login",
HideInMenu = true,
}
];
}
public async Task<bool> CheckLogin()
{
try
{
return (await session.GetAsync<bool>("Login")).Value;
}
catch
{
return false;
}
}
void Reload()
{
TabService.ReloadPage();
}
async Task ToSwagger()
{
await JSRuntime.InvokeVoidAsync("open", "/swagger/index.html", "_blank");
}
public void Dispose()
{
}
}
}

View File

@ -1,77 +0,0 @@
@page "/Project"
@using AntDesign
@using AntDesign.TableModels
@using System.ComponentModel.DataAnnotations
@using SqlSugar
@using VideoAnalysisCore.Model
<Table @ref="_table" Loading="tableLoading" TItem="CourseGradingCriteria" ScrollY="600px" PageSize="15" Total="_total" DataSource="_dataSource" @bind-SelectedRows="_selectedRows" OnChange="OnChange">
<TitleTemplate>
<Flex Justify="end" Gap="10">
<Button Type="primary" @onclick="()=> StartEdit(default)">新增</Button>
@* <Button Disabled="!_selectedRows.Any()" Danger @onclick="DeleteAll">Delete</Button> *@
</Flex>
</TitleTemplate>
<ColumnDefinitions Context="row">
<ActionColumn Title="操作列" Width ="230px">
<a @onclick="() => StartEdit(row)">修改</a>
<Button Type="@ButtonType.Link" Danger @onclick="() => Delete(row)">
删除</Button>
</ActionColumn>
<PropertyColumn Property="c=>c.Id" Width="130px" Filterable="true" Sortable="true" />
<PropertyColumn Property="c=>c.NamePrompt" />
</ColumnDefinitions>
</Table>
@inject ModalService ModalService
@code
{
/// <summary>
/// 新增或者修改
/// </summary>
/// <param name="row"></param>
void StartEdit(CourseGradingCriteria row)
{
var data = row == null ? new() : row;
IForm? form = default;
ModalRef<bool> modalRef = default;
modalRef = ModalService.CreateModal<bool>(new()
{
Title = data.Id > 0 ? "修改" : "新增",
Content =
@<Form @ref="form" Model="data" OnFinish="()=> modalRef.OkAsync(true)"
LabelColSpan="6" WrapperColSpan="18">
<FormItem Label="标准提问词" >
<TextArea Rows="4" @bind-Value="@context.NamePrompt" />
</FormItem>
</Form>
,
OkText = "确定",
CancelText = "取消",
OnOk = async (e) =>
{
if (!form.Validate())
return;
// save db and refresh
modalRef.SetConfirmLoading(true);
if (data.Id > 0)
await criteria.UpdateAsync(data);
else
data.Id = await criteria.InsertReturnBigIdentityAsync(data);
//弹窗按钮 show
modalRef.SetConfirmLoading(false);
await modalRef.CloseAsync();
_table.ReloadData();
StateHasChanged();
},
OnCancel = async (e) =>
{
if (form.IsModified && (!await Comfirm("表格已经更新,您确定要退出吗?")))
return;
await modalRef.CloseAsync();
}
});
}
}

View File

@ -1,70 +0,0 @@
using AntDesign.TableModels;
using Microsoft.AspNetCore.Components;
using SqlSugar;
using System.Linq.Expressions;
using VideoAnalysisCore.Common;
using VideoAnalysisCore.Model;
namespace Learn.VideoAnalysis.Components.Pages
{
public partial class EvaluationProject : ComponentBase
{
[Inject] private ConfirmService ComfirmService { get; set; } = default!;
[Inject] private Repository<CourseGradingCriteria> criteria { get; set; } = default!;
IEnumerable<CourseGradingCriteria> _selectedRows = [];
ITable _table;
List<CourseGradingCriteria> _dataSource = null;
RefAsync<int> _total = 0;
bool tableLoading = false;
/// <summary>
/// 分页 查询 筛选 时
/// </summary>
/// <param name="query"></param>
async void OnChange(QueryModel<CourseGradingCriteria> query)
{
tableLoading = true;
List<IConditionalModel> where = default!;
if (query.FilterModel != null && ((query.FilterModel?.Count() ?? 0) > 0))
{
where = query.ToSqlSugerWhere();
}
_dataSource = await criteria.AsQueryable()
.Where(where)
.ToPageListAsync(query.PageIndex, query.PageSize, _total);
tableLoading = false;
StateHasChanged();
}
/// <summary>
/// 删除行
/// </summary>
/// <param name="row"></param>
/// <returns></returns>
async Task Delete(CourseGradingCriteria row)
{
if (!await Comfirm($"确定要删除这条数据吗? [{row.NamePrompt}]?"))
return;
await criteria.DeleteByIdAsync(row.Id);
_table.ReloadData();
}
/// <summary>
/// 初始化
/// </summary>
protected override void OnInitialized()
{
}
private async Task<bool> Comfirm(string message)
{
return await ComfirmService.Show(message, "提示", ConfirmButtons.YesNo, ConfirmIcon.Warning) == ConfirmResult.Yes;
}
}
}

View File

@ -1,3 +0,0 @@
input[aria-hidden="true"] {
display: none !important;
}

View File

@ -1,54 +0,0 @@
@page "/Login"
@using AntDesign
@using AntDesign.TableModels
@using System.ComponentModel.DataAnnotations
@using SqlSugar
@using VideoAnalysisCore.Model
@using VideoAnalysisCore.Model.Dto
@attribute [ReuseTabsPage(Ignore = true)]
<section style="width:100%;height:100%">
<!-- 背景颜色 -->
<div class="color"></div>
<div class="color"></div>
<div class="color"></div>
<div class="box">
<!-- 背景圆 -->
<div class="circle" style="--x:0"></div>
<div class="circle" style="--x:1"></div>
<div class="circle" style="--x:2"></div>
<div class="circle" style="--x:3"></div>
<div class="circle" style="--x:4"></div>
<!-- 登录框 -->
<div class="container">
<div class="form">
<h2>登录 视频分析平台</h2>
<div class="cform">
<div class="inputBox">
<input type="text" placeholder="账号" @bind="InputAccount">
</div>
<div class="inputBox">
<input type="password" placeholder="密码" @bind="InputPassword">
</div>
<div class="inputBox">
<input type="button" class="submit" value="登录" @onclick="() => LoginFunAsync()">
</div>
@* <p class="forget">
忘记密码?
<a href="#">
点击这里
</a>
</p> *@
@* <p class="forget">
没有账户?
<a href="#">
注册
</a>
</p> *@
</div>
</div>
</div>
</div>
</section>

View File

@ -1,72 +0,0 @@
using AntDesign;
using AntDesign.TableModels;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.AspNetCore.Mvc;
using SqlSugar;
using System.Linq.Expressions;
using System.Threading.Tasks;
using VideoAnalysisCore.Common;
using VideoAnalysisCore.Enum;
using VideoAnalysisCore.Model;
using VideoAnalysisCore.Model.Dto;
namespace Learn.VideoAnalysis.Components.Pages
{
public partial class Login : ComponentBase
{
[Inject] IHttpContextAccessor HttpContextAccessor { get; set; } = default!;
[Inject] private Repository<VideoTask> taskDB { get; set; } = default!;
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
[Inject] private INotificationService _notice { get; set; } = default!;
[Inject] private ProtectedSessionStorage session { get; set; } = default!;
/// <summary>
/// 输入的账号
/// </summary>
public string InputAccount = string.Empty;
/// <summary>
/// 输入的密码
/// </summary>
public string InputPassword= string.Empty;
/// <summary>
/// 初始化
/// </summary>
protected override void OnInitialized()
{
}
/// <summary>
/// 登录函数
/// </summary>
public async Task LoginFunAsync()
{
if (string.IsNullOrEmpty(InputAccount) || string.IsNullOrEmpty(InputPassword))
{
await _notice.Open(new NotificationConfig()
{
Message = "提示",
Description = "账号/密码必填",
NotificationType = NotificationType.Warning
});
}
if (InputAccount ==AppCommon.Config.Admin.Account && InputPassword == AppCommon.Config.Admin.Password)
{
await session.SetAsync("Login", true);
NavigationManager.NavigateTo("/");
}
else
{
await _notice.Open(new NotificationConfig()
{
Message = "提示",
Description = "账号/密码输入错误",
NotificationType = NotificationType.Warning
});
}
}
}
}

View File

@ -1,242 +0,0 @@
input[aria-hidden="true"] {
display: none !important;
}
/* 清除浏览器默认边距
使边框和内边距的值包含在元素的width和height内 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* 使用flex布局让内容垂直和水平居中 */
section {
/* 相对定位 */
position: relative;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
/* linear-gradient() 函数用于创建一个表示两种或多种颜色线性渐变的图片 */
background: linear-gradient(to bottom, #f1f4f9, #dff1ff);
}
/* 背景颜色 */
section .color {
/* 绝对定位 */
position: absolute;
/* 使用filter(滤镜) 属性,给图像设置高斯模糊*/
filter: blur(200px);
}
/* :nth-child(n) 选择器匹配父元素中的第 n 个子元素 */
section .color:nth-child(1) {
top: -350px;
width: 600px;
height: 600px;
background: #ff359b;
}
section .color:nth-child(2) {
bottom: -150px;
left: 100px;
width: 500px;
height: 500px;
background: #fffd87;
}
section .color:nth-child(3) {
bottom: 50px;
right: 100px;
width: 500px;
height: 500px;
background: #00d2ff;
}
.box {
position: relative;
}
/* 背景圆样式 */
.box .circle {
position: absolute;
background: rgba(255, 255, 255, 0.1);
/* backdrop-filter属性为一个元素后面区域添加模糊效果 */
backdrop-filter: blur(5px);
box-shadow: 0 25px 45px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.5);
border-right: 1px solid rgba(255, 255, 255, 0.2);
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 50%;
/* 使用filter(滤镜) 属性改变颜色
hue-rotate(deg) 给图像应用色相旋转
calc() 函数用于动态计算长度值
var() 函数调用自定义的CSS属性值x*/
filter: hue-rotate(calc(var(--x) * 70deg));
/* 调用动画animate需要10s完成动画
linear表示动画从头到尾的速度是相同的
infinite指定动画应该循环播放无限次*/
animation: animate 10s linear infinite;
/* 动态计算动画延迟几秒播放 */
animation-delay: calc(var(--x) * -1s);
}
/* 背景圆动画 */
@keyframes animate {
0%, 100%, {
transform: translateY(-50px);
}
50% {
transform: translateY(50px);
}
}
.box .circle:nth-child(1) {
top: -50px;
right: -60px;
width: 100px;
height: 100px;
}
.box .circle:nth-child(2) {
top: 150px;
left: -100px;
width: 120px;
height: 120px;
z-index: 2;
}
.box .circle:nth-child(3) {
bottom: 50px;
right: -60px;
width: 80px;
height: 80px;
z-index: 2;
}
.box .circle:nth-child(4) {
bottom: -80px;
left: 100px;
width: 60px;
height: 60px;
}
.box .circle:nth-child(5) {
top: -80px;
left: 140px;
width: 60px;
height: 60px;
}
/* 登录框样式 */
.container {
position: relative;
width: 400px;
min-height: 400px;
height: 400px;
background: rgba(255, 255, 255, 0.1);
display: flex;
justify-content: center;
align-items: center;
backdrop-filter: blur(5px);
box-shadow: 0 25px 45px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.5);
border-right: 1px solid rgba(255, 255, 255, 0.2);
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
.form {
position: relative;
width: 100%;
height: 100%;
padding: 50px;
background: rgba(255, 255, 255, 0.1);
}
/* 登录标题样式 */
.form h2 {
position: relative;
color: #fff;
font-size: 24px;
font-weight: 600;
letter-spacing: 5px;
margin-bottom: 30px;
cursor: pointer;
}
/* 登录标题的下划线样式 */
.form h2::before {
content: "";
position: absolute;
left: 0;
bottom: -10px;
width: 0px;
height: 3px;
background: #fff;
transition: 0.5s;
}
.form h2:hover:before {
width: 53px;
}
.form .inputBox {
width: 100%;
margin-top: 20px;
}
/* 输入框样式 */
.form .inputBox input {
width: 100%;
padding: 10px 20px;
background: rgba(255, 255, 255, 0.2);
outline: none;
border: none;
border-radius: 30px;
border: 1px solid rgba(255, 255, 255, 0.5);
border-right: 1px solid rgba(255, 255, 255, 0.2);
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
font-size: 16px;
letter-spacing: 1px;
color: #fff;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
}
.form .inputBox input::placeholder {
color: #fff;
}
/* 登录按钮样式 */
.submit {
background: #fff !important;
color: #666 !important;
max-width: 100px;
margin-bottom: 20px;
font-weight: 600;
cursor: pointer !important;
}
.forget {
margin-top: 6px;
color: #fff;
letter-spacing: 1px;
}
.forget a {
color: #fff;
font-weight: 600;
text-decoration: none;
}

View File

@ -1,100 +0,0 @@
@page "/"
@using AntDesign
@using AntDesign.TableModels
@using System.ComponentModel.DataAnnotations
@using Learn.VideoAnalysis.Controllers.Dto
@using SqlSugar
@using VideoAnalysisCore.Model
@using VideoAnalysisCore.Model.Dto
@using VideoAnalysisCore.Enum
<Table @ref="_table" Loading="tableLoading" TItem="VideoTaskDto" ScrollY="600px" PageSize="10" Total="_total" DataSource="_dataSource"
OnRowClick="(r)=>r.Expanded = !r.Expanded"
@bind-SelectedRows="_selectedRows" OnChange="OnChange"
OnExpand="OnExpand">
<TitleTemplate>
<Flex Justify="end" Gap="10">
<Button Type="primary" @onclick="ShowErrorTask">错误任务</Button>
</Flex>
</TitleTemplate>
<ColumnDefinitions Context="row">
<Selection />
<PropertyColumn Property="c=>c.Id" Width="130px" Filterable="true" Sortable="true" />
<PropertyColumn Property="c=>c.TagId" Width="160px" />
<PropertyColumn Property="c=>c.LastEnum" Width="150px" />
<PropertyColumn Property="c=>c.ApiToken" Width="150px" />
<PropertyColumn Property="c=>c.ComeFrom" Width="100px" />
<PropertyColumn Property="c=>c.MediaUrl" Width="320px" />
<PropertyColumn Property="c=>c.TotalTokens" Width="100px" />
<PropertyColumn Property="c=>c.CreateTime" />
</ColumnDefinitions>
<ExpandTemplate Context="rowData">
<Descriptions Title="任务详情" Bordered>
<DescriptionsItem Title="@rowData.Data.LastEnum.ToString()">
@rowData.Data.Progress%
</DescriptionsItem>
<DescriptionsItem Title="操作" Span="2">
<Button Type="@ButtonType.Primary"
Loading="rowRestartLoading"
OnClick="()=>RowRload(rowData)">
刷新数据
</Button>
<Button Type="@ButtonType.Primary" Danger @onclick="() => ReStartClick(rowData.Data)">
重试
</Button>
</DescriptionsItem>
<DescriptionsItem Title="任务时间轴" Span="5">
<Steps Current="@RowSTIndex(rowData)" Status="@RowSTStatus(rowData)">
<Step Title="下载文件"
Description="@RowST(rowData,RedisChannelEnum.DownloadFile)" />
<Step Title="分离音频"
Description="@RowST(rowData,RedisChannelEnum.SeparateAudio)" />
<Step Title="解析字幕"
Description="@RowST(rowData,RedisChannelEnum.ParsingCaptions)" />
<Step Title="解析说话人"
Description="@RowST(rowData,RedisChannelEnum.ParsingSpeaker)" />
<Step Title="Chat模型分析"
Description="@RowST(rowData,RedisChannelEnum.ChatModelAnalysis)" />
<Step Title="结束任务"
Description="@RowST(rowData,RedisChannelEnum.EndTask)" />
</Steps>
</DescriptionsItem>
@if (!string.IsNullOrEmpty(@rowData.Data.ErrorMessage))
{
<DescriptionsItem Title="任务异常" Span="3">
@rowData.Data.ErrorMessage
</DescriptionsItem>
}
</Descriptions>
</ExpandTemplate>
</Table>
<Modal Title="重试任务"
Width="400"
OnOk="ReStart"
@bind-Visible="@modalShow">
<Title Level="3">ID : @reStartTask.Id</Title>
<p></p>
<p>将从哪个步骤重试?</p>
<Select Style="width:220px;"
DataSource="SelectDataSource"
LabelName="@nameof(TextValue.Text)"
ValueName="@nameof(TextValue.Value)"
@bind-Value="@selectEnum">
</Select>
<br />
<br />
</Modal>

View File

@ -1,182 +0,0 @@
using AntDesign;
using AntDesign.TableModels;
using FFmpeg.NET.Services;
using Learn.VideoAnalysis.Controllers.Dto;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using SqlSugar;
using System.Linq.Expressions;
using System.Threading.Tasks;
using VideoAnalysisCore.Common;
using VideoAnalysisCore.Enum;
using VideoAnalysisCore.Model;
using VideoAnalysisCore.Model.Dto;
namespace Learn.VideoAnalysis.Components.Pages
{
public partial class VideoTaskPage : ComponentBase
{
[Inject] private ConfirmService ComfirmService { get; set; } = default!;
[Inject] private Repository<VideoTask> taskDB { get; set; } = default!;
[Inject] private INotificationService _notice { get; set; } = default!;
IEnumerable<VideoTaskDto> _selectedRows = [];
ITable _table;
List<VideoTaskDto> _dataSource = null;
RefAsync<int> _total = 0;
bool modalShow = false;
bool tableLoading = false;
private VideoTaskDto selectData;
private bool rowRestartLoading = false;
private VideoTaskDto reStartTask ;
static TextValue[] SelectDataSource =
Enum.GetValues(typeof(RedisChannelEnum))
.Cast<RedisChannelEnum>()
.Select(s => new TextValue(s.ToString(), (int)s))
.ToArray();
int selectEnum = 1;
int selectDefaultValue =1;
/// <summary>
/// 点击重试
/// </summary>
/// <param name="query"></param>
async void ReStartClick(VideoTaskDto query)
{
selectDefaultValue =
(await RedisExpand.Redis.HMGetAsync<int>(RedisExpandKey.Task(query.Id), "LastEnum")).FirstOrDefault();
selectEnum = selectDefaultValue;
reStartTask = query;
modalShow = true;
}
/// <summary>
/// 重试
/// </summary>
/// <param name="query"></param>
async void ReStart()
{
await RedisExpand.SetTaskErrorMessage(reStartTask.Id, null);
RedisExpand.InsertChannel((RedisChannelEnum)selectEnum, reStartTask.Id);
modalShow = false;
}
private QueryModel<VideoTaskDto> lastQuery = null;
/// <summary>
/// 分页 查询 筛选 时
/// </summary>
/// <param name="query"></param>
/// <param name="changed"></param>
async void ShowErrorTask(MouseEventArgs e)
{
_dataSource = await taskDB.AsQueryable()
.Where(s => s.ErrorMessage != null && s.ErrorMessage != string.Empty)
.Select<VideoTaskDto>()
.ToListAsync();
_total = _dataSource.Count();
tableLoading = false;
StateHasChanged();
}
/// <summary>
/// 分页 查询 筛选 时
/// </summary>
/// <param name="query"></param>
/// <param name="changed"></param>
async void OnChange(QueryModel<VideoTaskDto> query)
{
lastQuery = query;
tableLoading = true;
List<IConditionalModel> where = default!;
if (query.FilterModel != null && ((query.FilterModel?.Count() ?? 0) > 0))
{
where = query.ToSqlSugerWhere();
}
_dataSource = await taskDB.AsQueryable()
.Where(where)
.Select<VideoTaskDto>()
.OrderByDescending(s => s.Id)
.ToPageListAsync(query.PageIndex , query.PageSize, _total);
tableLoading = false;
StateHasChanged();
}
/// <summary>
/// 刷新数据
/// </summary>
/// <param name="rowData"></param>
public void RowRload(RowData<VideoTaskDto> rowData)
{
rowRestartLoading = true;
var item = rowData.Data;
if (item is null)
return;
var data = RedisExpand.Redis.HMGet<string>(RedisExpandKey.Task(item.Id),
"Progress", "LastEnum", "StartTime", "ErrorMessage");
item.Progress = float.Parse(data[0]);
item.LastEnum = data[1].ToEnum<RedisChannelEnum>() ?? default;
item.StartTimeDic = System.Text.Json.JsonSerializer.Deserialize<Dictionary<RedisChannelEnum, DateTime>>(data[2]) ?? null;
item.ErrorMessage = data[3];
rowRestartLoading = false;
StateHasChanged();
}
/// <summary>
///
/// </summary>
/// <returns></returns>
private string RowST(RowData<VideoTaskDto> rowData, RedisChannelEnum e)
{
var dic = rowData.Data.StartTimeDic;
if (dic is null || !dic.ContainsKey(e))
return "--";
return dic[e].ToString();
}
private string RowSTStatus(RowData<VideoTaskDto> rowData)
{
var dic = rowData.Data.StartTimeDic;
if (dic is null)
return "wait";
if (!string.IsNullOrEmpty(rowData.Data.ErrorMessage))
return "error";
if (dic.ContainsKey(RedisChannelEnum.EndTask))
return "finish";
return "wait";
}
private int RowSTIndex(RowData<VideoTaskDto> rowData)
{
return (int)rowData.Data.LastEnum;
}
private void OnExpand(RowData<VideoTaskDto> rowData)
{
RowRload(rowData);
}
/// <summary>
/// 在渲染页面之后
/// </summary>
/// <param name="firstRender"></param>
/// <returns></returns>
protected override async Task OnAfterRenderAsync(bool firstRender)
{
}
/// <summary>
/// 初始化
/// </summary>
protected override void OnInitialized()
{
}
private async Task<bool> Comfirm(string message)
{
return await ComfirmService.Show(message, "提示", ConfirmButtons.YesNo, ConfirmIcon.Warning) == ConfirmResult.Yes;
}
}
}

View File

@ -1,8 +0,0 @@
input[aria-hidden="true"] {
display: none !important;
}
.task_status_tag {
display:flex;
}

View File

@ -1,6 +0,0 @@
namespace VideoAnalysisRazor.Resources;
internal class I18n
{
}

View File

@ -1,129 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="menu.account.center" xml:space="preserve">
<value>Account Center</value>
</data>
<data name="menu.account.logout" xml:space="preserve">
<value>Logout</value>
</data>
<data name="menu.account.settings" xml:space="preserve">
<value>Settings</value>
</data>
</root>

View File

@ -1,129 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="menu.account.center" xml:space="preserve">
<value>个人中心</value>
</data>
<data name="menu.account.logout" xml:space="preserve">
<value>退出登录</value>
</data>
<data name="menu.account.settings" xml:space="preserve">
<value>个人设置</value>
</data>
</root>

View File

@ -1,18 +0,0 @@
@using Learn.VideoAnalysis.Components.Pages
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<CascadingValue Value="routeData">
@if (routeData.PageType == typeof(Login))
{
<RouteView RouteData="@routeData" />
}
else
{
<RouteView RouteData="routeData" DefaultLayout="typeof(VideoAnalysisRazor.Layouts.BasicLayout)" />
}
</CascadingValue>
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>
<AntContainer />

View File

@ -1,16 +0,0 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using AntDesign
@using AntDesign.Charts
@using AntDesign.ProLayout
@using AntDesign.Extensions.Localization
@using Learn.VideoAnalysis
@using VideoAnalysisRazor
@using Learn.VideoAnalysis.Components

View File

@ -1,181 +0,0 @@
using VideoAnalysisCore.Common;
using Learn.VideoAnalysis.Controllers.Dto;
using Microsoft.AspNetCore.Mvc;
using System.Reflection;
using VideoAnalysisCore.Enum;
using VideoAnalysisCore.Model;
using VideoAnalysisCore.AICore.FFMPGE;
using VideoAnalysisCore.Model.Dto;
using VideoAnalysisCore.AICore.ChatGPT.Dto;
using AntDesign;
using System.Threading.Tasks;
using FFmpeg.NET.Services;
using MapsterMapper;
using Mapster;
using VideoAnalysisCore.AICore.SherpaOnnx;
using System.Net;
using System.Security.Policy;
using System.IO;
namespace Learn.VideoAnalysis.Controllers
{
[ApiController]
[Route("[controller]/[action]")]
public class ApiController : ControllerBase
{
private readonly ILogger<ApiController> _logger;
private readonly IMapper mp;
private readonly Repository<VideoTask> videoTaskDB;
public ApiController(ILogger<ApiController> logger, Repository<VideoTask> videoTaskDB, IMapper mp)
{
_logger = logger;
this.videoTaskDB = videoTaskDB;
this.mp = mp;
}
private string GetClientIpAddress()
{
// 检查 X-Forwarded-For 请求头
if (HttpContext.Request.Headers.ContainsKey("X-Forwarded-For")
&& !string.IsNullOrEmpty(HttpContext.Request.Headers["X-Forwarded-For"]))
return HttpContext.Request.Headers["X-Forwarded-For"].ToString();
if (HttpContext.Connection.RemoteIpAddress != null)
return HttpContext.Connection.RemoteIpAddress.ToString();
throw new Exception("未能获取到客户端ip地址");
}
/// <summary>
/// 语音识别
/// </summary>
/// <param name="url">文件流</param>
/// <returns></returns>
[HttpGet(Name = "AudioRecognitionUrl")]
public async Task<IActionResult> AudioRecognitionUrl(string url)
{
try
{
using HttpClient client = new HttpClient();
// 发送GET请求获取网络文件流
using var networkStream = await client.GetStreamAsync(url);
var res = await SenseVoice.RunTask(networkStream);
return Ok(res);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
/// <summary>
/// 语音识别
/// </summary>
/// <param name="file">文件流</param>
/// <returns></returns>
[HttpPost(Name = "AudioRecognition")]
public async Task<IActionResult> AudioRecognition(IFormFile file)
{
using var s = file.OpenReadStream();
var res = await SenseVoice.RunTask(s);
return Ok(res);
}
[NonAction]
private static List<TimeBase> MergeTimeBases(IEnumerable<TimeBase> timeBases)
{
if (timeBases == null || timeBases.Count() == 0)
{
return new List<TimeBase>();
}
var mergedList = new List<TimeBase>();
// 初始化合并段
var current = timeBases.First();
current.Content = string.Empty;
foreach (var next in timeBases)
{
// 如果类型相同,则扩展时间段
if (current.TimeBaseType == next.TimeBaseType)
current.End = Math.Max(current.End, next.End);
else
{
// 类型不同,将当前时间段加入结果列表,并开始新时间段
mergedList.Add(current);
current = next;
current.Content = string.Empty;
}
}
// 添加最后的时间段
mergedList.Add(current);
return mergedList;
}
/// <summary>
/// 获取视频信息<para>taskId/tagId二选一</para>
/// </summary>
/// <param name="taskId"></param>
/// <param name="tagId">自定义id</param>
/// <returns></returns>
[HttpGet(Name = "TaskInfo")]
public async Task<IActionResult> TaskInfo(long taskId,string? tagId)
{
var task = await videoTaskDB.AsQueryable()
.WhereIF(taskId!=0, s => s.Id == taskId)
.WhereIF(!string.IsNullOrEmpty(tagId), s => s.TagId == tagId)
.FirstAsync();
if (task is null)
return BadRequest();
var taskData = task.ChatAnalysis.Adapt<TaskInfoRes>();
if (taskData is null)
return BadRequest();
taskData.Status = task.LastEnum;
if (task.LastEnum != RedisChannelEnum.EndTask)
return BadRequest(taskData);
if (taskData != null && taskData.TimeBase != null)
taskData.TimeBase = MergeTimeBases(taskData.TimeBase);
return Ok(taskData);
}
/// <summary>
/// 插入队列
/// </summary>
/// <param name="enum"></param>
/// <param name="msg"></param>
/// <returns></returns>
[HttpPost(Name = "TestInsertChannel")]
public IActionResult TestInsertChannel(int @enum=1, string msg= "1")
{
RedisExpand.InsertChannel(@enum.ToEnum<RedisChannelEnum>().Value
, msg);
return Ok();
}
/// <summary>
/// 视频处理
/// </summary>
/// <param name="req">请求体</param>
/// <returns></returns>
[HttpPost(Name = "VideoAnalysis")]
public async Task<IActionResult> VideoAnalysis(VideoAnalysisReq req)
{
if (!ModelState.IsValid) return BadRequest(ModelState);
if(await videoTaskDB.IsAnyAsync(s=>s.TagId == req.TagId) )
return BadRequest("重复添加");
// 自动映射属性到哈希
var task = new VideoTask()
{
ComeFrom = GetClientIpAddress(),
MediaUrl = req.MediaUrl,
ApiToken = req.ApiToken,
Tag = req.Tag,
TagId = req.TagId,
};
//入库
task.Id = await videoTaskDB.InsertReturnBigIdentityAsync(task);
var hashEntries = task.GetType()
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.ToDictionary(s => s.Name, s => s.GetValue(task));
RedisExpand.Redis.HMSet(RedisExpandKey.Task(task.Id), hashEntries);
RedisExpand.Redis.LPush(RedisExpandKey.ChannelKey, task.Id);
return Ok(task.Id);
}
}
}

View File

@ -1,87 +0,0 @@
using AntDesign;
using System.ComponentModel.DataAnnotations;
using VideoAnalysisCore.AICore.ChatGPT.Dto;
using VideoAnalysisCore.Enum;
namespace Learn.VideoAnalysis.Controllers.Dto
{
/// <summary>
/// 视频处理 请求
/// </summary>
public class VideoAnalysisReq
{
/// <summary>
/// 媒体路径
/// </summary>
[Required(ErrorMessage = "资源URL是必填项")]
[Url(ErrorMessage = "请输入有效的 URL")]
public string MediaUrl { get; set; } = string.Empty;
/// <summary>
/// ApiKey
/// </summary>
[Required(ErrorMessage = "接口Token是必填项")]
public string ApiToken { get; set; } = string.Empty;
/// <summary>
/// 自定义值 任务完成后附带通知
/// </summary>
public string Tag { get; set; } = string.Empty;
/// <summary>
/// 自定义Id可用于任务完成之后的查询
/// </summary>
public string? TagId { get; set; }
/// <summary>
///回调Api地址
/// </summary>
//[Required(ErrorMessage = "回调Api地址是必填项")]
//[Url(ErrorMessage = "请输入有效的 URL")]
//public string CallBackUrl { get; set; } = string.Empty;
}
public class TextValue
{
public TextValue(float v)
{
var s = TimeSpan.FromSeconds((double)v);
var td = new[] { s.Hours, s.Minutes, s.Seconds };
Text = string.Join(':', td.Where(s => s > 0));
Value = v;
}
public TextValue(string t,object v)
{
Text = t;
Value = v;
}
public TextValue()
{
}
public string Text { get; set; }
public object Value { get; set; }
}
public class TaskInfoRes: TaskRes
{
public TaskInfoRes()
{
}
/// <summary>
/// 任务当前执行状态
/// </summary>
public RedisChannelEnum Status { get; set; }
/// <summary>
/// 时间轴状态枚举
/// </summary>
public Dictionary<int, string> TimeTypeEnum =>
Enum.GetValues(typeof(TimeBaseTypeEnum))
.Cast<TimeBaseTypeEnum>()
.ToDictionary(x => (int)x, x => x.ToString());
/// <summary>
/// 时间轴合计
/// </summary>
public Dictionary<TimeBaseTypeEnum, TextValue>? TimeBaseTotal =>
TimeBase?.GroupBy(s => s.TimeBaseType??TimeBaseTypeEnum.)?
.ToDictionary(s => s.Key, s => new TextValue(s.Sum(x => x.End - x.Start)));
}
}

View File

@ -1,21 +1,18 @@
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM dotnet/aspnet:8.0
FROM learnvideo-net8.0:v0.1 AS base
RUN ["apt-get", "--assume-yes", "update"]
RUN ["apt-get", "--assume-yes", "install", "ffmpeg"]
WORKDIR /app
EXPOSE 9040
COPY . .
#设置时间为中国上海 环境为开发环境
ENV TZ=Asia/Shanghai
# 给我们要传的参数一个初始值
ENV va_args=
#RUN echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ sid main contrib non-free" > /etc/apt/sources.list
#RUN apt-get update && apt-get install -y apt-transport-https ca-certificates apt-utils libgdiplus libc6-dev && apt-get install -y libfreetype6 && apt-get install -y libfontconfig1 && apt-get install -y fontconfig
#
ENV ASPNETCORE_URLS=http://+:9040
ENTRYPOINT ["dotnet", "Learn.VideoAnalysis.dll"]
ENTRYPOINT dotnet Learn.VideoAnalysis.dll $va_args

View File

@ -0,0 +1,82 @@
using System.IdentityModel.Tokens.Jwt;
using Microsoft.Extensions.DependencyInjection;
using System.Net;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using VideoAnalysisCore.Common;
namespace Learn.VideoAnalysis.Expand
{
public static class AuthorizeExpand
{
/// <summary>
/// 框架API授权
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddPermissionAuthentication(this IServiceCollection services)
{
services.AddAuthentication()
.AddJwtBearer(Authentication.vdAdmin, options =>
{
options.RequireHttpsMetadata = false;
options.UseSecurityTokenValidators = true;
options.MapInboundClaims = false; // .NET 5+
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
options.TokenValidationParameters = new TokenValidationParameters
{
SaveSigninToken = false,//保存token,后台验证token是否生效(重要)
RequireExpirationTime = true, // 设置请求需要携带accesstoken的过期时间
ValidateIssuer = false,//必须验证签发人
ValidateAudience = false,//验证受众
ValidateLifetime = true,//是否验证Token有效期
ValidateIssuerSigningKey = true,//是否验证签名,不验证 会被篡改数据,不安全
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(AppCommon.Config.AuthKey.Secret)),//解密的密钥
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var token = context.Request.Headers["Authorization"].FirstOrDefault();
// 3. 安全提取令牌
if (!string.IsNullOrEmpty(token) && token.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
// 移除"Bearer "前缀并清除两端空格
token = token.Substring("Bearer ".Length).Trim();
context.Token = token;
}
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
// 可选:标记一下是否过期
if (context.Exception!=null)
context.Response.Headers["Token-Expired"] = "true";
return Task.CompletedTask;
},
OnChallenge = context =>
{
//if (context.Response.Headers.ContainsKey("Token-Expired"))
//{
//}
context.HandleResponse();
context.Response.StatusCode = 401;
context.Response.ContentType = "application/json";
context.Response.Headers["Access-Control-Allow-Origin"] = "*"; // ✅ 补这个
var data = new
{
Code = 401,
Message = context.Error + context.AuthenticateFailure?.Message
};
return context.Response.WriteAsync(data.ToJson());
}
};
});
return services;
}
}
}

View File

@ -0,0 +1,43 @@
using Coravel;
using Coravel.Scheduling.Schedule;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VideoAnalysisCore.Job;
namespace Learn.VideoAnalysis.Expand
{
public static class CoravelExpand
{
public static void AddCoravel(this IServiceCollection service)
{
Console.WriteLine($"{DateTime.Now}=>初始化 Coravel");
service.AddScheduler();
#if !DEBUG
service.AddTransient<TaskFileClearJob>();
#endif
service.AddTransient<ClearAllCacheJob>();
service.AddTransient<NodePackageJob>();
// 注册心跳 Job
service.AddTransient<DeviceHeartbeatJob>();
}
public static void UseCoravelExpand(this IApplicationBuilder provider)
{
provider.ApplicationServices.UseScheduler(scheduler =>
{
//任务缓存清理
scheduler.Schedule<ClearAllCacheJob>().HourlyAt(10);
//在线心跳 30秒一次
scheduler.Schedule<DeviceHeartbeatJob>().EveryThirtySeconds();
//强制清理所有缓存内容
//scheduler.Schedule<ClearAllCacheJob>().Hourly();
//scheduler.Schedule<ClearAllCacheJob>().EverySeconds(40);
});
}
}
}

View File

@ -0,0 +1,106 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace Learn.VideoAnalysis.Expand
{
public static class SwaggerExpand
{
public static void AddSwaggerExpand(this IServiceCollection s, string name = "")
{
s.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Description = name
});
c.OperationFilter<SwaggerFileUploadFilter>();
//按Http类型排序
c.OrderActionsBy(o => o.GroupName);
c.DocInclusionPredicate((docName, apiDesc) =>
{
try
{
if (!apiDesc.TryGetMethodInfo(out MethodInfo methodInfo)) return false;
var versions = methodInfo.DeclaringType.GetCustomAttributes(true)
.OfType<ApiExplorerSettingsAttribute>().Select(attr => attr.GroupName);
if (docName.ToLower() == "v1" && versions.FirstOrDefault() == null)
return true;
return versions.Any(v => v.ToString() == docName);
}
catch (Exception ex)
{
throw;
}
});
//添加全局安全性需求
c.AddSecurityRequirement(new OpenApiSecurityRequirement{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "bearerAuth"
}
}, Array.Empty<string>()
}
});
//添加一个必须的全局安全信息和AddSecurityDefinition方法指定的方案名称要一致这里是Bearer。
c.AddSecurityDefinition("bearerAuth",
new OpenApiSecurityScheme
{
Description = "使用JWT授权头。示例:\"Authorization: Bearer {token}\"",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.Http,
//内容为以 bearer开头
Scheme = "bearer",
BearerFormat = "JWT"
});
DirectoryInfo dirs = new DirectoryInfo(AppContext.BaseDirectory);
FileInfo[] files = dirs.GetFiles("*.xml");
foreach (var path in files)
{
c.IncludeXmlComments(path.FullName);
}
});
//s.AddSwaggerGenNewtonsoftSupport();
}
}
class SwaggerFileUploadFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (context.ApiDescription.ActionDescriptor.Parameters.Any(w => w.ParameterType == typeof(IFormCollection)))
{
Dictionary<string, OpenApiSchema> schema = new Dictionary<string, OpenApiSchema>();
schema["fileName"] = new OpenApiSchema { Description = "选择上传文件", Type = "string", Format = "binary" };
Dictionary<string, OpenApiMediaType> content = new Dictionary<string, OpenApiMediaType>();
content["multipart/form-data"] = new OpenApiMediaType { Schema = new OpenApiSchema { Type = "object", Properties = schema } };
operation.RequestBody = new OpenApiRequestBody() { Content = content };
}
}
}
}

View File

@ -1,2 +1,4 @@
global using VideoAnalysisRazor.Resources;
global using AntDesign;

global using VideoAnalysisCore.Model;
global using VideoAnalysisCore.Model.Dto;
global using VideoAnalysisCore.Model.Enum;

View File

@ -17,24 +17,30 @@
<ItemGroup>
<None Remove="Dockerfile" />
<None Remove="sources.list" />
<None Remove="WebUI\dist\index.html" />
</ItemGroup>
<ItemGroup>
<Content Include="Dockerfile">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="sources.list">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="WebUI\dist\index.html">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\VideoAnalysisCore\VideoAnalysisCore.csproj" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.13.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.18" />
<PackageReference Include="Mapster.DependencyInjection" Version="1.0.2-pre01" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.5" />
<PackageReference Include="AntDesign.Charts" Version="0.4.0" />
<PackageReference Include="AntDesign.Extensions.Localization" Version="0.20.2.1" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.13.0" />
<PackageReference Include="AlibabaCloud.SDK.Vod20170321" Version="3.11.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageReference Include="System.Net.Http.Json" Version="8.0.0" />
@ -42,8 +48,11 @@
</ItemGroup>
<ItemGroup>
<Content Update="Components\Pages\Login.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<None Include="WebUI\dist\**\*.*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<Content Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>

View File

@ -1,11 +1,26 @@
using VideoAnalysisCore.Common;
using Learn.VideoAnalysis.Components;
using VideoAnalysisCore.Common;
using Microsoft.OpenApi.Models;
using AntDesign.ProLayout;
using VideoAnalysisCore.AICore.ChatGPT;
using VideoAnalysisCore.AICore.ChatGPT.KIMI;
using VideoAnalysisCore.AICore.SherpaOnnx;
using Mapster;
using VideoAnalysisCore.AICore.GPT;
using VideoAnalysisCore.AICore.GPT.ChatGPT;
using Microsoft.Extensions.FileProviders;
using VideoAnalysisCore.AICore.GPT.DeepSeek;
using Microsoft.Extensions.DependencyInjection;
using VideoAnalysisCore.Common.Expand;
using Learn.VideoAnalysis.Expand;
using Microsoft.AspNetCore.Mvc.Formatters;
using System.Security.Cryptography;
using System.Diagnostics;
using VideoAnalysisCore.AICore.FFMPGE;
using System.Text.Encodings.Web;
using System.Text.Unicode;
using System.Text.Json;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Hosting.Server;
using System.IO.Compression;
using System.Text;
using System.Text.Json.Nodes;
@ -15,45 +30,63 @@ namespace Learn.VideoAnalysis
{
public static void Main(string[] args)
{
//交互式环境选择函数
AppConfigExpand.SelectEnvironment(ref args);
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
//.AddInteractiveWebAssemblyComponents();
builder.Services.AddHttpContextAccessor();
//设置接口请求体最大100m
builder.WebHost.ConfigureKestrel(serverOptions =>
{
serverOptions.Limits.MaxRequestBodySize = 100_000_000; // 100MB
});
builder.Services.AddLogging(loggingBuilder =>
{
loggingBuilder.ClearProviders(); // 清除默认的日志提供程序
loggingBuilder.AddConsole(); // 添加控制台日志提供程序
loggingBuilder.SetMinimumLevel(LogLevel.Warning); // 设置最小日志级别为 Warning
});
builder.Services.AddControllers();
//绑定 appsetting 配置
builder.AddAppConfig(args);
//初始化 插件
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "Learn.VideoAnalysis",
Version = "v1",
Description = "教学视频分析平台v1"
});
var file = Path.Combine(AppContext.BaseDirectory, "Learn.VideoAnalysis.xml"); // xml文档绝对路径
c.IncludeXmlComments(file, true); // true : 显示控制器层注释
c.OrderActionsBy(o => o.RelativePath); // 对action的名称进行排序如果有多个就可以看见效果了。
});
//swagger
builder.Services.AddSwaggerExpand("AI视频分析");
//鉴权
builder.Services.AddPermissionAuthentication();
//数据库
builder.Services.AddSqlSugarExpand();
//reids
builder.Services.AddRedisExpand();
//工作流
builder.Services.AddSimpleTexOcrClient();
builder.Services.AddDownloadFileExpand();
builder.Services.AddFFMPGEExpand();
builder.Services.AddAlibabaCloudVod();
builder.Services.AddAliyunOSS();
//语音转写
builder.Services.AddSenseVoiceExpand();
builder.Services.AddFunASRNanoExpand();
builder.Services.AddSherpaVadExpand();
//builder.Services.AddSpeakerAI();
//定时任务
builder.Services.AddCoravel();
//绑定 appsetting 配置
builder.Configuration.GetSection("AppConfig").Bind(AppCommon.Config);
//初始化 插件
builder.Services.InitSqlSugar();
RedisExpand.Init();
Speaker.Init();
//SenseVoice.Init();
//异常过滤器
builder.Services.AddControllersWithViews(options =>
{
options.Filters.Add(typeof(ExceptionFilter));
});
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All);//中文转换时不使用Unicode
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;// 默认小驼峰 null 大驼峰
options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
});
builder.Services.AddScoped(sp =>
@ -69,47 +102,51 @@ namespace Learn.VideoAnalysis
return new HttpClient();
});
//VideoAnalysisRazor.Program.AddClientServices(builder.Services);
builder.Services.AddAntDesign();
builder.Services.AddMapster();
builder.Services.Configure<ProSettings>(builder.Configuration.GetSection("ProSettings"));
builder.Services.AddCorsExpand();
builder.Services.AddHttpClient();
builder.Services.AddSingleton<MoonshotClient>();
builder.Services.AddSingleton<IBserGPT, KIMI_GPT>();
builder.Services.AddHttpContextAccessor();
builder.Services.AddGPTService();
builder.Services.AddTaskSubscribe();
var app = builder.Build();
AppCommon.Services = app.Services;
//允许跨域
app.UseCorsExpand();
app.UseMiddleware<BasicAuthMiddleware>("Swagger");
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
app.UseExceptionHandler("/Error");
}
//else
//{
// app.UseExceptionHandler("/Login");
//}
//添加wwwroot 静态目录
app.UseStaticFiles();
//添加 自定义 静态目录
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(AppCommon.TaskCachedFile),
RequestPath = "/video",
});
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(AppCommon.WebUIFile),
RequestPath = "/ui",
});
app.UseAntiforgery();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
//.AddInteractiveWebAssemblyRenderMode()
//.AddAdditionalAssemblies(typeof(VideoAnalysisRazor._Imports).Assembly);
app.MapControllers();
SqlSugarExpand.InitDB();
//自定义 应用
app.UseSqlSugarExpand();
app.UseCoravelExpand();
app.UseServiceSystem(() =>
{
//开启redis队列服务
_ = AppCommon.Services.GetRequiredService<RedisInit>();
});
app.Run();

View File

@ -1,31 +1,15 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:9624",
"sslPort": 0
}
},
"profiles": {
"http:5238": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "",
"applicationUrl": "http://*:5238",
"launchUrl": "ui/index.html",
"applicationUrl": "http://*:7532",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,4 @@
> 1%
last 2 versions
not dead
not ie 11

View File

@ -0,0 +1,21 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
.eslintcache
report.html
yarn.lock
npm-debug.log*
.pnpm-error.log*
.pnpm-debug.log
tests/**/coverage/
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
tsconfig.tsbuildinfo

View File

@ -0,0 +1,14 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false

5
VideoAnalysis/WebUI/.env Normal file
View File

@ -0,0 +1,5 @@
# 平台本地运行端口号
VITE_PORT = 8848
# 是否隐藏首页 隐藏 true 不隐藏 false 勿删除VITE_HIDE_HOME只需在.env文件配置
VITE_HIDE_HOME = false

View File

@ -0,0 +1,17 @@
# 平台本地运行端口号
VITE_PORT = 8848
# 开发环境读取配置文件路径
VITE_PUBLIC_PATH = /
# 开发环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数"
VITE_ROUTER_HISTORY = "hash"
# 接口地址
VITE_API_BASEURL = "http://192.168.2.33:7532"
# # 接口地址
# VITE_API_BASEURL = "https://learn-archives-admin-dev.23544.com/api"
# #数据中心后台地址
# VITE_API_USERCENTER_URL = "https://dca.w.23544.com:8843/api"

View File

@ -0,0 +1,16 @@
# 线上环境平台打包路径
VITE_PUBLIC_PATH = /ui/
# 线上环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数"
VITE_ROUTER_HISTORY = "hash"
# 是否在打包时使用cdn替换本地库 替换 true 不替换 false
VITE_CDN = false
# 是否启用gzip压缩或brotli压缩分两种情况删除原始文件和不删除原始文件
# 压缩时不删除原始文件的配置gzip、brotli、both同时开启 gzip 与 brotli 压缩、none不开启压缩默认
# 压缩时删除原始文件的配置gzip-clear、brotli-clear、both-clear同时开启 gzip 与 brotli 压缩、none不开启压缩默认
VITE_COMPRESSION = "none"
VITE_API_BASEURL = "/"

View File

@ -0,0 +1,22 @@
# 预发布也需要生产环境的行为
# https://cn.vitejs.dev/guide/env-and-mode.html#modes
# NODE_ENV = development
VITE_PUBLIC_PATH = /
# 预发布环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数"
VITE_ROUTER_HISTORY = "hash"
# 是否在打包时使用cdn替换本地库 替换 true 不替换 false
VITE_CDN = false
# 是否启用gzip压缩或brotli压缩分两种情况删除原始文件和不删除原始文件
# 压缩时不删除原始文件的配置gzip、brotli、both同时开启 gzip 与 brotli 压缩、none不开启压缩默认
# 压缩时删除原始文件的配置gzip-clear、brotli-clear、both-clear同时开启 gzip 与 brotli 压缩、none不开启压缩默认
VITE_COMPRESSION = "none"
# 接口地址
VITE_API_BASEURL = "https://learn-archives-admin-dev.23544.com/api"
#数据中心后台地址
VITE_API_USERCENTER_URL = "https://dca.w.23544.com:8843/api"

View File

@ -0,0 +1,20 @@
{
"*.{js,jsx,ts,tsx}": [
"prettier --cache --ignore-unknown --write",
"eslint --cache --fix"
],
"{!(package)*.json,*.code-snippets,.!({browserslist,npm,nvm})*rc}": [
"prettier --cache --write--parser json"
],
"package.json": ["prettier --cache --write"],
"*.vue": [
"prettier --write",
"eslint --cache --fix",
"stylelint --fix --allow-empty-input"
],
"*.{css,scss,html}": [
"prettier --cache --ignore-unknown --write",
"stylelint --fix --allow-empty-input"
],
"*.md": ["prettier --cache --ignore-unknown --write"]
}

View File

@ -0,0 +1,11 @@
{
"default": true,
"MD003": false,
"MD033": false,
"MD013": false,
"MD001": false,
"MD025": false,
"MD024": false,
"MD007": { "indent": 4 },
"no-hard-tabs": false
}

View File

@ -0,0 +1,4 @@
shell-emulator=true
shamefully-hoist=true
enable-pre-post-scripts=false
strict-peer-dependencies=false

View File

@ -0,0 +1 @@
v22.14.0

View File

@ -0,0 +1,9 @@
// @ts-check
/** @type {import("prettier").Config} */
export default {
bracketSpacing: true,
singleQuote: false,
arrowParens: "avoid",
trailingComma: "none"
};

View File

@ -0,0 +1,4 @@
/dist/*
/public/*
public/*
src/style/reset.scss

View File

@ -0,0 +1,20 @@
FROM node:20-alpine as build-stage
WORKDIR /app
RUN corepack enable
RUN corepack prepare pnpm@latest --activate
RUN npm config set registry https://registry.npmmirror.com
COPY .npmrc package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-present, pure-admin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,47 @@
<h1>vue-pure-admin Lite Editionno i18n version</h1>
[![license](https://img.shields.io/github/license/pure-admin/vue-pure-admin.svg)](LICENSE)
**English** | [中文](./README.md)
## Introduce
The simplified version is based on the shelf extracted from [vue-pure-admin](https://github.com/pure-admin/vue-pure-admin), which contains main functions and is more suitable for actual project development. The packaged size is introduced globally [element-plus](https://element-plus.org) is still below `2.3MB`, and the full version of the code will be permanently synchronized. After enabling `brotli` compression and `cdn` to replace the local library mode, the package size is less than `350kb`
## `js` version
[Click me to view js version](https://pure-admin.cn/pages/js/)
## `max` version
[Click me to view the max version](https://pure-admin.cn/pages/max/)
## Supporting video
[Click me to view UI design](https://www.bilibili.com/video/BV17g411T7rq)
[Click me to view the rapid development tutorial](https://www.bilibili.com/video/BV1kg411v7QT)
## Nanny-level documents
[Click me to view vue-pure-admin documentation](https://pure-admin.cn/)
[Click me to view @pureadmin/utils documentation](https://pure-admin-utils.netlify.app)
## Quality service, software outsourcing, sponsorship support
[Click me to view details](https://pure-admin.cn/pages/service/)
## Preview
[Click me to view the preview station](https://pure-admin-thin.netlify.app/#/login)
## Maintainer
[xiaoxian521](https://github.com/xiaoxian521)
## ⚠️ Attention
The Lite version does not accept any issues and prs. If you have any questions, please go to the full version [issues](https://github.com/pure-admin/vue-pure-admin/issues/new/choose) to mention, thank you!
## License
[MIT © 2020-present, pure-admin](./LICENSE)

View File

@ -0,0 +1,51 @@
<h1>vue-pure-admin精简版非国际化版本</h1>
[![license](https://img.shields.io/github/license/pure-admin/vue-pure-admin.svg)](LICENSE)
**中文** | [English](./README.en-US.md)
## 介绍
精简版是基于 [vue-pure-admin](https://github.com/pure-admin/vue-pure-admin) 提炼出的架子,包含主体功能,更适合实际项目开发,打包后的大小在全局引入 [element-plus](https://element-plus.org) 的情况下仍然低于 `2.3MB`,并且会永久同步完整版的代码。开启 `brotli` 压缩和 `cdn` 替换本地库模式后,打包大小低于 `350kb`
## 版本选择
当前是非国际化版本,如果您需要国际化版本 [请点击](https://github.com/pure-admin/pure-admin-thin/tree/i18n)
## `js` 版本
[点我查看 js 版本](https://pure-admin.cn/pages/js/)
## `max` 版本
[点我查看 max 版本](https://pure-admin.cn/pages/max/)
## 配套视频
[点我查看 UI 设计](https://www.bilibili.com/video/BV17g411T7rq)
[点我查看快速开发教程](https://www.bilibili.com/video/BV1kg411v7QT)
## 配套保姆级文档
[点我查看 vue-pure-admin 文档](https://pure-admin.cn/)
[点我查看 @pureadmin/utils 文档](https://pure-admin-utils.netlify.app)
## 优质服务、软件外包、赞助支持
[点我查看详情](https://pure-admin.cn/pages/service/)
## 预览
[查看预览](https://pure-admin-thin.netlify.app/#/login)
## 维护者
[xiaoxian521](https://github.com/xiaoxian521)
## ⚠️ 注意
精简版不接受任何 `issues``pr`,如果有问题请到完整版 [issues](https://github.com/pure-admin/vue-pure-admin/issues/new/choose) 去提,谢谢!
## 许可证
[MIT © 2020-present, pure-admin](./LICENSE)

View File

@ -0,0 +1,55 @@
import { Plugin as importToCDN } from "vite-plugin-cdn-import";
/**
* @description `cdn`使cdn模式 .env.production VITE_CDN true
* cdnhttps://www.bootcdn.cn当然你也可以选择 https://unpkg.com 或者 https://www.jsdelivr.com
* 使jscss文件cdn
*/
export const cdn = importToCDN({
//prodUrl解释 name: 对应下面modules的nameversion: 自动读取本地package.json中dependencies依赖中对应包的版本号path: 对应下面modules的path当然也可写完整路径会替换prodUrl
prodUrl: "https://cdn.bootcdn.net/ajax/libs/{name}/{version}/{path}",
modules: [
{
name: "vue",
var: "Vue",
path: "vue.global.prod.min.js"
},
{
name: "vue-router",
var: "VueRouter",
path: "vue-router.global.min.js"
},
// 项目中没有直接安装vue-demi但是pinia用到了所以需要在引入pinia前引入vue-demihttps://github.com/vuejs/pinia/blob/v2/packages/pinia/package.json#L77
{
name: "vue-demi",
var: "VueDemi",
path: "index.iife.min.js"
},
{
name: "pinia",
var: "Pinia",
path: "pinia.iife.min.js"
},
{
name: "element-plus",
var: "ElementPlus",
path: "index.full.min.js",
css: "index.min.css"
},
{
name: "axios",
var: "axios",
path: "axios.min.js"
},
{
name: "dayjs",
var: "dayjs",
path: "dayjs.min.js"
},
{
name: "echarts",
var: "echarts",
path: "echarts.min.js"
}
]
});

View File

@ -0,0 +1,63 @@
import type { Plugin } from "vite";
import { isArray } from "@pureadmin/utils";
import compressPlugin from "vite-plugin-compression";
export const configCompressPlugin = (
compress: ViteCompression
): Plugin | Plugin[] => {
if (compress === "none") return null;
const gz = {
// 生成的压缩包后缀
ext: ".gz",
// 体积大于threshold才会被压缩
threshold: 0,
// 默认压缩.js|mjs|json|css|html后缀文件设置成true压缩全部文件
filter: () => true,
// 压缩后是否删除原始文件
deleteOriginFile: false
};
const br = {
ext: ".br",
algorithm: "brotliCompress",
threshold: 0,
filter: () => true,
deleteOriginFile: false
};
const codeList = [
{ k: "gzip", v: gz },
{ k: "brotli", v: br },
{ k: "both", v: [gz, br] }
];
const plugins: Plugin[] = [];
codeList.forEach(item => {
if (compress.includes(item.k)) {
if (compress.includes("clear")) {
if (isArray(item.v)) {
item.v.forEach(vItem => {
plugins.push(
compressPlugin(Object.assign(vItem, { deleteOriginFile: true }))
);
});
} else {
plugins.push(
compressPlugin(Object.assign(item.v, { deleteOriginFile: true }))
);
}
} else {
if (isArray(item.v)) {
item.v.forEach(vItem => {
plugins.push(compressPlugin(vItem));
});
} else {
plugins.push(compressPlugin(item.v));
}
}
}
});
return plugins;
};

View File

@ -0,0 +1,57 @@
import type { Plugin } from "vite";
import gradient from "gradient-string";
import { getPackageSize } from "./utils";
import dayjs, { type Dayjs } from "dayjs";
import duration from "dayjs/plugin/duration";
import boxen, { type Options as BoxenOptions } from "boxen";
dayjs.extend(duration);
const welcomeMessage = gradient(["cyan", "magenta"]).multiline(
`您好! 欢迎使用 pure-admin 开源项目\n我们为您精心准备了下面两个贴心的保姆级文档\nhttps://pure-admin.cn\nhttps://pure-admin-utils.netlify.app`
);
const boxenOptions: BoxenOptions = {
padding: 0.5,
borderColor: "cyan",
borderStyle: "round"
};
export function viteBuildInfo(): Plugin {
let config: { command: string };
let startTime: Dayjs;
let endTime: Dayjs;
let outDir: string;
return {
name: "vite:buildInfo",
configResolved(resolvedConfig) {
config = resolvedConfig;
outDir = resolvedConfig.build?.outDir ?? "dist";
},
buildStart() {
console.log(boxen(welcomeMessage, boxenOptions));
if (config.command === "build") {
startTime = dayjs(new Date());
}
},
closeBundle() {
if (config.command === "build") {
endTime = dayjs(new Date());
getPackageSize({
folder: outDir,
callback: (size: string) => {
console.log(
boxen(
gradient(["cyan", "magenta"]).multiline(
`🎉 恭喜打包完成(总用时${dayjs
.duration(endTime.diff(startTime))
.format("mm分ss秒")}${size}`
),
boxenOptions
)
);
}
});
}
}
};
}

View File

@ -0,0 +1,29 @@
/**
* `vite.config.ts` `optimizeDeps.include`
* `vite` include esm node_modules/.vite
* include里vite 使 node_modules/.vite
* 使 src/main.ts include vite node_modules/.vite
*/
const include = [
"qs",
"mitt",
"dayjs",
"axios",
"pinia",
"vue-types",
"js-cookie",
"vue-tippy",
"pinyin-pro",
"sortablejs",
"@vueuse/core",
"@pureadmin/utils",
"responsive-storage"
];
/**
*
* 使
*/
const exclude = ["@iconify/json"];
export { include, exclude };

View File

@ -0,0 +1,66 @@
import { cdn } from "./cdn";
import vue from "@vitejs/plugin-vue";
import { viteBuildInfo } from "./info";
import svgLoader from "vite-svg-loader";
import Icons from "unplugin-icons/vite";
import type { PluginOption } from "vite";
import vueJsx from "@vitejs/plugin-vue-jsx";
import tailwindcss from "@tailwindcss/vite";
import { configCompressPlugin } from "./compress";
import removeNoMatch from "vite-plugin-router-warn";
import { visualizer } from "rollup-plugin-visualizer";
import removeConsole from "vite-plugin-remove-console";
import { codeInspectorPlugin } from "code-inspector-plugin";
// import { vitePluginFakeServer } from "vite-plugin-fake-server";
export function getPluginsList(
VITE_CDN: boolean,
VITE_COMPRESSION: ViteCompression
): PluginOption[] {
const lifecycle = process.env.npm_lifecycle_event;
return [
tailwindcss(),
vue(),
// jsx、tsx语法支持
vueJsx(),
/**
* DOM IDE
* Mac Option + Shift
* Windows Alt + Shift
* https://inspector.fe-dev.cn/guide/start.html
*/
codeInspectorPlugin({
bundler: "vite",
hideConsole: true
}),
viteBuildInfo(),
/**
* vue-router动态路由警告No match found for location with path
* https://github.com/vuejs/router/issues/521 和 https://github.com/vuejs/router/issues/359
* vite-plugin-router-warn只在开发环境下启用vue-router文件并且只在服务启动或重启时运行一次
*/
removeNoMatch(),
// mock支持
// vitePluginFakeServer({
// logger: false,
// include: "mock",
// infixName: false,
// enableProd: true
// }),
// svg组件化支持
svgLoader(),
// 自动按需加载图标
Icons({
compiler: "vue3",
scale: 1
}),
VITE_CDN ? cdn : null,
configCompressPlugin(VITE_COMPRESSION),
// 线上环境删除console
removeConsole({ external: ["src/assets/iconfont/iconfont.js"] }),
// 打包分析
lifecycle === "report"
? visualizer({ open: true, brotliSize: true, filename: "report.html" })
: (null as any)
];
}

View File

@ -0,0 +1,110 @@
import dayjs from "dayjs";
import { readdir, stat } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import { sum, formatBytes } from "@pureadmin/utils";
import {
name,
version,
engines,
dependencies,
devDependencies
} from "../package.json";
/** 启动`node`进程时所在工作目录的绝对路径 */
const root: string = process.cwd();
/**
* @description
* @param dir `build`
* @param metaUrl `url``build``import.meta.url`
*/
const pathResolve = (dir = ".", metaUrl = import.meta.url) => {
// 当前文件目录的绝对路径
const currentFileDir = dirname(fileURLToPath(metaUrl));
// build 目录的绝对路径
const buildDir = resolve(currentFileDir, "build");
// 解析的绝对路径
const resolvedPath = resolve(currentFileDir, dir);
// 检查解析的绝对路径是否在 build 目录内
if (resolvedPath.startsWith(buildDir)) {
// 在 build 目录内,返回当前文件路径
return fileURLToPath(metaUrl);
}
// 不在 build 目录内,返回解析后的绝对路径
return resolvedPath;
};
/** 设置别名 */
const alias: Record<string, string> = {
"@": pathResolve("../src"),
"@build": pathResolve()
};
/** 平台的名称、版本、运行所需的`node`和`pnpm`版本、依赖、最后构建时间的类型提示 */
const __APP_INFO__ = {
pkg: { name, version, engines, dependencies, devDependencies },
lastBuildTime: dayjs(new Date()).format("YYYY-MM-DD HH:mm:ss")
};
/** 处理环境变量 */
const wrapperEnv = (envConf: Recordable): ViteEnv => {
// 默认值
const ret: ViteEnv = {
VITE_PORT: 8848,
VITE_PUBLIC_PATH: "",
VITE_ROUTER_HISTORY: "",
VITE_CDN: false,
VITE_HIDE_HOME: "false",
VITE_COMPRESSION: "none"
};
for (const envName of Object.keys(envConf)) {
let realName = envConf[envName].replace(/\\n/g, "\n");
realName =
realName === "true" ? true : realName === "false" ? false : realName;
if (envName === "VITE_PORT") {
realName = Number(realName);
}
ret[envName] = realName;
if (typeof realName === "string") {
process.env[envName] = realName;
} else if (typeof realName === "object") {
process.env[envName] = JSON.stringify(realName);
}
}
return ret;
};
const fileListTotal: number[] = [];
/** 获取指定文件夹中所有文件的总大小 */
const getPackageSize = options => {
const { folder = "dist", callback, format = true } = options;
readdir(folder, (err, files: string[]) => {
if (err) throw err;
let count = 0;
const checkEnd = () => {
++count == files.length &&
callback(format ? formatBytes(sum(fileListTotal)) : sum(fileListTotal));
};
files.forEach((item: string) => {
stat(`${folder}/${item}`, async (err, stats) => {
if (err) throw err;
if (stats.isFile()) {
fileListTotal.push(stats.size);
checkEnd();
} else if (stats.isDirectory()) {
getPackageSize({
folder: `${folder}/${item}/`,
callback: checkEnd
});
}
});
});
files.length === 0 && callback(0);
});
};
export { root, pathResolve, alias, __APP_INFO__, wrapperEnv, getPackageSize };

View File

@ -0,0 +1,35 @@
// @ts-check
/** @type {import("@commitlint/types").UserConfig} */
export default {
ignores: [commit => commit.includes("init")],
extends: ["@commitlint/config-conventional"],
rules: {
"body-leading-blank": [2, "always"],
"footer-leading-blank": [1, "always"],
"header-max-length": [2, "always", 108],
"subject-empty": [2, "never"],
"type-empty": [2, "never"],
"type-enum": [
2,
"always",
[
"feat",
"fix",
"perf",
"style",
"docs",
"test",
"refactor",
"build",
"ci",
"chore",
"revert",
"wip",
"workflow",
"types",
"release"
]
]
}
};

View File

@ -0,0 +1,173 @@
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginVue from "eslint-plugin-vue";
import * as parserVue from "vue-eslint-parser";
import configPrettier from "eslint-config-prettier";
import pluginPrettier from "eslint-plugin-prettier";
import { defineConfig, globalIgnores } from "eslint/config";
export default defineConfig([
globalIgnores([
"**/.*",
"dist/*",
"*.d.ts",
"public/*",
"src/assets/**",
"src/**/iconfont/**"
]),
{
...js.configs.recommended,
languageOptions: {
globals: {
// types/index.d.ts
RefType: "readonly",
EmitType: "readonly",
TargetContext: "readonly",
ComponentRef: "readonly",
ElRef: "readonly",
ForDataType: "readonly",
AnyFunction: "readonly",
PropType: "readonly",
Writable: "readonly",
Nullable: "readonly",
NonNullable: "readonly",
Recordable: "readonly",
ReadonlyRecordable: "readonly",
Indexable: "readonly",
DeepPartial: "readonly",
Without: "readonly",
Exclusive: "readonly",
TimeoutHandle: "readonly",
IntervalHandle: "readonly",
Effect: "readonly",
ChangeEvent: "readonly",
WheelEvent: "readonly",
ImportMetaEnv: "readonly",
Fn: "readonly",
PromiseFn: "readonly",
ComponentElRef: "readonly",
parseInt: "readonly",
parseFloat: "readonly"
}
},
plugins: {
prettier: pluginPrettier
},
rules: {
...configPrettier.rules,
...pluginPrettier.configs.recommended.rules,
"no-debugger": "off",
"no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_"
}
],
"prettier/prettier": [
"error",
{
endOfLine: "auto"
}
]
}
},
...tseslint.config({
extends: [...tseslint.configs.recommended, "plugin:prettier/recommended"],
files: ["**/*.?([cm])ts", "**/*.?([cm])tsx"],
rules: {
"@typescript-eslint/no-redeclare": "error",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/prefer-as-const": "warn",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unused-expressions": "off",
"@typescript-eslint/no-unsafe-function-type": "off",
"@typescript-eslint/no-import-type-side-effects": "error",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/consistent-type-imports": [
"error",
{ disallowTypeAnnotations: false, fixStyle: "inline-type-imports" }
],
"@typescript-eslint/prefer-literal-enum-member": [
"error",
{ allowBitwiseExpressions: true }
],
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_"
}
]
}
}),
{
files: ["**/*.d.ts"],
rules: {
"eslint-comments/no-unlimited-disable": "off",
"import/no-duplicates": "off",
"no-restricted-syntax": "off",
"unused-imports/no-unused-vars": "off"
}
},
{
files: ["**/*.?([cm])js"],
rules: {
"@typescript-eslint/no-require-imports": "off"
}
},
{
files: ["**/*.vue"],
languageOptions: {
globals: {
$: "readonly",
$$: "readonly",
$computed: "readonly",
$customRef: "readonly",
$ref: "readonly",
$shallowRef: "readonly",
$toRef: "readonly"
},
parser: parserVue,
parserOptions: {
ecmaFeatures: {
jsx: true
},
extraFileExtensions: [".vue"],
parser: tseslint.parser,
sourceType: "module"
}
},
plugins: {
"@typescript-eslint": tseslint.plugin,
vue: pluginVue
},
processor: pluginVue.processors[".vue"],
rules: {
...pluginVue.configs.base.rules,
...pluginVue.configs.essential.rules,
...pluginVue.configs.recommended.rules,
"no-undef": "off",
"no-unused-vars": "off",
"vue/no-v-html": "off",
"vue/require-default-prop": "off",
"vue/require-explicit-emits": "off",
"vue/multi-word-component-names": "off",
"vue/no-setup-props-reactivity-loss": "off",
"vue/html-self-closing": [
"error",
{
html: {
void: "always",
normal: "always",
component: "always"
},
svg: "always",
math: "always"
}
]
}
}
]);

View File

@ -0,0 +1,88 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="Expires" content="0">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-control" content="no-cache">
<meta http-equiv="Cache" content="no-cache">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="renderer" content="webkit" />
<meta
name="viewport"
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
/>
<title>pure-admin-thin</title>
<link rel="icon" href="/favicon.ico" />
</head>
<body>
<div id="app">
<style>
html,
body,
#app {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
overflow: hidden;
}
.loader,
.loader::before,
.loader::after {
width: 2.5em;
height: 2.5em;
border-radius: 50%;
animation: load-animation 1.8s infinite ease-in-out;
animation-fill-mode: both;
}
.loader {
position: relative;
top: 0;
margin: 80px auto;
font-size: 10px;
color: #406eeb;
text-indent: -9999em;
transform: translateZ(0);
transform: translate(-50%, 0);
animation-delay: -0.16s;
}
.loader::before,
.loader::after {
position: absolute;
top: 0;
content: "";
}
.loader::before {
left: -3.5em;
animation-delay: -0.32s;
}
.loader::after {
left: 3.5em;
}
@keyframes load-animation {
0%,
80%,
100% {
box-shadow: 0 2.5em 0 -1.3em;
}
40% {
box-shadow: 0 2.5em 0 0;
}
}
</style>
<div class="loader"></div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -0,0 +1,159 @@
{
"name": "pure-admin-thin",
"version": "6.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "NODE_OPTIONS=--max-old-space-size=4096 vite",
"serve": "pnpm dev",
"build": "rimraf dist && NODE_OPTIONS=--max-old-space-size=8192 vite build",
"build:staging": "rimraf dist && vite build --mode staging",
"report": "rimraf dist && vite build",
"preview": "vite preview",
"preview:build": "pnpm build && vite preview",
"typecheck": "tsc --noEmit && vue-tsc --noEmit --skipLibCheck",
"svgo": "svgo -f . -r",
"clean:cache": "rimraf .eslintcache && rimraf pnpm-lock.yaml && rimraf node_modules && pnpm store prune && pnpm install",
"lint:eslint": "eslint --cache --max-warnings 0 \"{src,mock,build}/**/*.{vue,js,ts,tsx}\" --fix",
"lint:prettier": "prettier --write \"src/**/*.{js,ts,json,tsx,css,scss,vue,html,md}\"",
"lint:stylelint": "stylelint --cache --fix \"**/*.{html,vue,css,scss}\" --cache-location node_modules/.cache/stylelint/",
"lint": "pnpm lint:eslint && pnpm lint:prettier && pnpm lint:stylelint",
"prepare": "husky",
"preinstall": "npx only-allow pnpm"
},
"keywords": [
"pure-admin-thin",
"vue-pure-admin",
"element-plus",
"tailwindcss",
"pure-admin",
"typescript",
"pinia",
"vue3",
"vite",
"esm"
],
"homepage": "https://github.com/pure-admin/pure-admin-thin",
"repository": {
"type": "git",
"url": "git+https://github.com/pure-admin/pure-admin-thin.git"
},
"bugs": {
"url": "https://github.com/pure-admin/vue-pure-admin/issues"
},
"license": "MIT",
"author": {
"name": "xiaoxian521",
"email": "pureadmin@163.com",
"url": "https://github.com/xiaoxian521"
},
"dependencies": {
"@pureadmin/descriptions": "^1.2.1",
"@pureadmin/table": "^3.2.1",
"@pureadmin/utils": "^2.6.0",
"@vueuse/core": "^13.1.0",
"@vueuse/motion": "^3.0.3",
"animate.css": "^4.1.1",
"axios": "^1.9.0",
"dayjs": "^1.11.13",
"echarts": "^5.6.0",
"element-plus": "^2.9.8",
"js-cookie": "^3.0.5",
"localforage": "^1.10.0",
"mitt": "^3.0.1",
"nprogress": "^0.2.0",
"path-browserify": "^1.0.1",
"pinia": "^3.0.2",
"pinyin-pro": "^3.26.0",
"qs": "^6.14.0",
"responsive-storage": "^2.2.0",
"sortablejs": "^1.15.6",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"vue-tippy": "^6.7.0",
"vue-types": "^6.0.0"
},
"devDependencies": {
"@commitlint/cli": "^19.8.0",
"@commitlint/config-conventional": "^19.8.0",
"@commitlint/types": "^19.8.0",
"@eslint/js": "^9.25.1",
"@faker-js/faker": "^9.7.0",
"@iconify/json": "^2.2.331",
"@iconify/vue": "4.2.0",
"@tailwindcss/vite": "^4.1.4",
"@types/js-cookie": "^3.0.6",
"@types/node": "^20.17.30",
"@types/nprogress": "^0.2.3",
"@types/path-browserify": "^1.0.3",
"@types/qs": "^6.9.18",
"@types/sortablejs": "^1.15.8",
"@vitejs/plugin-vue": "^5.2.3",
"@vitejs/plugin-vue-jsx": "^4.1.2",
"boxen": "^8.0.1",
"code-inspector-plugin": "^0.20.10",
"cssnano": "^7.0.6",
"eslint": "^9.25.1",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-prettier": "^5.2.6",
"eslint-plugin-vue": "^10.0.0",
"gradient-string": "^3.0.0",
"husky": "^9.1.7",
"lint-staged": "^15.5.1",
"postcss": "^8.5.3",
"postcss-html": "^1.8.0",
"postcss-load-config": "^6.0.1",
"postcss-scss": "^4.0.9",
"prettier": "^3.5.3",
"rimraf": "^6.0.1",
"rollup-plugin-visualizer": "^5.14.0",
"sass": "^1.87.0",
"stylelint": "^16.19.0",
"stylelint-config-recess-order": "^6.0.0",
"stylelint-config-recommended-vue": "^1.6.0",
"stylelint-config-standard-scss": "^14.0.0",
"stylelint-prettier": "^5.0.3",
"svgo": "^3.3.2",
"tailwindcss": "^4.1.4",
"typescript": "^5.8.3",
"typescript-eslint": "^8.31.0",
"unplugin-icons": "^22.1.0",
"vite": "^6.3.3",
"vite-plugin-cdn-import": "^1.0.1",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-fake-server": "^2.2.0",
"vite-plugin-remove-console": "^2.2.0",
"vite-plugin-router-warn": "^1.0.0",
"vite-svg-loader": "^5.1.0",
"vue-eslint-parser": "^10.1.3",
"vue-tsc": "^2.2.10"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=22.0.0",
"pnpm": ">=9"
},
"pnpm": {
"allowedDeprecatedVersions": {
"are-we-there-yet": "*",
"sourcemap-codec": "*",
"lodash.isequal": "*",
"domexception": "*",
"w3c-hr-time": "*",
"inflight": "*",
"npmlog": "*",
"rimraf": "*",
"stable": "*",
"gauge": "*",
"abab": "*",
"glob": "*"
},
"onlyBuiltDependencies": [
"@parcel/watcher",
"core-js",
"es5-ext",
"esbuild",
"typeit",
"vue-demi"
]
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,8 @@
// @ts-check
/** @type {import('postcss-load-config').Config} */
export default {
plugins: {
...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {})
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path fill="#386BF3" d="M410.558.109c0 210.974-300.876 361.752-300.876 633.548 0 174.943 134.704 316.787 300.876 316.787s300.877-141.817 300.877-316.787C711.408 361.752 410.558 210.974 410.558.109"/><path fill="#C3D2FB" d="M613.469 73.665c0 211.055-300.877 361.914-300.877 633.547C312.592 882.156 447.296 1024 613.47 1024s300.876-141.817 300.876-316.788C914.29 435.58 613.469 284.72 613.469 73.665"/><path fill="#303F5B" d="M312.592 707.212c0-183.713 137.636-312.171 226.723-441.39 81.702 106.112 172.12 218.74 172.12 367.726A309.755 309.755 0 0 1 420.36 950.064a323.1 323.1 0 0 1-107.769-242.852z"/></svg>

After

Width:  |  Height:  |  Size: 706 B

View File

@ -0,0 +1,26 @@
{
"Version": "6.0.0",
"Title": "AI视频分析",
"FixedHeader": true,
"HiddenSideBar": false,
"MultiTagsCache": false,
"KeepAlive": true,
"Layout": "vertical",
"Theme": "light",
"DarkMode": false,
"OverallStyle": "light",
"Grey": false,
"Weak": false,
"HideTabs": false,
"HideFooter": false,
"Stretch": false,
"SidebarStatus": true,
"EpThemeColor": "#409EFF",
"ShowLogo": true,
"ShowModel": "chrome",
"MenuArrowIconNoTransition": false,
"CachingAsyncRoutes": false,
"TooltipEffect": "light",
"ResponsiveStorageNameSpace": "responsive-",
"MenuSearchHistory": 6
}

View File

@ -0,0 +1,26 @@
<template>
<el-config-provider :locale="currentLocale">
<router-view />
<ReDialog />
</el-config-provider>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { ElConfigProvider } from "element-plus";
import { ReDialog } from "@/components/ReDialog";
import zhCn from "element-plus/es/locale/lang/zh-cn";
export default defineComponent({
name: "app",
components: {
[ElConfigProvider.name]: ElConfigProvider,
ReDialog,
},
computed: {
currentLocale() {
return zhCn;
},
},
});
</script>

View File

@ -0,0 +1,20 @@
import { ComboModel } from "@/components/hTable/hTable";
import { http } from "@/utils/http";
import type { Res } from "@/utils/http/types";
/**
* @description
* @param {string} type type=StatusEnum
* @return {object}
*/
export function getenum(type) {
return http.request<ComboModel[]>("get", `api/Public/enum/${type}`);
}
/**
* @description
* @param {string} type type=StatusEnum
* @return {object}
*/
export function getenumDic(type) {
return http.request<any>("get", `api/Public/enum/${type}/Dic`);
}

View File

@ -0,0 +1,35 @@
import { http } from "@/utils/http";
import type { Res } from "@/utils/http/types";
import type { ComboModel } from "@/components/hTable/hTable";
export class hTableAPI {
url = "";
/** 构造函数 */
constructor(url) {
this.url = url;
}
PageList(data = {}) {
return http.request<any>("post", `api/${this.url}/PageList`, { data });
}
Info(tag) {
const pUrl = `api/${this.url}/${tag}`;
let getUrl = pUrl;
return http.request<any>("get", getUrl);
}
edit(data) {
return http.request<any>("post", `api/${this.url}/Edit`, { data });
}
delete(data) {
return http.request<any>("post", `api/${this.url}/Del`, { data });
}
querycombo(data = {}) {
return http.request<ComboModel[]>(
"post",
`api/${this.url}/QueryCombo`,
{
data
}
);
}
}

View File

@ -0,0 +1,15 @@
import { http } from "@/utils/http";
type Result = {
success: boolean;
data: Array<any>;
};
export const getAsyncRoutes = () => {
return new Promise<Result>((resolve, reject) => {
resolve({
success: true,
data: []
});
});
};

View File

@ -0,0 +1,43 @@
import { http } from "@/utils/http";
import type { Res } from "@/utils/http/types";
export type UserResult = {
/** 头像 */
avatar: string;
/** 用户名 */
userName: string;
/** 昵称 */
nickName: string;
/** 当前登录用户的角色 */
roles: Array<string>;
/** 按钮级别权限 */
permissions: Array<string>;
/** `token` */
accessToken: string;
/** 用于调用刷新`accessToken`的接口时所需的`token` */
refreshToken: string;
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx' */
expires: Date;
};
export type RefreshTokenResult = {
success: boolean;
data: {
/** `token` */
accessToken: string;
/** 用于调用刷新`accessToken`的接口时所需的`token` */
refreshToken: string;
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx' */
expires: Date;
};
};
/** 登录 */
export const getLogin = (data?: object) => {
return http.request<UserResult>("post", "/api/Admin/Login", { data });
};
/** 刷新`token` */
export const refreshTokenApi = (data?: object) => {
return http.request<RefreshTokenResult>("post", "/refresh-token", { data });
};

View File

@ -0,0 +1,105 @@
import { http } from "@/utils/http";
import type { Res } from "@/utils/http/types";
// 定义类型
export interface Question {
startTime: number;
topicStem: string;
question: string;
pptImageUrl: string;
}
export interface VideoKnowRes {
theme: string;
content: string;
knowPoint: string;
knowPointId: number;
questionArr: Question[];
startTime: number;
}
export interface SenseVoiceRes {
text: string;
start: number;
end: number;
}
export interface ShowTaskInfoRes {
captions: SenseVoiceRes[];
captions1: SenseVoiceRes[];
videoKnows: VideoKnowRes[];
mediaUrl: string;
}
export interface VideoTaskWorkflow {
id: number;
videoTaskId: number;
workflowName: string;
currentStep: string;
currentStepValue: number;
message?: string;
updateTime: string;
}
export interface RowRloadResult {
progress: string;
lastEnum: string;
startTime: string;
errorMessage: string;
workflows: VideoTaskWorkflow[]; // 新增字段
}
/** 刷新任务实时数据 */
export const RowRload = (id: any) => {
return http.request<RowRloadResult>("get", "/api/VideoTask/RowRload", {
params: { id }
});
};
/** 重试任务 (VideoSliceWorkflow) */
export const ReStart = (id: any, selectEnum: number) => {
return http.request<any>("get", "/api/VideoTask/ReStart", {
params: { id, selectEnum }
});
};
/** 重试任务 (TidySlideWorkflow) */
export const ReStartTidySlide = (id: any, selectEnum: number) => {
return http.request<any>("get", "/api/VideoTask/ReStartTidySlide", {
params: { id, selectEnum }
});
};
/** 展示数据 (VideoSliceWorkflow) */
export const ShowTaskInfo = (id: any) => {
return http.request<ShowTaskInfoRes>("get", "/api/VideoTask/ShowTaskInfo", {
params: { id }
});
};
/** 展示数据 (TidySlideWorkflow) */
export const ShowTidySlideTaskInfo = (id: any) => {
return http.request<any>("get", "/api/VideoTask/ShowTidySlideTaskInfo", {
params: { id }
});
};
/** 展示数据 */
export const RunningTaskList = (data: any) => {
return http.request<any>("post", "/api/VideoTask/RunningTaskList", {
data
});
};
/** 获取在线设备列表 */
export const GetOnlineDevices = () => {
return http.request<string[]>("get", "/api/VideoTask/OnlineDevices");
};
/** 展示数据 */
export const ErrorTaskList = (data: any) => {
return http.request<any>("post", "/api/VideoTask/ErrorTaskList", {
data
});
};

View File

@ -0,0 +1,27 @@
@font-face {
font-family: "iconfont"; /* Project id 2208059 */
src:
url("iconfont.woff2?t=1671895108120") format("woff2"),
url("iconfont.woff?t=1671895108120") format("woff"),
url("iconfont.ttf?t=1671895108120") format("truetype");
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.pure-iconfont-tabs:before {
content: "\e63e";
}
.pure-iconfont-logo:before {
content: "\e620";
}
.pure-iconfont-new:before {
content: "\e615";
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,30 @@
{
"id": "2208059",
"name": "pure-admin",
"font_family": "iconfont",
"css_prefix_text": "pure-iconfont-",
"description": "pure-admin-iconfont",
"glyphs": [
{
"icon_id": "20594647",
"name": "Tabs",
"font_class": "tabs",
"unicode": "e63e",
"unicode_decimal": 58942
},
{
"icon_id": "22129506",
"name": "PureLogo",
"font_class": "logo",
"unicode": "e620",
"unicode_decimal": 58912
},
{
"icon_id": "7795615",
"name": "New",
"font_class": "new",
"unicode": "e615",
"unicode_decimal": 58901
}
]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path fill="#386BF3" d="M410.558.109c0 210.974-300.876 361.752-300.876 633.548 0 174.943 134.704 316.787 300.876 316.787s300.877-141.817 300.877-316.787C711.408 361.752 410.558 210.974 410.558.109"/><path fill="#C3D2FB" d="M613.469 73.665c0 211.055-300.877 361.914-300.877 633.547C312.592 882.156 447.296 1024 613.47 1024s300.876-141.817 300.876-316.788C914.29 435.58 613.469 284.72 613.469 73.665"/><path fill="#303F5B" d="M312.592 707.212c0-183.713 137.636-312.171 226.723-441.39 81.702 106.112 172.12 218.74 172.12 367.726A309.755 309.755 0 0 1 420.36 950.064a323.1 323.1 0 0 1-107.769-242.852z"/></svg>

After

Width:  |  Height:  |  Size: 706 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M2.88 18.054a35.9 35.9 0 0 1 8.531-16.32.8.8 0 0 1 1.178 0q.25.27.413.455a35.9 35.9 0 0 1 8.118 15.865c-2.141.451-4.34.747-6.584.874l-2.089 4.178a.5.5 0 0 1-.894 0l-2.089-4.178a44 44 0 0 1-6.584-.874m6.698-1.123 1.157.066L12 19.527l1.265-2.53 1.157-.066a42 42 0 0 0 4.227-.454A33.9 33.9 0 0 0 12 4.09a33.9 33.9 0 0 0-6.649 12.387q2.093.334 4.227.454M12 15a3 3 0 1 1 0-6 3 3 0 0 1 0 6m0-2a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/></svg>

After

Width:  |  Height:  |  Size: 533 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11.38 2.019a7.5 7.5 0 1 0 10.6 10.6C21.662 17.854 17.316 22 12.001 22 6.477 22 2 17.523 2 12c0-5.315 4.146-9.661 9.38-9.981"/></svg>

After

Width:  |  Height:  |  Size: 262 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12M11 1h2v3h-2zm0 19h2v3h-2zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414zm2.121-14.85 1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414zM23 11v2h-3v-2zM4 11v2H1v-2z"/></svg>

After

Width:  |  Height:  |  Size: 435 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" class="iconify iconify--ant-design" viewBox="0 0 1024 1024"><path fill="currentColor" d="M864 170h-60c-4.4 0-8 3.6-8 8v518H310v-73c0-6.7-7.8-10.5-13-6.3l-141.9 112a8 8 0 0 0 0 12.6l141.9 112c5.3 4.2 13 .4 13-6.3v-75h498c35.3 0 64-28.7 64-64V178c0-4.4-3.6-8-8-8"/></svg>

After

Width:  |  Height:  |  Size: 332 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" class="re-screen" color="#00000073" viewBox="0 0 16 16"><path fill="currentColor" d="M3.5 4H1V3h2V1h1v2.5zM13 3V1h-1v2.5l.5.5H15V3zm-1 9.5V15h1v-2h2v-1h-2.5zM1 12v1h2v2h1v-2.5l-.5-.5zm11-1.5-.5.5h-7l-.5-.5v-5l.5-.5h7l.5.5zM10 7H6v2h4z"/></svg>

After

Width:  |  Height:  |  Size: 308 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" class="re-screen" color="#00000073" viewBox="0 0 16 16"><path fill="currentColor" d="M3 12h10V4H3zm2-6h6v4H5zM2 6H1V2.5l.5-.5H5v1H2zm13-3.5V6h-1V3h-3V2h3.5zM14 10h1v3.5l-.5.5H11v-1h3zM2 13h3v1H1.5l-.5-.5V10h1z"/></svg>

After

Width:  |  Height:  |  Size: 283 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" class="iconify iconify--mdi" viewBox="0 0 24 24"><path fill="currentColor" d="M1 7h6v2H3v2h4v2H3v2h4v2H1zm10 0h4v2h-4v2h2a2 2 0 0 1 2 2v2c0 1.11-.89 2-2 2H9v-2h4v-2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2m8 0h2a2 2 0 0 1 2 2v1h-2V9h-2v6h2v-1h2v1c0 1.11-.89 2-2 2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2"/></svg>

After

Width:  |  Height:  |  Size: 360 B

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