Compare commits

...

125 Commits

Author SHA1 Message Date
0a4bb5426e fix: 限制最长新闻标签最多为4个字符 2025-04-24 21:42:18 +08:00
6fc064fbc8 feat: 标签及模板接口联调完成 2025-04-24 17:09:22 +08:00
611a00a550 feat: 保留腾讯播放器 2025-04-24 16:38:05 +08:00
0ccbfb5f5a feat: update video player 2025-04-22 16:14:41 +08:00
74f37055bc feat: ️更新依赖版本 2025-04-22 07:59:13 +08:00
d270d615a2 feat: 新闻编辑UI新增背景选择 2025-04-21 17:13:57 +08:00
b7b15e7471 fixed: 移除回收站页面中已推送条数 2025-04-16 18:45:20 +08:00
a2b5df22f8 feat: ️ 新增图片上传类型校验及错误提示 2025-04-16 18:32:03 +08:00
116c171249 fixed: 回收站时间宽度修复 2025-04-16 16:36:15 +08:00
c8e5d8a6ab fixed: 重置筛选或全部删除时已选数据 2025-04-16 16:18:12 +08:00
99323df02b feat: ️ 新增订单/回收站导航
fix:直播间回退
style: 📚️调整相关UI
2025-04-16 11:29:54 +08:00
42e2d3fcc0 feat: ️ 移除订单分页在单页时的隐藏逻辑 2025-04-15 15:29:51 +08:00
cbd476d1e2 fixed: 订单效果 2025-04-15 15:03:08 +08:00
1be407d34e fixed: 订单分页效果 2025-04-14 21:34:46 +08:00
605a769b89 feat: 添加余额提醒 2025-04-14 14:10:45 +08:00
4e23bb623f feat: 统一样式 2025-04-14 10:19:53 +08:00
cea77ea231 merge 2025-04-11 19:20:06 +08:00
be34a8bc9b feat: ️ 新增MP4视频下载支持
- 优化视频列表项下载逻辑及状态判断
2025-04-09 12:31:28 +08:00
fdb125c7ba feat: ️ 将热点填写统一调整为"自定义" 2025-04-09 11:26:49 +08:00
bcbdac6673 feat: ️ 优化文章编辑模态框标题布局,完善素材区空内容提示文案 2025-04-08 23:06:58 +08:00
0520cb8e1d feat: ️ 优化数字人面板高度及布局,调整热点新闻编辑模式切换逻辑及国际化文案 2025-04-08 23:00:32 +08:00
4dee84a459 feat: ️ 直播间页调整锁定相关状态及逻辑
- 新增直播视频回滚功能
- 优化编辑模式操作流程
2025-04-08 22:39:00 +08:00
3d47964580 fix: 🪲️ 修复视频删除接口路径错误,将/outside更正为/remove 2025-04-08 16:15:25 +08:00
17c9fa6c10 feat: ️ 新增视频下载功能
- 支持从OSS直接下载已生成视频
2025-04-07 23:39:32 +08:00
500c849140 feat: ️ 新增推送视频状态控制及国际化支持,优化热点新闻数据加载逻辑 2025-04-07 23:24:53 +08:00
64ee960846 feat: ️ 完成接口联调;去除生成视频判断 2025-04-07 11:56:31 +08:00
e61bfcc26c feat: ️ 新增新闻热点功能,支持手动/自动填充热点内容并完善相关验证逻辑 2025-04-06 20:26:24 +08:00
de7088f642 fix: 🪲️修复空素材编辑验证 2025-04-06 16:00:36 +08:00
fcf31294b7 feat(dev/main): ️更新错误页面展示内容 2025-03-10 13:45:18 +08:00
45b0912d48 🐛 fixed : 跳过异常新闻和选中新闻一致时直接关闭 2025-03-08 22:15:54 +08:00
0bf20343d0 feat: 不能删除最后一组素材 2025-03-08 21:43:01 +08:00
d782801420 🐛 feat: 更新英文推送double check notice文案 2025-03-07 19:10:31 +08:00
0dec5aa1f2 🐛 feat: 直播界面解锁排序时禁止全选操作 2025-03-02 22:51:48 +08:00
51e133b273 💄 feat: 更新回滚图标 2025-03-02 22:50:42 +08:00
54056aec3a 🐛 feat: 新增 disableRemoveMessage 属性以控制删除提示信息 2025-03-02 12:24:50 +08:00
496192061f 📝 update: update toast message 2025-03-02 12:18:04 +08:00
b65631ad9c 🐛 fixed: deselect in empty data 2025-03-02 12:17:00 +08:00
826712f910 fixed: deselect in empty data 2025-03-02 11:11:27 +08:00
90bd5cbde6 update: 跳过异常后加载新闻列表 2025-02-17 14:31:32 +08:00
3ec2ae6d0e update: 跳过异常后加载新闻列表 2025-02-15 13:02:38 +08:00
17b0348ca6 feat: 推送新闻前进行异常确认 2025-02-15 11:33:59 +08:00
227688be25 fixed ui 2025-02-14 16:55:57 +08:00
5977e1f76e fixed 2025-02-14 16:28:34 +08:00
3551601709 fixed: 直播间删除视频导致的直播状态异常 2025-02-14 15:37:14 +08:00
a3643ee9e5 feat: 失败的视频重新生成 2025-02-13 22:52:13 +08:00
1db6a1e19c update: 调整直播间时长UI 2025-02-13 17:44:05 +08:00
ac0ab41dcb fixed: 素材为空时的报错 2025-02-11 14:53:27 +08:00
ee0651fa36 feat: 添加多语言编译命令 2025-02-09 20:48:34 +08:00
43ca886b74 fixed: 更新英文文案 2025-02-09 16:11:45 +08:00
34cc9a75bb fixed: 更新中文文案 2025-02-09 15:33:47 +08:00
e1779d7923 fixed: 直播将空数据导致异常 2025-02-09 14:56:31 +08:00
098791edf4 fixed: push error message 2025-02-06 16:56:49 +08:00
8c384d5723 fixed: ts build error 2025-02-06 16:19:37 +08:00
2c1ea4a31a fixed: 视频播放时调整顺寻影响播放状态 2025-02-06 16:19:37 +08:00
829a135ef3 fixed: 不可以删除正在直播的视频;素材正文内容和图片都必须填写 2025-02-06 16:19:37 +08:00
ac7e4b1b27 fixed: 编辑内容时,当内容为空时禁止新建组 2025-02-06 16:19:37 +08:00
9eecaa4294 fixed: 调整标题样式 2025-02-06 16:19:37 +08:00
f7823b7390 fixed: 登录token失效后验证失效 2025-02-06 16:19:37 +08:00
61426ace81 feat: add coding ci script file 2025-02-05 11:13:05 +08:00
38351e6873 fixed: 英文文案调整 2025-01-26 10:03:36 +08:00
ab9e1b7e10 更新英文相关文案 2025-01-26 00:00:41 +08:00
1e3f8dc3c6 fixed: ts error 2025-01-25 16:34:44 +08:00
07d3bb2bb9 fixed: 编译无法切换语言 2025-01-25 15:02:45 +08:00
381e1f16d1 添加多语言请求接口参数 2025-01-25 11:38:12 +08:00
e022bc8036 添加并完善多语言 2025-01-24 23:57:44 +08:00
71e90e7edd Merge branch 'dev/i18n' into dev/main 2025-01-24 13:02:04 +08:00
5b791716e2 update:添加单页编译说明 2025-01-22 23:15:01 +08:00
1b72d9d4f5 feat: 添加单页直播 2025-01-22 23:07:53 +08:00
a478c9dd09 fixed: 推送视频前先保存 2025-01-22 22:02:16 +08:00
a8b672037c update: 添加删除 2025-01-22 21:38:59 +08:00
08e4de4a90 Merge remote-tracking branch 'origin/dev/main' into dev/main 2025-01-22 21:15:48 +08:00
a1bae30e2d feat: 添加多语言 2025-01-22 21:07:44 +08:00
Reana
213074760d fix: 修复更新的数字人播报字段展示错误的问题 2025-01-22 08:35:55 +00:00
Reana
3161a5ee27 fix: 修复语法错误 2025-01-22 08:20:10 +00:00
Reana
68691c0e54 feat: 将 content_group 中的第一个文本内容移动至 metahuman_text 字段 2025-01-22 07:53:40 +00:00
fae2e9c4ae fixed: 页面销毁后没有停止refresh 2025-01-07 10:27:16 +08:00
c7d964965d fixed 空数据时导致的异常 2025-01-07 10:04:25 +08:00
daba38f188 历史视频添加 2025-01-07 10:04:25 +08:00
1026c35c08 update: 更新退出系统菜单 2025-01-07 10:04:25 +08:00
b9212c14de 🐛 fixed: 修复详情详情宽度不足 2025-01-07 10:04:25 +08:00
779b366062 💄 update: 更新历史视频库列表样式 2025-01-07 10:04:25 +08:00
2da4527ab8 update: 更新历史视频库 2025-01-07 10:04:25 +08:00
c383aff66a fixed: 更新样式 2025-01-07 10:04:25 +08:00
3eeb6a4c8e fixed: 更新样式 2025-01-07 10:04:25 +08:00
03c1449268 fixed confirm modal style 2025-01-07 10:04:25 +08:00
f6a987b77d fixed confirm modal style 2025-01-07 10:04:25 +08:00
bee63a73c0 fixed confirm modal style 2025-01-07 10:04:25 +08:00
461ee2dd67 fixed 直播间因为字体大小问题导致无法对齐 2025-01-07 10:04:25 +08:00
ed68a39669 update 2025-01-07 10:04:25 +08:00
1100a154b0 fixed ts build error 2025-01-07 10:04:25 +08:00
c1ad86e775 style update 2025-01-07 10:04:25 +08:00
313dbea9e2 spin style update 2025-01-07 10:04:25 +08:00
d294fc711b update ui 2025-01-07 10:04:25 +08:00
7ee5cac052 update ui 2025-01-07 10:04:25 +08:00
72d967fbc6 update: 新闻素材筛选条件缓存 2025-01-07 10:04:25 +08:00
d35594fbb6 fixed: ts build error 2025-01-07 10:04:25 +08:00
e69d8f3728 update ui 2025-01-07 10:04:25 +08:00
8e41b9c07b 添加播放时间更新 2025-01-07 10:04:25 +08:00
4cdcfb0636 update 删除 2025-01-07 10:04:25 +08:00
ae6dd65e7a updated 2025-01-07 10:04:25 +08:00
9c90ffda26 update 视频排序调整 2025-01-07 10:04:25 +08:00
1beabc7376 update: 新闻编辑 2025-01-07 10:04:25 +08:00
6abc6ab3d1 fixed: 新闻编辑无法删除 2025-01-07 10:04:25 +08:00
800c14a4f8 fixed: 新闻素材全选bug 2025-01-07 10:04:25 +08:00
0f9d6ddb63 update: 添加字数和图片数;新闻编辑失去焦点自动搜索 2025-01-07 10:04:25 +08:00
50cd1c1538 fixed: 新闻编辑无法确定问题 2025-01-07 10:04:25 +08:00
18c53d8f30 update: 新闻素材页面添加全局选择 2025-01-07 10:04:25 +08:00
c17c4c7180 update: pinned panel 添加展开和收缩动画 2025-01-07 10:04:25 +08:00
6764ca59c6 update: 更新功能及配置
* 添加登录页面背景
* 新闻素材页面时间只精确到分钟;筛选时间默认为半小时内;添加已推送标签
2025-01-07 10:04:25 +08:00
a0b83e981b news detail 修改完成 2025-01-07 10:04:25 +08:00
4ce2f19ca2 fixed ts build error 2025-01-07 10:04:25 +08:00
9c1a8f4a07 fixed 新闻编辑器样式及新增位置问题 2025-01-07 10:04:25 +08:00
bd11e6c836 update 当前播放视频时长 2025-01-07 10:04:25 +08:00
9beddf1aad update 全选逻辑,批量操作按钮样式 2025-01-07 10:04:25 +08:00
66628a7e83 update 全选逻辑,批量操作按钮样式 2025-01-07 10:04:25 +08:00
ea75f8727f 💄 update 来源筛选隐藏 2025-01-07 10:04:25 +08:00
2db8d7aac8 💄 update 视频操作ui 2025-01-07 10:04:25 +08:00
d4e9dde7df 💄 update 新闻内容编辑器 2025-01-07 10:04:25 +08:00
cb1eb51ba4 💄 update 新闻编辑样式 2025-01-07 10:04:25 +08:00
957dce1613 💄 update 来源筛选 2025-01-07 10:04:25 +08:00
3688b1d2f1 💄 update 新闻编辑ui 2025-01-07 10:04:25 +08:00
9c2117da36 💄 麻烦的新闻来源过滤 2025-01-07 10:04:25 +08:00
83b87074ec 💄 新闻素材推入编辑后直接跳转 2025-01-07 10:04:25 +08:00
bea93d9094 💄 新闻素材更新 2025-01-07 10:04:25 +08:00
d0d84e61ed 💄 更新框架及登录UI 2025-01-07 10:04:25 +08:00
106 changed files with 6619 additions and 2696 deletions

24
.coding-ci.yml Normal file
View File

@ -0,0 +1,24 @@
.pipeline: &pipeline
runner:
cpus: 2
docker:
image: node:20.11.1
stages:
- name: 启用yarn
script: corepack enable yarn
- name: 安装依赖
script: yarn install
- name: build
script: yarn run build
dev/main:
push:
- <<: *pipeline
main:
push:
- <<: *pipeline
release:
push:
- <<: *pipeline

1
.gitignore vendored
View File

@ -18,6 +18,7 @@ _local
.vscode/*
!.vscode/extensions.json
.idea
.ide
.DS_Store
*.suo
*.ntvs*

19
.ide/Dockerfile Normal file
View File

@ -0,0 +1,19 @@
FROM node:20
# 以及按需安装其他软件
# RUN apt-get update && apt-get install -y git
# 安装 code-server 和 vscode 常用插件
RUN curl -fsSL https://code-server.dev/install.sh | sh \
&& code-server --install-extension redhat.vscode-yaml \
&& code-server --install-extension dbaeumer.vscode-eslint \
&& code-server --install-extension eamodio.gitlens \
&& code-server --install-extension tencent-cloud.coding-copilot \
&& echo done
# 安装 ssh 服务,用于支持 VSCode 客户端通过 Remote-SSH 访问开发环境
RUN apt-get update && apt-get install -y wget unzip openssh-server
# 指定字符集支持命令行输入中文(根据需要选择字符集)
ENV LANG C.UTF-8
ENV LANGUAGE C.UTF-8

12
.prettierignore Normal file
View File

@ -0,0 +1,12 @@
/node_modules
package*.json
.gitignore
*.local
*_local
__test__
.ide
.vscode
.idea
test
dist
public

7
.prettierrc Normal file
View File

@ -0,0 +1,7 @@
{
"useTabs": true,
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100
}

View File

@ -17,6 +17,16 @@ npm run build
```
生成的资源在dist目录中将此目录中所有文件放置在待部署web目录即可。
#### 直播页面
1、直接正常部署后使用访问 /live.html 即可
2、单独域名部署设置环境变量ONLY_LIVE=yes使用正常编译即可
#### 多语言部署
应用默认使用简体中文进行编译;如果需要指定其他语言需要设置环境变量:`APP_LANGUAGE`;
`APP_LANGUAGE`目前支持的值有:`zh-CN`(中文)、`en-US`(英语)。
**使用docker**
[x] TODO

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8"/>
<link rel="icon" type="image/png" href="/logo.svg"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>数字人直播</title>
<title></title>
<style>
.app-loading-text{font-family:"PingFang SC","Microsoft YaHei",sans-serif;position:fixed;left:0;right:0;text-align:center;top:50%;transform:translateY(-50%);font-size:20px}
</style>

View File

@ -1,13 +1,16 @@
{
"name": "ai-live",
"private": true,
"version": "1.0.2",
"version": "newui-0.0.1",
"type": "module",
"description": "数字人直播间",
"scripts": {
"dev": "vite --host",
"dev-test": "vite --host --mode=test",
"dev-lang-en": "set APP_LANGUAGE=en-US && vite --host --mode=lang-en",
"build": "tsc && vite build",
"zh": "cross-env APP_LANGUAGE=zh-CN tsc && vite build --mode=production",
"en": "cross-env APP_LANGUAGE=en-US vite build --mode=production",
"build-test": "tsc && vite build --mode=test",
"build-relative": "tsc && vite build --mode=relative",
"build-prod": "tsc && vite build --mode=production",
@ -16,24 +19,28 @@
"preview": "vite preview"
},
"dependencies": {
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/modifiers": "^7.0.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"ahooks": "^3.8.1",
"antd": "^5.22.1",
"antd": "^5.22.5",
"axios": "^1.7.7",
"clsx": "^2.1.1",
"cross-env": "^7.0.3",
"dayjs": "^1.11.11",
"file-saver": "^2.0.5",
"flv.js": "^1.6.2",
"i18next": "^25.0.1",
"jszip": "^3.10.1",
"qs": "^6.12.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^15.4.0",
"react-player": "^2.16.0",
"react-router-dom": "^6.28.0",
"sass": "^1.81.0",
"tcplayer.js": "^5.2.0"
"tcplayer.js": "^5.2.0",
"zustand": "^5.0.2"
},
"devDependencies": {
"@types/file-saver": "^2.0.7",
@ -44,15 +51,15 @@
"@types/react-dom": "^18.2.22",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"@vitejs/plugin-react": "^4.2.1",
"@vitejs/plugin-react": "^4.4.1",
"autoprefixer": "^10.4.19",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"postcss": "^8.4.40",
"postcss": "^8.5.3",
"tailwindcss": "^3.4.7",
"typescript": "^5.2.2",
"vite": "^5.2.0"
"typescript": "^5.8.3",
"vite": "^6.3.2"
},
"packageManager": "yarn@1.22.21+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72"
}

52
public/live.html Normal file
View File

@ -0,0 +1,52 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>数字人直播</title>
<link href="https://web.sdk.qcloud.com/player/tcplayer/release/v5.2.0/tcplayer.min.css" rel="stylesheet"/>
<style>
*, :before, :after {
box-sizing: border-box;
border-width: 0;
border-style: solid;
}
body{
margin: 0;
}
.wrapper{}
.wrapper .video-js{
max-width: 480px;
margin: auto;
height: 100vh;
}
.wrapper video{
height: 100vh;
}
</style>
</head>
<body>
<div class="wrapper">
<video id="player-container" width="414" height="270" preload="auto" playsinline webkit-playsinline>
</video>
</div>
<!--播放器脚本文件-->
<script src="https://web.sdk.qcloud.com/player/tcplayer/release/v5.2.0/tcplayer.v5.2.0.min.js"></script>
<script>
function init(){
fetch('/api/v1/tencent/get_pull_url').then(r=>r.json()).then(ret=>{
console.log(ret)
TCPlayer('player-container', {
sources: [{
src: ret.data.flv_url, // 播放地址
}],
licenseUrl: 'https://license.vod2.myqcloud.com/license/v2/1328581896_1/v_cube.license', // license 地址,必传。参考准备工作部分,在视立方控制台申请 license 后可获得 licenseUrl
});
})
}
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>

View File

@ -1,6 +1,6 @@
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<path d="M671.584 201.142c22.034 0 39.896 17.398 39.896 38.858v163.2l207.98-141.828a20.4 20.4 0 0 1 20.666-1.322c6.602 3.35 10.732 10.002 10.732 17.252V746.7c0 7.25-4.13 13.902-10.732 17.252a20.4 20.4 0 0 1-20.668-1.32L711.48 620.8V784c0 21.46-17.86 38.858-39.896 38.858H113.04c-22.034 0-39.896-17.398-39.896-38.858V240c0-21.46 17.862-38.858 39.896-38.858h558.546z"
fill="#9C34FE"></path>
<path d="M328.478 388.784c-7.584 0-14.122 5.196-15.64 12.434l-0.32 3.07v215.346c0 5.11 2.58 9.894 6.896 12.796a16.32 16.32 0 0 0 14.73 1.736l2.912-1.398 173.748-107.712c4.106-2.56 6.786-6.806 7.276-11.532 0.49-4.724-1.264-9.408-4.764-12.714l-2.512-1.944-173.748-107.712a16.28 16.28 0 0 0-8.578-2.332v-0.038z"
fill="#FFFFFF"></path>
<svg width="37" height="34" viewBox="0 0 37 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M28.9241 2.04763C29.2793 1.00838 30.7152 0.976901 31.1363 1.95321L31.1719 2.04823L31.6512 3.44974C31.761 3.77115 31.9385 4.06528 32.1716 4.31227C32.4048 4.55927 32.6882 4.75339 33.0028 4.88154L33.1316 4.92964L34.5332 5.40829C35.5724 5.76342 35.6039 7.19937 34.6282 7.62042L34.5332 7.65605L33.1316 8.1353C32.8101 8.24506 32.5159 8.42251 32.2688 8.65567C32.0217 8.88884 31.8275 9.1723 31.6993 9.48693L31.6512 9.6152L31.1725 11.0173C30.8174 12.0566 29.3814 12.088 28.961 11.1123L28.9241 11.0173L28.4455 9.61579C28.3357 9.29427 28.1583 9.00003 27.9251 8.75292C27.6919 8.50582 27.4085 8.31161 27.0939 8.1834L26.9656 8.1353L25.5641 7.65665C24.5242 7.30152 24.4928 5.86556 25.4691 5.44511L25.5641 5.40829L26.9656 4.92964C27.287 4.81981 27.5811 4.64233 27.8281 4.40916C28.0751 4.176 28.2692 3.89257 28.3974 3.57801L28.4455 3.44974L28.9241 2.04763ZM34.7992 1.07303e-07C34.9103 -1.40135e-07 35.0192 0.0311648 35.1135 0.089953C35.2077 0.148741 35.2836 0.232795 35.3325 0.332562L35.361 0.402044L35.5689 1.01135L36.1787 1.2192C36.2901 1.25702 36.3877 1.32705 36.4592 1.42041C36.5307 1.51376 36.5729 1.62624 36.5804 1.74359C36.5879 1.86094 36.5604 1.97787 36.5013 2.07957C36.4423 2.18128 36.3544 2.26317 36.2488 2.31487L36.1787 2.34337L35.5694 2.55123L35.3616 3.16112C35.3237 3.27243 35.2536 3.36998 35.1602 3.44142C35.0668 3.51286 34.9543 3.55496 34.837 3.56241C34.7197 3.56985 34.6027 3.54229 34.5011 3.48322C34.3994 3.42415 34.3176 3.33623 34.2659 3.2306L34.2374 3.16112L34.0296 2.55182L33.4197 2.34397C33.3083 2.30614 33.2107 2.23611 33.1392 2.14276C33.0677 2.0494 33.0256 1.93693 33.0181 1.81958C33.0105 1.70223 33.038 1.58529 33.0971 1.48359C33.1561 1.38189 33.244 1.3 33.3496 1.2483L33.4197 1.21979L34.029 1.01194L34.2368 0.402044C34.2769 0.284712 34.3526 0.182855 34.4535 0.110754C34.5543 0.0386538 34.6752 -7.43997e-05 34.7992 1.07303e-07Z" fill="#9C34FE"/>
<path d="M18.481 20.1202C19.2177 19.3857 19.8018 18.5128 20.2 17.5517C20.5981 16.5906 20.8023 15.5604 20.8009 14.5201C20.8009 10.1463 17.2548 6.6001 12.8809 6.6001C8.50712 6.6001 4.96094 10.1463 4.96094 14.5201C4.96094 16.7073 5.84732 18.6873 7.28084 20.1202" stroke="black" stroke-width="2" stroke-linecap="round"/>
<path d="M1 32.9999L1.66 29.6999L8.92 24.4199L12.88 28.3799L16.84 24.4199L24.1 29.6999L24.76 32.9999" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.61523 17.8246C5.62403 15.112 6.06557 13.1305 6.93985 11.88C8.25193 10.0049 9.17527 10.1429 9.84715 10.4346C10.519 10.7263 10.915 12.635 12.0377 13.1855C13.1597 13.7353 16.0333 13.8032 17.0174 14.4652C18.0008 15.1265 20.2514 16.3574 19.6904 18.4589" stroke="black" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 859 B

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -1,15 +1,19 @@
import AppRouter from "@/routes";
import {ConfigProvider} from "@/contexts/config";
import {AuthProvider} from "@/contexts/auth";
import React from 'react';
// import LivePlayer from "@/pages/live-player";
console.log(`APP-BUILD-AT: ${AppBuildVersion}`)
const LivePlayer = React.lazy(() => import('@/pages/live-player'));
function App() {
return (
<ConfigProvider>
<AuthProvider>
{AppConfig.ONLY_LIVE && AppConfig.ONLY_LIVE == 'yes'?<LivePlayer />:<AuthProvider>
<AppRouter/>
</AuthProvider>
</AuthProvider>}
</ConfigProvider>
)
}

View File

@ -8,12 +8,14 @@
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
--main-bg-color: #f6f6f6;
--main-bg-color: #f4f7fc;
--brand-color: #43ABFF;
--navigation-width: 100vw;
--navigation-active-color: #ffe0e0;
--app-header-header: 90px;
--container-width: 1440px;
--app-header-header: 70px;
--container-width: 1800px;
--header-z-index: 99999;
--message-z-index: 100001;
}
@tailwind base;
@ -27,12 +29,12 @@
}
::-webkit-scrollbar-thumb {
background: #ccc;
background: rgba(64, 150, 255, 0.5);
height: 10px;
border-radius: 5px;
&:hover {
background: #999;
background: rgba(64, 150, 255, 1);
cursor: pointer;
}
}
@ -129,7 +131,7 @@
.page-live {
.live-player {
max-height: calc(100vh - var(--app-header-header) - 130px);
max-height: calc(100vh - var(--app-header-header) - 150px);
overflow: hidden;
iframe {
@ -146,18 +148,90 @@
}
.video-list-sort-container {
min-height: 300px;
max-height: calc(100vh - var(--app-header-header) - 300px);
overflow: auto;
padding-right: 10px;
.list-header {
}
.list-row {
@apply flex bg-white mt-2 py-1 rounded-xl gap-2 border;
border-width: 2px;
&.playing{
@apply border-primary-blue bg-[#d9eaff];
}
&.disabled{
@apply border-primary-blue bg-[#f4f7fc];
}
&.status-generating{
background: rgba(209, 209, 209, 1);
}
&.status-generate-failed{
background: rgba(255, 0, 0, 0.12);
}
&.header-row{
@apply text-sm;
background: none;
.col{
@apply text-base text-gray-800;
height: 42px;
}
}
&.checked {
@apply border-primary-blue bg-primary-blue-bg;
}
.col {
@apply flex items-center relative pl-4 text-center justify-center text-sm;
height: 60px;
&:after {
@apply absolute;
border-right: solid 1px rgba(0,0,0,0.1);
content: ' ';
top: 2px;
bottom: 2px;
left: 0;
}
}
.number {
width: 70px;
padding-left: 10px;
&:after {
display: none;
}
}
.cover {
width: 120px;
img{
@apply rounded-lg;
}
}
.title {
@apply flex-1;
}
.generated-time {
width: 180px;
}
.operation {
@apply flex items-center ml-2 text-lg text-gray-400 justify-center;
width: 180px;
padding: 0;
}
}
}
.live-video-list-sort-container {
min-height: 300px;
padding-right: 10px;
max-height: calc(100vh - var(--app-header-header) - 200px);
overflow: auto;
//min-height: 300px;
//padding-right: 10px;
//max-height: calc(100vh - var(--app-header-header) - 200px);
//overflow: auto;
}
.video-player {
.video-js {
@apply w-full h-full;
@ -172,3 +246,358 @@
display: none;
}
}
.page-action-to-top {
@apply text-right;
.btn-to-top {
@apply w-[44px] h-[44px] inline-block bg-blue-300 text-center p-0 transition hover:bg-blue-500;
min-width: 0 !important;
border-radius: 50px;
font-size: 24px;
position: absolute;
top:0;
right:0;
transform: translateY(-100%);
svg{
margin-left: 0 !important;
}
}
}
.w-min-60px{
min-width: 60px;
}
.list-scroller-container {
overflow: auto;
margin-right: -20px;
padding-right: 16px;
scrollbar-gutter: stable;
}
.data-list-container {
@apply list-scroller-container;
height: calc(100vh - var(--app-header-header) - 200px);
.data-list-container-inner {
}
}
.video-history-list-container{
height: calc(100vh - var(--app-header-header) - 130px);
}
.checkbox{
@apply bg-black/10 backdrop-blur border border-white hover:border-blue-500 cursor-pointer relative;
--size: 22px;
border-width: 2px;
width: var(--size);
height: var(--size);
border-radius: 2px;
&::before {
@apply absolute hidden;
border-left:solid 2px white;
border-bottom:solid 2px white;
left: 3px;
top: 4px;
content: ' ';
width: calc(var(--size) - 8px);
height: 6px;
transform: rotate(-45deg);
}
&.checked{
@apply border-blue-500 bg-blue-500;
&:before{
@apply block;
}
}
}
// override antd style
.data-list-load-spin{
.ant-spin-container::after{
opacity: 0;
}
}
.popconfirm-main{
.ant-popover-inner{
border-radius: 4px;
padding: 20px 24px;
min-width: 360px;
background-color: #f2f2f2;
box-shadow: 0 0 10px rgba(25, 25, 25, 0.1);
}
.icon-warning{
@apply text-red-500;
font-size: 20px;
transform: translateY(5px);
margin-right: 20px;
}
.ant-popconfirm-message{
.ant-popconfirm-title{
@apply text-xl;
font-weight: 400;
}
.ant-popconfirm-description{
@apply mt-2 text-gray-600 text-sm;
margin-left: 0px;
}
}
.ant-popconfirm-buttons{
@apply mt-6;
button{
font-size: 14px;
line-height: 1.5714285714285714;
height: 32px;
padding: 4px 15px;
border-radius: 4px;
min-width: 88px;
}
//.ant-btn-default{
// @apply bg-white shadow-none text-popconfirm-btn-cancel border border-popconfirm-btn-cancel hover:border-popconfirm-btn-cancel hover:text-popconfirm-btn-cancel hover:bg-white hover:bg-popconfirm-btn-cancel/10;
//}
//.ant-btn-primary{
// @apply bg-white shadow-none text-popconfirm-bg border border-popconfirm-bg hover:text-popconfirm-bg hover:bg-white hover:bg-popconfirm-btn-primary-hover/10;
//}
}
}
.root-modal-confirm{
z-index: calc(var(--header-z-index) + 1) !important;
background: rgba(0, 0, 0, 0.05);
//anticon anticon-exclamation-circle
.icon-warning{
@apply text-red-500;
font-size: 20px;
//transform: translateY(5px);
margin-right: 20px;
}
.ant-modal-confirm-title{
font-size: 1rem;
line-height: 1.5rem;
font-weight: 400;
}
.ant-modal-confirm-content{
margin-top: 10px;
margin-left: 0px;
}
.ant-modal-confirm-btns{
@apply mt-6;
button{
font-size: 14px;
line-height: 1.5714285714285714;
height: 32px;
padding: 4px 15px;
border-radius: 4px;
min-width: 88px;
}
}
}
.ant-checkbox {
border-radius: 2px;
.ant-checkbox-inner {
border-radius: 2px;
width: 18px;
height: 18px;
&:after {
//inset-inline-start: 28%;
}
}
}
.ant-message {
z-index: var(--message-z-index);
}
.background-template-popover{
.ant-popover-inner{
background-color: #E9E9E9;
}
}
.ant-modal-root {
.ant-modal-mask {
@apply bg-black/20;
backdrop-filter: blur(7px);
}
.ant-modal {
.ant-modal-content {
background: #f2f2f2;
}
.ant-modal-body {
padding: 10px 0;
}
.ant-modal-confirm-content {
color: #999;
}
.ant-modal-confirm-btns {
@apply mt-6;
}
}
}
.article-edit-modal {
.ant-modal {
.ant-modal-content {
@apply bg-white p-0;
.ant-modal-body {
@apply p-0;
}
}
}
.article-title {
border-bottom: 1px solid rgba(0,0,0,0.09);
}
.article-body {
@apply p-6 pt-1;
}
.modal-control-footer {
@apply p-6
}
.hot-news-list{
@apply focus-within:bg-[#e6ebf1] focus-within:border-gray-100;
}
.input-box {
// focus-within:shadow
@apply text-base bg-[#f8f8f8] border border-transparent w-full px-3 focus-within:bg-[#f3f3f3] focus-within:border-gray-100;
border-radius: 8px;
color:#3d3d3d;
}
.main-human-text{
@apply focus-within:bg-[#e6ebf1] focus-within:border-gray-100;
}
.main-human-text-input{
// focus-within:shadow
@apply text-base bg-[#f8f8f8] border border-transparent w-full p-2;
min-height: 100%;
}
}
.icon-language{
@apply relative text-gray-500 p-1.5 hover:bg-[#e3eeff] hover:text-gray-600 rounded cursor-pointer text-xl;
}
@keyframes animation_loading {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.icon-generating{
animation: animation_loading 6s linear infinite;
}
// 全局按钮
.page-action {
@apply fixed right-10 bottom-10 flex flex-col gap-4 z-10;
button {
@apply border-0 min-w-[120px] h-[40px] rounded-3xl pr-4 flex items-center justify-between drop-shadow;
.text {
flex: 1;
text-align: left;
padding-left: 15px;
margin-right: 10px;
}
&:disabled {
@apply bg-gray-400;
}
&.btn-info {
@apply bg-info text-gray-800;
}
}
}
.lang-en-US{
.page-action {
button{
@apply min-w-[140px];
.text{
text-align: center;
}
svg{
margin-left: 5px;
}
}
}
}
// 时间选择
.timer-select-container {
.timer-select-value {
@apply text-blue-500 px-4 cursor-pointer h-[31px];
}
.timer-select-options {
@apply rounded-xl py-1 overflow-hidden drop-shadow absolute inset-x-0 top-[30px];
background: linear-gradient(180deg, rgb(244, 247, 252) 0%, rgb(217, 232, 255) 100%);
}
.timer-select-option-item {
@apply py-1.5 px-4 cursor-pointer text-gray-800 hover:text-blue-500;
&.selected {
@apply text-blue-500;
}
}
}
//来源选择
.tag-select-container {
.select-value {
@apply text-blue-500 px-4 cursor-pointer h-[31px];
}
.options-list-container {
@apply py-2 drop-shadow absolute top-[30px];
border-radius: 0 0 0.75rem 0.75rem;
background: linear-gradient(180deg, rgb(244, 247, 252) 0%, rgb(217, 232, 255) 100%);
.inner{
@apply overflow-auto;
max-height: 300px;
}
}
.options-list {
overflow-y: auto;
overflow-x: hidden;
}
.select-option-item {
@apply py-1.5 px-4 cursor-pointer text-gray-800 hover:text-blue-500 hover:bg-[#d9eaff];
&.selected {
@apply text-blue-500;
}
}
}
.tag-select-child-container {
.ant-popover-inner {
@apply p-0 overflow-hidden drop-shadow shadow-none;
//
background: linear-gradient(180deg, rgb(235, 242, 253) 0%, rgb(244, 247, 252) 100%);
border-radius: 0 0.75rem 0.75rem 0;
transform: translate(16px, -7px);
//backdrop-filter: 0 3px 1px 5px rgb(0, 0, 0,0.1);
}
.sub-options-list {
@apply py-1.5 top-0;
max-height: 150px;
.select-option-item {
@apply py-1.5 px-4 cursor-pointer text-gray-800 hover:text-blue-500 hover:bg-[#d9eaff];
&.selected {
@apply text-blue-500;
}
}
}
}
.modal-count-normal {
color:rgba(87, 148, 247, 1);
}
.modal-count-warning {
color:rgba(255, 0, 4, 0.85);
}

View File

@ -0,0 +1,4 @@
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3726" width="256" height="256">
<path d="M896 213.333333v281.173334l-97.706667-98.133334c-16.64-16.64-43.946667-16.64-60.586666 0L597.333333 537.173333 456.96 396.8a42.496 42.496 0 0 0-60.16 0L256 537.173333 128 408.746667V213.333333c0-46.933333 38.4-85.333333 85.333333-85.333333h597.333334c46.933333 0 85.333333 38.4 85.333333 85.333333z m-128 273.92l128 128.426667V810.666667c0 46.933333-38.4 85.333333-85.333333 85.333333H213.333333c-46.933333 0-85.333333-38.4-85.333333-85.333333v-280.746667l97.706667 97.706667c16.64 16.64 43.52 16.64 60.16 0l140.8-140.8 140.373333 140.373333c16.64 16.64 43.52 16.64 60.16 0l140.8-139.946667z"
fill="#e6e6e6" />
</svg>

After

Width:  |  Height:  |  Size: 770 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -15,7 +15,6 @@ body {
font-size: 16px;
font-family: -apple-system, "PingFang SC", 'Microsoft YaHei', sans-serif;
background-color: var(--main-bg-color);
min-width: 1000px;
}
.dashboard-layout {
@ -25,20 +24,26 @@ body {
.navigation-container {
//width: var(--navigation-width);
background-color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
//box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
.nav-item {
padding: 0px 20px;
color: rgb(90, 90, 90);
font-size: 20px;
&.active {
@apply text-blue-500;
color: rgb(19, 19, 19);
font-weight: bold;
}
}
}
.app-header {
@apply w-full navigation-container flex justify-between items-center p-basic fixed top-0 inset-x-0 z-10;
@apply w-full navigation-container flex justify-between items-center p-basic fixed top-0 inset-x-0 bg-white/90;
z-index: var(--header-z-index);
height: var(--app-header-header);
}
.app-content {
padding-top: var(--app-header-header);
}
@ -52,14 +57,64 @@ body {
}
.container {
max-width: 90%;
width: var(--container-width,1200px);
max-width: 95%;
width: var(--container-width, 1800px);
margin: 0 auto;
}
.article-action-icon {
@apply cursor-pointer text-gray-500 hover:text-blue-500 block;
}
.news-detail-modal{
.ant-modal-content{
padding-right: 0;
padding-left: 0;
}
.news-detail{
video{
width: 100%;
max-width: 100%;
max-height: 400px;
}
}
}
.userinfo-drop-menu{
@apply drop-shadow bg-gray-50 pb-2 rounded px-4;
.user-profile{
@apply py-3;
.info{
min-width: 100px;
}
.user-avatar{
width: 36px;
height: 36px;
}
}
.menu-list-container{
.ant-dropdown-menu{
@apply shadow-none bg-transparent p-0 rounded-none;
.ant-dropdown-menu-item{
@apply hover:bg-gray-200 text-gray-600 hover:text-gray-800 rounded-none text-base py-2;
}
.nav-item{
@apply flex items-center;
.svg-icon{
transform: translateY(1px);
}
.nav-text{
@apply ml-2;
}
}
}
}
.logout{
@apply text-center py-2 hover:bg-gray-200 text-gray-600 hover:text-gray-800 cursor-pointer;
}
}
.news-detail-content-container{
margin-right: -30px;
padding-right: 30px;
}
img {
max-width: 100%;
@ -68,7 +123,6 @@ img{
@media screen and (max-width: 2000px) {
:root {
line-height: 1.2;
--app-header-header: 70px;
}
.logo-container {
height: 36px;
@ -80,7 +134,7 @@ img{
--navigation-width: 200px;
}
.container {
max-width: 98%;
max-width: 90%;
padding: 20px 0;
}

View File

@ -0,0 +1,61 @@
import styles from './article.module.scss'
import {useTranslation} from "react-i18next";
import {Input, Switch} from "antd";
import {useMemo} from "react";
type HotNewsProps = {
news: string[];
mode: string;
onValueChange: (values: {
news: string[],
mode: string
}) => void;
}
function HotNews({news, mode, onValueChange}: HotNewsProps) {
const {t,i18n} = useTranslation()
const demoPlaceholderList = useMemo(()=>{
return i18n.language == 'zh-CN' ? [
'例:韩正会见英国汇丰集团主席',
'例: 丁薛祥出席全国高校毕业生等青年就业创业工作视频...',
'例:俄称乌方再度袭击俄能源设施 乌称击退俄军进攻',
] : [
'eg.China\'s tax policies invigorate private economy',
'eg.China\'s central bank conducts reverse repos Wednesday',
'eg.China intensifies law enforcement in cyberspace',
]
},[i18n.language])
const handleValueChange = (value: string, index: number) => {
const values = [...news]
values[index] = value
onValueChange({news: values, mode})
}
return (
<div className={`${styles.hotNews} mt-3`}>
<div className="flex justify-between">
<div className="area-title">
<span className="title">{t("modal.hot_news.title")}</span>
</div>
<div className="mode">
<span className="mr-2">{mode == 'auto' ? t("modal.hot_news.edit_auto") : t("modal.hot_news.edit_manual")}</span>
<Switch size="small" checked={mode != 'auto'} onChange={checked => {
onValueChange({news, mode: checked ? 'manual' : 'auto'})
}}/>
</div>
</div>
{mode != 'auto' && <div className="hot-news-list panel-body p-3 ">
{news.map((item, index) => <div key={index} className={`hot-news-item bg-gray-50 ${index == 0?'':'mt-3'} rounded-xl`}>
<Input
variant={"borderless"}
readOnly={mode == 'auto'}
placeholder={mode != 'auto' ? demoPlaceholderList[index] : ''}
value={item}
onChange={e => handleValueChange(e.target.value, index)}/>
</div>)}
</div>}
</div>
)
}
export default HotNews

View File

@ -1,10 +1,30 @@
.blockContainer {
@apply flex mb-5;
@apply relative;
:global{
.divider-container{
@apply absolute inset-x-2 z-10;
&.before{
top:-12px;
}
&.after{
bottom: -10px;
}
.ant-divider-horizontal{
margin: 0;
border-block-start: 1px rgba(5, 5, 5,0.1);
}
}
.article-action-add{
@apply text-gray-600 text-sm inline-block bg-[#cce2ff] w-[80px] justify-center flex rounded-xl cursor-pointer hover:bg-blue-300 hover:text-white;
}
}
}
.blockInner{
@apply flex px-4 py-10 hover:bg-[#e6ebf1] ;
}
.blockFooter{}
.block {
@apply border border-gray-300 border-dashed p-3 rounded flex-1;
@apply flex-1;
&:last-child {
@apply mb-0;
}
@ -23,10 +43,28 @@
}
.group {
@apply flex gap-4;
:global{
.area-title{
@apply text-gray-400 text-base text-gray-800;
}
.digital-person{
width: 450px;
}
.panel{
@apply flex flex-col;
}
.panel-body{
@apply bg-[#f0f0f0] flex-1 rounded-xl mt-2;
}
}
}
.imagerOrText{
@apply bg-[#f8f8f8] rounded-xl py-2;
}
.imageList {
@apply grid grid-cols-4 gap-4 p-3 border border-blue-200;
@apply grid grid-cols-4 imagerOrText px-2 gap-2;
:global {
.ant-upload-wrapper {
display: block;
@ -57,19 +95,14 @@
}
}
.imageDelete{
@apply absolute flex items-center justify-center p-0.5 w-[22px] h-[22px] rounded-full border border-red-500 text-red-500 cursor-pointer z-10;
right:-10px;
top:-10px;
font-size: 14px;
&:hover{
@apply text-white bg-red-500;
}
@apply absolute flex items-center justify-center right-0 top-0 w-[22px] h-[22px] rounded-full cursor-pointer z-10 ;
font-size: 16px;
}
.uploadImage {
@apply flex justify-center items-center relative h-[100px] text-gray-400;
.uploadTips {
@apply absolute inset-0 cursor-pointer opacity-0 transition rounded flex items-center justify-center bg-black/20 text-white;
@apply absolute inset-0 cursor-pointer opacity-0 transition rounded flex items-center justify-center bg-black/50 text-white;
}
.imagePlaceholder {
@ -86,16 +119,23 @@
}
.text {
@apply border border-blue-200 overflow-hidden flex-1 rounded focus:border-blue-200 transition;
@apply imagerOrText overflow-hidden flex-1 rounded focus:border-blue-200 transition;
&:hover {
@apply border-blue-500;
}
&:focus-within {
@apply border-blue-500 shadow-md;
:global{
.ant-input{
@apply px-4;
}
}
}
.textarea {
@apply border-0
}
// hot news
.hotNews{
.title{}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -1,12 +1,14 @@
import React from "react";
import clsx from "clsx";
import {Popconfirm} from "antd";
import {Divider, Popconfirm} from "antd";
import {IconAdd, IconDelete} from "@/components/icons";
import {IconAdd, IconDelete, IconWarningCircle} from "@/components/icons";
import ImageList from "@/components/article/list.tsx";
import { BlockText} from "./item.tsx";
import styles from './article.module.scss'
import {useTranslation} from "react-i18next";
import ModalWarning from "@/components/icons/ModalWarning.tsx";
type Props = {
children?: React.ReactNode;
@ -15,8 +17,9 @@ type Props = {
blocks: BlockContent[];
editable?: boolean;
onChange?: (blocks: BlockContent[]) => void;
disableRemoveMessage?:string;
onRemove?: () => void;
onAdd?: () => void;
onAdd?: (index:number,checkIndex:number) => void;
errorMessage?: string;
}
@ -45,11 +48,12 @@ export default function ArticleBlock(
blocks: defaultBlocks,
editable,
onRemove,
disableRemoveMessage,
onAdd,
onChange,
index,
errorMessage
}: Props) {
const {t} = useTranslation()
const blocks = rebuildBlockArray(defaultBlocks)
const handleBlockChange = (index: number, block: BlockContent) => {
@ -58,7 +62,11 @@ export default function ArticleBlock(
onChange?.(_blocks)
}
return <div className={styles.blockContainer}>
return <div className={`${styles.blockContainer} group`}>
{editable && index == 1 && <div className={'divider-container before'}><Divider>
<span onClick={()=>onAdd?.(1,1)} className="article-action-add" title={t('news.edit_add_group')}><IconAdd style={{fontSize: 24}}/></span>
</Divider></div> }
<div className={styles.blockInner}>
<div className={clsx(className || '', styles.block, index == 0 ? styles.blockFist : '', ' hover:bg-blue-10')}>
<div className={styles.blockBody}>
<div>
@ -69,30 +77,33 @@ export default function ArticleBlock(
isFirstBlock={index == 0}
editable={editable}/>
</div>
{index == 0 && <div className="flex items-center text-red-500 justify-between text-sm mt-1">
<div>{errorMessage}</div>
<div></div>
</div>}
</div>
{index > 0 && <ImageList blocks={blocks} editable={editable} onChange={onChange}/>}
<ImageList blocks={blocks} editable={editable} onChange={onChange}/>
</div>
</div>
{editable && <div className="ml-2 flex flex-col justify-between ">
{editable && <div className="ml-2 flex items-center">
{
index > 0 ? <Popconfirm
title={<div style={{minWidth: 150}}><span>?</span></div>}
disableRemoveMessage? <span></span> : <Popconfirm
rootClassName={'popconfirm-main'}
placement={'left'}
arrow={false}
icon={<ModalWarning.Icon />}
title={<ModalWarning.Title />}
description={<div style={{minWidth: 150}}><span>{t('news.edit_delete_group_confirm')}</span></div>}
onConfirm={onRemove}
okText="删除"
cancelText="取消"
okText={t('delete')}
cancelText={t('cancel')}
>
<span className="article-action-icon" title="删除此分组">
<span className="article-action-icon hidden group-hover:block ml-1" title={t('news.edit_delete_group')}>
<IconDelete style={{fontSize: 24}}/>
</span>
</Popconfirm> : <span></span>
</Popconfirm>
}
<span onClick={onAdd} className="article-action-icon" title="新增分组"><IconAdd
style={{fontSize: 24}}/></span>
</div>}
</div>
{editable && <div className={'divider-container after'}><Divider>
<span onClick={()=>onAdd?.(index + 1,index)} className="article-action-add" title={t('news.materials.add_group')}><IconAdd style={{fontSize: 24}}/></span>
</Divider></div> }
</div>
}

View File

@ -1,14 +1,25 @@
import {Input, Modal} from "antd";
import ArticleGroup from "@/components/article/group.tsx";
import {useEffect, useState} from "react";
import {useSetState} from "ahooks";
import * as article from "@/service/api/article.ts";
import {regenerate} from "@/service/api/video.ts";
import { Modal, App, Radio, Popover } from 'antd';
import React, { useEffect, useState } from 'react';
import { useSetState } from 'ahooks';
import { useTranslation } from 'react-i18next';
import { TFunction } from 'i18next';
import * as article from '@/service/api/article.ts';
import { regenerate } from '@/service/api/video.ts';
import { push2video } from '@/service/api/article.ts';
import { showErrorToast, showToast } from '@/components/message.ts';
import ArticleGroup, { HotNewsData } from '@/components/article/group.tsx';
import type { HookAPI as ModalHookAPI } from 'antd/es/modal/useModal';
import { IconWarningCircle } from '@/components/icons';
import Bg1 from './assets/bg1.jpg'
import Bg2 from './assets/bg2.jpg'
type Props = {
id?: number;
type: 'news' | 'video';
onClose?: (saved?: boolean) => void;
onRefresh?: () => void
}
const DEFAULT_STATE = {
@ -17,15 +28,15 @@ const DEFAULT_STATE = {
msgTitle: '',
msgGroup: '',
error: ''
}
};
function pushBlocksToGroup(blocks: BlockContent[], groups: BlockContent[][]) {
const lastGroup = groups[groups.length - 1]
const lastGroup = groups[groups.length - 1];
if (lastGroup && lastGroup.filter(s => s.type == 'text').length == 0) {
// 如果上一个group中没有文本则直接合并
lastGroup.push(...blocks)
lastGroup.push(...blocks);
} else {
groups.push(blocks)
groups.push(blocks);
}
}
@ -36,114 +47,299 @@ function rebuildGroups(groups: BlockContent[][]) {
if (!blocks) return;
blocks = blocks.filter(s => !!s).sort((a, b) => {
if (a.type == 'text' && b.type == 'text') return 1;
return a.type == 'text' ? -1 : 1
})
return a.type == 'text' ? -1 : 1;
});
if (blocks.length == 1) {
if(index == 0) _groups.push(blocks)
else pushBlocksToGroup(blocks,_groups)
if (index == 0) _groups.push(blocks);
else pushBlocksToGroup(blocks, _groups);
} else {
if (index == 0) {
_groups.push([blocks[0]])
_groups.push(blocks.slice(1))
_groups.push([blocks[0]]);
_groups.push(blocks.slice(1));
} else {
pushBlocksToGroup(blocks,_groups)
pushBlocksToGroup(blocks, _groups);
}
}
});
if (_groups.length < 2) {
Array(2 - _groups.length).fill([{ type: 'text', content: '' }]).forEach((it) => {
_groups.push(it)
})
_groups.push(it);
});
}
console.log('rebuildGroups', _groups)
// console.log('rebuildGroups', _groups)
return _groups;
}
function groupHasImageAndText(blocks: BlockContent[]) {
return blocks.some(s => s.type == 'image' && s.content.trim().length > 0) && blocks.some(s => s.type == 'text' && s.content.trim().length > 0);
}
// 验证分组数据是否合法
function checkGroupsValid(_groups: BlockContent[][]) {
const groups = _groups.filter((_, index) => {
if (index == 0) return true;
return _.length > 1 || (_.length == 1 && _[0].content.trim().length > 0);
});
if (groups.length == 1) return true;
for (let index = 1; index < groups.length; index++) {
if (!groupHasImageAndText(groups[index])) return false;
}
return true;
}
function checkHotNewsValid(hotNews: HotNewsData, modal: ModalHookAPI, t: TFunction<'translation', undefined>) {
return new Promise<boolean>((resolve) => {
// 验证热点新闻数据是否正确
if (hotNews.mode == 'manual' && hotNews.list.filter(s => s.trim().length > 0).length < 3) {
modal.warning({
wrapClassName: 'root-modal-confirm',
icon: <span className="anticon anticon-exclamation-circle"><IconWarningCircle /></span>,
title: t('modal.hot_news.empty_notice_title'),
content: <span dangerouslySetInnerHTML={{ __html: t('modal.hot_news.empty_notice_message') }}></span>,
centered: true,
onOk: () => {
resolve(false);
},
onCancel: () => {
resolve(false);
}
});
return;
}
resolve(true);
});
}
export default function ArticleEditModal(props: Props) {
const { t, i18n } = useTranslation();
const { modal } = App.useApp();
const [groups, setGroups] = useState<BlockContent[][]>([]);
const [title, setTitle] = useState('')
const [title, setTitle] = useState('');
const [tag, setTag] = useState('');
const [articleTemplateInfo, setArticleTemplateInfo] = useState<ArticleTemplateInfo>({
select:'',
options:[]
});
const [hotNews, setHotNews] = useState<HotNewsData>({
list: ['', '', ''],
mode: 'auto'
});
const [state, setState] = useSetState({
...DEFAULT_STATE
})
...DEFAULT_STATE,
generating: false,
pushed: false
});
// 保存数据
const handleSave = () => {
setState({error: ''})
const handleSave = async () => {
setState({ error: '' });
if (!title) {
// setState({msgTitle: '请输入标题内容'});
return;
}
if (groups.length == 0 || groups[0].length == 0 || !groups[0][0].content) {
// setState({msgGroup: '请输入正文文本内容'});
if(i18n.language == 'zh-CN' && tag.length > 4 ){
// 获取图文设置不正确的数据
setState({ msgGroup: t('news.edit.tag_length_error') });
return;
}
const save = props.type == 'news' ? article.save : regenerate
setState({loading: true})
save(title, groups, props.id && props.id > 0 ? props.id : undefined).then(() => {
props.onClose?.(true)
}).catch(e=>{
setState({error: e.data || '保存失败,请重试!'})
}).finally(() => {
setState({loading: false})
});
if (groups.length == 0 || groups[0].length == 0 || !groups[0][0].content) {
setState({ msgGroup: t('news.edit_empty_human_content') });
return;
}
// 验证图文都存在时,文图是否匹配
if (!checkGroupsValid(groups)) {
// 获取图文设置不正确的数据
setState({ msgGroup: t('news.edit_empty_group_content') });
return;
}
const hotNewsValid = await checkHotNewsValid(hotNews, modal, t);
if (!hotNewsValid) return;
// if (groups.length == 0 || groups[0].length == 0 || !groups[0][0].content) {
// // setState({msgGroup: '请输入正文文本内容'});
// return;
// }
const save = props.type == 'news' ? article.save : regenerate;
setState({ loading: true });
save({
title,
metahuman_text: groups[0][0].content,
content_group: groups.slice(1),
hot_news: hotNews.mode == 'auto' ? [''] : hotNews.list,
video_tag:tag,
background: articleTemplateInfo.select,
id: props.id && props.id > 0 ? props.id : undefined
}).then(() => {
props.onClose?.(true);
}).catch(e => {
setState({ error: e.message || t('news.edit_save_failed') });
}).finally(() => {
setState({ loading: false });
});
};
const handlePush2Video = async () => {
if (state.pushed) return;
if (!title) {
// setState({msgTitle: '请输入标题内容'});
return;
}
if(i18n.language == 'zh-CN' && tag.length > 4 ){
// 获取图文设置不正确的数据
setState({ msgGroup: t('news.edit.tag_length_error') });
return;
}
if (groups.length == 0 || groups[0].length == 0 || !groups[0][0].content) {
setState({ msgGroup: t('news.edit_empty_human_content') });
return;
}
// 验证图文都存在时,文图是否匹配
if (!checkGroupsValid(groups)) {
// 获取图文设置不正确的数据
setState({ msgGroup: t('news.edit_empty_group_content') });
return;
}
if (!props.id || state.generating) return;
const hotNewsValid = await checkHotNewsValid(hotNews, modal, t);
if (!hotNewsValid) return;
setState({ generating: true });
await article.save({
title,
metahuman_text: groups[0][0].content,
content_group: groups.slice(1),
hot_news: hotNews.mode == 'auto' ? [''] : hotNews.list,
video_tag:tag,
background: articleTemplateInfo.select,
id: props.id
});
push2video([props.id]).then(() => {
showToast(t('news.push_stream_success'), 'success');
setState({ pushed: true });
props.onClose?.(true);
// props.onRefresh?.();
// navigate('/create?state=push-success',{
// state: 'push-success'
// })
// props.onSuccess?.()
}).catch(showErrorToast).finally(() => {
setState({ generating: false });
});
};
useEffect(() => {
setState({...DEFAULT_STATE})
setState({ ...DEFAULT_STATE });
if (typeof (props.id) != 'undefined') {
// 如果传入了id则获取数据
if (props.id > 0) {
article.getById(props.id).then(res => {
setGroups(rebuildGroups(res.content_group))
setTitle(res.title)
})
if (res.hot_news) {
const len = res.hot_news.length;
const list = len >= 3 ? res.hot_news : res.hot_news.concat(Array(3 - len).fill(''));
const mode = res.hot_news && res.hot_news.filter(s => s.length > 0).length == 3 ? 'manual' : 'auto';
setHotNews({
list,
mode
});
}
setGroups(rebuildGroups([[{ content: res.metahuman_text, type: 'text' }], ...res.content_group]));
setTitle(res.title);
setTag(res.video_tag)
setArticleTemplateInfo(res.template_info)
});
} else {
// 新增
setGroups([])
setTitle('')
setGroups([]);
setTitle('');
}
}
}, [props.id])
}, [props.id]);
return (<Modal
title={'编辑文章'}
title={null}
centered={true}
rootClassName={'article-edit-modal'}
open={props.id != undefined && props.id >= 0}
maskClosable={false}
keyboard={false}
width={800}
width={'1200px'}
footer={null}
closeIcon={null}
onCancel={() => props.onClose?.()}
okButtonProps={{ loading: state.loading }}
onOk={handleSave}
okText={props.type == 'news' ? '确定' : '重新生成'}
okText={props.type == 'news' ? t('confirm_text') : t('news.edit_generate_video_again')}
>
<div className="article-title mt-5">
<div className="title">
<span className="text text-base"></span>
<span className="require ml-1 font-bold text-red-500">*</span>
<div className="mt-5 px-6 pt-10">
<div className="flex items-center pb-3 article-title">
<span className="mr-2 text-lg">{t('news.title')}</span>
<input className={'input-box text-lg flex-1 py-2'} value={title} onChange={e => {
setTitle(e.target.value);
setState({ msgTitle: e.target.value ? '' : t('news.edit_notice_enter_article_title1') });
}} placeholder={t('news.edit_notice_enter_article_title')} />
</div>
<div className="box mt-1">
<Input rootClassName={state.msgTitle ? 'border-red-500' : ''} value={title} onChange={e => {
setTitle(e.target.value)
setState({msgTitle: e.target.value ? '' : '请输入标题内容'})
}} placeholder={'请输入文章标题'}/>
<div className="text-red-500 mt-2">{state.msgTitle}</div>
</div>
<div className="text-red-500">{state.msgTitle}</div>
</div>
<div className="aricle-body mt-3">
<div className="title">
<span className="text text-base"></span>
<span className="require ml-1 font-bold text-red-500">*</span>
</div>
<div className="box mt-1">
<div className="article-body">
<div className="box text-base">
<ArticleGroup
errorMessage={state.msgGroup} editable groups={groups}
onChange={list => {
setGroups(() => list)
setState({msgGroup: (list.length == 0 || list[0].length == 0 || !list[0][0].content) ? '请输入正文文本内容' : ''});
errorMessage={state.msgGroup}
editable
groups={groups}
hotNews={hotNews}
onChange={(list, hotNews) => {
setHotNews(hotNews);
setGroups(() => list);
setState({ msgGroup: (list.length == 0 || list[0].length == 0 || !list[0][0].content) ? t('news.edit_empty_human_content') : '' });
}}
/>
leftPanelHeader={<div>
<div className="row tag flex items-center mt-2">
<span className="mr-2">{t('news.edit.tag')}</span>
<input className={'input-box flex-1 py-1.5'} value={tag} onChange={e => {
setTag(e.target.value);
}} placeholder={t('news.edit.tag_placeholder')} />
</div>
<div className="row bg flex items-center my-3">
<span className="mr-2">{t('news.edit.bg')}</span>
<div className="bg-radio-container">
<Radio.Group
value={articleTemplateInfo.select}
onChange={e=>{
setArticleTemplateInfo(prev=>(
{
...prev,
select: e.target.value
}
))
}}>
{articleTemplateInfo.options.map((opt,idx)=>(
<Popover
rootClassName="background-template-popover"
key={idx} placement="bottomLeft" arrow={false}
content={<img className="w-[150px] rounded" src={opt.background} />}
>
<Radio value={opt.template_id}>{t('news.edit.bg')}{idx + 1}</Radio></Popover>
))}
</Radio.Group>
</div>
</div>
</div>}
/>
<div className="text-red-500 mt-2">{state.msgGroup}</div>
</div>
{state.error && <div className="text-red-500 mt-2">{state.error}</div>}
</div>
<div className="modal-control-footer flex justify-end">
<div className="flex gap-10 ">
{props.type == 'news' && props.id ? <button
className="text-gray-400 hover:text-gray-800"
onClick={handlePush2Video}
>
{t('news.edit_generate_video')}{state.pushed ? `${i18n.language == 'zh-CN' ? '中' : ''}...` : (state.generating ? `${i18n.language == 'zh-CN' ? '推送中' : 'Pushing'}...` : '')}
</button> : null}
<button className="text-gray-400 hover:text-gray-800" onClick={() => props.onClose?.()}>{t('cancel')}</button>
<button onClick={handleSave}
className="text-gray-800 hover:text-blue-500">{props.type == 'news' ? t('news.save_text') : t('news.edit_generate_again')}</button>
</div>
{state.error && <div className="text-red-500">{state.error}</div>}
</div>
</Modal>);
}

View File

@ -1,30 +1,42 @@
import {message} from "antd"
import {Divider, Input, message} from "antd"
import ArticleBlock from "@/components/article/block.tsx";
import styles from './article.module.scss'
import {showToast} from "@/components/message.ts";
import React from "react";
import {useTranslation} from "react-i18next";
import {IconAdd} from "@/components/icons";
import HotNews from "@/components/article/HotNews.tsx";
export type HotNewsData = {
list: string[];
mode: string
}
type Props = {
groups: BlockContent[][];
editable?: boolean;
onChange?: (groups: BlockContent[][]) => void;
onChange?: (groups: BlockContent[][], hotNews: HotNewsData) => void;
errorMessage?: string;
hotNews: HotNewsData;
leftPanelHeader?: React.ReactNode;
}
export default function ArticleGroup({groups, editable, onChange, errorMessage}: Props) {
export default function ArticleGroup({groups, editable, onChange, errorMessage, hotNews, leftPanelHeader}: Props) {
const {t, i18n} = useTranslation()
// const groups = rebuildGroups(_groups)
/**
*
* @param insertIndex -1
*/
const handleAddGroup = (insertIndex: number = -1) => {
if (insertIndex !== -1 && insertIndex !== 1) {
const triggerGroup = insertIndex == -1 || insertIndex >= groups.length ? groups[groups.length - 1] : groups[insertIndex - 1];
const handleAddGroup = (insertIndex: number, checkId: number) => {
// && insertIndex !== 1
if (checkId > 0 && checkId < groups.length) {
//const index = insertIndex == -1 || insertIndex >= groups.length ? groups.length - 1 : insertIndex - 1
const triggerGroup = groups[checkId];
// 判断
if (triggerGroup.length == 0 || triggerGroup.some(s => !s.content)) {
showToast('请先添加内容')
if (!triggerGroup || triggerGroup.length == 0 || triggerGroup.some(s => !s.content)) {
showToast(t('news.edit_notice_enter_text'))
return;
}
}
@ -36,34 +48,95 @@ export default function ArticleGroup({groups, editable, onChange, errorMessage}:
} else {
_groups.splice(insertIndex, 0, newGroup)
}
onChange?.(_groups)
onChange?.(_groups, hotNews)
}
const handleDigitalPersonContentChange = (content: string) => {
groups[0] = [{type: 'text', content}]
onChange?.([...groups], hotNews)
}
return <div className={styles.group}>
<div className={'panel digital-person h-[544px]'}>
{leftPanelHeader}
<div className="area-title">
<span className="">{t('news.edit_digital_text')}</span>
{i18n.language == 'zh-CN' && <span className="text-gray-400"></span>}
</div>
<div className="panel-body p-3 flex-1 main-human-text">
{/* value={groups || groups[0][0].content}*/}
<div className={`h-full rounded-xl overflow-hidden bg-gray-50`}>
<div className="human-tts h-full">
{editable ? <div className="relative h-full">
<Input.TextArea
placeholder={t('news.edit_notice_enter_text')}
className="main-human-text-input"
value={groups && groups.length > 0 ? groups[0][0].content : ''}
autoSize={{maxRows: hotNews.mode == 'auto'?15:8}}
variant={"borderless"}
onChange={e => {
handleDigitalPersonContentChange(e.target.value)
}}
/>
</div> : <p className="p-2">{groups && groups.length > 0 ? groups[0][0].content : ''}</p>}
</div>
</div>
</div>
<div className="hot-news-container">
<HotNews
news={hotNews.list} mode={hotNews.mode}
onValueChange={(hotNews) => {
onChange?.([...groups], {
list:hotNews.news,mode: hotNews.mode
})
}}/>
</div>
</div>
<div className={"panel groups-list flex-1"}>
<div className={"area-title"}>
<span className="">{t('news.edit_other_text')}</span>
{i18n.language == 'zh-CN' && <span className="text-gray-400"></span>}
</div>
<div className="panel-body py-3">
<div className="max-h-[485px] overflow-auto py-4">
{editable && groups.length == 1 && <div className={`${styles.blockContainer} group`}>
<div className={'divider-container before'}><Divider>
<span onClick={() => handleAddGroup?.(1, 1)} className="article-action-add"
title={t('news.materials.add_group')}><IconAdd style={{fontSize: 24}}/></span>
</Divider></div>
</div>}
{groups.map((g, index) => (
<ArticleBlock
index == 0 ? null : <ArticleBlock
editable={editable}
key={index}
blocks={g}
onChange={(blocks) => {
groups[index] = blocks
onChange?.([...groups])
onChange?.([...groups], hotNews)
}}
errorMessage={errorMessage}
index={index}
onAdd={() => {
handleAddGroup?.(index + 1)
onAdd={(_index, checkIndex) => {
handleAddGroup?.(_index ? _index : index + 1, checkIndex)
}}
disableRemoveMessage={groups.length <= 1 ? t('news.edit_notice_keep_1') : ''}
onRemove={async () => {
if (groups.length == 1) {
message.warning('至少保留一个内容块')
if (groups.length <= 1) {
message.warning(t('news.edit_notice_keep_1'))
return;
}
onChange?.(groups.filter((_, idx) => index !== idx))
onChange?.(groups.filter((_, idx) => index !== idx), hotNews)
}}
/>
))}
</div>
</div>
</div>
{groups.length == 0 && editable &&
<ArticleBlock editable onChange={blocks => onChange?.([blocks])} index={0}
<ArticleBlock editable onChange={blocks => onChange?.([blocks],hotNews)} index={0}
blocks={[{type: 'text', content: ''}]}/>}
</div>
}

View File

@ -2,11 +2,14 @@ import React, {useState} from "react";
import {Input, Popconfirm, Spin, Upload, UploadProps} from "antd";
import {CloseOutlined} from "@ant-design/icons";
import {clsx} from "clsx";
import {useTranslation} from "react-i18next";
import styles from './article.module.scss'
import {getOssPolicy} from "@/service/api/common.ts";
import {showToast} from "@/components/message.ts";
import {IconAddImage} from "@/components/icons";
import {ModalWarningIcon, ModalWarningTitle} from "@/components/icons/ModalWarning.tsx";
import { BizError } from '@/service/types.ts';
type Props = {
children?: React.ReactNode;
@ -25,7 +28,7 @@ const MimeTypes = ['image/jpeg', 'image/png', 'image/jpg']
const Data: { uploadConfig?: TOSSPolicy } = {}
export function BlockImage({data, editable, onChange, onlyUpload, onRemove}: ImageProps) {
const {t} = useTranslation()
const [loading, setLoading] = useState<number>(-1)
// oss上传文件所需的数据
const getUploadData: UploadProps['data'] = (file) => ({
@ -36,6 +39,10 @@ export function BlockImage({data, editable, onChange, onlyUpload, onRemove}: Ima
});
const beforeUpload = async (file: any) => {
try {
// 判断文件类型
if (!MimeTypes.includes(file.type)) {
throw new Error('upload_file_type_error')
}
// 因为有超时问题,所以每次上传都重新获取参数
Data.uploadConfig = await getOssPolicy();
const suffix = file.name.slice(file.name.lastIndexOf('.'));
@ -51,28 +58,25 @@ export function BlockImage({data, editable, onChange, onlyUpload, onRemove}: Ima
const onUploadChange = async (info) => {
if (info.fileList.length == 0) return;
const file = info.fileList[0];
console.log('onChange', file);
console.log('onUploadChange', file);
if (file.status == 'done') {
setLoading(-1)
onChange?.({type: 'image', content: Data.uploadConfig?.host + '/' + file.url})
setLoading(-1);
onChange?.({ type: 'image', content: Data.uploadConfig?.host + '/' + file.url });
} else if (file.status == 'error') {
setLoading(-1)
showToast('上传图片失败,请重试', 'warning')
if (!MimeTypes.includes(file.type)) {
showToast(t('upload.upload_file_type_error'), 'warning');
return;
}
setLoading(-1);
showToast(t('upload.upload_failed'), 'warning');
} else if (file.status == 'uploading') {
setLoading(file.percent)
}
setLoading(file.percent);
}
};
//
return <div className={styles.image}>
{editable ? <div className={'relative'}>
{!onlyUpload && <Popconfirm
title={<div style={{minWidth: 150}}><span>?</span></div>}
onConfirm={onRemove}
okText="删除"
cancelText="取消"
>
<span className={styles.imageDelete}><CloseOutlined/></span>
</Popconfirm>}
{editable && onlyUpload ? <div className={'relative'}>
<Spin spinning={loading >= 0} percent={loading == 0 ? 'auto' : loading}>
<Upload
multiple={false} maxCount={1} data={getUploadData}
@ -84,31 +88,59 @@ export function BlockImage({data, editable, onChange, onlyUpload, onRemove}: Ima
{data.content ? <>
<img src={data.content}/>
<div className={styles.uploadTips}>
<span></span>
{!onlyUpload && <Popconfirm
rootClassName={'popconfirm-main'}
placement={'right'}
title={<div style={{minWidth: 150}}><span>{t('upload.delete_confirm')}</span></div>}
onConfirm={onRemove}
okText={t('delete')}
cancelText={t('cancel')}
>
<span className={styles.imageDelete}><CloseOutlined/></span>
</Popconfirm>}
</div>
</> : <div className={styles.imagePlaceholder}>
<div className={'text-center'}>
<IconAddImage className={"text-4xl inline-block"} />
<div className={'text-sm'}></div>
<div className={'text-sm'}>{t('upload.upload_image')}</div>
</div>
</div>}
</div>
</Upload>
</Spin>
</div> : <div className={styles.uploadImage}><img src={data.content}/></div>}
</div> : <div className={styles.uploadImage}>
<img src={data.content}/>
<div className={styles.uploadTips}>
{!onlyUpload && <Popconfirm
rootClassName={'popconfirm-main'}
placement={'right'}
arrow={false}
icon={<ModalWarningIcon/>}
title={<ModalWarningTitle/>}
description={<div style={{minWidth: 150}}><span>{t('upload.delete_confirm')}</span></div>}
onConfirm={onRemove}
okText={t('delete')}
cancelText={t('cancel')}
>
<span className={styles.imageDelete}><CloseOutlined/></span>
</Popconfirm>}
</div>
</div>}
</div>
}
export function BlockText({data, editable, onChange, isFirstBlock}: Props) {
const {t} = useTranslation()
return <div className='flex-1'>
<div
className={clsx(styles.text, isFirstBlock ? 'border-red-400 hover:border-red-500 focus-within:border-red-500' : '')}>
{editable ? <div className="relative">
{/*请输入文本内容*/}
<Input.TextArea
onChange={e => {
onChange?.({type: 'text', content: e.target.value})
}}
placeholder={'请输入文本内容'} value={data.content} autoSize={{minRows: 3, maxRows: 8}}
placeholder={t('news.edit_notice_enter_article_content')} value={data.content} autoSize={{minRows: 4, maxRows: 5}}
variant={"borderless"}/>
</div> : <p className="p-2">{data.content}</p>}
</div>

View File

@ -1,29 +1,39 @@
import React, {useState} from "react";
import {Button, Modal} from "antd";
import {App} from "antd";
import {ButtonType} from "antd/es/button";
import {showErrorToast, showToast} from "@/components/message.ts";
import {BizError} from "@/service/types.ts";
import {IconWarningCircle} from "@/components/icons";
import {LoadingOutlined} from "@ant-design/icons";
import {useTranslation} from "react-i18next";
import ModalWarning from "@/components/icons/ModalWarning.tsx";
type Props = {
selected: any[],
type?: ButtonType;
emptyMessage: string,
confirmMessage: React.ReactNode,
confirmMessage?: React.ReactNode,
icon?: React.ReactNode,
onProcess: (ids: Id[]) => Promise<any | void>
successMessage?: string;
onSuccess?: () => void;
children?: React.ReactNode
children?: React.ReactNode;
title?: React.ReactNode;
className?: string;
onError?: (e: Error | BizError) => void;
}
/**
*
*/
export default function ButtonBatch(
{
selected, emptyMessage, successMessage, children,
type, confirmMessage, onProcess,onSuccess
selected, emptyMessage, successMessage, children, icon,
title, confirmMessage, onProcess, onSuccess, className, onError
}: Props) {
const {t} = useTranslation()
const [loading, setLoading] = useState(false)
const {modal} = App.useApp()
const onBatchProcess = async () => {
setLoading(true)
try {
@ -33,25 +43,38 @@ export default function ButtonBatch(
onSuccess()
}
} catch (e) {
showErrorToast(e as unknown as BizError)
const _e = e as unknown as BizError
onError?onError(_e) : showErrorToast(_e)
} finally {
setLoading(false)
}
}
const handleBtnClick = () => {
if (loading) return;
if (selected.length == 0) {
showToast(emptyMessage, 'warning')
return;
}
Modal.confirm({
title: '操作提示',
if(confirmMessage){
modal.confirm({
wrapClassName: 'root-modal-confirm',
title: <ModalWarning.Title />,
centered: true,
content: confirmMessage,
icon: <ModalWarning.Icon />,
content: <div>
<div>{confirmMessage}</div>
</div>,
onOk: onBatchProcess
})
}else{
onBatchProcess().catch(onError || showErrorToast);
}
}
return (
<Button loading={loading} type={type} onClick={handleBtnClick}>{children}</Button>
<button disabled={loading} className={className} onClick={handleBtnClick}>
{icon ? <span className="text">{children}</span>:children}
{loading ? <LoadingOutlined/> : icon}
</button>
)
}

View File

@ -2,13 +2,13 @@ import React, {useEffect} from "react";
type DocumentTitleProps = {
children?: string;
text?: string;
title?: string;
}
export const DocumentTitle: React.FC<DocumentTitleProps> = ({children, text}) => {
export const DocumentTitle: React.FC<DocumentTitleProps> = ({children, title}) => {
useEffect(() => {
if (text || children) {
document.title = text || children || '';
if (title || children) {
document.title = title || children || '';
}
}, []);
}, [title,children]);
return <></>
}

View File

@ -0,0 +1,189 @@
import React, {useEffect, useMemo, useRef} from "react";
import {Checkbox, Popover} from "antd";
import {useBoolean, useClickAway} from "ahooks";
import {CaretUpOutlined} from "@ant-design/icons";
import {useTranslation} from "react-i18next";
type ValueType = Id[][];
type ValueFunc = (prev:ValueType)=>ValueType;
const TagSelect = (props: {
options: OptionItem[];
onChange: (values: Id[][]) => void;
className?: string;
defaultSelectTags?: Id[][];
}) => {
const [selectValues, __setSelectValues] = React.useState<ValueType>([])
const [checkedAll, _setCheckedAll] = React.useState<boolean>(false)
const [visible, {set}] = useBoolean(false);
const allValues = useMemo(()=>{
const values:Id[][] = []
props.options.forEach(item=>{
if(item && item.children?.length){
// eslint-disable-next-line no-unsafe-optional-chaining
values.push(...(item.children?.map(c => [item.value, c.value])))
}
})
return values
},[props.options])
const _setSelectValues = (values: (ValueType | ValueFunc)) => {
if(typeof values == 'function'){
__setSelectValues((prev)=>{
const data = values(prev);
props.onChange(data)
return data
})
}else{
__setSelectValues(values)
props.onChange(values)
}
}
const handleAllChanged = (checked: boolean) => {
_setCheckedAll(checked)
const values:Id[][] = []
if (checked){
// set(false)
values.push(...allValues)
}
_setSelectValues(values)
}
const handleLevel1Change = (checked: boolean, item: OptionItem) => {
console.log('handleLevel1Change', checked, item)
if (checked) {
const values = selectValues.filter(s => s[0] != item.value)
const checkedIds: Id[][] = [];
item.children?.forEach(c => {
checkedIds.push([item.value, c.value])
})
const _values = [...values, ...checkedIds];
_setCheckedAll(_values.length == allValues.length)
_setSelectValues(_values)
} else {
_setCheckedAll(false)
const values = selectValues.filter(s => s[0] != item.value)
_setSelectValues([...values])
}
}
const level1Checked = (item: OptionItem) => {
// 完全没有选中
if (selectValues.findIndex(s => s[0] == item.value) == -1) return -1
const myList = selectValues.filter(s => s[0] == item.value)
// 只有1个但是全选或者选中元素和子元素个数相等
if (myList.length == item.children?.length) return 1;
return 0;
}
const handleLevel2Change = (checked: boolean, item: OptionItem, parent: OptionItem) => {
// 获取一级选项的选中状态
const parentList = selectValues.filter(s => s[0] == parent.value)
_setSelectValues((prev) => {
let values = [...prev]
if (checked) {
values = [...prev,[parent.value, item.value]]
} else {
_setCheckedAll(false)
if(parentList.length == 0){ // && parentList[0].length == 1
return prev;
}
values = values.filter(s => s.length == 1 || s[1] != item.value)
}
_setCheckedAll(values.length == allValues.length)
return values
})
}
const level2Checked = (item: OptionItem, parent: OptionItem) => {
// 获取一级选项的选中状态
const parentList = selectValues.filter(s => s[0] == parent.value)
// 没有找到父级
if (parentList.length == 0) return false;
if (parentList.length == 1) {
// 只有一个 且长度为0 说明是全选
if (parentList[0].length == 1) return true;
return parentList[0][1] == item.value;
}
return parentList.findIndex(s => s[1] == item.value) != -1;
}
const {t} = useTranslation()
const ref = useRef<HTMLDivElement|null>(null)
useClickAway(()=>{
set(false)
},ref)
useEffect(()=>{
if(props.defaultSelectTags){
__setSelectValues(props.defaultSelectTags)
}
//console.log('props.defaultSelectTags',props.defaultSelectTags)
},[props.defaultSelectTags])
return (<div ref={ref} className={`tag-select-container z-10 select-none relative group ${props.className}`}>
<div className="select-value w-[120px] flex justify-center items-center"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
set(!visible)
}}
>
<span>{(checkedAll || selectValues.length == 0) ? t('news.news_all_source') : t('news.source')}</span>
<CaretUpOutlined className={`ml-2 arrow-icon ${visible ? 'rotate-0' : 'rotate-180'}`}/>
</div>
<div className={`options-list-container absolute ${visible ? 'block' : 'hidden'}`}>
<div className="inner">
<ul className="options-list">
<li className="select-option-item relative">
<div className="option-value whitespace-nowrap flex justify-between">
<span className="text-center flex-1"
onClick={() => handleAllChanged(!checkedAll)}>{t('news.news_all_source')}</span>
<Checkbox className="ml-6" checked={checkedAll}
onChange={e => handleAllChanged(e.target.checked)}/>
</div>
</li>
{props.options.filter(s => s.value != 999999).map((option) => {
const checkStatus = level1Checked(option)
return (<li key={option.value} className="select-option-item relative">
{(option.children && option.children.length > 0) ?
<Popover placement="rightTop" trigger={['hover']}
rootClassName="tag-select-child-container" arrow={false}
content={option.children && <ul className="sub-options-list select-none" onClick={(e)=>{
e.preventDefault();
e.stopPropagation()
}}>
{option.children.map((subOption) => {
const myCheckStatus = level2Checked(subOption, option)
return (<li key={subOption.value}
className="sub-option-item select-option-item whitespace-nowrap">
<div
className="option-value whitespace-nowrap flex justify-between">
<span
onClick={() => {
handleLevel2Change(!myCheckStatus, subOption, option)
}}>{subOption.label}</span>
<Checkbox className="ml-6" checked={myCheckStatus}
onChange={e => handleLevel2Change(e.target.checked, subOption, option)}/>
</div>
</li>)
})}
</ul>}>
<div className="option-value whitespace-nowrap flex justify-between">
<span className="text-center flex-1" onClick={() => {
handleLevel1Change(checkStatus == 1 ? false:true, option)
}}>{option.label}</span>
<Checkbox className="ml-6"
checked={checkStatus == 1}
indeterminate={checkStatus == 0}
onChange={e => {
handleLevel1Change(e.target.checked, option)
}}/>
</div>
</Popover> : <div className="option-value whitespace-nowrap flex justify-between">
<span>{option.label}</span>
</div>
}
</li>)
})}
</ul>
</div>
</div>
</div>
)
}
export default TagSelect

View File

@ -0,0 +1,68 @@
import {useMemo, useState} from "react";
import {CaretUpOutlined} from "@ant-design/icons";
import {useTranslation} from "react-i18next";
export type TimeSelectProps = {
value: number;
className?: string;
onChange: (value: number) => void;
}
type OptionItem = {
label: string;
value: number;
}
const TimeSelect = (props: TimeSelectProps) => {
const {t,i18n} = useTranslation();
const AllTimeOption: OptionItem[] = useMemo(()=>([
{
label: t('time_filter.past_30_min'),
value: 1
},
{
label: t('time_filter.past_hour'),
value: 2
},
{
label: t('time_filter.past_4_hour'),
value: 3
},
{
label: t('time_filter.past_24_hour'),
value: 4
},
{
label: t('time_filter.last_week'),
value: 5
},
{
label: t('time_filter.all'),
value: 0
}
]),[i18n.language])
const selectLabel = useMemo(() => {
return AllTimeOption.find(item => item.value == props.value)?.label || ''
}, [props.value])
const [visible, setVisible] = useState<boolean>(false);
const handleClick = (item: OptionItem) => {
setVisible(false)
props.onChange(item.value)
}
return (<div className={`timer-select-container relative group ${props.className}`}
onMouseLeave={() => setVisible(false)}>
<div className={`timer-select-value flex items-center`} onMouseEnter={() => setVisible(true)}>
<div>
<span>{selectLabel}</span>
<CaretUpOutlined className={'ml-2 arrow-icon rotate-180 group-hover:rotate-0'} />
</div>
</div>
<div className={`timer-select-options z-10 ${visible ? 'block' : 'hidden'}`}>
{AllTimeOption.map((item, index) => {
return <div className={`timer-select-option-item ${item.value == props.value?'selected':''}`} key={index}
onClick={() => handleClick(item)}>{item.label}</div>
})}
</div>
</div>)
}
export default TimeSelect

View File

@ -0,0 +1,17 @@
import {IconWarningCircle} from "@/components/icons/index.tsx";
import React from "react";
import {useTranslation} from "react-i18next";
export function ModalWarningIcon({size=24}:{size?:number}) {
return <IconWarningCircle
style={{fontSize: size, color: 'rgba(250, 173, 20, 1)'}}/>
}
export function ModalWarningTitle(){
const {t} = useTranslation()
return <span className="text-base">{t('modal.warning')}</span>
}
const ModalWarning = {
Icon: ModalWarningIcon,
Title: ModalWarningTitle
}
export default ModalWarning

View File

@ -1,407 +1,33 @@
import React from "react";
import RecycleIndex from "@/pages/recycle";
type IconProps = { style?: React.CSSProperties }
export const IconRise = ({style}: IconProps) => {
return (
<svg className="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
width="1em" height="1em" style={style}>
<path
d="M928 361.6v182.4c0 19.2 12.8 32 32 32s32-12.8 32-32v-224c0-35.2-28.8-64-64-64h-224c-19.2 0-32 12.8-32 32s12.8 32 32 32h176l-272 272-204.8-204.8c-6.4-6.4-16-9.6-22.4-9.6-9.6 0-16 3.2-22.4 9.6l-316.8 316.8c-12.8 12.8-12.8 32 0 44.8 12.8 12.8 32 12.8 44.8 0l294.4-294.4 204.8 204.8c6.4 6.4 16 9.6 22.4 9.6 9.6 0 16-3.2 22.4-9.6l297.6-297.6z"
fill="currentColor"></path>
</svg>
)
type IconProps = { style?: React.CSSProperties; className?: string; }
}
export const IconDown = ({style}: IconProps) => {
return (
<svg className="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
width="1em" height="1em" style={style}>
<path
d="M886.4 736h-182.4c-19.2 0-32 12.8-32 32s12.8 32 32 32h224c35.2 0 64-28.8 64-64v-224c0-19.2-12.8-32-32-32s-32 12.8-32 32v172.8l-297.6-297.6c-6.4-6.4-16-9.6-22.4-9.6-9.6 0-16 3.2-22.4 9.6l-204.8 204.8-294.4-294.4c-12.8-12.8-32-12.8-44.8 0-12.8 12.8-12.8 32 0 44.8l316.8 316.8c6.4 6.4 16 9.6 22.4 9.6 9.6 0 16-3.2 22.4-9.6l204.8-204.8 278.4 281.6z"
fill="currentColor"></path>
</svg>
)
}
export const IconChecked = ({style}: IconProps) => {
return (
<svg className="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
width="1em" height="1em" style={style}>
<path
d="M369.792 704.32L930.304 128 1024 223.616 369.984 896l-20.288-20.864-0.128 0.128L0 516.8 96.128 423.68l273.664 280.64z"
fill="currentColor" p-id="4399"></path>
</svg>
)
}
export const IconCheckedFill = ({style}: IconProps) => {
return (
<svg className="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
width="1em" height="1em" style={style}>
<path
d="M512.45568 57.344c251.35104 0 455.11168 203.76064 455.11168 455.11168 0 251.35104-203.76064 455.11168-455.11168 455.11168C261.10464 967.56736 57.344 763.80672 57.344 512.45568 57.344 261.10464 261.10464 57.344 512.45568 57.344z m272.444416 288.876544c-5.842944-5.445632-16.21504-5.953536-22.995968-1.074176l-0.315392 0.232448-301.232128 228.097024c-5.515264 4.176896-15.601664 4.72064-21.655552 1.186816l-0.320512-0.193536-113.998848-71.015424c-7.031808-4.380672-16.93696-2.89792-22.390784 3.219456l-0.24576 0.2816-13.169664 15.497216c-5.553152 6.53312-4.62848 15.625216 1.519616 21.586944l0.27136 0.258048 140.93312 131.342336c10.247168 9.550848 26.529792 10.051584 36.615168 0.9728l0.313344-0.28672 323.310592-301.310976c6.679552-6.223872 7.225344-15.606784 1.036288-21.63712l-0.254976-0.241664-7.419904-6.915072z"
fill="currentColor"></path>
</svg>
)
}
export const IconLoading = ({size, color}: { size?: string | number, color?: string; }) => (
<svg xmlns="http://www.w3.org/2000/svg" style={{
margin: 'auto', display: 'block', ...(size ? {fontSize: size} : {}), ...(color ? {color} : {})
}} width="1em" height="1em" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<circle cx="50" cy="50" r="30" stroke="#6a6a6a" strokeWidth="15" fill="none"></circle>
<circle cx="50" cy="50" r="30" stroke="#aaaaaa" strokeWidth="15" strokeLinecap="round" fill="none">
<animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="1s"
values="0 50 50;180 50 50;720 50 50" keyTimes="0;0.5;1"></animateTransform>
<animate attributeName="stroke-dasharray" repeatCount="indefinite" dur="1s"
values="18.84955592153876 169.64600329384882;94.2477796076938 94.24777960769377;18.84955592153876 169.64600329384882"
keyTimes="0;0.5;1"></animate>
</circle>
</svg>)
export const IconQRCode = ({style}: { style?: React.CSSProperties }) => (
<svg className={'svg-icon'} style={style} xmlns="http://www.w3.org/2000/svg" fill="none" version="1.1"
width="1em" height="1em" viewBox="0 0 15 15">
<path
d="M40.022 29.257C39.8411 28.9562 38.2126 26.9064 37.1812 25.2328C36.8193 24.6499 36.6565 23.9541 36.6927 23.2771C36.8555 19.4785 35.6613 15.0406 33.7614 11.9942C33.11 10.9599 32.0425 9.5872 30.4864 8.28967C32.7662 4.22783 33.7071 1.16264 32.6758 0.260012C32.6396 0.241207 32.6215 0.222402 32.5853 0.184792C32.5672 0.184792 32.5672 0.165988 32.5491 0.165988C31.4815 -0.492181 28.9665 0.842961 25.7819 3.53205C23.3211 5.63819 20.4622 8.55293 17.5852 11.9566C16.7348 12.878 8.82758 22.0924 8.89996 23.8224C8.97233 25.6653 12.5369 23.4463 11.8131 24.0105C11.1979 24.4806 8.99043 25.6841 8.14 24.8191C7.28957 23.9353 8.41141 21.5659 10.4742 18.6135C13.134 14.8149 14.5634 13.2353 16.0653 11.4865C15.2872 11.2984 14.473 11.2044 13.6225 11.2044C10.7456 11.2044 8.1219 12.3327 6.13154 14.1756C8.70092 10.8847 12.5188 8.77859 16.8976 8.77859C17.4042 8.77859 17.8928 8.79739 18.3632 8.85381C19.7746 7.29301 21.204 5.84504 22.5611 4.56631C21.4573 4.35946 20.2631 4.22783 18.9965 4.22783C17.3319 4.22783 15.7577 4.41587 14.292 4.77317C11.0532 5.65699 7.00006 8.47771 4.684 12.107C1.66226 16.9023 1.35466 22.1864 3.07361 27.0193H3.09171C3.32693 27.6586 3.61644 28.2792 3.94214 28.8621C0.667082 34.2027 -0.816645 38.3586 0.449951 39.468C1.71655 40.5963 5.48015 38.3209 10.0399 34.0146C13.8578 30.3853 19.1051 23.9353 19.2137 21.6787C19.3222 19.4221 14.7806 22.0172 15.6672 21.359C16.4272 20.7949 18.9242 19.3845 19.9193 20.4752C20.9145 21.5659 19.4127 24.6687 17.8023 27.1509C16.3005 29.5015 14.2378 32.0025 11.9217 34.466C12.4826 34.56 13.0616 34.5976 13.6406 34.5976C19.8651 34.5976 24.9133 29.3511 24.9133 22.8822C24.9133 18.3878 22.4887 14.4953 18.9242 12.5207C20.4079 10.7719 21.4212 9.68122 21.4212 9.66242L21.5478 9.53078C26.759 11.4489 30.215 16.545 30.215 22.807C30.215 22.8258 31.4273 33.0744 21.9097 39.6373C20.0822 40.8972 19.5755 42.4204 19.847 44C24.0629 43.5675 26.6323 43.9436 31.0654 43.9624C34.25 43.9624 35.1004 43.0974 35.607 41.9503C36.1318 40.8032 35.3899 39.468 35.3899 39.1672C35.3899 38.8663 35.6613 38.697 36.2584 38.0389C36.8555 37.3807 36.9098 36.6285 36.7108 36.4029C36.5298 36.1772 36.1318 35.8387 36.3127 35.8011C37.3803 35.6883 37.706 35.237 37.4888 34.8797C37.2717 34.5224 36.566 33.5445 37.1631 32.8487C37.7602 32.153 39.2259 32.1718 40.0039 31.7017C40.9086 31.1751 40.5106 30.0656 40.022 29.257ZM2.15081 37.6252C1.98796 37.6252 1.8613 37.5876 1.77083 37.5123C1.33657 37.1362 1.44513 35.0865 4.57543 29.8776C5.60681 31.3444 6.98197 32.5479 8.53807 33.3753C5.64299 36.0268 3.27265 37.6252 2.15081 37.6252ZM15.3053 22.6377L14.871 22.7318C14.2558 22.8634 13.7854 23.3523 13.6587 23.9917L13.5683 24.443C13.5321 24.5934 13.333 24.5934 13.2969 24.443L13.2064 23.9917C13.0797 23.3523 12.6093 22.8634 11.9941 22.7318L11.5598 22.6377C11.4151 22.6001 11.4151 22.3933 11.5598 22.3557L11.9941 22.2616C12.6093 22.13 13.0797 21.6411 13.2064 21.0017L13.2969 20.5504C13.333 20.4 13.5321 20.4 13.5683 20.5504L13.6587 21.0017C13.7854 21.6411 14.2558 22.13 14.871 22.2616L15.3053 22.3557C15.4501 22.3933 15.4501 22.6001 15.3053 22.6377ZM29.6902 7.6315C28.5684 6.80409 27.2656 6.03309 25.7276 5.45014C29.2198 2.32854 30.6492 1.97125 31.1197 1.97125C31.2644 1.97125 31.373 2.00886 31.4454 2.06527C31.8434 2.42256 31.6263 4.09619 29.6902 7.6315Z"
fill="#F5222D"/>
</svg>
)
export const IconQuery = ({style}: { style?: React.CSSProperties }) => (
<svg className={'svg-icon'} style={style} xmlns="http://www.w3.org/2000/svg" fill="none" version="1.1"
width="1em" height="1em" viewBox="0 0 15 15">
export const IconNavigationArrow = ({style, className}: IconProps) => (
<svg className={`svg-icon ${className || ''} icon-nav-arrow`} style={style} xmlns="http://www.w3.org/2000/svg"
width="0.9em" height="1em" viewBox="0 0 25 29">
<path d="M24.75 14.2894L-1.24922e-06 28.5788L11.75 14.2891L0 -1.08186e-06L24.75 14.2894Z"
fill="url(#paint0_linear_1030_7835)"/>
<defs>
<clipPath id="master_svg0_1_3654/1_0572">
<rect x="0" y="0" width="15" height="15" rx="0"/>
</clipPath>
</defs>
<g clipPath="url(#master_svg0_1_3654/1_0572)">
<g>
<path
d="M8.4375,8.20313Q8.4375,8.2607,8.43186,8.31799Q8.426210000000001,8.37528,8.41498,8.431750000000001Q8.40375,8.48821,8.387039999999999,8.5433Q8.37033,8.59839,8.3483,8.65158Q8.32626,8.70477,8.29913,8.75554Q8.27199,8.80632,8.24,8.85418Q8.20802,8.90205,8.1715,8.94655Q8.13497,8.991060000000001,8.09427,9.03177Q8.053560000000001,9.07247,8.00905,9.109Q7.96455,9.14552,7.9166799999999995,9.1775Q7.8688199999999995,9.20949,7.81804,9.23663Q7.76727,9.26376,7.71408,9.2858Q7.66089,9.30783,7.6058,9.324539999999999Q7.5507100000000005,9.34125,7.49425,9.35248Q7.43778,9.363710000000001,7.38049,9.36936Q7.3232,9.375,7.26563,9.375Q7.20805,9.375,7.15076,9.36936Q7.093468,9.363710000000001,7.037003,9.35248Q6.980539,9.34125,6.925448,9.324539999999999Q6.870356,9.30783,6.817168,9.2858Q6.76398,9.26376,6.713207,9.23663Q6.662434,9.20949,6.614566,9.1775Q6.566698,9.14552,6.522195,9.109Q6.477693,9.07247,6.436984,9.03177Q6.396276,8.991060000000001,6.3597529999999995,8.94655Q6.323231,8.90205,6.291247,8.85418Q6.259262,8.80632,6.232123,8.75554Q6.204985,8.70477,6.1829537,8.65158Q6.1609224,8.59839,6.1442105,8.5433Q6.1274987,8.48821,6.1162672,8.431750000000001Q6.1050358,8.37528,6.0993929,8.31799Q6.09375,8.2607,6.09375,8.20313Q6.09375,8.14555,6.0993929,8.08826Q6.1050358,8.030968,6.1162672,7.974503Q6.1274987,7.918039,6.1442105,7.862948Q6.1609224,7.807856,6.1829537,7.754668Q6.204985,7.70148,6.232123,7.650707Q6.259262,7.599934,6.291247,7.552066Q6.323231,7.504198,6.3597529999999995,7.459695Q6.396276,7.415193,6.436984,7.374484Q6.477693,7.333776,6.522195,7.2972529999999995Q6.566698,7.260731,6.614566,7.228747Q6.662434,7.196762,6.713207,7.169623Q6.76398,7.142485,6.817168,7.1204537Q6.870356,7.0984224,6.925448,7.0817105Q6.980539,7.0649987,7.037003,7.0537672Q7.093468,7.0425358,7.15076,7.0368929Q7.20805,7.03125,7.26563,7.03125Q7.3232,7.03125,7.38049,7.0368929Q7.43778,7.0425358,7.49425,7.0537672Q7.5507100000000005,7.0649987,7.6058,7.0817105Q7.66089,7.0984224,7.71408,7.1204537Q7.76727,7.142485,7.81804,7.169623Q7.8688199999999995,7.196762,7.9166799999999995,7.228747Q7.96455,7.260731,8.00905,7.2972529999999995Q8.053560000000001,7.333776,8.09427,7.374484Q8.13497,7.415193,8.1715,7.459695Q8.20802,7.504198,8.24,7.552066Q8.27199,7.599934,8.29913,7.650707Q8.32626,7.70148,8.3483,7.754668Q8.37033,7.807856,8.387039999999999,7.862948Q8.40375,7.918039,8.41498,7.974503Q8.426210000000001,8.030968,8.43186,8.08826Q8.4375,8.14555,8.4375,8.20313Z"
fill="currentColor"/>
</g>
<g>
<path
d="M3.28125,0.9375C2.5046049999999997,0.9375,1.875,1.567105,1.875,2.34375L1.875,12.6563C1.875,13.4329,2.5046049999999997,14.0625,3.28125,14.0625L11.71875,14.0625C12.4954,14.0625,13.125,13.4329,13.125,12.6563L13.125,5.15622L10.3125,5.15622C9.53585,5.15622,8.90625,4.526619999999999,8.90625,3.74997L8.90625,0.9375L3.28125,0.9375ZM9.375,8.20312C9.375,8.61589,9.25644,9.00097,9.05152,9.32616L10.32722,10.60187L9.66431,11.2648L8.38859,9.98906C8.06342,10.19396,7.67836,10.3125,7.26562,10.3125C6.10065,10.3125,5.15625,9.3681,5.15625,8.20312C5.15625,7.03815,6.10065,6.09375,7.26562,6.09375C8.4306,6.09375,9.375,7.03815,9.375,8.20312Z"
fill="currentColor"/>
</g>
<g>
<path
d="M12.84958,3.55950103515625C13.02483,3.73431103515625,13.1238,3.97131103515625,13.12499,4.21872103515625L10.3125,4.21872103515625C10.053619,4.21872103515625,9.84375,4.008851035156249,9.84375,3.74997103515625L9.84375,0.93756103515625C10.088203,0.94027833515625,10.322109,1.03840803515625,10.495386,1.21125403515625L12.84958,3.55950103515625Z"
fill="currentColor"/>
</g>
</g>
</svg>
)
export const IconAiChat = ({style}: { style?: React.CSSProperties }) => (
<svg className={'svg-icon icon-ai-chat'} style={style} xmlns="http://www.w3.org/2000/svg" fill="none" version="1.1"
width="1em" height="1em" viewBox="0 0 32 32">
<path fillRule="evenodd" clipRule="evenodd"
d="M16 32C24.8366 32 32 24.8366 32 16C32 7.16344 24.8366 0 16 0C7.16344 0 0 7.16344 0 16C0 18.9091 0.776402 21.637 2.1333 23.9875V29.8666H8.01239C10.363 31.2236 13.0908 32 16 32Z"
fill="#F5222D"/>
<path fillRule="evenodd" clipRule="evenodd"
d="M16 32C24.8366 32 32 24.8366 32 16C32 7.16344 24.8366 0 16 0C7.16344 0 0 7.16344 0 16C0 18.9091 0.776402 21.637 2.1333 23.9875V29.8666H8.01239C10.363 31.2236 13.0908 32 16 32Z"
fill="url(#paint0_linear_145_39)"/>
<circle cx="8" cy="16" r="2" fill="white"/>
<circle cx="16" cy="16" r="2" fill="white"/>
<circle cx="24" cy="16" r="2" fill="white"/>
<defs>
<linearGradient id="paint0_linear_145_39" x1="28" y1="2.5332e-07" x2="2" y2="32"
<linearGradient id="paint0_linear_1030_7835" x1="27.5" y1="15" x2="1" y2="15"
gradientUnits="userSpaceOnUse">
<stop stopColor="#FFCB7E"/>
<stop offset="1" stopColor="#F5222D"/>
<stop stopColor="#1890FF"/>
<stop offset="1" stopColor="#8FC9FF"/>
</linearGradient>
</defs>
</svg>
)
export const IconAiIndex = ({style}: { style?: React.CSSProperties }) => (
<svg className={'svg-icon icon-ai-chat'} style={style} xmlns="http://www.w3.org/2000/svg" fill="none" version="1.1"
width="1em" height="1em" viewBox="0 0 27 27">
<path
d="M22.8219 10.5025L16.5786 4.25927C16.4139 4.09457 16.1986 4.01349 15.9832 4.01349C15.7678 4.01349 15.5525 4.09457 15.3878 4.25927L11.3058 8.34373C10.9967 8.30825 10.6851 8.29305 10.3734 8.29305C8.51867 8.29305 6.66394 8.90369 5.14114 10.125C4.75094 10.4366 4.72053 11.0219 5.07273 11.3767L9.67661 15.9805L4.21884 21.4333C4.15196 21.4997 4.11069 21.5877 4.10229 21.6816L4.01614 22.6241C3.99334 22.8623 4.18337 23.065 4.41901 23.065C4.43168 23.065 4.44435 23.065 4.45702 23.0625L5.39959 22.9763C5.49334 22.9687 5.58202 22.9256 5.6479 22.8598L11.1057 17.402L15.7096 22.0059C15.8742 22.1706 16.0896 22.2517 16.305 22.2517C16.5508 22.2517 16.794 22.1452 16.9612 21.9375C18.3878 20.1562 18.9807 17.9265 18.74 15.7702L22.8219 11.6883C23.1487 11.364 23.1487 10.8319 22.8219 10.5025Z"
fill="#F5222D"/>
<path
d="M22.8219 10.5025L16.5786 4.25927C16.4139 4.09457 16.1986 4.01349 15.9832 4.01349C15.7678 4.01349 15.5525 4.09457 15.3878 4.25927L11.3058 8.34373C10.9967 8.30825 10.6851 8.29305 10.3734 8.29305C8.51867 8.29305 6.66394 8.90369 5.14114 10.125C4.75094 10.4366 4.72053 11.0219 5.07273 11.3767L9.67661 15.9805L4.21884 21.4333C4.15196 21.4997 4.11069 21.5877 4.10229 21.6816L4.01614 22.6241C3.99334 22.8623 4.18337 23.065 4.41901 23.065C4.43168 23.065 4.44435 23.065 4.45702 23.0625L5.39959 22.9763C5.49334 22.9687 5.58202 22.9256 5.6479 22.8598L11.1057 17.402L15.7096 22.0059C15.8742 22.1706 16.0896 22.2517 16.305 22.2517C16.5508 22.2517 16.794 22.1452 16.9612 21.9375C18.3878 20.1562 18.9807 17.9265 18.74 15.7702L22.8219 11.6883C23.1487 11.364 23.1487 10.8319 22.8219 10.5025Z"
fill="url(#paint0_linear_541_309)"/>
<defs>
<linearGradient id="paint0_linear_541_309" x1="20.6854" y1="4.01349" x2="5.20628" y2="23.066"
gradientUnits="userSpaceOnUse">
<stop stop-color="#FFCB7E"/>
<stop offset="1" stop-color="#F5222D"/>
</linearGradient>
</defs>
export const IconArrowRight = ({style, className}: IconProps) => (
<svg className={`svg-icon ${className || ''} icon-arrow-right`} style={style} width="1em" height="1em"
viewBox="0 0 20 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 11.5L9.01987e-07 23L9.49495 11.4997L1.90735e-06 -8.74228e-07L20 11.5Z" fill="currentColor"/>
</svg>
)
export const IconFileSearch = ({style}: { style?: React.CSSProperties }) => (
<svg className={'svg-icon icon-file-search'} style={style} xmlns="http://www.w3.org/2000/svg" fill="none"
version="1.1"
width="1em" height="1em" viewBox="0 0 32 32">
<path
d="M26.7062 9.02188C26.8937 9.20938 27 9.4625 27 9.72812V29C27 29.5531 26.5531 30 26 30H6C5.44687 30 5 29.5531 5 29V3C5 2.44687 5.44687 2 6 2H19.2719C19.5375 2 19.7938 2.10625 19.9813 2.29375L26.7062 9.02188ZM24.6938 10.1875L18.8125 4.30625V10.1875H24.6938ZM10 15.0625C9.9337 15.0625 9.87011 15.0888 9.82322 15.1357C9.77634 15.1826 9.75 15.2462 9.75 15.3125V16.8125C9.75 16.8788 9.77634 16.9424 9.82322 16.9893C9.87011 17.0362 9.9337 17.0625 10 17.0625H22C22.0663 17.0625 22.1299 17.0362 22.1768 16.9893C22.2237 16.9424 22.25 16.8788 22.25 16.8125V15.3125C22.25 15.2462 22.2237 15.1826 22.1768 15.1357C22.1299 15.0888 22.0663 15.0625 22 15.0625H10ZM10 19.3125C9.9337 19.3125 9.87011 19.3388 9.82322 19.3857C9.77634 19.4326 9.75 19.4962 9.75 19.5625V21.0625C9.75 21.1288 9.77634 21.1924 9.82322 21.2393C9.87011 21.2862 9.9337 21.3125 10 21.3125H15.75C15.8163 21.3125 15.8799 21.2862 15.9268 21.2393C15.9737 21.1924 16 21.1288 16 21.0625V19.5625C16 19.4962 15.9737 19.4326 15.9268 19.3857C15.8799 19.3388 15.8163 19.3125 15.75 19.3125H10Z"
fill="#F5222D"/>
<path
d="M26.7062 9.02188C26.8937 9.20938 27 9.4625 27 9.72812V29C27 29.5531 26.5531 30 26 30H6C5.44687 30 5 29.5531 5 29V3C5 2.44687 5.44687 2 6 2H19.2719C19.5375 2 19.7938 2.10625 19.9813 2.29375L26.7062 9.02188ZM24.6938 10.1875L18.8125 4.30625V10.1875H24.6938ZM10 15.0625C9.9337 15.0625 9.87011 15.0888 9.82322 15.1357C9.77634 15.1826 9.75 15.2462 9.75 15.3125V16.8125C9.75 16.8788 9.77634 16.9424 9.82322 16.9893C9.87011 17.0362 9.9337 17.0625 10 17.0625H22C22.0663 17.0625 22.1299 17.0362 22.1768 16.9893C22.2237 16.9424 22.25 16.8788 22.25 16.8125V15.3125C22.25 15.2462 22.2237 15.1826 22.1768 15.1357C22.1299 15.0888 22.0663 15.0625 22 15.0625H10ZM10 19.3125C9.9337 19.3125 9.87011 19.3388 9.82322 19.3857C9.77634 19.4326 9.75 19.4962 9.75 19.5625V21.0625C9.75 21.1288 9.77634 21.1924 9.82322 21.2393C9.87011 21.2862 9.9337 21.3125 10 21.3125H15.75C15.8163 21.3125 15.8799 21.2862 15.9268 21.2393C15.9737 21.1924 16 21.1288 16 21.0625V19.5625C16 19.4962 15.9737 19.4326 15.9268 19.3857C15.8799 19.3388 15.8163 19.3125 15.75 19.3125H10Z"
fill="url(#paint0_linear_253_170)"/>
<defs>
<linearGradient id="paint0_linear_253_170" x1="24.25" y1="2" x2="1.02087" y2="24.4633"
gradientUnits="userSpaceOnUse">
<stop stopColor="#FFCB7E"/>
<stop offset="1" stopColor="#F5222D"/>
</linearGradient>
</defs>
</svg>
)
export const IconProfile = ({style}: { style?: React.CSSProperties }) => (
<svg className={'svg-icon icon-profile'} style={style} xmlns="http://www.w3.org/2000/svg" fill="none" version="1.1"
width="1em" height="1em" viewBox="0 0 32 32">
<path
d="M27.5 3.5H4.5C3.94687 3.5 3.5 3.94687 3.5 4.5V27.5C3.5 28.0531 3.94687 28.5 4.5 28.5H27.5C28.0531 28.5 28.5 28.0531 28.5 27.5V4.5C28.5 3.94687 28.0531 3.5 27.5 3.5ZM11.875 21.75C11.1844 21.75 10.625 21.1906 10.625 20.5C10.625 19.8094 11.1844 19.25 11.875 19.25C12.5656 19.25 13.125 19.8094 13.125 20.5C13.125 21.1906 12.5656 21.75 11.875 21.75ZM11.875 17.25C11.1844 17.25 10.625 16.6906 10.625 16C10.625 15.3094 11.1844 14.75 11.875 14.75C12.5656 14.75 13.125 15.3094 13.125 16C13.125 16.6906 12.5656 17.25 11.875 17.25ZM11.875 12.75C11.1844 12.75 10.625 12.1906 10.625 11.5C10.625 10.8094 11.1844 10.25 11.875 10.25C12.5656 10.25 13.125 10.8094 13.125 11.5C13.125 12.1906 12.5656 12.75 11.875 12.75ZM21.375 21.25C21.375 21.3875 21.2625 21.5 21.125 21.5H15.375C15.2375 21.5 15.125 21.3875 15.125 21.25V19.75C15.125 19.6125 15.2375 19.5 15.375 19.5H21.125C21.2625 19.5 21.375 19.6125 21.375 19.75V21.25ZM21.375 16.75C21.375 16.8875 21.2625 17 21.125 17H15.375C15.2375 17 15.125 16.8875 15.125 16.75V15.25C15.125 15.1125 15.2375 15 15.375 15H21.125C21.2625 15 21.375 15.1125 21.375 15.25V16.75ZM21.375 12.25C21.375 12.3875 21.2625 12.5 21.125 12.5H15.375C15.2375 12.5 15.125 12.3875 15.125 12.25V10.75C15.125 10.6125 15.2375 10.5 15.375 10.5H21.125C21.2625 10.5 21.375 10.6125 21.375 10.75V12.25Z"
fill="#F5222D"/>
<path
d="M27.5 3.5H4.5C3.94687 3.5 3.5 3.94687 3.5 4.5V27.5C3.5 28.0531 3.94687 28.5 4.5 28.5H27.5C28.0531 28.5 28.5 28.0531 28.5 27.5V4.5C28.5 3.94687 28.0531 3.5 27.5 3.5ZM11.875 21.75C11.1844 21.75 10.625 21.1906 10.625 20.5C10.625 19.8094 11.1844 19.25 11.875 19.25C12.5656 19.25 13.125 19.8094 13.125 20.5C13.125 21.1906 12.5656 21.75 11.875 21.75ZM11.875 17.25C11.1844 17.25 10.625 16.6906 10.625 16C10.625 15.3094 11.1844 14.75 11.875 14.75C12.5656 14.75 13.125 15.3094 13.125 16C13.125 16.6906 12.5656 17.25 11.875 17.25ZM11.875 12.75C11.1844 12.75 10.625 12.1906 10.625 11.5C10.625 10.8094 11.1844 10.25 11.875 10.25C12.5656 10.25 13.125 10.8094 13.125 11.5C13.125 12.1906 12.5656 12.75 11.875 12.75ZM21.375 21.25C21.375 21.3875 21.2625 21.5 21.125 21.5H15.375C15.2375 21.5 15.125 21.3875 15.125 21.25V19.75C15.125 19.6125 15.2375 19.5 15.375 19.5H21.125C21.2625 19.5 21.375 19.6125 21.375 19.75V21.25ZM21.375 16.75C21.375 16.8875 21.2625 17 21.125 17H15.375C15.2375 17 15.125 16.8875 15.125 16.75V15.25C15.125 15.1125 15.2375 15 15.375 15H21.125C21.2625 15 21.375 15.1125 21.375 15.25V16.75ZM21.375 12.25C21.375 12.3875 21.2625 12.5 21.125 12.5H15.375C15.2375 12.5 15.125 12.3875 15.125 12.25V10.75C15.125 10.6125 15.2375 10.5 15.375 10.5H21.125C21.2625 10.5 21.375 10.6125 21.375 10.75V12.25Z"
fill="url(#paint0_linear_253_60)"/>
<defs>
<linearGradient id="paint0_linear_253_60" x1="25.375" y1="3.5" x2="5.0625" y2="28.5"
gradientUnits="userSpaceOnUse">
<stop stopColor="#FFCB7E"/>
<stop offset="1" stopColor="#F5222D"/>
</linearGradient>
</defs>
</svg>
)
export const IconInteraction = ({style}: { style?: React.CSSProperties }) => (
<svg className={'svg-icon icon-interaction'} style={style} xmlns="http://www.w3.org/2000/svg" fill="none"
version="1.1" width="1em" height="1em" viewBox="0 0 32 32">
<path
d="M27.5 3.5H4.5C3.94687 3.5 3.5 3.94687 3.5 4.5V27.5C3.5 28.0531 3.94687 28.5 4.5 28.5H27.5C28.0531 28.5 28.5 28.0531 28.5 27.5V4.5C28.5 3.94687 28.0531 3.5 27.5 3.5ZM22.6875 18.3031C22.6875 20.0312 21.2906 21.4312 19.5719 21.4312H13.1438V23.1C13.1438 23.2781 12.9406 23.375 12.8031 23.2656L9.39375 20.5875C9.28438 20.5031 9.28438 20.3375 9.39375 20.2531L12.8031 17.575C12.9406 17.4656 13.1438 17.5656 13.1438 17.7406V19.4094H19.5719C20.1844 19.4094 20.6812 18.9094 20.6812 18.2969V15.8313C20.6812 15.7156 20.775 15.6187 20.8937 15.6187H22.4781C22.5938 15.6187 22.6906 15.7125 22.6906 15.8313V18.3031H22.6875ZM22.6063 11.7437L19.1969 14.4219C19.0594 14.5312 18.8563 14.4313 18.8563 14.2563V12.5875H12.4281C11.8156 12.5875 11.3188 13.0875 11.3188 13.7V16.1656C11.3188 16.2812 11.225 16.3781 11.1062 16.3781H9.52188C9.40625 16.3781 9.30937 16.2844 9.30937 16.1656V13.7C9.30937 11.9719 10.7063 10.5719 12.425 10.5719H18.8531V8.90312C18.8531 8.725 19.0563 8.62813 19.1938 8.7375L22.6031 11.4156C22.7156 11.4937 22.7156 11.6594 22.6063 11.7437Z"
fill="#F5222D"/>
<path
d="M27.5 3.5H4.5C3.94687 3.5 3.5 3.94687 3.5 4.5V27.5C3.5 28.0531 3.94687 28.5 4.5 28.5H27.5C28.0531 28.5 28.5 28.0531 28.5 27.5V4.5C28.5 3.94687 28.0531 3.5 27.5 3.5ZM22.6875 18.3031C22.6875 20.0312 21.2906 21.4312 19.5719 21.4312H13.1438V23.1C13.1438 23.2781 12.9406 23.375 12.8031 23.2656L9.39375 20.5875C9.28438 20.5031 9.28438 20.3375 9.39375 20.2531L12.8031 17.575C12.9406 17.4656 13.1438 17.5656 13.1438 17.7406V19.4094H19.5719C20.1844 19.4094 20.6812 18.9094 20.6812 18.2969V15.8313C20.6812 15.7156 20.775 15.6187 20.8937 15.6187H22.4781C22.5938 15.6187 22.6906 15.7125 22.6906 15.8313V18.3031H22.6875ZM22.6063 11.7437L19.1969 14.4219C19.0594 14.5312 18.8563 14.4313 18.8563 14.2563V12.5875H12.4281C11.8156 12.5875 11.3188 13.0875 11.3188 13.7V16.1656C11.3188 16.2812 11.225 16.3781 11.1062 16.3781H9.52188C9.40625 16.3781 9.30937 16.2844 9.30937 16.1656V13.7C9.30937 11.9719 10.7063 10.5719 12.425 10.5719H18.8531V8.90312C18.8531 8.725 19.0563 8.62813 19.1938 8.7375L22.6031 11.4156C22.7156 11.4937 22.7156 11.6594 22.6063 11.7437Z"
fill="url(#paint0_linear_253_68)"/>
<defs>
<linearGradient id="paint0_linear_253_68" x1="25.375" y1="3.5" x2="5.0625" y2="28.5"
gradientUnits="userSpaceOnUse">
<stop stopColor="#FFCB7E"/>
<stop offset="1" stopColor="#F5222D"/>
</linearGradient>
</defs>
</svg>
)
export const IconFire = ({style}: { style?: React.CSSProperties }) => (
<svg className={'svg-icon icon-interaction'} style={style} xmlns="http://www.w3.org/2000/svg" fill="none"
version="1.1" width="1em" height="1em" viewBox="0 0 29 29">
<path
d="M23.3009 13.2035C22.768 12.0041 21.9933 10.9276 21.0253 10.0413L20.2266 9.30843C20.1994 9.28423 20.1668 9.26706 20.1315 9.25842C20.0962 9.24978 20.0593 9.24993 20.0241 9.25888C19.9888 9.26782 19.9563 9.28528 19.9294 9.30971C19.9025 9.33414 19.882 9.36481 19.8697 9.39902L19.5129 10.4229C19.2905 11.0652 18.8816 11.7212 18.3024 12.3663C18.2639 12.4075 18.22 12.4184 18.1898 12.4212C18.1596 12.4239 18.113 12.4184 18.0718 12.38C18.0334 12.3471 18.0142 12.2977 18.0169 12.2483C18.1185 10.5958 17.6244 8.732 16.5429 6.70349C15.648 5.0181 14.4046 3.70328 12.8509 2.78647L11.7173 2.11946C11.5691 2.03162 11.3797 2.14691 11.3879 2.31984L11.4483 3.6374C11.4894 4.53774 11.3851 5.33377 11.1381 5.9953C10.8362 6.80506 10.4025 7.55717 9.84798 8.23242C9.4621 8.7017 9.02475 9.12617 8.54414 9.49784C7.38661 10.3876 6.4454 11.5278 5.79097 12.8329C5.13815 14.1494 4.7981 15.5989 4.7973 17.0684C4.7973 18.364 5.05258 19.6184 5.55765 20.8015C6.04533 21.9405 6.74911 22.9743 7.63007 23.8456C8.51943 24.724 9.55153 25.4157 10.7017 25.8961C11.893 26.3956 13.1556 26.6482 14.4595 26.6482C15.7633 26.6482 17.026 26.3956 18.2173 25.8988C19.3646 25.4213 20.4079 24.7248 21.2889 23.8483C22.1782 22.97 22.8754 21.9434 23.3613 20.8042C23.8656 19.6244 24.1243 18.3542 24.1216 17.0711C24.1216 15.7316 23.8471 14.4305 23.3009 13.2035Z"
fill="#F5222D"/>
<path
d="M23.3009 13.2035C22.768 12.0041 21.9933 10.9276 21.0253 10.0413L20.2266 9.30843C20.1994 9.28423 20.1668 9.26706 20.1315 9.25842C20.0962 9.24978 20.0593 9.24993 20.0241 9.25888C19.9888 9.26782 19.9563 9.28528 19.9294 9.30971C19.9025 9.33414 19.882 9.36481 19.8697 9.39902L19.5129 10.4229C19.2905 11.0652 18.8816 11.7212 18.3024 12.3663C18.2639 12.4075 18.22 12.4184 18.1898 12.4212C18.1596 12.4239 18.113 12.4184 18.0718 12.38C18.0334 12.3471 18.0142 12.2977 18.0169 12.2483C18.1185 10.5958 17.6244 8.732 16.5429 6.70349C15.648 5.0181 14.4046 3.70328 12.8509 2.78647L11.7173 2.11946C11.5691 2.03162 11.3797 2.14691 11.3879 2.31984L11.4483 3.6374C11.4894 4.53774 11.3851 5.33377 11.1381 5.9953C10.8362 6.80506 10.4025 7.55717 9.84798 8.23242C9.4621 8.7017 9.02475 9.12617 8.54414 9.49784C7.38661 10.3876 6.4454 11.5278 5.79097 12.8329C5.13815 14.1494 4.7981 15.5989 4.7973 17.0684C4.7973 18.364 5.05258 19.6184 5.55765 20.8015C6.04533 21.9405 6.74911 22.9743 7.63007 23.8456C8.51943 24.724 9.55153 25.4157 10.7017 25.8961C11.893 26.3956 13.1556 26.6482 14.4595 26.6482C15.7633 26.6482 17.026 26.3956 18.2173 25.8988C19.3646 25.4213 20.4079 24.7248 21.2889 23.8483C22.1782 22.97 22.8754 21.9434 23.3613 20.8042C23.8656 19.6244 24.1243 18.3542 24.1216 17.0711C24.1216 15.7316 23.8471 14.4305 23.3009 13.2035Z"
fill="url(#paint0_linear_541_295)"/>
<defs>
<linearGradient id="paint0_linear_541_295" x1="21.7061" y1="2.08929" x2="1.3308" y2="21.8216"
gradientUnits="userSpaceOnUse">
<stop stop-color="#FFCB7E"/>
<stop offset="1" stop-color="#F5222D"/>
</linearGradient>
</defs>
</svg>
)
export const IconAISearch = ({style}: { style?: React.CSSProperties }) => (
<svg className={'svg-icon icon-ai-search'} style={style} xmlns="http://www.w3.org/2000/svg" fill="none"
version="1.1" width="1em" height="1em" viewBox="0 0 32 32">
<path
d="M27.0906 5.30937L16.4719 1.69062C16.3437 1.64687 16.1719 1.625 16 1.625C15.8281 1.625 15.6562 1.64687 15.5281 1.69062L4.90938 5.30937C4.65 5.39687 4.4375 5.69688 4.4375 5.97187V21.0469C4.4375 21.3219 4.61563 21.6844 4.83125 21.8562L15.6031 30.25C15.7125 30.3344 15.8531 30.3781 15.9969 30.3781C16.1406 30.3781 16.2844 30.3344 16.3906 30.25L27.1625 21.8562C27.3781 21.6875 27.5562 21.325 27.5562 21.0469V5.97187C27.5625 5.69688 27.35 5.4 27.0906 5.30937ZM19.5875 17.3125C18.0719 18.8281 15.7438 19.0375 14.0063 17.9406L11.5844 20.3625C11.5374 20.409 11.4739 20.4351 11.4078 20.4351C11.3417 20.4351 11.2782 20.409 11.2312 20.3625L10.1687 19.3C10.1222 19.253 10.0961 19.1896 10.0961 19.1234C10.0961 19.0573 10.1222 18.9939 10.1687 18.9469L12.5906 16.525C11.4937 14.7844 11.7031 12.4594 13.2188 10.9437C14.9781 9.18437 17.8281 9.18437 19.5875 10.9437C21.3469 12.7031 21.3469 15.5531 19.5875 17.3125ZM14.6331 15.8978C14.8649 16.1332 15.141 16.3205 15.4455 16.4487C15.7499 16.5769 16.0768 16.6436 16.4072 16.6448C16.7375 16.6461 17.0649 16.582 17.3704 16.4562C17.6758 16.3303 17.9534 16.1453 18.187 15.9116C18.4205 15.678 18.6056 15.4005 18.7314 15.095C18.8572 14.7895 18.9213 14.4621 18.92 14.1318C18.9187 13.8014 18.852 13.4746 18.7237 13.1701C18.5955 12.8656 18.4083 12.5896 18.1728 12.3578C17.941 12.1224 17.665 11.9352 17.3605 11.807C17.056 11.6787 16.7291 11.6121 16.3988 11.6108C16.0684 11.6095 15.7411 11.6736 15.4356 11.7995C15.1301 11.9253 14.8526 12.1104 14.619 12.344C14.3854 12.5776 14.2003 12.8552 14.0745 13.1606C13.9487 13.4661 13.8846 13.7935 13.8859 14.1238C13.8873 14.4542 13.954 14.7811 14.0822 15.0855C14.2104 15.39 14.3977 15.6661 14.6331 15.8978Z"
fill="#F5222D"/>
<path
d="M27.0906 5.30937L16.4719 1.69062C16.3437 1.64687 16.1719 1.625 16 1.625C15.8281 1.625 15.6562 1.64687 15.5281 1.69062L4.90938 5.30937C4.65 5.39687 4.4375 5.69688 4.4375 5.97187V21.0469C4.4375 21.3219 4.61563 21.6844 4.83125 21.8562L15.6031 30.25C15.7125 30.3344 15.8531 30.3781 15.9969 30.3781C16.1406 30.3781 16.2844 30.3344 16.3906 30.25L27.1625 21.8562C27.3781 21.6875 27.5562 21.325 27.5562 21.0469V5.97187C27.5625 5.69688 27.35 5.4 27.0906 5.30937ZM19.5875 17.3125C18.0719 18.8281 15.7438 19.0375 14.0063 17.9406L11.5844 20.3625C11.5374 20.409 11.4739 20.4351 11.4078 20.4351C11.3417 20.4351 11.2782 20.409 11.2312 20.3625L10.1687 19.3C10.1222 19.253 10.0961 19.1896 10.0961 19.1234C10.0961 19.0573 10.1222 18.9939 10.1687 18.9469L12.5906 16.525C11.4937 14.7844 11.7031 12.4594 13.2188 10.9437C14.9781 9.18437 17.8281 9.18437 19.5875 10.9437C21.3469 12.7031 21.3469 15.5531 19.5875 17.3125ZM14.6331 15.8978C14.8649 16.1332 15.141 16.3205 15.4455 16.4487C15.7499 16.5769 16.0768 16.6436 16.4072 16.6448C16.7375 16.6461 17.0649 16.582 17.3704 16.4562C17.6758 16.3303 17.9534 16.1453 18.187 15.9116C18.4205 15.678 18.6056 15.4005 18.7314 15.095C18.8572 14.7895 18.9213 14.4621 18.92 14.1318C18.9187 13.8014 18.852 13.4746 18.7237 13.1701C18.5955 12.8656 18.4083 12.5896 18.1728 12.3578C17.941 12.1224 17.665 11.9352 17.3605 11.807C17.056 11.6787 16.7291 11.6121 16.3988 11.6108C16.0684 11.6095 15.7411 11.6736 15.4356 11.7995C15.1301 11.9253 14.8526 12.1104 14.619 12.344C14.3854 12.5776 14.2003 12.8552 14.0745 13.1606C13.9487 13.4661 13.8846 13.7935 13.8859 14.1238C13.8873 14.4542 13.954 14.7811 14.0822 15.0855C14.2104 15.39 14.3977 15.6661 14.6331 15.8978Z"
fill="url(#paint0_linear_253_116)"/>
<defs>
<linearGradient id="paint0_linear_253_116" x1="24.6665" y1="1.625" x2="0.80049" y2="25.2428"
gradientUnits="userSpaceOnUse">
<stop stopColor="#FFCB7E"/>
<stop offset="1" stopColor="#F5222D"/>
</linearGradient>
</defs>
</svg>
)
export const IconAIQuestion = ({style}: { style?: React.CSSProperties }) => (
<svg className={'svg-icon icon-ai-question'} style={style} xmlns="http://www.w3.org/2000/svg" fill="none"
version="1.1" width="1em" height="1em" viewBox="0 0 32 32">
<path
d="M26.7062 9.02188C26.8937 9.20938 27 9.4625 27 9.72812V29C27 29.5531 26.5531 30 26 30H6C5.44687 30 5 29.5531 5 29V3C5 2.44687 5.44687 2 6 2H19.2719C19.5375 2 19.7938 2.10625 19.9813 2.29375L26.7062 9.02188ZM24.6938 10.1875L18.8125 4.30625V10.1875H24.6938ZM12.5625 17.1562C12.5625 17.325 12.7 17.4531 12.8687 17.4531H13.8813C14.05 17.4531 14.1875 17.3219 14.1875 17.1594C14.1875 16.2781 14.9937 15.5469 16 15.5469C17.0063 15.5469 17.8125 16.2781 17.8125 17.1562C17.8125 17.9469 17.1562 18.6313 16.2719 18.7469C15.6688 18.8344 15.1938 19.3813 15.1875 20V21C15.1875 21.1719 15.3281 21.3125 15.5 21.3125H16.5C16.6719 21.3125 16.8125 21.1719 16.8125 21V20.6187C16.8125 20.4312 16.9375 20.2594 17.1156 20.2031C18.5094 19.7531 19.4594 18.5156 19.4375 17.1125C19.4125 15.3781 17.9 13.9625 16.0469 13.9375C14.1281 13.9156 12.5625 15.3625 12.5625 17.1562ZM16 24.25C16.2652 24.25 16.5196 24.1446 16.7071 23.9571C16.8946 23.7696 17 23.5152 17 23.25C17 22.9848 16.8946 22.7304 16.7071 22.5429C16.5196 22.3554 16.2652 22.25 16 22.25C15.7348 22.25 15.4804 22.3554 15.2929 22.5429C15.1054 22.7304 15 22.9848 15 23.25C15 23.5152 15.1054 23.7696 15.2929 23.9571C15.4804 24.1446 15.7348 24.25 16 24.25Z"
fill="#F5222D"/>
<path
d="M26.7062 9.02188C26.8937 9.20938 27 9.4625 27 9.72812V29C27 29.5531 26.5531 30 26 30H6C5.44687 30 5 29.5531 5 29V3C5 2.44687 5.44687 2 6 2H19.2719C19.5375 2 19.7938 2.10625 19.9813 2.29375L26.7062 9.02188ZM24.6938 10.1875L18.8125 4.30625V10.1875H24.6938ZM12.5625 17.1562C12.5625 17.325 12.7 17.4531 12.8687 17.4531H13.8813C14.05 17.4531 14.1875 17.3219 14.1875 17.1594C14.1875 16.2781 14.9937 15.5469 16 15.5469C17.0063 15.5469 17.8125 16.2781 17.8125 17.1562C17.8125 17.9469 17.1562 18.6313 16.2719 18.7469C15.6688 18.8344 15.1938 19.3813 15.1875 20V21C15.1875 21.1719 15.3281 21.3125 15.5 21.3125H16.5C16.6719 21.3125 16.8125 21.1719 16.8125 21V20.6187C16.8125 20.4312 16.9375 20.2594 17.1156 20.2031C18.5094 19.7531 19.4594 18.5156 19.4375 17.1125C19.4125 15.3781 17.9 13.9625 16.0469 13.9375C14.1281 13.9156 12.5625 15.3625 12.5625 17.1562ZM16 24.25C16.2652 24.25 16.5196 24.1446 16.7071 23.9571C16.8946 23.7696 17 23.5152 17 23.25C17 22.9848 16.8946 22.7304 16.7071 22.5429C16.5196 22.3554 16.2652 22.25 16 22.25C15.7348 22.25 15.4804 22.3554 15.2929 22.5429C15.1054 22.7304 15 22.9848 15 23.25C15 23.5152 15.1054 23.7696 15.2929 23.9571C15.4804 24.1446 15.7348 24.25 16 24.25Z"
fill="url(#paint0_linear_253_172)"/>
<defs>
<linearGradient id="paint0_linear_253_172" x1="24.25" y1="2" x2="1.02087" y2="24.4633"
gradientUnits="userSpaceOnUse">
<stop stopColor="#FFCB7E"/>
<stop offset="1" stopColor="#F5222D"/>
</linearGradient>
</defs>
</svg>
)
export const IconSummary = ({style}: { style?: React.CSSProperties }) => (
<svg className={'svg-icon icon-summary'} style={style} xmlns="http://www.w3.org/2000/svg" fill="none"
version="1.1" width="1em" height="1em" viewBox="0 0 32 32">
<path
d="M29 7H24V5.25C24 5.1125 23.8875 5 23.75 5H22C21.8625 5 21.75 5.1125 21.75 5.25V7H17.125V5.25C17.125 5.1125 17.0125 5 16.875 5H15.125C14.9875 5 14.875 5.1125 14.875 5.25V7H10.25V5.25C10.25 5.1125 10.1375 5 10 5H8.25C8.1125 5 8 5.1125 8 5.25V7H3C2.44687 7 2 7.44688 2 8V26C2 26.5531 2.44687 27 3 27H29C29.5531 27 30 26.5531 30 26V8C30 7.44688 29.5531 7 29 7ZM13.25 21.5C13.25 21.6375 13.1375 21.75 13 21.75H7.25C7.1125 21.75 7 21.6375 7 21.5V20C7 19.8625 7.1125 19.75 7.25 19.75H13C13.1375 19.75 13.25 19.8625 13.25 20V21.5ZM13.25 17.25C13.25 17.3875 13.1375 17.5 13 17.5H7.25C7.1125 17.5 7 17.3875 7 17.25V15.75C7 15.6125 7.1125 15.5 7.25 15.5H13C13.1375 15.5 13.25 15.6125 13.25 15.75V17.25ZM24.9531 14.3969L19.7969 21.5438C19.7508 21.6076 19.6903 21.6597 19.6202 21.6956C19.5501 21.7314 19.4725 21.7502 19.3937 21.7502C19.315 21.7502 19.2374 21.7314 19.1673 21.6956C19.0972 21.6597 19.0367 21.6076 18.9906 21.5438L15.4219 16.6C15.3031 16.4344 15.4219 16.2031 15.625 16.2031H17.3406C17.5 16.2031 17.65 16.2812 17.7437 16.4094L19.3937 18.6938L22.6344 14.2031C22.7281 14.0719 22.8781 13.9969 23.0375 13.9969H24.75C24.9531 14 25.0719 14.2313 24.9531 14.3969Z"
fill="#F5222D"/>
<path
d="M29 7H24V5.25C24 5.1125 23.8875 5 23.75 5H22C21.8625 5 21.75 5.1125 21.75 5.25V7H17.125V5.25C17.125 5.1125 17.0125 5 16.875 5H15.125C14.9875 5 14.875 5.1125 14.875 5.25V7H10.25V5.25C10.25 5.1125 10.1375 5 10 5H8.25C8.1125 5 8 5.1125 8 5.25V7H3C2.44687 7 2 7.44688 2 8V26C2 26.5531 2.44687 27 3 27H29C29.5531 27 30 26.5531 30 26V8C30 7.44688 29.5531 7 29 7ZM13.25 21.5C13.25 21.6375 13.1375 21.75 13 21.75H7.25C7.1125 21.75 7 21.6375 7 21.5V20C7 19.8625 7.1125 19.75 7.25 19.75H13C13.1375 19.75 13.25 19.8625 13.25 20V21.5ZM13.25 17.25C13.25 17.3875 13.1375 17.5 13 17.5H7.25C7.1125 17.5 7 17.3875 7 17.25V15.75C7 15.6125 7.1125 15.5 7.25 15.5H13C13.1375 15.5 13.25 15.6125 13.25 15.75V17.25ZM24.9531 14.3969L19.7969 21.5438C19.7508 21.6076 19.6903 21.6597 19.6202 21.6956C19.5501 21.7314 19.4725 21.7502 19.3937 21.7502C19.315 21.7502 19.2374 21.7314 19.1673 21.6956C19.0972 21.6597 19.0367 21.6076 18.9906 21.5438L15.4219 16.6C15.3031 16.4344 15.4219 16.2031 15.625 16.2031H17.3406C17.5 16.2031 17.65 16.2812 17.7437 16.4094L19.3937 18.6938L22.6344 14.2031C22.7281 14.0719 22.8781 13.9969 23.0375 13.9969H24.75C24.9531 14 25.0719 14.2313 24.9531 14.3969Z"
fill="url(#paint0_linear_253_64)"/>
<defs>
<linearGradient id="paint0_linear_253_64" x1="26.5" y1="5" x2="9.93478" y2="30.9483"
gradientUnits="userSpaceOnUse">
<stop stopColor="#FFCB7E"/>
<stop offset="1" stopColor="#F5222D"/>
</linearGradient>
</defs>
</svg>
)
export const IconAIPPT = ({style}: { style?: React.CSSProperties }) => (
<svg className={'svg-icon icon-ai-ppt'} style={style} xmlns="http://www.w3.org/2000/svg" fill="none"
version="1.1" width="1em" height="1em" viewBox="0 0 32 32">
<path
d="M26.7062 9.02188C26.8937 9.20938 27 9.4625 27 9.72812V29C27 29.5531 26.5531 30 26 30H6C5.44687 30 5 29.5531 5 29V3C5 2.44687 5.44687 2 6 2H19.2719C19.5375 2 19.7938 2.10625 19.9813 2.29375L26.7062 9.02188ZM24.6938 10.1875L18.8125 4.30625V10.1875H24.6938ZM14.6416 23.75V20.8894H16.4937C18.3866 20.8894 19.625 19.6503 19.625 17.8231C19.625 16.0038 18.3881 14.75 16.5006 14.75H13.25C13.1505 14.75 13.0552 14.7895 12.9848 14.8598C12.9145 14.9302 12.875 15.0255 12.875 15.125V23.75C12.875 23.8495 12.9145 23.9448 12.9848 24.0152C13.0552 24.0855 13.1505 24.125 13.25 24.125H14.2666C14.366 24.125 14.4614 24.0855 14.5317 24.0152C14.6021 23.9448 14.6416 23.8495 14.6416 23.75ZM14.6416 19.3959H15.7322C17.2266 19.3959 17.8319 18.9919 17.8319 17.8231C17.8319 16.8216 17.2656 16.2569 16.2734 16.2569H14.6416V19.3959Z"
fill="#F5222D"/>
<path
d="M26.7062 9.02188C26.8937 9.20938 27 9.4625 27 9.72812V29C27 29.5531 26.5531 30 26 30H6C5.44687 30 5 29.5531 5 29V3C5 2.44687 5.44687 2 6 2H19.2719C19.5375 2 19.7938 2.10625 19.9813 2.29375L26.7062 9.02188ZM24.6938 10.1875L18.8125 4.30625V10.1875H24.6938ZM14.6416 23.75V20.8894H16.4937C18.3866 20.8894 19.625 19.6503 19.625 17.8231C19.625 16.0038 18.3881 14.75 16.5006 14.75H13.25C13.1505 14.75 13.0552 14.7895 12.9848 14.8598C12.9145 14.9302 12.875 15.0255 12.875 15.125V23.75C12.875 23.8495 12.9145 23.9448 12.9848 24.0152C13.0552 24.0855 13.1505 24.125 13.25 24.125H14.2666C14.366 24.125 14.4614 24.0855 14.5317 24.0152C14.6021 23.9448 14.6416 23.8495 14.6416 23.75ZM14.6416 19.3959H15.7322C17.2266 19.3959 17.8319 18.9919 17.8319 17.8231C17.8319 16.8216 17.2656 16.2569 16.2734 16.2569H14.6416V19.3959Z"
fill="url(#paint0_linear_253_52)"/>
<defs>
<linearGradient id="paint0_linear_253_52" x1="24.25" y1="2" x2="1.02087" y2="24.4633"
gradientUnits="userSpaceOnUse">
<stop stopColor="#FFCB7E"/>
<stop offset="1" stopColor="#F5222D"/>
</linearGradient>
</defs>
</svg>
)
export const IconArticleReview = ({style}: { style?: React.CSSProperties }) => (
<svg className={'svg-icon icon-article-review'} style={style} xmlns="http://www.w3.org/2000/svg" fill="none"
version="1.1" width="1em" height="1em" viewBox="0 0 32 32">
<path fillRule="evenodd" clipRule="evenodd"
d="M27 9.72813C27 9.4625 26.8937 9.20938 26.7062 9.02188L19.9813 2.29375C19.7938 2.10625 19.5375 2 19.2719 2H6C5.44688 2 5 2.44688 5 3V29C5 29.5531 5.44688 30 6 30H26C26.5531 30 27 29.5531 27 29V9.72813ZM18.8125 4.30625L24.6938 10.1875H18.8125V4.30625ZM8.04545 13.1843C8.09233 13.1375 8.15592 13.1111 8.22222 13.1111H20.2222C20.2885 13.1111 20.3521 13.1375 20.399 13.1843C20.4459 13.2312 20.4722 13.2948 20.4722 13.3611V14.8611C20.4722 14.9274 20.4459 14.991 20.399 15.0379C20.3521 15.0848 20.2885 15.1111 20.2222 15.1111H8.22222C8.15592 15.1111 8.09233 15.0848 8.04545 15.0379C7.99856 14.991 7.97222 14.9274 7.97222 14.8611V13.3611C7.97222 13.2948 7.99856 13.2312 8.04545 13.1843ZM8.04545 18.4968C8.09233 18.45 8.15592 18.4236 8.22222 18.4236H13.9722C14.0385 18.4236 14.1021 18.45 14.149 18.4968C14.1959 18.5437 14.2222 18.6073 14.2222 18.6736V20.1736C14.2222 20.2399 14.1959 20.3035 14.149 20.3504C14.1021 20.3973 14.0385 20.4236 13.9722 20.4236H8.22222C8.15592 20.4236 8.09233 20.3973 8.04545 20.3504C7.99856 20.3035 7.97222 20.2399 7.97222 20.1736V18.6736C7.97222 18.6073 7.99856 18.5437 8.04545 18.4968ZM24.8524 19.6923L19.7174 26.5129C19.6813 26.5605 19.6341 26.5993 19.5795 26.6261C19.525 26.6528 19.4646 26.6667 19.4035 26.6667C19.3423 26.6667 19.2819 26.6528 19.2274 26.6261C19.1728 26.5993 19.1256 26.5605 19.0895 26.5129L16.0365 22.4583C15.9448 22.3348 16.0365 22.1623 16.1935 22.1623H17.5268C17.65 22.1623 17.7683 22.2206 17.8408 22.3161L19.4035 24.39L23.0481 19.5478C23.1206 19.4522 23.2365 19.394 23.3621 19.394H24.6954C24.8524 19.3963 24.9441 19.5688 24.8524 19.6923Z"
fill="#F5222D"/>
<path fillRule="evenodd" clipRule="evenodd"
d="M27 9.72813C27 9.4625 26.8937 9.20938 26.7062 9.02188L19.9813 2.29375C19.7938 2.10625 19.5375 2 19.2719 2H6C5.44688 2 5 2.44688 5 3V29C5 29.5531 5.44688 30 6 30H26C26.5531 30 27 29.5531 27 29V9.72813ZM18.8125 4.30625L24.6938 10.1875H18.8125V4.30625ZM8.04545 13.1843C8.09233 13.1375 8.15592 13.1111 8.22222 13.1111H20.2222C20.2885 13.1111 20.3521 13.1375 20.399 13.1843C20.4459 13.2312 20.4722 13.2948 20.4722 13.3611V14.8611C20.4722 14.9274 20.4459 14.991 20.399 15.0379C20.3521 15.0848 20.2885 15.1111 20.2222 15.1111H8.22222C8.15592 15.1111 8.09233 15.0848 8.04545 15.0379C7.99856 14.991 7.97222 14.9274 7.97222 14.8611V13.3611C7.97222 13.2948 7.99856 13.2312 8.04545 13.1843ZM8.04545 18.4968C8.09233 18.45 8.15592 18.4236 8.22222 18.4236H13.9722C14.0385 18.4236 14.1021 18.45 14.149 18.4968C14.1959 18.5437 14.2222 18.6073 14.2222 18.6736V20.1736C14.2222 20.2399 14.1959 20.3035 14.149 20.3504C14.1021 20.3973 14.0385 20.4236 13.9722 20.4236H8.22222C8.15592 20.4236 8.09233 20.3973 8.04545 20.3504C7.99856 20.3035 7.97222 20.2399 7.97222 20.1736V18.6736C7.97222 18.6073 7.99856 18.5437 8.04545 18.4968ZM24.8524 19.6923L19.7174 26.5129C19.6813 26.5605 19.6341 26.5993 19.5795 26.6261C19.525 26.6528 19.4646 26.6667 19.4035 26.6667C19.3423 26.6667 19.2819 26.6528 19.2274 26.6261C19.1728 26.5993 19.1256 26.5605 19.0895 26.5129L16.0365 22.4583C15.9448 22.3348 16.0365 22.1623 16.1935 22.1623H17.5268C17.65 22.1623 17.7683 22.2206 17.8408 22.3161L19.4035 24.39L23.0481 19.5478C23.1206 19.4522 23.2365 19.394 23.3621 19.394H24.6954C24.8524 19.3963 24.9441 19.5688 24.8524 19.6923Z"
fill="url(#paint0_linear_253_91)"/>
<defs>
<linearGradient id="paint0_linear_253_91" x1="24.25" y1="2" x2="1.02087" y2="24.4633"
gradientUnits="userSpaceOnUse">
<stop stopColor="#FFCB7E"/>
<stop offset="1" stopColor="#F5222D"/>
</linearGradient>
</defs>
</svg>
)
export const IconDataManagement = ({style,className}: { style?: React.CSSProperties;className?:string; }) => (
<svg className={`svg-icon ${className||''} icon-data-management`} style={style} xmlns="http://www.w3.org/2000/svg" fill="none"
version="1.1" width="1em" height="1em" viewBox="0 0 32 32">
<path
d="M26 2H6C5.44687 2 5 2.44687 5 3V10H27V3C27 2.44687 26.5531 2 26 2ZM9 7.25C8.30938 7.25 7.75 6.69063 7.75 6C7.75 5.30938 8.30938 4.75 9 4.75C9.69063 4.75 10.25 5.30938 10.25 6C10.25 6.69063 9.69063 7.25 9 7.25ZM5 29C5 29.5531 5.44687 30 6 30H26C26.5531 30 27 29.5531 27 29V22H5V29ZM9 24.75C9.69063 24.75 10.25 25.3094 10.25 26C10.25 26.6906 9.69063 27.25 9 27.25C8.30938 27.25 7.75 26.6906 7.75 26C7.75 25.3094 8.30938 24.75 9 24.75ZM5 20H27V12H5V20ZM9 14.75C9.69063 14.75 10.25 15.3094 10.25 16C10.25 16.6906 9.69063 17.25 9 17.25C8.30938 17.25 7.75 16.6906 7.75 16C7.75 15.3094 8.30938 14.75 9 14.75Z"
fill="#F5222D"/>
<path
d="M26 2H6C5.44687 2 5 2.44687 5 3V10H27V3C27 2.44687 26.5531 2 26 2ZM9 7.25C8.30938 7.25 7.75 6.69063 7.75 6C7.75 5.30938 8.30938 4.75 9 4.75C9.69063 4.75 10.25 5.30938 10.25 6C10.25 6.69063 9.69063 7.25 9 7.25ZM5 29C5 29.5531 5.44687 30 6 30H26C26.5531 30 27 29.5531 27 29V22H5V29ZM9 24.75C9.69063 24.75 10.25 25.3094 10.25 26C10.25 26.6906 9.69063 27.25 9 27.25C8.30938 27.25 7.75 26.6906 7.75 26C7.75 25.3094 8.30938 24.75 9 24.75ZM5 20H27V12H5V20ZM9 14.75C9.69063 14.75 10.25 15.3094 10.25 16C10.25 16.6906 9.69063 17.25 9 17.25C8.30938 17.25 7.75 16.6906 7.75 16C7.75 15.3094 8.30938 14.75 9 14.75Z"
fill="url(#paint0_linear_253_47)"/>
<defs>
<linearGradient id="paint0_linear_253_47" x1="24.25" y1="2" x2="1.02087" y2="24.4633"
gradientUnits="userSpaceOnUse">
<stop stopColor="#FFCB7E"/>
<stop offset="1" stopColor="#F5222D"/>
</linearGradient>
</defs>
</svg>
)
export const IconIdCard = ({style,className}: { style?: React.CSSProperties;className?:string; }) => (
<svg className={`svg-icon ${className||''} icon-id-card`} style={style} xmlns="http://www.w3.org/2000/svg" fill="none" version="1.1"
width="1em" height="1em" viewBox="0 0 32 32">
<path
d="M11.6562 12.8438C10.7656 12.8438 10.0406 13.5719 10.0406 14.4688C10.0406 15.3656 10.7656 16.0938 11.6562 16.0938C12.5469 16.0938 13.2719 15.3656 13.2719 14.4688C13.2719 13.5719 12.5469 12.8438 11.6562 12.8438ZM29 5H3C2.44687 5 2 5.44687 2 6V26C2 26.5531 2.44687 27 3 27H29C29.5531 27 30 26.5531 30 26V6C30 5.44687 29.5531 5 29 5ZM19 13.125C19 12.9875 19.0312 12.875 19.0719 12.875H22.9281C22.9688 12.875 23 12.9875 23 13.125V14.625C23 14.7625 22.9688 14.875 22.9281 14.875H19.0719C19.0312 14.875 19 14.7625 19 14.625V13.125ZM16.3125 21.0312H14.9406C14.8094 21.0312 14.7031 20.9281 14.6938 20.7969C14.575 19.2188 13.2563 17.9688 11.6562 17.9688C10.0562 17.9688 8.7375 19.2188 8.61875 20.7969C8.60937 20.9281 8.50312 21.0312 8.37187 21.0312H7C6.96609 21.0313 6.93253 21.0244 6.90135 21.0111C6.87017 20.9978 6.84204 20.9782 6.81865 20.9537C6.79527 20.9291 6.77712 20.9001 6.76532 20.8683C6.75352 20.8365 6.7483 20.8026 6.75 20.7687C6.8375 19.1031 7.75 17.6531 9.08125 16.8281C8.49418 16.1828 8.16979 15.3412 8.17188 14.4688C8.17188 12.5344 9.73125 10.9688 11.6531 10.9688C13.575 10.9688 15.1344 12.5344 15.1344 14.4688C15.1344 15.3781 14.7906 16.2031 14.225 16.8281C15.5594 17.6562 16.4688 19.1031 16.5562 20.7687C16.5687 20.9125 16.4563 21.0312 16.3125 21.0312ZM25.0281 19.375H19.2219C19.1 19.375 19 19.2625 19 19.125V17.625C19 17.4875 19.1 17.375 19.2219 17.375H25.025C25.1469 17.375 25.2469 17.4875 25.2469 17.625V19.125H25.25C25.25 19.2625 25.15 19.375 25.0281 19.375Z"
fill="#F5222D"/>
<path
d="M11.6562 12.8438C10.7656 12.8438 10.0406 13.5719 10.0406 14.4688C10.0406 15.3656 10.7656 16.0938 11.6562 16.0938C12.5469 16.0938 13.2719 15.3656 13.2719 14.4688C13.2719 13.5719 12.5469 12.8438 11.6562 12.8438ZM29 5H3C2.44687 5 2 5.44687 2 6V26C2 26.5531 2.44687 27 3 27H29C29.5531 27 30 26.5531 30 26V6C30 5.44687 29.5531 5 29 5ZM19 13.125C19 12.9875 19.0312 12.875 19.0719 12.875H22.9281C22.9688 12.875 23 12.9875 23 13.125V14.625C23 14.7625 22.9688 14.875 22.9281 14.875H19.0719C19.0312 14.875 19 14.7625 19 14.625V13.125ZM16.3125 21.0312H14.9406C14.8094 21.0312 14.7031 20.9281 14.6938 20.7969C14.575 19.2188 13.2563 17.9688 11.6562 17.9688C10.0562 17.9688 8.7375 19.2188 8.61875 20.7969C8.60937 20.9281 8.50312 21.0312 8.37187 21.0312H7C6.96609 21.0313 6.93253 21.0244 6.90135 21.0111C6.87017 20.9978 6.84204 20.9782 6.81865 20.9537C6.79527 20.9291 6.77712 20.9001 6.76532 20.8683C6.75352 20.8365 6.7483 20.8026 6.75 20.7687C6.8375 19.1031 7.75 17.6531 9.08125 16.8281C8.49418 16.1828 8.16979 15.3412 8.17188 14.4688C8.17188 12.5344 9.73125 10.9688 11.6531 10.9688C13.575 10.9688 15.1344 12.5344 15.1344 14.4688C15.1344 15.3781 14.7906 16.2031 14.225 16.8281C15.5594 17.6562 16.4688 19.1031 16.5562 20.7687C16.5687 20.9125 16.4563 21.0312 16.3125 21.0312ZM25.0281 19.375H19.2219C19.1 19.375 19 19.2625 19 19.125V17.625C19 17.4875 19.1 17.375 19.2219 17.375H25.025C25.1469 17.375 25.2469 17.4875 25.2469 17.625V19.125H25.25C25.25 19.2625 25.15 19.375 25.0281 19.375Z"
fill="url(#paint0_linear_253_56)"/>
<defs>
<linearGradient id="paint0_linear_253_56" x1="26.5" y1="5" x2="9.93478" y2="30.9483"
gradientUnits="userSpaceOnUse">
<stop stopColor="#FFCB7E"/>
<stop offset="1" stopColor="#F5222D"/>
</linearGradient>
</defs>
</svg>
)
export const IconKnowledge = ({style,className}: { style?: React.CSSProperties;className?:string; }) => (
<svg className={`svg-icon ${className||''} icon-knowledge`} style={style} xmlns="http://www.w3.org/2000/svg" fill="none"
version="1.1"
width="1em" height="1em" viewBox="0 0 32 32">
<path
d="M27 4.5H17.5C17.225 4.5 17 4.725 17 5V14.5C17 14.775 17.225 15 17.5 15H27C27.275 15 27.5 14.775 27.5 14.5V5C27.5 4.725 27.275 4.5 27 4.5ZM27 17H17.5C17.225 17 17 17.225 17 17.5V27C17 27.275 17.225 27.5 17.5 27.5H27C27.275 27.5 27.5 27.275 27.5 27V17.5C27.5 17.225 27.275 17 27 17ZM14.5 4.5H5C4.725 4.5 4.5 4.725 4.5 5V14.5C4.5 14.775 4.725 15 5 15H14.5C14.775 15 15 14.775 15 14.5V5C15 4.725 14.775 4.5 14.5 4.5ZM14.5 17H5C4.725 17 4.5 17.225 4.5 17.5V27C4.5 27.275 4.725 27.5 5 27.5H14.5C14.775 27.5 15 27.275 15 27V17.5C15 17.225 14.775 17 14.5 17Z"
fill="#F5222D"/>
<path
d="M27 4.5H17.5C17.225 4.5 17 4.725 17 5V14.5C17 14.775 17.225 15 17.5 15H27C27.275 15 27.5 14.775 27.5 14.5V5C27.5 4.725 27.275 4.5 27 4.5ZM27 17H17.5C17.225 17 17 17.225 17 17.5V27C17 27.275 17.225 27.5 17.5 27.5H27C27.275 27.5 27.5 27.275 27.5 27V17.5C27.5 17.225 27.275 17 27 17ZM14.5 4.5H5C4.725 4.5 4.5 4.725 4.5 5V14.5C4.5 14.775 4.725 15 5 15H14.5C14.775 15 15 14.775 15 14.5V5C15 4.725 14.775 4.5 14.5 4.5ZM14.5 17H5C4.725 17 4.5 17.225 4.5 17.5V27C4.5 27.275 4.725 27.5 5 27.5H14.5C14.775 27.5 15 27.275 15 27V17.5C15 17.225 14.775 17 14.5 17Z"
fill="url(#paint0_linear_253_41)"/>
<defs>
<linearGradient id="paint0_linear_253_41" x1="24.625" y1="4.5" x2="5.9375" y2="27.5"
gradientUnits="userSpaceOnUse">
<stop stopColor="#FFCB7E"/>
<stop offset="1" stopColor="#F5222D"/>
</linearGradient>
</defs>
</svg>
)
export const IconAudio = ({style,className}: { style?: React.CSSProperties;className?:string; }) => (
<svg className={`svg-icon ${className||''} icon-audio`} style={style} xmlns="http://www.w3.org/2000/svg" fill="none" version="1.1"
width="1em" height="1em" viewBox="0 0 20 20">
<path
d="M10 12.1875C11.834 12.1875 13.3203 10.7188 13.3203 8.90625V4.53125C13.3203 2.71875 11.834 1.25 10 1.25C8.16602 1.25 6.67969 2.71875 6.67969 4.53125V8.90625C6.67969 10.7188 8.16602 12.1875 10 12.1875ZM16.4453 8.86719C16.4453 8.78125 16.375 8.71094 16.2891 8.71094H15.1172C15.0312 8.71094 14.9609 8.78125 14.9609 8.86719C14.9609 11.6074 12.7402 13.8281 10 13.8281C7.25977 13.8281 5.03906 11.6074 5.03906 8.86719C5.03906 8.78125 4.96875 8.71094 4.88281 8.71094H3.71094C3.625 8.71094 3.55469 8.78125 3.55469 8.86719C3.55469 12.1621 6.02734 14.8809 9.21875 15.2656V17.2656H6.38086C6.11328 17.2656 5.89844 17.5449 5.89844 17.8906V18.5938C5.89844 18.6797 5.95313 18.75 6.01953 18.75H13.9805C14.0469 18.75 14.1016 18.6797 14.1016 18.5938V17.8906C14.1016 17.5449 13.8867 17.2656 13.6191 17.2656H10.7031V15.2754C13.9316 14.9238 16.4453 12.1895 16.4453 8.86719Z"
fill="#515151"/>
</svg>
)
export const IconSend = ({style,className}: { style?: React.CSSProperties;className?:string; }) => (
<svg className={`svg-icon ${className||''} icon-audio`} style={style} xmlns="http://www.w3.org/2000/svg" fill="none" version="1.1"
width="1em" height="1em" viewBox="0 0 24 24">
<path
d="M23.2348 11.6535L0.828538 0.419538C0.737466 0.374002 0.633002 0.363288 0.533895 0.387395C0.423901 0.414587 0.329163 0.484257 0.270425 0.581149C0.211687 0.678041 0.193736 0.79426 0.220502 0.904359L2.52943 10.3383C2.56425 10.4803 2.66872 10.5954 2.808 10.641L6.76425 11.999L2.81068 13.357C2.67139 13.4053 2.56693 13.5178 2.53479 13.6597L0.220502 23.107C0.196395 23.2061 0.207109 23.3106 0.252645 23.399C0.357109 23.6106 0.614252 23.6963 0.828538 23.5919L23.2348 12.4222C23.3178 12.382 23.3848 12.3124 23.4276 12.232C23.5321 12.0178 23.4464 11.7606 23.2348 11.6535ZM2.86157 20.4231L4.2089 14.916L12.116 12.2026C12.1776 12.1811 12.2285 12.1329 12.25 12.0686C12.2875 11.9561 12.2285 11.8356 12.116 11.7954L4.2089 9.08472L2.86693 3.599L19.6884 12.0338L2.86157 20.4231Z"
fill="currentColor"/>
</svg>
)
export const IconSync = ({style,className}: { style?: React.CSSProperties;className?:string; }) => (
<svg className={`svg-icon ${className||''} icon-sync`} style={style} xmlns="http://www.w3.org/2000/svg" fill="none" version="1.1"
width="1em" height="1em" viewBox="0 0 16 16">
<g clipPath="url(#clip0_144_44)">
<path
d="M1.85719 7.86072C1.87504 7.08036 2.03576 6.32322 2.33754 5.61072C2.64647 4.87857 3.08933 4.22321 3.65362 3.65714C4.2179 3.09107 4.87504 2.64821 5.60719 2.33929C6.36433 2.01964 7.1679 1.85714 7.99826 1.85714C8.82862 1.85714 9.63219 2.01964 10.3875 2.33929C11.1176 2.64758 11.7805 3.09521 12.3393 3.65714C12.5161 3.83393 12.6822 4.02143 12.8358 4.21786L11.7608 5.05714C11.7395 5.0736 11.7233 5.09572 11.714 5.12097C11.7048 5.14622 11.7028 5.17356 11.7084 5.19987C11.714 5.22617 11.7269 5.25036 11.7457 5.26965C11.7644 5.28895 11.7882 5.30256 11.8143 5.30893L14.9518 6.07679C15.0411 6.09822 15.1286 6.03036 15.1286 5.93929L15.1429 2.70893C15.1429 2.58929 15.0054 2.52143 14.9125 2.59643L13.9054 3.38393C12.5322 1.62679 10.3965 0.5 7.99647 0.5C3.9054 0.5 0.57683 3.77857 0.500045 7.85357C0.499568 7.87263 0.502911 7.89159 0.509876 7.90934C0.516842 7.92709 0.527289 7.94326 0.540603 7.9569C0.553916 7.97055 0.569826 7.98139 0.587395 7.9888C0.604964 7.9962 0.623837 8.00001 0.642902 8H1.71433C1.7929 8 1.8554 7.9375 1.85719 7.86072ZM15.3572 8H14.2858C14.2072 8 14.1447 8.0625 14.1429 8.13929C14.125 8.91964 13.9643 9.67679 13.6625 10.3893C13.3536 11.1214 12.9108 11.7786 12.3465 12.3429C11.7769 12.9148 11.0998 13.3683 10.3541 13.6772C9.60843 13.9861 8.80897 14.1444 8.00183 14.1429C7.19496 14.1444 6.39578 13.9861 5.6504 13.6771C4.90502 13.3682 4.22819 12.9147 3.65897 12.3429C3.48219 12.1661 3.31612 11.9786 3.16254 11.7821L4.23754 10.9429C4.25881 10.9264 4.27501 10.9043 4.28426 10.879C4.29352 10.8538 4.29546 10.8264 4.28986 10.8001C4.28427 10.7738 4.27136 10.7496 4.25263 10.7304C4.2339 10.7111 4.2101 10.6974 4.18397 10.6911L1.04647 9.92322C0.957188 9.90179 0.869687 9.96964 0.869687 10.0607L0.857188 13.2929C0.857188 13.4125 0.994688 13.4804 1.08754 13.4054L2.09469 12.6179C3.4679 14.3732 5.60362 15.5 8.00362 15.5C12.0965 15.5 15.4233 12.2196 15.5 8.14643C15.5005 8.12737 15.4972 8.10841 15.4902 8.09066C15.4832 8.07292 15.4728 8.05674 15.4595 8.0431C15.4462 8.02945 15.4303 8.01861 15.4127 8.01121C15.3951 8.00381 15.3763 8 15.3572 8Z"
fill="currentColor"/>
</g>
<defs>
<clipPath id="clip0_144_44">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>
)
export const IconCopy = ({style,className}: { style?: React.CSSProperties;className?:string; }) => (
<svg className={`svg-icon ${className||''} icon-copy`} style={style} xmlns="http://www.w3.org/2000/svg" fill="none" version="1.1"
export const IconCopy = ({style, className}: IconProps) => (
<svg className={`svg-icon ${className || ''} icon-copy`} style={style} xmlns="http://www.w3.org/2000/svg"
fill="none" version="1.1"
width="1em" height="1em" viewBox="0 0 16 16">
<g clipPath="url(#clip0_144_46)">
<path
@ -415,45 +41,101 @@ export const IconCopy = ({style,className}: { style?: React.CSSProperties;classN
</defs>
</svg>
)
export const IconShare = ({style,className}: { style?: React.CSSProperties;className?:string; }) => (
<svg className={`svg-icon ${className||''} icon-share`} style={style} xmlns="http://www.w3.org/2000/svg" fill="none" version="1.1"
export const IconShare = ({style, className}: IconProps) => (
<svg className={`svg-icon ${className || ''} icon-share`} style={style} xmlns="http://www.w3.org/2000/svg"
fill="none" version="1.1"
width="1em" height="1em" viewBox="0 0 16 16">
<path
d="M12.2856 10.7143C11.7766 10.7143 11.307 10.8929 10.9391 11.1911L7.23915 8.51429C7.3011 8.17423 7.3011 7.82577 7.23915 7.48572L10.9391 4.80893C11.307 5.10714 11.7766 5.28572 12.2856 5.28572C13.4677 5.28572 14.4284 4.325 14.4284 3.14286C14.4284 1.96071 13.4677 1 12.2856 1C11.1034 1 10.1427 1.96071 10.1427 3.14286C10.1427 3.35 10.1713 3.54821 10.2266 3.7375L6.71236 6.28214C6.19093 5.59107 5.36236 5.14286 4.42843 5.14286C2.84986 5.14286 1.57129 6.42143 1.57129 8C1.57129 9.57857 2.84986 10.8571 4.42843 10.8571C5.36236 10.8571 6.19093 10.4089 6.71236 9.71786L10.2266 12.2625C10.1713 12.4518 10.1427 12.6518 10.1427 12.8571C10.1427 14.0393 11.1034 15 12.2856 15C13.4677 15 14.4284 14.0393 14.4284 12.8571C14.4284 11.675 13.4677 10.7143 12.2856 10.7143ZM12.2856 2.21429C12.7981 2.21429 13.2141 2.63036 13.2141 3.14286C13.2141 3.65536 12.7981 4.07143 12.2856 4.07143C11.7731 4.07143 11.357 3.65536 11.357 3.14286C11.357 2.63036 11.7731 2.21429 12.2856 2.21429ZM4.42843 9.57143C3.56236 9.57143 2.857 8.86607 2.857 8C2.857 7.13393 3.56236 6.42857 4.42843 6.42857C5.2945 6.42857 5.99986 7.13393 5.99986 8C5.99986 8.86607 5.2945 9.57143 4.42843 9.57143ZM12.2856 13.7857C11.7731 13.7857 11.357 13.3696 11.357 12.8571C11.357 12.3446 11.7731 11.9286 12.2856 11.9286C12.7981 11.9286 13.2141 12.3446 13.2141 12.8571C13.2141 13.3696 12.7981 13.7857 12.2856 13.7857Z"
fill="currentColor"/>
</svg>
)
export const IconDownload = ({style,className}: { style?: React.CSSProperties;className?:string; }) => (
<svg className={`svg-icon ${className||''} icon-download`} style={style} xmlns="http://www.w3.org/2000/svg" fill="none" version="1.1"
width="1em" height="1em" viewBox="0 0 16 16">
export const IconDownload = ({style, className}: IconProps) => (
<svg className={`svg-icon ${className || ''} icon-download`} style={style} xmlns="http://www.w3.org/2000/svg"
fill="none" version="1.1" width="1em" height="1em" viewBox="0 0 20 24">
<path
d="M7.88736 10.6575C7.90072 10.6745 7.9178 10.6883 7.93729 10.6978C7.95678 10.7073 7.97818 10.7123 7.99986 10.7123C8.02154 10.7123 8.04294 10.7073 8.06243 10.6978C8.08192 10.6883 8.099 10.6745 8.11236 10.6575L10.1124 8.1271C10.1856 8.03424 10.1195 7.89674 9.99986 7.89674H8.67665V1.85389C8.67665 1.77531 8.61236 1.71103 8.53379 1.71103H7.46236C7.38379 1.71103 7.3195 1.77531 7.3195 1.85389V7.89496H5.99986C5.88022 7.89496 5.81415 8.03246 5.88736 8.12532L7.88736 10.6575ZM14.5356 10.0325H13.4641C13.3856 10.0325 13.3213 10.0967 13.3213 10.1753V12.9253H2.67843V10.1753C2.67843 10.0967 2.61415 10.0325 2.53557 10.0325H1.46415C1.38557 10.0325 1.32129 10.0967 1.32129 10.1753V13.711C1.32129 14.0271 1.57665 14.2825 1.89272 14.2825H14.107C14.4231 14.2825 14.6784 14.0271 14.6784 13.711V10.1753C14.6784 10.0967 14.6141 10.0325 14.5356 10.0325Z"
d="M16.5571 8.47059H14.2857V1.41176C14.2857 0.635294 13.6429 0 12.8571 0H7.14286C6.35714 0 5.71429 0.635294 5.71429 1.41176V8.47059H3.44286C2.17143 8.47059 1.52857 9.99529 2.42857 10.8847L8.98571 17.3647C9.54286 17.9153 10.4429 17.9153 11 17.3647L17.5571 10.8847C18.4571 9.99529 17.8286 8.47059 16.5571 8.47059ZM0 22.5882C0 23.3647 0.642857 24 1.42857 24H18.5714C19.3571 24 20 23.3647 20 22.5882C20 21.8118 19.3571 21.1765 18.5714 21.1765H1.42857C0.642857 21.1765 0 21.8118 0 22.5882Z"
fill="currentColor"/>
</svg>
)
export const IconOrderFill = ({style, className}: IconProps) => (
<svg
className={`svg-icon ${className || ''} icon-download`} style={style}
xmlns="http://www.w3.org/2000/svg"
fill="none" width="1em" height="1em" viewBox="0 0 22 22"
>
<path
d="M1.15485 4.02687C0.839494 4.03116 0.539152 4.16202 0.322478 4.39157C0.105803 4.61897 -0.0100427 4.92575 0.000683739 5.2411C-0.00146155 5.88898 0.506973 6.42531 1.15485 6.45534H3.46533C3.78069 6.45105 4.08103 6.32019 4.29771 6.09064C4.51438 5.86324 4.63023 5.55646 4.6195 5.2411C4.62165 4.59323 4.11321 4.0569 3.46533 4.02687H1.15485ZM1.15485 9.98006C0.839494 9.98435 0.539152 10.1152 0.322478 10.3448C0.105803 10.5722 -0.0100427 10.8789 0.000683739 11.1943C-0.00146155 11.8422 0.506973 12.3785 1.15485 12.4085H3.46533C3.78069 12.4042 4.08103 12.2734 4.29771 12.0438C4.51438 11.8164 4.63023 11.5097 4.6195 11.1943C4.62165 10.5464 4.11321 10.0101 3.46533 9.98006H1.15485ZM1.15485 15.9912C0.532717 16.0555 0.0628972 16.5811 0.0628973 17.2054C0.0628973 17.8297 0.534862 18.3531 1.15485 18.4196H3.46533C4.08747 18.3531 4.55729 17.8297 4.55729 17.2054C4.55729 16.5811 4.08532 16.0577 3.46533 15.9912H1.15485ZM20.8186 0.0216038H3.40741C3.09205 0.0258944 2.79171 0.156757 2.57504 0.386304C2.35836 0.613705 2.24252 0.920482 2.25324 1.23584V2.81263H3.40741C4.69244 2.84481 5.71789 3.896 5.71789 5.18104C5.72862 5.80317 5.49049 6.40171 5.05714 6.84578C4.62379 7.28986 4.02955 7.543 3.40956 7.54944H2.25539V8.76368H3.40956C4.69459 8.79585 5.72004 9.84705 5.72004 11.1321C5.73076 11.7542 5.49264 12.3528 5.05929 12.7968C4.62594 13.2409 4.03169 13.494 3.4117 13.5005H2.25753V14.7147H3.4117C4.69244 14.749 5.71789 15.8002 5.71789 17.0853C5.72862 17.7074 5.49049 18.3059 5.05714 18.75C4.62379 19.1941 4.02955 19.4472 3.40956 19.4537H2.25539V20.7752C2.25324 21.4231 2.76168 21.9594 3.40956 21.9894H20.8208C21.1361 21.9851 21.4365 21.8543 21.6531 21.6247C21.8698 21.3973 21.9857 21.0905 21.9749 20.7752V1.06636C21.9234 0.467825 21.4172 0.0130226 20.8186 0.0216038ZM17.7015 8.87738C18.1799 8.87738 18.5682 9.26567 18.5682 9.74407C18.5682 10.2225 18.1799 10.6108 17.7015 10.6108H15.1014V11.6512H17.6994C18.1778 11.6512 18.5661 12.0395 18.5661 12.5179C18.5661 12.9963 18.1778 13.3846 17.6994 13.3846H15.1014V16.2743C15.1143 16.5919 14.9512 16.89 14.6788 17.0509C14.4063 17.214 14.0652 17.214 13.7928 17.0509C13.5203 16.8879 13.3573 16.5897 13.3702 16.2743V13.3954H10.7701C10.2917 13.3954 9.90336 13.0071 9.90336 12.5287C9.90336 12.0503 10.2917 11.662 10.7701 11.662H13.368V10.6215H10.7701C10.2917 10.6215 9.90336 10.2332 9.90336 9.7548C9.90336 9.2764 10.2917 8.8881 10.7701 8.8881H12.0401L10.1372 6.98094C9.90765 6.76641 9.81111 6.44461 9.88834 6.13998C9.96557 5.83535 10.2037 5.59722 10.5083 5.51999C10.813 5.44276 11.1348 5.53715 11.3493 5.7667L14.0052 8.42472L14.0631 8.48264H14.4106L14.4685 8.42472L17.1244 5.7667C17.4634 5.45349 17.989 5.46207 18.3151 5.7903C18.6411 6.11638 18.6497 6.64198 18.3365 6.98094L16.4336 8.87738H17.7015Z"
fill="currentColor"
/>
</svg>
)
export const IconRecycleFill = ({style, className}: IconProps) => (
<svg
className={`svg-icon ${className || ''} icon-download`} style={style}
xmlns="http://www.w3.org/2000/svg"
fill="none" width="1em" height="1em" viewBox="0 0 22 22"
>
<path
d="M21.1832 3.92852H17.262V2.35739C17.2638 1.73447 17.019 1.1363 16.5814 0.694283C16.1437 0.252265 15.549 0.00255176 14.9279 0H7.08268C6.45926 0.000364354 5.86146 0.248804 5.42051 0.690782C4.97956 1.13276 4.73149 1.73216 4.73076 2.35739V3.92852H0.816386C0.675283 3.92266 0.535222 3.95514 0.411003 4.02252C0.286784 4.0899 0.183019 4.18968 0.110673 4.31132C0.0382409 4.43311 0 4.57228 0 4.71409C0 4.8559 0.0382409 4.99507 0.110673 5.11686C0.183019 5.23849 0.286784 5.33828 0.411003 5.40566C0.535222 5.47304 0.675283 5.50551 0.816386 5.49966H21.1832C21.2888 5.50409 21.3943 5.48706 21.4932 5.44958C21.5921 5.4121 21.6824 5.35495 21.7587 5.28157C21.835 5.20818 21.8957 5.12008 21.9371 5.02256C21.9786 4.92504 22 4.82011 22 4.71409C22 4.60807 21.9786 4.50314 21.9371 4.40562C21.8957 4.3081 21.835 4.21999 21.7587 4.14661C21.6824 4.07323 21.5921 4.01608 21.4932 3.9786C21.3943 3.94112 21.2888 3.92409 21.1832 3.92852ZM18.0542 6.87801H3.95091C3.74286 6.87801 3.54331 6.9608 3.39607 7.10822C3.24883 7.25563 3.16593 7.45561 3.16556 7.66427V19.644C3.16665 20.2687 3.41469 20.8676 3.85531 21.3092C4.29592 21.7509 4.89317 21.9993 5.51611 22H16.4849C17.1078 21.9993 17.7051 21.7509 18.1457 21.3092C18.5863 20.8676 18.8343 20.2687 18.8354 19.644V7.69456C18.8366 7.4827 18.7554 7.27873 18.609 7.12599C18.4626 6.97326 18.2626 6.8838 18.0514 6.87664L18.0542 6.87801ZM5.66576 16.0845C5.60496 15.9767 5.56272 15.8594 5.54082 15.7375C5.53845 15.6251 5.56797 15.5143 5.62595 15.418C5.62595 15.418 6.26439 14.3082 6.27262 14.2972C6.28086 14.2862 5.66988 13.9309 5.66988 13.9309L7.75682 13.4614L8.65338 15.6976L8.06299 15.3561L7.20351 16.7468C7.04274 17.0229 6.94126 17.3297 6.90557 17.6474C6.87595 17.9301 6.9234 18.2157 7.04287 18.4736L5.66576 16.0845ZM7.70465 18.9073C7.60718 18.8801 7.51453 18.8378 7.43005 18.782C7.30763 18.6826 7.21302 18.5531 7.15545 18.4061C7.04694 18.1357 7.00915 17.8419 7.04568 17.5527C7.0822 17.2635 7.19183 16.9885 7.36415 16.7537H10.1307V18.9114H7.70465V18.9073ZM9.61309 12.8307L7.7527 11.7525L8.96642 9.63886C9.03647 9.56856 9.11766 9.5104 9.20669 9.46673C9.35308 9.41038 9.51138 9.39237 9.66664 9.41441C9.98643 9.46441 10.287 9.59945 10.5371 9.80547C10.7304 9.97913 10.8864 10.1904 10.9957 10.4265L9.61309 12.8307ZM11.0836 10.2805C10.9264 10.0033 10.7126 9.76262 10.4561 9.57414C10.2286 9.40715 9.96083 9.30401 9.68037 9.27533H12.4263C12.5495 9.2769 12.6716 9.29924 12.7874 9.34143C12.8859 9.39611 12.9669 9.47778 13.0208 9.57689C13.0208 9.57689 13.6593 10.6867 13.6634 10.7005C13.6634 10.6922 14.2799 10.359 14.2799 10.359L13.6346 12.4093L11.2524 12.0651L11.8469 11.7236L11.0836 10.2805ZM15.0295 18.5286C14.9669 18.6352 14.8871 18.7306 14.7934 18.8109C14.6971 18.8696 14.5861 18.8992 14.4735 18.8963H13.1829C13.1677 18.8963 13.1691 19.6027 13.1691 19.6027L11.7151 18.0205L13.2007 16.1203V16.8088L14.8304 16.8597C15.1488 16.8615 15.4641 16.7963 15.7558 16.6683C16.0132 16.554 16.2351 16.3723 16.3984 16.1423L15.0254 18.5273L15.0295 18.5286ZM16.2844 16.0666C16.0817 16.3194 15.8146 16.5126 15.5114 16.6256C15.2657 16.707 15.0058 16.737 14.7481 16.7138L13.3627 14.3137L15.2217 13.2341L16.4354 15.3464C16.4721 15.47 16.477 15.6008 16.4497 15.7267C16.4223 15.8527 16.3636 15.9696 16.2789 16.0666H16.2844Z"
fill="currentColor"
/>
</svg>
)
export const IconDownloadOutline = ({style, className}: IconProps)=>(
<svg
className={`svg-icon ${className || ''} icon-download`} style={style} xmlns="http://www.w3.org/2000/svg"
fill="none" width="1em" height="1em" viewBox="0 0 22 22"
>
<path
d="M9.94889 12.052V1.05935C9.94889 0.778394 10.0596 0.508944 10.2568 0.310277C10.4539 0.11161 10.7212 0 11 0C11.2788 0 11.5461 0.11161 11.7432 0.310277C11.9404 0.508944 12.0511 0.778394 12.0511 1.05935V12.052L13.9236 10.1648C14.0204 10.0634 14.1364 9.98239 14.2648 9.92663C14.3931 9.87087 14.5311 9.84146 14.6708 9.84011C14.8106 9.83876 14.9492 9.8655 15.0785 9.91876C15.2079 9.97203 15.3254 10.0508 15.4242 10.1503C15.523 10.2499 15.6011 10.3683 15.6539 10.4987C15.7068 10.6291 15.7333 10.7687 15.732 10.9096C15.7306 11.0504 15.7015 11.1895 15.6461 11.3188C15.5908 11.4482 15.5105 11.5651 15.4098 11.6627L11.7431 15.3581C11.546 15.5567 11.2787 15.6683 11 15.6683C10.7213 15.6683 10.454 15.5567 10.2569 15.3581L6.59022 11.6627C6.48954 11.5651 6.40919 11.4482 6.35387 11.3188C6.29854 11.1895 6.26936 11.0504 6.26802 10.9096C6.26668 10.7687 6.29321 10.6291 6.34606 10.4987C6.39892 10.3683 6.47703 10.2499 6.57583 10.1503C6.67464 10.0508 6.79215 9.97203 6.9215 9.91876C7.05085 9.8655 7.18944 9.83876 7.32916 9.84011C7.46888 9.84146 7.60694 9.87087 7.73525 9.92663C7.86356 9.98239 7.97955 10.0634 8.07644 10.1648L9.94889 12.052ZM17.5181 3.52296C17.194 3.52291 16.8832 3.39311 16.654 3.1621C16.4248 2.9311 16.2961 2.61782 16.2961 2.29115C16.2961 1.96449 16.4248 1.65121 16.654 1.4202C16.8832 1.1892 17.194 1.0594 17.5181 1.05935H19.5556C20.2039 1.05935 20.8256 1.31891 21.284 1.78092C21.7425 2.24294 22 2.86957 22 3.52296V19.5364C22 20.1898 21.7425 20.8164 21.284 21.2784C20.8256 21.7404 20.2039 22 19.5556 22H2.44444C1.79614 22 1.17438 21.7404 0.715961 21.2784C0.257539 20.8164 0 20.1898 0 19.5364V3.52296C0 2.86957 0.257539 2.24294 0.715961 1.78092C1.17438 1.31891 1.79614 1.05935 2.44444 1.05935H4.48189C4.64241 1.05933 4.80136 1.09117 4.94967 1.15306C5.09798 1.21495 5.23274 1.30568 5.34625 1.42007C5.45977 1.53446 5.54981 1.67026 5.61124 1.81972C5.67268 1.96918 5.7043 2.12937 5.7043 2.29115C5.7043 2.45293 5.67268 2.61313 5.61124 2.76259C5.54981 2.91205 5.45977 3.04785 5.34625 3.16224C5.23274 3.27662 5.09798 3.36735 4.94967 3.42925C4.80136 3.49114 4.64241 3.52298 4.48189 3.52296H2.44444V19.5364H19.5556V3.52296H17.5181Z"
fill="currentColor"/>
</svg>
)
export const IconDelete = ({style,className}: { style?: React.CSSProperties;className?:string; }) => (
<svg className={`svg-icon ${className||''} icon-delete`} style={style} xmlns="http://www.w3.org/2000/svg"
width="1em" height="1em" viewBox="0 0 1024 1024" version="1.1">
export const IconPin = ({style, className}: IconProps) => (
<svg className={`svg-icon ${className || ''} icon-download`} style={style} xmlns="http://www.w3.org/2000/svg"
fill="none" version="1.1" width="0.6em" height="1em" viewBox="0 0 12 21">
<path
d="M765.505691 191.942567H639.627772c0-35.32453-28.636201-63.960731-63.96073-63.960731H447.74558c-35.32453 0-63.960731 28.636201-63.96073 63.960731H257.905908c-36.452213 0-66.00325 29.551036-66.00325 66.00325v59.875692c0 36.452213 29.551036 66.00325 66.00325 66.00325h-2.042519v445.681572c0 36.452213 29.551036 66.00325 66.003249 66.00325h379.679346c36.452213 0 66.00325-29.551036 66.00325-66.00325V383.823736h-2.04252c36.452213 0 66.00325-29.551036 66.00325-66.00325v-59.875693c-0.001023-36.452213-29.551036-66.002226-66.004273-66.002226z m-61.918211 611.470479c-0.101307 3.123131-1.743714 27.813462-27.961842 28.134781H347.905688c-27.988448-0.343831-27.969005-28.459169-27.969005-28.459169l-0.112564 0.031722V383.823736h383.763361v419.58931z m31.980365-483.550041H287.843754c-17.662265 0-31.980365-14.3181-31.980365-31.980365 0-17.662265 14.3181-31.980365 31.980365-31.980366H735.568868c17.662265 0 31.980365 14.3181 31.980366 31.980366-0.001023 17.662265-14.319124 31.980365-31.981389 31.980365z"
fill="currentColor"/>
<path
d="M447.74558 767.588119c17.662265 0 31.980365-14.3181 31.980366-31.980365V479.764831c0-17.662265-14.3181-31.980365-31.980366-31.980365-17.662265 0-31.980365 14.3181-31.980365 31.980365v255.842923c0 17.662265 14.3181 31.980365 31.980365 31.980365zM575.667042 767.588119c17.662265 0 31.980365-14.3181 31.980365-31.980365V479.764831c0-17.662265-14.3181-31.980365-31.980365-31.980365-17.662265 0-31.980365 14.3181-31.980366 31.980365v255.842923c0 17.662265 14.3181 31.980365 31.980366 31.980365z"
d="M10 10.5V2.5H11V0.5H1V2.5H2V10.5L0 12.5V14.5H5.2V20.5H6.8V14.5H12V12.5L10 10.5ZM2.8 12.5L4 11.3V2.5H8V11.3L9.2 12.5H2.8Z"
fill="currentColor"/>
</svg>
)
export const IconAddText = ({style,className}: { style?: React.CSSProperties;className?:string; }) => (
export const IconDelete = ({style, className}: IconProps) => (
<svg className={`svg-icon ${className || ''} icon-delete`} style={style} xmlns="http://www.w3.org/2000/svg"
width="0.86em" height="1em" viewBox="0 0 20 23" version="1.1">
<path
d="M3.66667 22.3828C2.99444 22.3828 2.41918 22.1437 1.94089 21.6654C1.46259 21.1871 1.22304 20.6114 1.22222 19.9384V4.04948H0V1.60503H6.11111V0.382812H13.4444V1.60503H19.5556V4.04948H18.3333V19.9384C18.3333 20.6106 18.0942 21.1863 17.6159 21.6654C17.1376 22.1445 16.5619 22.3836 15.8889 22.3828H3.66667ZM15.8889 4.04948H3.66667V19.9384H15.8889V4.04948ZM6.11111 17.4939H8.55555V6.49392H6.11111V17.4939ZM11 17.4939H13.4444V6.49392H11V17.4939Z"
fill="currentColor"/>
</svg>
)
export const IconAddText = ({style, className}: IconProps) => (
<svg className={`svg-icon ${className || ''} icon-delete`} style={style} xmlns="http://www.w3.org/2000/svg"
width="1em" height="1em" viewBox="0 0 1024 1024" version="1.1">
<path
d="M714.688 512a224 224 0 1 1 0 448 224 224 0 0 1 0-448z m96-448a64 64 0 0 1 64 64v284.992a36.032 36.032 0 0 1-71.424 5.76l-0.64-5.76V136h-624v752h212.032a35.968 35.968 0 1 1 0 72H170.688a64 64 0 0 1-64-64V128a64 64 0 0 1 64-64h640z m-96 512a160 160 0 1 0 0 320 160 160 0 0 0 0-320z m0 28.032c15.424 0 27.968 12.48 27.968 27.968v76.032h76.032a28.032 28.032 0 0 1 0 55.936h-76.032v76.032a28.032 28.032 0 0 1-56 0v-76.032H610.688a27.968 27.968 0 1 1 0-55.936h75.968V631.936c0-15.488 12.544-28.032 28.032-28.032zM401.664 672a32 32 0 1 1 0 64h-112a32 32 0 1 1 0-64h112z m57.984-192a32 32 0 0 1 0 64H289.664a32 32 0 1 1 0-64h169.984z m230.016-192a32 32 0 0 1 0 64h-400a32 32 0 1 1 0-64h400z"
fill="currentColor"/>
<path
d="M12.2222 0.382812C12.5337 0.383158 12.8334 0.502443 13.0599 0.716294C13.2864 0.930145 13.4227 1.22242 13.441 1.53341C13.4592 1.84439 13.3581 2.15061 13.1581 2.3895C12.9582 2.62838 12.6746 2.78191 12.3652 2.8187L12.2222 2.82726H2.44444V19.9384H19.5556V10.1606C19.5559 9.84907 19.6752 9.54944 19.889 9.32292C20.1029 9.0964 20.3952 8.96008 20.7061 8.94182C21.0171 8.92357 21.3234 9.02475 21.5622 9.22469C21.8011 9.42463 21.9547 9.70825 21.9914 10.0176L22 10.1606V19.9384C22.0002 20.5551 21.7673 21.1491 21.3479 21.6013C20.9286 22.0535 20.3539 22.3305 19.7389 22.3767L19.5556 22.3828H2.44444C1.82774 22.383 1.23375 22.1501 0.781553 21.7308C0.329353 21.3114 0.0523642 20.7367 0.00611137 20.1217L1.2255e-07 19.9384V2.82726C-0.000195041 2.21055 0.232719 1.61656 0.652052 1.16436C1.07138 0.712165 1.64614 0.435177 2.26111 0.388924L2.44444 0.382812H12.2222ZM19.8526 0.802035C20.0725 0.582832 20.3676 0.455567 20.678 0.446089C20.9884 0.436611 21.2908 0.545631 21.5237 0.751005C21.7566 0.95638 21.9026 1.24271 21.932 1.55184C21.9615 1.86096 21.8722 2.16971 21.6822 2.41537L21.5808 2.53148L9.48078 14.6303C9.26083 14.8495 8.96569 14.9767 8.65531 14.9862C8.34493 14.9957 8.04257 14.8867 7.80966 14.6813C7.57675 14.4759 7.43074 14.1896 7.40129 13.8805C7.37184 13.5713 7.46116 13.2626 7.65111 13.0169L7.75256 12.902L19.8526 0.802035Z"
fill="currentColor"/>
</svg>
)
export const IconVideo = ({style, className}: IconProps) => (
<svg className={`svg-icon ${className || ''} icon-video`} style={style} xmlns="http://www.w3.org/2000/svg"
width="1em" height="1em" viewBox="0 0 21 20" version="1.1">
<path fillRule="evenodd" clipRule="evenodd" d="M18 1.00268e-07C18.5046 -0.000159579 18.9906 0.190406 19.3605 0.533497C19.7305 0.876588 19.9572 1.34684 19.995 1.85L20 2V16C20.0002 16.5046 19.8096 16.9906 19.4665 17.3605C19.1234 17.7305 18.6532 17.9572 18.15 17.995L18 18H2C1.49542 18.0002 1.00943 17.8096 0.639452 17.4665C0.269471 17.1234 0.0428434 16.6532 0.00500021 16.15L1.00268e-07 16V2C-0.000159579 1.49542 0.190406 1.00943 0.533497 0.639452C0.876588 0.269471 1.34684 0.0428434 1.85 0.00500021L2 1.00268e-07H18ZM18 2H2V16H18V2ZM8.34 4.638L8.858 4.868L9.196 5.028L9.583 5.218L10.013 5.436L10.483 5.686L10.99 5.966L11.256 6.118L11.774 6.423L12.248 6.715L12.678 6.988L13.058 7.241L13.538 7.571L13.902 7.834L13.997 7.904C14.1513 8.01883 14.2767 8.16816 14.363 8.34005C14.4494 8.51194 14.4943 8.70164 14.4943 8.894C14.4943 9.08636 14.4494 9.27606 14.363 9.44795C14.2767 9.61984 14.1513 9.76917 13.997 9.884L13.674 10.119L13.234 10.427L12.878 10.666L12.473 10.929L12.02 11.212L11.521 11.512L10.987 11.821L10.478 12.103L10.007 12.353L9.577 12.573L9.191 12.761L8.569 13.049L8.339 13.149C8.16242 13.2251 7.97051 13.2589 7.77856 13.2476C7.58662 13.2364 7.39995 13.1805 7.23346 13.0843C7.06696 12.9881 6.92524 12.8544 6.8196 12.6937C6.71396 12.5331 6.64732 12.35 6.625 12.159L6.567 11.594L6.535 11.22L6.493 10.556L6.47 10.048L6.455 9.493L6.451 9.199L6.449 8.894C6.449 8.68733 6.451 8.48733 6.455 8.294L6.47 7.739L6.493 7.232L6.52 6.775L6.55 6.374L6.625 5.63C6.64719 5.43882 6.71376 5.25547 6.81939 5.09458C6.92502 4.93369 7.0668 4.79972 7.2334 4.70335C7.4 4.60698 7.58682 4.55089 7.77896 4.53954C7.97109 4.5282 8.16321 4.56191 8.34 4.638ZM8.951 7.139L8.515 6.921L8.486 7.408L8.464 7.959L8.451 8.569L8.449 8.894L8.451 9.219L8.464 9.828L8.474 10.111L8.5 10.631L8.515 10.866L8.949 10.648L9.436 10.392L9.971 10.098L10.255 9.936L10.806 9.61L11.3 9.304L11.736 9.024L11.932 8.894L11.525 8.624L11.059 8.33C10.7938 8.16584 10.5261 8.00582 10.256 7.85L9.973 7.689L9.439 7.395L8.951 7.139Z" fill="black"/>
</svg>
)
export const IconAddImage = ({style,className}: { style?: React.CSSProperties;className?:string; }) => (
export const IconAddImage = ({style, className}: IconProps) => (
<svg className={`svg-icon ${className || ''} icon-delete`} style={style} xmlns="http://www.w3.org/2000/svg"
width="1em" height="1em" viewBox="0 0 1024 1024" version="1.1">
<path
@ -462,7 +144,7 @@ export const IconAddImage = ({style,className}: { style?: React.CSSProperties;cl
</svg>
)
export const IconAdd = ({style,className}: { style?: React.CSSProperties;className?:string; }) => (
export const IconAddCircle = ({style, className}: IconProps) => (
<svg className={`svg-icon ${className || ''} icon-delete`} style={style} xmlns="http://www.w3.org/2000/svg"
width="1em" height="1em" viewBox="0 0 1024 1024" version="1.1">
<path
@ -474,8 +156,85 @@ export const IconAdd = ({style,className}: { style?: React.CSSProperties;classNa
fill="currentColor"/>
</svg>
)
export const IconRollbackCircle = ({style, className}: IconProps) => (
<svg className={`svg-icon ${className || ''} icon-warning`} style={style} xmlns="http://www.w3.org/2000/svg"
width="1em" height="1em" viewBox="0 0 21 21" version="1.1">
<g>
<path d="M6.35192 0.404738C6.35236 0.404738 6.35272 0.4051 6.35272 0.405547V3.78207H12.3726C14.6365 3.78177 16.8099 4.66937 18.4238 6.25343C20.0378 7.8375 20.963 9.99107 21 12.2496V12.3918C20.9999 14.6502 20.1103 16.818 18.5231 18.428C16.9359 20.0379 14.7782 20.9611 12.5151 20.9984L12.3726 21H4.16146C4.05408 21 3.9511 20.9574 3.87516 20.8817C3.79923 20.8059 3.75657 20.7032 3.75657 20.596V18.9801C3.75657 18.8729 3.79923 18.7702 3.87516 18.6944C3.9511 18.6186 4.05408 18.5761 4.16146 18.5761H12.3726C14.0164 18.5932 15.5998 17.9581 16.7743 16.8105C17.9488 15.6628 18.6183 14.0966 18.6354 12.4565C18.6526 10.8163 18.0161 9.23653 16.8659 8.06464C15.7157 6.89274 14.146 6.22475 12.5022 6.20761H0.405711C0.324927 6.20777 0.24594 6.18382 0.178909 6.13883C0.111878 6.09384 0.0598667 6.02988 0.0295641 5.95516C-0.000738426 5.88044 -0.00794696 5.79839 0.00886551 5.71955C0.025678 5.64071 0.0657426 5.56869 0.123908 5.51275L5.66279 0.115483C5.71978 0.0597486 5.792 0.0220586 5.87039 0.00714763C5.94878 -0.00776331 6.02983 0.000769782 6.10338 0.031675C6.17692 0.0625803 6.23967 0.114479 6.28374 0.180854C6.32767 0.247003 6.35109 0.324588 6.35111 0.403927C6.35111 0.404375 6.35147 0.404738 6.35192 0.404738Z" fill="#B6A8AB"/>
</g>
export const IconPlay = ({style,className}: { style?: React.CSSProperties;className?:string; }) => (
</svg>
)
export const IconWarningCircle = ({style, className}: IconProps) => (
<svg className={`svg-icon ${className || ''} icon-warning`} style={style} xmlns="http://www.w3.org/2000/svg"
width="1em" height="1em" viewBox="0 0 22 22" version="1.1">
<g>
<path
d="M9.625 12.375V5.5H12.375V12.375H9.625ZM11 16.5C11.3647 16.5 11.7144 16.3551 11.9723 16.0973C12.2301 15.8394 12.375 15.4897 12.375 15.125C12.375 14.7603 12.2301 14.4106 11.9723 14.1527C11.7144 13.8949 11.3647 13.75 11 13.75C10.6353 13.75 10.2856 13.8949 10.0277 14.1527C9.76987 14.4106 9.625 14.7603 9.625 15.125C9.625 15.4897 9.76987 15.8394 10.0277 16.0973C10.2856 16.3551 10.6353 16.5 11 16.5Z"
fill="currentColor"/>
<path
d="M0 11C0 8.08262 1.15893 5.28473 3.22183 3.22183C5.28473 1.15893 8.08262 0 11 0C13.9174 0 16.7153 1.15893 18.7782 3.22183C20.8411 5.28473 22 8.08262 22 11C22 13.9174 20.8411 16.7153 18.7782 18.7782C16.7153 20.8411 13.9174 22 11 22C8.08262 22 5.28473 20.8411 3.22183 18.7782C1.15893 16.7153 0 13.9174 0 11ZM11 2.75C9.91659 2.75 8.8438 2.96339 7.84286 3.37799C6.84193 3.7926 5.93245 4.40029 5.16637 5.16637C4.40029 5.93245 3.7926 6.84193 3.37799 7.84286C2.96339 8.8438 2.75 9.91659 2.75 11C2.75 12.0834 2.96339 13.1562 3.37799 14.1571C3.7926 15.1581 4.40029 16.0675 5.16637 16.8336C5.93245 17.5997 6.84193 18.2074 7.84286 18.622C8.8438 19.0366 9.91659 19.25 11 19.25C13.188 19.25 15.2865 18.3808 16.8336 16.8336C18.3808 15.2865 19.25 13.188 19.25 11C19.25 8.81196 18.3808 6.71354 16.8336 5.16637C15.2865 3.61919 13.188 2.75 11 2.75Z"
fill="currentColor"/>
</g>
</svg>
)
export const IconAdd = ({style, className}: IconProps) => (
<svg className={`svg-icon ${className || ''} icon-delete`} style={style} xmlns="http://www.w3.org/2000/svg"
width="1em" height="1em" viewBox="0 0 1024 1024" version="1.1">
<path d="M544 288v448c0 17.6-14.4 32-32 32s-32-14.4-32-32V288c0-17.6 14.4-32 32-32s32 14.4 32 32z"
fill="currentColor"/>
<path d="M736 544H288c-17.6 0-32-14.4-32-32s14.4-32 32-32h448c17.6 0 32 14.4 32 32s-14.4 32-32 32z"
fill="currentColor"/>
</svg>
)
export const IconLocked = ({style, className}: IconProps) => (
<svg className={`svg-icon ${className || ''} icon-delete`} style={style} xmlns="http://www.w3.org/2000/svg"
width="0.81em" height="1em" viewBox="0 0 18 22" version="1.1">
<path fillRule="evenodd" clipRule="evenodd"
d="M5.625 5.5C5.625 4.62479 5.98058 3.78542 6.61351 3.16655C7.24645 2.54768 8.10489 2.2 9 2.2C9.89511 2.2 10.7536 2.54768 11.3865 3.16655C12.0194 3.78542 12.375 4.62479 12.375 5.5V7.7H5.625V5.5ZM3.375 7.7V5.5C3.375 4.04131 3.96763 2.64236 5.02252 1.61091C6.07742 0.579463 7.50816 0 9 0C10.4918 0 11.9226 0.579463 12.9775 1.61091C14.0324 2.64236 14.625 4.04131 14.625 5.5V7.7H16.875C17.1734 7.7 17.4595 7.81589 17.6705 8.02218C17.8815 8.22847 18 8.50826 18 8.8V18.7C18 19.5752 17.6444 20.4146 17.0115 21.0335C16.3786 21.6523 15.5201 22 14.625 22H3.375C2.47989 22 1.62145 21.6523 0.988515 21.0335C0.355579 20.4146 0 19.5752 0 18.7V8.8C0 8.50826 0.118527 8.22847 0.329505 8.02218C0.540484 7.81589 0.826631 7.7 1.125 7.7H3.375ZM10.125 14.85C10.125 14.4124 10.3028 13.9927 10.6193 13.6833C10.9357 13.3738 11.3649 13.2 11.8125 13.2H11.8238C12.2713 13.2 12.7005 13.3738 13.017 13.6833C13.3335 13.9927 13.5113 14.4124 13.5113 14.85V14.861C13.5113 15.2986 13.3335 15.7183 13.017 16.0277C12.7005 16.3372 12.2713 16.511 11.8238 16.511H11.8125C11.3649 16.511 10.9357 16.3372 10.6193 16.0277C10.3028 15.7183 10.125 15.2986 10.125 14.861V14.85Z"
fill="currentColor"/>
</svg>
)
export const IconUnlock = ({style, className}: IconProps) => (
<svg className={`svg-icon ${className || ''} icon-delete`} style={style} xmlns="http://www.w3.org/2000/svg"
width="1em" height="1em" viewBox="0 0 24 24" version="1.1">
<path fillRule="evenodd" clipRule="evenodd"
d="M11.418 7.68673V5.50001C11.418 4.04132 10.8166 2.64237 9.7459 1.61092C8.67525 0.579463 7.22314 0 5.70902 0C4.19489 0 2.74278 0.579463 1.67213 1.61092C0.601484 2.64237 0 4.04132 0 5.50001V7.68676H2.28361V5.50001C2.28361 4.62479 2.6445 3.78542 3.28689 3.16655C3.92927 2.54768 4.80054 2.2 5.70902 2.2C6.61749 2.2 7.48876 2.54768 8.13115 3.16655C8.77354 3.78542 9.13443 4.62479 9.13443 5.50001V7.68676H9.15656V7.69998H6.87295C6.57012 7.69998 6.2797 7.81587 6.06557 8.02216C5.85144 8.22845 5.73115 8.50824 5.73115 8.79998V18.7C5.73115 19.5752 6.09204 20.4146 6.73443 21.0335C7.37681 21.6523 8.24808 22 9.15656 22H20.5746C21.4831 22 22.3543 21.6523 22.9967 21.0335C23.6391 20.4146 24 19.5752 24 18.7V8.79998C24 8.50824 23.8797 8.22845 23.6656 8.02216C23.4514 7.81587 23.161 7.69998 22.8582 7.69998H20.5746V7.68673H18.291V7.69998H11.4402V7.68673H11.418ZM16.0074 14.85C16.0074 14.4124 16.1878 13.9927 16.509 13.6833C16.8302 13.3738 17.2658 13.2 17.7201 13.2H17.7315C18.1857 13.2 18.6214 13.3738 18.9426 13.6833C19.2638 13.9927 19.4442 14.4124 19.4442 14.85V14.861C19.4442 15.2986 19.2638 15.7183 18.9426 16.0277C18.6214 16.3372 18.1857 16.511 17.7315 16.511H17.7201C17.2658 16.511 16.8302 16.3372 16.509 16.0277C16.1878 15.7183 16.0074 15.2986 16.0074 14.861V14.85Z"
fill="currentColor"/>
</svg>
)
export const IconPlaying = ({style, className}: IconProps) => (
<svg className={`svg-icon ${className || ''} icon-playing`} style={style} xmlns="http://www.w3.org/2000/svg"
width="1em" height="1em" viewBox="0 0 32 30" version="1.1">
<path d="M1 11.7057V18.2943M7 6.76424V23.2358M13 1V29M19 7.22275V22.7772M25 11.1114V18.8886M31 13.3528V16.6472"
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
)
export const IconGenerating = ({style, className}: IconProps) => (
<svg className={`svg-icon ${className || ''} icon-generating`} style={style} xmlns="http://www.w3.org/2000/svg"
width="1em" height="1em" viewBox="0 0 20 20" version="1.1">
<path d="M3.463 2.43301C5.27751 0.860592 7.59897 -0.00342947 10 1.02307e-05C15.523 1.02307e-05 20 4.47701 20 10C20 12.136 19.33 14.116 18.19 15.74L15 10H18C18.0001 8.43163 17.5392 6.89781 16.6747 5.58927C15.8101 4.28072 14.5799 3.25517 13.1372 2.64013C11.6944 2.0251 10.1027 1.84771 8.55996 2.13003C7.0172 2.41234 5.59145 3.14191 4.46 4.22801L3.463 2.43301ZM16.537 17.567C14.7225 19.1394 12.401 20.0034 10 20C4.477 20 0 15.523 0 10C0 7.86401 0.67 5.88401 1.81 4.26001L5 10H2C1.99987 11.5684 2.46075 13.1022 3.32534 14.4108C4.18992 15.7193 5.42007 16.7449 6.86282 17.3599C8.30557 17.9749 9.89729 18.1523 11.44 17.87C12.9828 17.5877 14.4085 16.8581 15.54 15.772L16.537 17.567Z" fill="white"/>
</svg>
)
export const IconGenerateFailed = ({style, className}: IconProps) => (
<svg className={`svg-icon ${className || ''} icon-generate-fail`} style={style} xmlns="http://www.w3.org/2000/svg"
width="1em" height="1em" viewBox="0 0 20 20" version="1.1">
<path d="M18 0H2C0.9 0 0.00999999 0.9 0.00999999 2L0 20L4 16H18C19.1 16 20 15.1 20 14V2C20 0.9 19.1 0 18 0ZM11 12H9V10H11V12ZM11 8H9V4H11V8Z" fill="#FFA800"/>
</svg>
)
export const IconRegenerate = ({style, className}: IconProps) => (
<svg className={`svg-icon ${className || ''} icon-regenerate`} style={style} xmlns="http://www.w3.org/2000/svg"
width="1em" height="1em" viewBox="0 0 24 24" version="1.1">
<path d="M20.4728 3.525C19.3618 2.4074 18.0406 1.52056 16.5851 0.915578C15.1297 0.310592 13.5688 -0.000577199 11.9925 8.03759e-07C5.35835 8.03759e-07 0 5.37 0 12C0 18.63 5.35835 24 11.9925 24C17.591 24 22.2589 20.175 23.5947 15H20.4728C19.8545 16.7543 18.7067 18.2736 17.1878 19.3483C15.6688 20.4229 13.8536 21.0001 11.9925 21C7.02439 21 2.98687 16.965 2.98687 12C2.98687 7.035 7.02439 3 11.9925 3C14.4841 3 16.7054 4.035 18.3265 5.67L13.4934 10.5H24V8.03759e-07L20.4728 3.525Z" fill="currentColor"/>
</svg>
)
export const IconPlay = ({style, className}: IconProps) => (
<svg className={`svg-icon ${className || ''} icon-delete`} style={style} xmlns="http://www.w3.org/2000/svg"
width="1em" height="1em" viewBox="0 0 1024 1024" version="1.1">
<path
@ -484,7 +243,7 @@ export const IconPlay = ({style,className}: { style?: React.CSSProperties;classN
</svg>
)
export const IconLive = ({style,className}: { style?: React.CSSProperties;className?:string; }) => (
export const IconLive = ({style, className}: IconProps) => (
<svg className={`svg-icon ${className || ''} icon-delete`} style={style} xmlns="http://www.w3.org/2000/svg"
width="1em" height="1em" viewBox="0 0 1024 1024" version="1.1">
<path
@ -493,11 +252,11 @@ export const IconLive = ({style,className}: { style?: React.CSSProperties;classN
</svg>
)
export const IconEdit = ({style,className}: { style?: React.CSSProperties;className?:string; }) => (
export const IconEdit = ({style, className}: IconProps) => (
<svg className={`svg-icon ${className || ''} icon-delete`} style={style} xmlns="http://www.w3.org/2000/svg"
width="1em" height="1em" viewBox="0 0 1024 1024" version="1.1">
width="1em" height="1em" viewBox="0 0 23 24" version="1.1">
<path
d="M804.6 689.8l64-64c10-10 27.4-3 27.4 11.4V928c0 53-43 96-96 96H96c-53 0-96-43-96-96V224c0-53 43-96 96-96h547c14.2 0 21.4 17.2 11.4 27.4l-64 64c-3 3-7 4.6-11.4 4.6H96v704h704V701c0-4.2 1.6-8.2 4.6-11.2z m313.2-403.6L592.6 811.4l-180.8 20c-52.4 5.8-97-38.4-91.2-91.2l20-180.8L865.8 34.2c45.8-45.8 119.8-45.8 165.4 0l86.4 86.4c45.8 45.8 45.8 120 0.2 165.6zM920.2 348L804 231.8 432.4 603.6l-14.6 130.6 130.6-14.6L920.2 348z m129.6-159.4l-86.4-86.4c-8.2-8.2-21.6-8.2-29.6 0L872 164l116.2 116.2 61.8-61.8c8-8.4 8-21.6-0.2-29.8z"
d="M12.2222 0.382812C12.5337 0.383158 12.8334 0.502443 13.0599 0.716294C13.2864 0.930145 13.4227 1.22242 13.441 1.53341C13.4592 1.84439 13.3581 2.15061 13.1581 2.3895C12.9582 2.62838 12.6746 2.78191 12.3652 2.8187L12.2222 2.82726H2.44444V19.9384H19.5556V10.1606C19.5559 9.84907 19.6752 9.54944 19.889 9.32292C20.1029 9.0964 20.3952 8.96008 20.7061 8.94182C21.0171 8.92357 21.3234 9.02475 21.5622 9.22469C21.8011 9.42463 21.9547 9.70825 21.9914 10.0176L22 10.1606V19.9384C22.0002 20.5551 21.7673 21.1491 21.3479 21.6013C20.9286 22.0535 20.3539 22.3305 19.7389 22.3767L19.5556 22.3828H2.44444C1.82774 22.383 1.23375 22.1501 0.781553 21.7308C0.329353 21.3114 0.0523642 20.7367 0.00611137 20.1217L1.2255e-07 19.9384V2.82726C-0.000195041 2.21055 0.232719 1.61656 0.652052 1.16436C1.07138 0.712165 1.64614 0.435177 2.26111 0.388924L2.44444 0.382812H12.2222ZM19.8526 0.802035C20.0725 0.582832 20.3676 0.455567 20.678 0.446089C20.9884 0.436611 21.2908 0.545631 21.5237 0.751005C21.7566 0.95638 21.9026 1.24271 21.932 1.55184C21.9615 1.86096 21.8722 2.16971 21.6822 2.41537L21.5808 2.53148L9.48078 14.6303C9.26083 14.8495 8.96569 14.9767 8.65531 14.9862C8.34493 14.9957 8.04257 14.8867 7.80966 14.6813C7.57675 14.4759 7.43074 14.1896 7.40129 13.8805C7.37184 13.5713 7.46116 13.2626 7.65111 13.0169L7.75256 12.902L19.8526 0.802035Z"
fill="currentColor"/>
</svg>
)

View File

@ -0,0 +1,32 @@
import useGlobalConfig from "@/hooks/useGlobalConfig.ts";
import {useTranslation} from "react-i18next";
import React from "react";
import {useSearchParams} from "react-router-dom";
function LanguageSwitcher() {
const {i18n} = useTranslation();
const [params] = useSearchParams();
const {globalConfig} = useGlobalConfig()
const handleChangeLang = async () => {
const key = i18n.language == 'zh-CN' ? 'en-US' : 'zh-CN'
await i18n.changeLanguage(key)
globalConfig.i18n = key
localStorage.setItem('ai-human-lang',key)
}
return (
(params.get('lang') == 'yes' || AppConfig.APP_LANG == 'multiple') ?
<div className="icon-language" onClick={handleChangeLang}>
<span className="hover:bg-gray-200">
<svg
className="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
width="1em" height="1em">
<path
d="M757.205333 473.173333c5.333333 0 10.453333 2.090667 14.250667 5.717334a19.029333 19.029333 0 0 1 5.888 13.738666v58.154667h141.184c11.093333 0 20.138667 8.704 20.138667 19.413333v232.704a19.797333 19.797333 0 0 1-20.138667 19.413334h-141.184v96.981333a19.754667 19.754667 0 0 1-20.138667 19.370667H716.8a20.565333 20.565333 0 0 1-14.250667-5.674667 19.029333 19.029333 0 0 1-5.888-13.696v-96.981333h-141.141333a20.565333 20.565333 0 0 1-14.250667-5.674667 19.029333 19.029333 0 0 1-5.930666-13.738667v-232.704c0-5.12 2.133333-10.112 5.930666-13.738666a20.565333 20.565333 0 0 1 14.250667-5.674667h141.141333v-58.154667c0-5.162667 2.133333-10.112 5.888-13.738666a20.565333 20.565333 0 0 1 14.250667-5.674667h40.362667zM192.597333 628.394667c22.272 0 40.32 17.365333 40.32 38.826666v38.741334c0 40.618667 32.512 74.368 74.624 77.397333l6.058667 0.213333h80.64c21.930667 0.469333 39.424 17.706667 39.424 38.784 0 21.077333-17.493333 38.314667-39.424 38.784H313.6c-89.088 0-161.28-69.461333-161.28-155.178666v-38.741334c0-21.461333 18.005333-38.826667 40.277333-38.826666z m504.106667 0h-80.64v116.394666h80.64v-116.394666z m161.28 0h-80.64v116.394666h80.64v-116.394666zM320.170667 85.333333c8.234667 0 15.658667 4.778667 18.773333 12.202667H338.773333l161.322667 387.84c2.517333 5.973333 1.706667 12.8-2.005333 18.090667a20.394667 20.394667 0 0 1-16.725334 8.533333h-43.52a20.181333 20.181333 0 0 1-18.688-12.202667L375.850667 395.648H210.901333l-43.264 104.149333A20.181333 20.181333 0 0 1 148.906667 512H105.514667a20.394667 20.394667 0 0 1-16.725334-8.533333 18.773333 18.773333 0 0 1-2.005333-18.090667l161.28-387.84A20.181333 20.181333 0 0 1 266.88 85.333333h53.290667zM716.8 162.901333c42.794667 0 83.84 16.341333 114.090667 45.44a152.234667 152.234667 0 0 1 47.232 109.738667v38.741333c-0.469333 21.077333-18.389333 37.930667-40.32 37.930667s-39.808-16.853333-40.32-37.930667v-38.741333c0-20.608-8.490667-40.32-23.637334-54.869333a82.304 82.304 0 0 0-57.045333-22.741334h-80.64c-21.888-0.469333-39.424-17.706667-39.424-38.784 0-21.077333 17.493333-38.314667 39.424-38.784h80.64z m-423.424 34.304L243.2 318.037333h100.48L293.418667 197.205333z"
fill="currentColor"/>
</svg>
</span>
</div> : null
)
}
export default LanguageSwitcher

View File

@ -1,31 +1,32 @@
import React from "react";
import useConfig from "@/hooks/useConfig.ts";
import {useTranslation} from "react-i18next";
const AppLogo = ({style}: { style?: React.CSSProperties, theme?: 'origin' | 'color' }) => {
return (
<svg style={style} viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<svg style={style} width="1em" height="0.95em" viewBox="0 0 37 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M671.584 201.142c22.034 0 39.896 17.398 39.896 38.858v163.2l207.98-141.828a20.4 20.4 0 0 1 20.666-1.322c6.602 3.35 10.732 10.002 10.732 17.252V746.7c0 7.25-4.13 13.902-10.732 17.252a20.4 20.4 0 0 1-20.668-1.32L711.48 620.8V784c0 21.46-17.86 38.858-39.896 38.858H113.04c-22.034 0-39.896-17.398-39.896-38.858V240c0-21.46 17.862-38.858 39.896-38.858h558.546z"
fill="#9C34FE"></path>
d="M28.9241 2.04763C29.2793 1.00838 30.7152 0.976901 31.1363 1.95321L31.1719 2.04823L31.6512 3.44974C31.761 3.77115 31.9385 4.06528 32.1716 4.31227C32.4048 4.55927 32.6882 4.75339 33.0028 4.88154L33.1316 4.92964L34.5332 5.40829C35.5724 5.76342 35.6039 7.19937 34.6282 7.62042L34.5332 7.65605L33.1316 8.1353C32.8101 8.24506 32.5159 8.42251 32.2688 8.65567C32.0217 8.88884 31.8275 9.1723 31.6993 9.48693L31.6512 9.6152L31.1725 11.0173C30.8174 12.0566 29.3814 12.088 28.961 11.1123L28.9241 11.0173L28.4455 9.61579C28.3357 9.29427 28.1583 9.00003 27.9251 8.75292C27.6919 8.50582 27.4085 8.31161 27.0939 8.1834L26.9656 8.1353L25.5641 7.65665C24.5242 7.30152 24.4928 5.86556 25.4691 5.44511L25.5641 5.40829L26.9656 4.92964C27.287 4.81981 27.5811 4.64233 27.8281 4.40916C28.0751 4.176 28.2692 3.89257 28.3974 3.57801L28.4455 3.44974L28.9241 2.04763ZM34.7992 1.07303e-07C34.9103 -1.40135e-07 35.0192 0.0311648 35.1135 0.089953C35.2077 0.148741 35.2836 0.232795 35.3325 0.332562L35.361 0.402044L35.5689 1.01135L36.1787 1.2192C36.2901 1.25702 36.3877 1.32705 36.4592 1.42041C36.5307 1.51376 36.5729 1.62624 36.5804 1.74359C36.5879 1.86094 36.5604 1.97787 36.5013 2.07957C36.4423 2.18128 36.3544 2.26317 36.2488 2.31487L36.1787 2.34337L35.5694 2.55123L35.3616 3.16112C35.3237 3.27243 35.2536 3.36998 35.1602 3.44142C35.0668 3.51286 34.9543 3.55496 34.837 3.56241C34.7197 3.56985 34.6027 3.54229 34.5011 3.48322C34.3994 3.42415 34.3176 3.33623 34.2659 3.2306L34.2374 3.16112L34.0296 2.55182L33.4197 2.34397C33.3083 2.30614 33.2107 2.23611 33.1392 2.14276C33.0677 2.0494 33.0256 1.93693 33.0181 1.81958C33.0105 1.70223 33.038 1.58529 33.0971 1.48359C33.1561 1.38189 33.244 1.3 33.3496 1.2483L33.4197 1.21979L34.029 1.01194L34.2368 0.402044C34.2769 0.284712 34.3526 0.182855 34.4535 0.110754C34.5543 0.0386538 34.6752 -7.43997e-05 34.7992 1.07303e-07Z"
fill="#9C34FE"/>
<path
d="M328.478 388.784c-7.584 0-14.122 5.196-15.64 12.434l-0.32 3.07v215.346c0 5.11 2.58 9.894 6.896 12.796a16.32 16.32 0 0 0 14.73 1.736l2.912-1.398 173.748-107.712c4.106-2.56 6.786-6.806 7.276-11.532 0.49-4.724-1.264-9.408-4.764-12.714l-2.512-1.944-173.748-107.712a16.28 16.28 0 0 0-8.578-2.332v-0.038z"
fill="#FFFFFF"></path>
d="M18.481 20.1202C19.2177 19.3857 19.8018 18.5128 20.2 17.5517C20.5981 16.5906 20.8023 15.5604 20.8009 14.5201C20.8009 10.1463 17.2548 6.6001 12.8809 6.6001C8.50712 6.6001 4.96094 10.1463 4.96094 14.5201C4.96094 16.7073 5.84732 18.6873 7.28084 20.1202"
stroke="black" strokeWidth="2" strokeLinecap="round"/>
<path d="M1 32.9999L1.66 29.6999L8.92 24.4199L12.88 28.3799L16.84 24.4199L24.1 29.6999L24.76 32.9999"
stroke="black"
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path
d="M5.61523 17.8246C5.62403 15.112 6.06557 13.1305 6.93985 11.88C8.25193 10.0049 9.17527 10.1429 9.84715 10.4346C10.519 10.7263 10.915 12.635 12.0377 13.1855C13.1597 13.7353 16.0333 13.8032 17.0174 14.4652C18.0008 15.1265 20.2514 16.3574 19.6904 18.4589"
stroke="black" strokeWidth="2"/>
</svg>
)
}
export const LogoText = ({style}: { style?: React.CSSProperties, theme?: 'origin' | 'color' }) => {
const {appName} = useConfig()
export const LogoText = ({style, className}: { style?: React.CSSProperties, className?: string }) => {
const {t} = useTranslation()
return (
<div className={'align-middle flex items-center h-full'}>
<svg style={style} viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="44" height="44">
<path
d="M671.584 201.142c22.034 0 39.896 17.398 39.896 38.858v163.2l207.98-141.828a20.4 20.4 0 0 1 20.666-1.322c6.602 3.35 10.732 10.002 10.732 17.252V746.7c0 7.25-4.13 13.902-10.732 17.252a20.4 20.4 0 0 1-20.668-1.32L711.48 620.8V784c0 21.46-17.86 38.858-39.896 38.858H113.04c-22.034 0-39.896-17.398-39.896-38.858V240c0-21.46 17.862-38.858 39.896-38.858h558.546z"
fill="#9C34FE"></path>
<path
d="M328.478 388.784c-7.584 0-14.122 5.196-15.64 12.434l-0.32 3.07v215.346c0 5.11 2.58 9.894 6.896 12.796a16.32 16.32 0 0 0 14.73 1.736l2.912-1.398 173.748-107.712c4.106-2.56 6.786-6.806 7.276-11.532 0.49-4.724-1.264-9.408-4.764-12.714l-2.512-1.944-173.748-107.712a16.28 16.28 0 0 0-8.578-2.332v-0.038z"
fill="#FFFFFF"></path>
</svg>
<span className={'ml-4 text-xl'}>{appName}</span>
<div className={`flex h-full ${className}`}>
<AppLogo style={style}/>
<span className={'ml-2 text-lg relative top-1'}>{t('AppTitle')}</span>
</div>
)
}

View File

@ -4,7 +4,6 @@ import React from "react";
export const UserAvatar = ({className, style}: { style?: React.CSSProperties; className?: string }) => {
return (
<span>
{/* <img src={Avatar} style={style} className={className}/> */}
<svg style={style} className={className} viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" width="128" height="128">
<path

View File

@ -12,7 +12,7 @@ export function showToast(content: string, type?: 'success' | 'info' | 'warning'
}
export function showErrorToast(e: Error | BizError) {
showToast(String(((e instanceof BizError) ? e.data : '') || e.message), 'error')
showToast(String(e.message), 'error')
}

View File

@ -0,0 +1,21 @@
import React from "react";
import {Popconfirm} from "antd";
import {useTranslation} from "react-i18next";
import ModalWarning from "@/components/icons/ModalWarning.tsx";
export function DeleteItemPopoverConfirm({children,description,onConfirm}: {
onConfirm: ((e?: React.MouseEvent<HTMLElement>) => void) | undefined
description?: React.ReactNode;
children?: React.ReactNode;
}){
const {t} = useTranslation()
return <Popconfirm
rootClassName={'popconfirm-main'}
placement={'left'}
arrow={false}
icon={<ModalWarning.Icon />}
title={<ModalWarning.Title />}
description={<div dangerouslySetInnerHTML={{__html:description || t('modal.delete_item_confirm')}}></div>}
onConfirm={onConfirm}
>{children}</Popconfirm>
}

View File

@ -0,0 +1,24 @@
import {ArrowUpOutlined} from "@ant-design/icons";
type ButtonToTopProps = {
onClick?: () => void;
visible?: boolean;
container?: HTMLElement | string;
}
export default function ButtonToTop(props: ButtonToTopProps) {
return (
<div className={'page-action-to-top'}>
{props.visible && <button className="btn-to-top text-white" onClick={()=>{
if(props.onClick){
props.onClick()
}else if(props.container){
const container = typeof(props.container) == 'string'? document.querySelector(props.container):props.container
if(container){
container.scrollTop = 0
}
}
}}><ArrowUpOutlined/></button>}
</div>
)
}

View File

@ -0,0 +1,79 @@
import React, {CSSProperties, useCallback, useEffect, useImperativeHandle, useRef} from "react";
import {useInViewport, useScroll} from "ahooks";
import { LoadingOutlined } from '@ant-design/icons';
import {Spin} from "antd";
import {t} from "i18next";
export type InfiniteScrollerRef = {
scrollToPosition: (top: number) => void
};
export type InfiniteScrollerProps = {
children?: React.ReactNode;
className?: string;
rootClassName?: string;
style?: CSSProperties;
loadingPlaceholder?: React.ReactNode;
onCallback?: (page: number, prevPage) => void;
onScroll?: (top: number) => void;
empty?: React.ReactNode;
loading?: boolean;
pagination?: {
page: number;
limit: number;
total: number;
};
}
const InfiniteScroller = React.forwardRef<InfiniteScrollerRef, InfiniteScrollerProps>((props, ref) => {
const {pagination} = props;
const [inView] = useInViewport(() => document.querySelector('.data-load-control-element'))
const scrollContainerRef = useRef<HTMLDivElement | null>(null)
const scrollPosition = useScroll(scrollContainerRef);
const scrollToPosition = useCallback((top: number) => {
if (scrollContainerRef.current) {
scrollContainerRef.current!.scrollTo({
top,
behavior: 'smooth'
})
}
}, [scrollContainerRef])
useImperativeHandle(ref, () => {
return {
scrollToPosition
}
})
useEffect(() => {
if (scrollPosition && props.onScroll) {
props.onScroll(scrollPosition.top)
}
}, [scrollPosition])
useEffect(() => {
if (!pagination) return;
if (inView && !props.loading && pagination.total > 0) {
const maxPage = Math.ceil((pagination.total || 0) / pagination.limit)
const currentPage = pagination.page
if (maxPage > currentPage) {
props.onCallback?.(currentPage + 1, currentPage)
}
}
}, [inView])
return (<div ref={scrollContainerRef} className={`data-list-container ${props.rootClassName}`} style={props.style}>
<Spin wrapperClassName="data-list-load-spin" spinning={props.loading} indicator={<LoadingOutlined style={{fontSize:30}} spin />}>
<div className={`data-list-container-inner ${props.className}`}>{props.children}</div>
{props.loading && <div style={{minHeight:'30vh'}}></div>}
{props?.pagination && props.pagination.total > props.pagination.limit * props.pagination.page && (props.loadingPlaceholder ||
<div className="data-load-control-element py-10 text-center">
<div className="loading-text">{t('loading')}</div>
</div>)}
{props?.empty && !props.loading && props.pagination?.total == 0 && <div className="flex justify-center text-center pt-20">
<div className="rounded-lg px-4 py-10">
{props.empty}
</div>
</div>}
</Spin>
</div>);
}) //(props: InfiniteScrollerProps) =>{}
export default InfiniteScroller

View File

@ -0,0 +1,135 @@
import React, {useEffect, useState} from "react";
// import TCPlayer from 'tcplayer.js';
import flvPlayer, { VideoPlayer } from '@/components/video/VideoPlayer.ts';
import {PlayerInstance} from "@/hooks/useCache.ts";
// import 'tcplayer.js/dist/tcplayer.min.css';
type State = {
playing: boolean
muted: boolean
end?: boolean
error?: boolean
fullscreen: boolean
progress: number
playedSeconds: number
duration: number
}
type StateUpdate = Partial<State> | ((prev: State) => Partial<State>)
type Props = {
url?: string; cover?: string; showControls?: boolean; className?: string;
poster?: string;
onChange?: (state: State) => void;
onProgress?: (current:number,duration:number) => void;
onPause?: () => void;
onPlay?: () => void;
muted?: boolean;
autoPlay?: boolean;
}
export type PlayerInstance = {
play: (url: string, currentTime: number) => void;
pause: () => void;
getState: () => State;
}
export const Mp4Player = React.forwardRef<PlayerInstance, Props>((props, ref) => {
const [tcPlayer, setTcPlayer] = useState<VideoPlayer | null>(null)
const [prevUrl, setPrevUrl] = useState<string | undefined>();
const [state, _setState] = useState<State>({
playing: false,
muted: false,
// 是否全屏
fullscreen: false,
progress: 0,
playedSeconds: 0,
duration: 0
})
const setState = (data: StateUpdate) => {
_setState(prev => {
const _state = typeof (data) === 'function' ? {...prev, ...data(prev)} : {...prev, ...data}
props.onChange?.(_state)
return _state
})
}
useEffect(()=>{
if(props.url && tcPlayer){
tcPlayer.src(props.url)
}
},[props.url, tcPlayer])
useEffect(() => {
const player = flvPlayer.newInstance({className:props.className});
document.querySelector('.video-player-container-inner')!.appendChild(player.getVideo())
// const player = TCPlayer(playerVideo, {
// controls: props.showControls,
// poster: props.poster,
// autoplay: typeof(props.autoPlay) != 'undefined' ? props.autoPlay : true,
// licenseUrl: AppConfig.TCPlayerLicense
// }
// ) as TCPlayerInstance;
player.on('pause', () => {
setState({playing: false, end: false, error: false})
props.onPause?.()
})
player.on('playing', () => {
setState({playing: true, end: false, error: false})
props.onPlay?.()
})
player.on('ended', () => {
setState({end: true, playing: false, error: false})
})
player.on('timeupdate', () => {
props.onProgress?.(player.currentTime(), player.duration())
})
player.on('error', () => {
setState({end: false, playing: false, error: true})
})
setTcPlayer(() => player)
return () => {
console.log('destroy video')
try{
// player.unload();
player.dispose();
setTcPlayer(() => null)
//Array.from(document.querySelectorAll('video')).forEach(v => v.pause())
}catch (e){
console.log(e)
}
//playerVideo.parentElement?.removeChild(playerVideo)
}
}, [])
React.useImperativeHandle(ref, () => {
return {
pause(){
if (!tcPlayer) return;
tcPlayer.pause()
},
play: (url, currentTime = 0) => {
// console.log('play', url, currentTime)
url = url.replace('.flv','.mp4');
if (!tcPlayer) return;
const player = tcPlayer
if (prevUrl == url) {
player.currentTime(0)
} else {
player.src(url)
}
player.play()
setPrevUrl(url)
if (currentTime > 0) {
player.currentTime(currentTime)
}
},
getState: () => state
}
})
return <div className={`video-player relative ${props.className} video-player-container-inner`}>
</div>
})

View File

@ -0,0 +1,165 @@
type VideoPlayerEvents =
'playing'
| 'pause'
| 'ended'
| 'timeupdate'
| 'error'
| 'canplay'
| 'durationchange'
| 'progress'
type PlayerOptions = {
enableLog?: boolean;
className?: string;
}
enum PlayState {
playing = 'playing',
pause = 'pause',
ended = 'ended',
error = 'error',
}
export class VideoPlayer {
private video?: HTMLVideoElement;
private State: PlayState = PlayState.pause;
private enable_log = true;
private currentDuration = 0;
newInstance(options?: PlayerOptions) {
const { className, enableLog=true } = options || {};
this.enable_log = enableLog || false;
if (this.video) {
this.video.remove();
}
if (this.video) {
this.video.pause();
}
// Create video element
const playerVideo = document.createElement('video');
const playerId = `player-container-${Date.now().toString(16)}`;
playerVideo.setAttribute('id', playerId);
playerVideo.setAttribute('preload', 'auto');
playerVideo.setAttribute('playsInline', 'true');
playerVideo.setAttribute('webkit-playsinline', 'true');
if (className) playerVideo.setAttribute('className', className);
playerVideo.classList.add('digital-video-player');
playerVideo.addEventListener('durationchange', (e) => {
this.currentDuration = Math.floor(this.video?.duration || 0);
this.log('video duration change',e)
});
playerVideo.addEventListener('error', (e) => {
this.log('video error:',e)
});
this.video = playerVideo;
return this;
}
/**
*
* @param args
*/
log(...args: any[]) {
if (this.enable_log) {
console.log(...args);
}
}
getVideo() {
return this.video!;
}
// 暂停视频
pause() {
this.video?.pause();
// 更新状态
this.State = PlayState.pause;
}
/**
*
* @param event
* @param callback
*/
on(event: VideoPlayerEvents, callback: () => void) {
this.video?.addEventListener(event, callback);
}
off(event: VideoPlayerEvents, callback: () => void) {
this.video?.removeEventListener(event, callback);
}
/**
*
* @param time
*/
currentTime(time?: number) {
if (!this.video) return 0;
if (typeof time === 'number' && this.video.currentTime !== time) {
try {
this.video.currentTime = time;
} catch (err) {
this.log('Failed to set currentTime', err);
}
}
return this.video.currentTime || 0;
}
src(url: string) {
if (!this.video) return;
this.video.src = url;
this.video.load();
}
/**
*
*/
play() {
if (!this.video) return;
this.video.play().then(() => {
this.State = PlayState.playing;
}).catch((err) => {
this.State = PlayState.error;
this.log('play error', err);
});
}
duration() {
return this.currentDuration || this.video?.duration || 0;
}
dispose(){
if(!this.video){
return;
}
// 暂停
this.pause();
// 移除
this.video.parentNode?.removeChild(this.video);
this.video = undefined;
}
}
/**
* player.on('pause', () => {
* setState({playing: false, end: false, error: false})
* })
* player.on('playing', () => {
* setState({playing: true, end: false, error: false})
* })
* player.on('ended', () => {
* setState({end: true, playing: false, error: false})
* })
* player.on('timeupdate', () => {
* props.onProgress?.(player.currentTime(), player.duration())
* })
* player.on('error', () => {
* setState({end: false, playing: false, error: true})
* })
*/
const videoPlayer = new VideoPlayer();
export default videoPlayer;

View File

@ -1,10 +1,10 @@
// import ReactPlayer from 'react-player'
// import {PauseOutlined, PlayCircleOutlined, FullscreenOutlined, FullscreenExitOutlined} from "@ant-design/icons"
// import {Progress} from "antd";
import React, {useEffect, useState} from "react";
import React, { useEffect, useState } from 'react';
import TCPlayer from 'tcplayer.js';
import 'tcplayer.js/dist/tcplayer.min.css';
import { PlayerInstance } from '@/hooks/useCache.ts';
import videoPlayer from '@/components/video/VideoPlayer.ts';
type State = {
playing: boolean
muted: boolean
@ -21,15 +21,19 @@ type Props = {
url?: string; cover?: string; showControls?: boolean; className?: string;
poster?: string;
onChange?: (state: State) => void;
onProgress?: (current: number, duration: number) => void;
muted?: boolean;
autoPlay?: boolean;
}
export type PlayerInstance = {
play: (url: string, currentTime: number) => void;
pause: () => void;
getState: () => State;
}
export const Player = React.forwardRef<PlayerInstance, Props>((props, ref) => {
const [tcPlayer, setTcPlayer] = useState<TCPlayer | null>(null)
const [prevUrl, setPrevUrl] = useState<string | undefined>();
const [tcPlayer, setTcPlayer] = useState<TCPlayerInstance | null>(null);
const [state, _setState] = useState<State>({
playing: false,
muted: false,
@ -38,83 +42,85 @@ export const Player = React.forwardRef<PlayerInstance, Props>((props, ref) => {
progress: 0,
playedSeconds: 0,
duration: 0
})
});
const setState = (data: StateUpdate) => {
console.log('playstate change', data)
console.log('playstate change', data);
_setState(prev => {
const _state = typeof (data) === 'function' ? {...prev, ...data(prev)} : {...prev, ...data}
props.onChange?.(_state)
return _state
})
}
const _state = typeof (data) === 'function' ? { ...prev, ...data(prev) } : { ...prev, ...data };
props.onChange?.(_state);
return _state;
});
};
useEffect(() => {
if (props.url && tcPlayer) {
tcPlayer.src(props.url)
tcPlayer.src(props.url);
}
},[props.url, tcPlayer])
}, [props.url, tcPlayer]);
useEffect(() => {
const playerVideo = document.createElement('video');
const playerId = `player-container-${Date.now().toString(16)}`;
playerVideo.setAttribute('id', playerId)
playerVideo.setAttribute('preload', 'auto')
playerVideo.setAttribute('playsInline', 'true')
playerVideo.setAttribute('webkit-playsinline', 'true')
if(props.className) playerVideo.setAttribute('className', props.className)
document.querySelector('.video-player-container-inner')!.appendChild(playerVideo)
const playerVideo = videoPlayer.newInstance().getVideo();
document.querySelector('.video-player-container-inner')!.appendChild(playerVideo);
const player = TCPlayer(playerId, {
const flvPlayer = TCPlayer(playerVideo, {
//sources: [{src: props.url}],
controls: props.showControls,
// muted:props.muted,
poster: props.poster,
autoplay: true,
autoplay: typeof (props.autoPlay) != 'undefined' ? props.autoPlay : true,
licenseUrl: 'https://license.vod2.myqcloud.com/license/v2/1328581896_1/v_cube.license'
}
)
player.on('pause', () => {
setState({playing: false, end: false, error: false})
})
player.on('playing', () => {
setState({playing: true, end: false, error: false})
})
player.on('ended', () => {
setState({end: true, playing: false, error: false})
})
player.on('error', () => {
setState({end: false, playing: false, error: true})
})
setTcPlayer(() => player)
);
flvPlayer.on('pause', () => {
setState({ playing: false, end: false, error: false });
});
flvPlayer.on('playing', () => {
setState({ playing: true, end: false, error: false });
});
flvPlayer.on('ended', () => {
setState({ end: true, playing: false, error: false });
});
flvPlayer.on('timeupdate', () => {
props.onProgress?.(flvPlayer.currentTime(), flvPlayer.duration());
});
flvPlayer.on('error', () => {
setState({ end: false, playing: false, error: true });
});
setTcPlayer(() => flvPlayer);
return () => {
if (tcPlayer) {
tcPlayer.pause()
tcPlayer.unload()
}
}
}, [])
tcPlayer?.pause();
console.log('destroy video');
tcPlayer?.dispose();
};
}, []);
React.useImperativeHandle(ref, () => {
return {
play: (url, currentTime = 0) => {
console.log('play', url, currentTime)
pause() {
if (!tcPlayer) return;
const player = tcPlayer
tcPlayer.pause();
},
play: (url, currentTime = 0) => {
console.log('play', url, currentTime);
if (!tcPlayer) return;
const player = tcPlayer;
if (prevUrl == url) {
player.currentTime(0)
player.currentTime(0);
} else {
player.src(url)
player.src(url);
}
player.play()
setPrevUrl(url)
player.play();
setPrevUrl(url);
if (currentTime > 0) {
player.currentTime(currentTime)
player.currentTime(currentTime);
}
},
getState: () => state
}
})
};
});
return <div className={`video-player relative ${props.className} video-player-container-inner`}>
</div>
})
</div>;
});

View File

@ -1,25 +1,43 @@
import {useSortable} from "@dnd-kit/sortable";
import {useSetState} from "ahooks";
import React, {useEffect} from "react";
import {clsx} from "clsx";
import {Popconfirm} from "antd";
import {CheckCircleFilled, MenuOutlined, MinusCircleFilled,LoadingOutlined} from "@ant-design/icons";
import {Checkbox, Popconfirm} from "antd";
import ImageCover from '@/assets/images/cover.png'
import {IconEdit, IconPlay} from "@/components/icons";
import {
IconDelete, IconDownloadOutline,
IconEdit,
IconGenerateFailed,
IconGenerating,
IconPlaying, IconRegenerate, IconRollbackCircle,
IconWarningCircle
} from "@/components/icons";
import {VideoStatus} from "@/service/api/video.ts";
import {formatTime} from "@/util/strings.ts";
import {useTranslation} from "react-i18next";
import {saveAs} from "file-saver";
import {DeleteItemPopoverConfirm} from "@/components/message/confirm.tsx";
import {showLoading, showToast} from "@/components/message.ts";
type Props = {
video: VideoInfo | LiveVideoInfo,
additionOperationBefore?: React.ReactNode;
additionOperationAfter?: React.ReactNode;
editable?: boolean;
downloadUrl?: string;
sortable?: boolean;
index?: number;
checked?: boolean;
active?: boolean;
playing?: boolean;
onCheckedChange?: (checked: boolean) => void;
onPlay?: () => void;
onEdit?: () => void;
onRemove?: () => void;
onRegenerate?: () => void;
hideCheckBox?: boolean;
onItemClick?: () => void;
onRemove?: (action?: 'delete' | 'rollback') => void;
removeIcon?: React.ReactNode;
id: number;
className?: string;
type?: 'live' | 'create'
@ -27,63 +45,149 @@ type Props = {
export const VideoListItem = (
{
id, video, onPlay, onRemove, checked,
onCheckedChange, onEdit, active, editable,
className, sortable,type
id, video, onRemove, removeIcon, checked, playing,
onCheckedChange, onEdit, active, editable, downloadUrl,
className, sortable, type, index, onItemClick,
additionOperationAfter, additionOperationBefore, onRegenerate, hideCheckBox
}: Props) => {
const {
attributes, listeners,
setNodeRef, transform
} = useSortable({resizeObserverConfig: {}, id})
const {t,i18n} = useTranslation()
const [state, setState] = useSetState<{ checked?: boolean }>({})
useEffect(() => {
setState({checked})
}, [checked])
const generating = (type == 'create' && video.status == VideoStatus.Generating)
const failed = (type == 'create' && (video.status != VideoStatus.Generating && video.status != VideoStatus.Generated))
const handleDownloadVideo = () => {
if (downloadUrl && video.status == VideoStatus.Generated) {
const ext = downloadUrl.substring(downloadUrl.lastIndexOf('.'))
const loading = showLoading(t('downloading'))
try{
saveAs(downloadUrl, `${video.title || video.video_title}${ext}`)
loading.close()
}catch (e){
loading.update(t('download_failed'),'error')
}
}
}
return <div
className={`video-item flex items-center gap-3 ${className}`}
ref={setNodeRef} style={{transform: `translateY(${transform?.y || 0}px)`,}}>
{/*{index && index > 0 && <div className="flex items-center px-2">*/}
{/* <div className="index-value w-[40px] h-[40px] flex items-center justify-center bg-gray-100 rounded-3xl">{index}</div>*/}
{/*</div>}*/}
className={`video-item ${className}`}
ref={setNodeRef} style={{transform: `translateY(${transform?.y || 0}px)`,}}
>
<div
className={`video-item-info relative flex gap-2 flex-1 bg-gray-100 h-[80px] overflow-hidden rounded-lg p-3 shadow-blue-500 ${active ? 'video-item-shadow' : ''}`}>
<div className={'video-title leading-7 flex-1'}>{video.title || video.video_title}</div>
<div className={'video-item-cover bg-white rounded-md overflow-hidden'}>
<img className="w-[100px] h-[56px] object-cover" src={video.cover || ImageCover} />
className={`list-row ${generating ? ' status-generating' : ''} ${failed ? 'status-generate-failed' : ''} ${active ? 'playing' : ''}`}>
<div
className="col number"
{...(sortable && !generating ? listeners : {})}
{...(sortable && !generating ? attributes : {})}
>{index}</div>
<div className="col cover cursor-pointer" onClick={onItemClick}>
<div className="relative">
<img className="w-[100px] h-[56px] object-cover border border-gray-200" src={video.cover || ImageCover}/>
{generating &&
<div
className={'absolute rounded inset-0 bg-black/40 backdrop-blur-[1px] text-white flex items-center justify-center'}>
<div className="text-center">
<IconGenerating className="inline-block text-xl"/>
<div className="text-xs">{t('video.generating')}</div>
</div>
</div>
}
{failed &&
<div
className={'absolute rounded inset-0 bg-black/40 backdrop-blur-[1px] text-white flex items-center justify-center'}>
<div className="text-center">
<IconGenerateFailed className="inline-block text-xl"/>
<div className="text-xs">{t('video.generate_failed')}</div>
</div>
</div>
}
{/* && active*/}
{!generating && !failed && playing && <div
className={'absolute rounded inset-0 backdrop-blur-[1px] bg-black/40 text-white flex items-center justify-center'}>
<div className="text-center">
<IconPlaying className="inline-block text-xl"/>
<div className="text-xs">{t('video.playing')}</div>
</div>
{type == 'create' && video.status == VideoStatus.Generating && <div className={'absolute inset-0 bg-black/30 text-white flex items-center justify-center'}>
<span className="ml-1"></span>
</div>}
</div>
<div className="operation flex items-center ml-2 gap-3 text-lg text-gray-400 w-[116px]">
{sortable && (!active ? <button className="hover:text-blue-500 cursor-move" {...attributes} {...listeners}>
<MenuOutlined/>
</button> : <button disabled className="cursor-not-allowed"><MenuOutlined/></button>)}
{onPlay &&<button className="hover:text-blue-500" onClick={onPlay} style={{fontSize: '1.3em'}}><IconPlay/></button>}
{editable && <>
{onEdit &&
<button className="hover:text-blue-500" onClick={onEdit} style={{fontSize: '1.1em'}}><IconEdit/>
</div>
<div
className="col title"
{...(sortable && !generating ? listeners : {})}
{...(sortable && !generating ? attributes : {})}
>
<div className="line-clamp-2">
{video.title || video.video_title}
</div>
</div>
<div
className="col generated-time"
{...(sortable && !generating ? listeners : {})}
{...(sortable && !generating ? attributes : {})}
>{video.ctime ? formatTime(video.ctime, 'min') : '-'}</div>
<div className="col operation">
{/*{sortable && !generating && (!active ?*/}
{/* <button className="hover:text-blue-500 cursor-move">*/}
{/* <MenuOutlined/>*/}
{/* </button> : <button disabled className="cursor-not-allowed"><MenuOutlined/></button>)}*/}
<div className={"flex items-center justify-center gap-5"}>
{downloadUrl && video.status == VideoStatus.Generated &&
<button className="hover:text-blue-500" onClick={e => {
e.preventDefault()
e.stopPropagation()
handleDownloadVideo?.()
}} style={{fontSize: '1.1em'}} title={i18n.language == 'zh-CN'?'下载':'Download'}>
<IconDownloadOutline/>
</button>}
<button className="hover:text-blue-300" onClick={() => {
{additionOperationBefore}
{editable && !generating && <>
{onEdit && <button
className="hover:text-blue-500" onClick={e => {
e.preventDefault()
e.stopPropagation()
onEdit?.()
}} style={{fontSize: '1.1em'}} title={i18n.language == 'zh-CN'?'修改':'Modify'}>
<IconEdit/>
</button>}
{onRegenerate && <button
className="text-red-400 hover:text-blue-500" onClick={e => {
e.preventDefault()
e.stopPropagation()
onRegenerate?.()
}} style={{fontSize: '1.1em'}} title={i18n.language == 'zh-CN'?'重新生成':'Regenerate'}>
<IconRegenerate/>
</button>}
{onRemove && !failed && <DeleteItemPopoverConfirm
description={failed ? t('video.rollback_confirm_title') : undefined}
onConfirm={() => onRemove(failed ? 'rollback' : 'delete')}>
<button className="hover:text-blue-500" title={
failed ? (i18n.language == 'zh-CN'?'重新生成':'Regenerate') : i18n.language == 'zh-CN'?'删除':'Delete'
} style={{fontSize:20}}>
{removeIcon ? removeIcon : (failed ?
<IconRollbackCircle/> :
<IconDelete/>)}
</button>
</DeleteItemPopoverConfirm>}
{hideCheckBox ? <></> :
<Checkbox checked={state.checked} onChange={() => {
if (onCheckedChange) {
onCheckedChange(!state.checked)
} else {
setState({checked: !state.checked})
}
}}><CheckCircleFilled className={clsx({'text-blue-500': state.checked})}/></button>
{onRemove && <Popconfirm
title={<div style={{minWidth: 150}}><span>?</span></div>}
onConfirm={onRemove}
okText="删除"
cancelText="取消"
>
<button className="hover:text-blue-500"><MinusCircleFilled/></button>
</Popconfirm>}
}}/>}
</>}
{additionOperationAfter}
</div>
</div>
</div>
</div>
}

View File

View File

@ -3,6 +3,12 @@ import React, {createContext, useEffect, useReducer} from "react";
import Loader from "@/components/loader";
import {getAuthToken, setAuthToken} from "@/hooks/useAuth.ts";
import {auth} from "@/service/api/user.ts";
import {getAllCategory} from "@/service/api/article.ts";
import {BizError} from "@/service/types.ts";
import {getRemainingDuration} from "@/service/api/order.ts";
import {Modal} from "antd";
import ModalWarning from "@/components/icons/ModalWarning.tsx";
import {useTranslation} from "react-i18next";
const UserRoleStorageKey = 'user-current-role';
@ -39,6 +45,10 @@ export const AuthProvider = ({children}: { children: React.ReactNode }) => {
if (token) {
const result = localStorage.getItem(AppConfig.AUTHED_PERSON_DATA_KEY)
if (result) {
// 验证用户token是否正确
try{
// 获取一个分类接口进行验证token是否有效
await getAllCategory();
const user = JSON.parse(result) as UserProfile
dispatch({
payload: {
@ -48,6 +58,22 @@ export const AuthProvider = ({children}: { children: React.ReactNode }) => {
token
}
})
} catch (e) {
const err = e as BizError;
if (err.code == 1001) {
// token失效
setAuthToken(null)
dispatch({
payload: {
isInitialized: true,
isLoggedIn: false,
user: null,
token: null
}
})
return 'token was invalid'
}
}
return 'initialized'
}
}

View File

@ -1,5 +1,5 @@
import useLocalStorage from "@/hooks/useLocalStorage";
import {createContext} from "react";
import React, {createContext} from "react";
const config: ConfigProps = {
fontFamily: `'Public Sans', sans-serif`,
@ -21,7 +21,7 @@ const initialState: CustomizationProps = {
const ConfigContext = createContext(initialState);
export const ConfigProvider = ({children}: { children: React.ReactNode }) => {
const [config, setConfig] = useLocalStorage('app-payment-config', initialState);
const [config, setConfig] = useLocalStorage('app-video-admin-config', initialState);
// 改变语言
const onChangeLocalization = (lang: I18n) => {
setConfig({

View File

@ -18,7 +18,7 @@ const clearAuth = () => {
localStorage.removeItem(AppConfig.AUTH_TOKEN_KEY);
localStorage.removeItem(AppConfig.AUTHED_PERSON_DATA_KEY);
}
export const setAuthToken = (token: string | null,profileData:UserProfile|null, expiry_time = -1) => {
export const setAuthToken = (token: string | null,profileData:UserProfile|null = null, expiry_time = -1) => {
if (!token) {
clearAuth();
return;

33
src/hooks/useCache.ts Normal file
View File

@ -0,0 +1,33 @@
import {create} from "zustand"
export const PlayerInstance: HTMLVideoElement[] = [];
export const defaultCache:{
firstLoadPath?: string
} = {};
type StoreInstance<T> = {
cache: T[];
clear: () => void;
set: (values: T[]) => void;
add: (value: T) => void;
remove: (value: T) => void
}
export const useIndexArrayCache = create<StoreInstance<Id>>((set) => ({
cache: [],
set: (values: Id[]) => {
set({cache: values})
},
clear: () => {
set({cache: []})
},
add: (id: Id) => {
set((state) => ({
cache: [...(state.cache || []), id]
}))
},
remove: (id: Id) => {
set((state) => ({
cache: (state.cache || []).filter((item) => item != id)
}))
}
}))

View File

@ -0,0 +1,13 @@
const globalConfig:{
i18n?:I18n
} = {
}
function useGlobalConfig(){
return {
globalConfig
}
}
export default useGlobalConfig;

View File

@ -0,0 +1,3 @@
function useInfiniteScroller<T>(fetch: (page: number) => Promise<T[]>, deps: any[]) {
}

15
src/i18n/config.ts Normal file
View File

@ -0,0 +1,15 @@
import i18next from 'i18next';
import {initReactI18next} from 'react-i18next';
import LangEN from './translations/en-US.json';
import LangCN from './translations/zh-CN.json';
console.log('AppConfig',AppMode,AppConfig)
i18next.use(initReactI18next).init({
debug: true,
lng: (AppConfig.APP_LANG || 'zh-CN').trim(),
fallbackLng: 'en-US',
resources: {
'en-US': {translation:LangEN},
'zh-CN': {translation:LangCN},
},
});

0
src/i18n/index.ts Normal file
View File

View File

@ -0,0 +1,232 @@
{
"AppTitle": "AI Livesteam",
"Hello": "Hello",
"cancel": "Cancel",
"close": "Close",
"confirm": {
"ok": "Confirm",
"push_title": "Push Notice",
"push_video": "Are you sure editing selected news?",
"title": "Notice"
},
"confirm_text": "Confirm",
"delete": "Delete",
"delete_batch": "Delete Select",
"delete_failed": "Delete failed",
"delete_success": "Delete success",
"download": "Download",
"download_fail": "Download Failed",
"downloading": "Downloading...",
"error_401": "You do not have permission to access this page",
"error_403": "You do not have permission to access this page",
"error_404": "Page not found",
"error_500": "Service exception, please contact customer support.",
"generating": {
"title": "Preview - Click the video to play"
},
"go_to_home": "Go to Homepage",
"history": {
"delete_confirm": "Are you sure you want to delete this video?",
"push_success": "Streaming success",
"search_key": "Please enter title keywords",
"text": "Recycle Bin"
},
"history.pushed": "Streaming: {{count}}",
"live": {
"duration": "Duration",
"edit_locked": "Locked",
"edit_unlock": "Unlock",
"play_first": "Play first video",
"playlist_count": "{{count}} videos in total",
"title": "Livestream"
},
"loading": "Loading...",
"login": {
"code_sending": "Sending...",
"invalid_username_or_pwd": "Invalid phone number or code",
"loading": "Login...",
"password": "Enter the verification code",
"send_sms_code": "Send code",
"text": "Sign in",
"title": "Sign in",
"username": "Please enter your phone number",
"welcome": "Welcome"
},
"message": {
"save_failed": "Save failed",
"save_success": "Save success"
},
"modal": {
"delete_item_confirm": "Are you sure you want to delete this item?",
"hot_news": {
"edit_auto": "Customize",
"edit_manual": "Customize",
"empty_notice_message": "Some live news items are incomplete. Please update them or disable the customization feature.",
"empty_notice_title": "Warning",
"title": "Live news"
},
"push_article": {
"action_all": "Still generating",
"action_cancel": "Cancel",
"action_skip": "Skip the news",
"content_error": "<span class=\"modal-count-normal\">{{count}}</span> news are selected, and <span class=\"modal-count-warning\">{{error_count}}</span> metahuman contents are too short in these news below. Do you want to transfer them to videos?",
"content_error_single": "<span class=\"modal-count-normal\">{{count}}</span> news is selected, and the metahuman content is too short in this news. Do you want to transfer it to a video?",
"content_normal": "<span class=\"modal-count-normal\">{{count}}</span> news are selected, Do you want to transfer them into videos?",
"content_normal_single": "<span class=\"modal-count-normal\">{{count}}</span> news is selected. Do you want to transfer it to a video?",
"empty_notice_title": "Warning",
"error_title": "Abnormal news"
},
"warning": "Warning"
},
"nav": {
"editing": "Editing",
"generating": "Generating",
"live": "Streaming",
"materials": "News"
},
"news": {
"delete_confirm": "Are you sure you want to delete this item?",
"delete_confirm_count": "Are you sure you want to delete these {{count}} items?",
"delete_description": "This item will be deleted.<br/>It can be recovered from the \"News\" page. ",
"delete_description_count": "These items will be deleted.<br/>They can be recovered from the \"News\" page. ",
"delete_empty": "Please select the items to delete",
"delete_the_picture": "Are you sure delete the picture?",
"download_empty": "Please select the news to download",
"download_failed": "Download failed!",
"edit": {
"bg": "Background",
"tag": "Tag",
"tag_length_error": "Video tag only limit 4 words ",
"tag_placeholder": "Example: Enterprise dynamics"
},
"edit_add_group": "Add Group",
"edit_delete_group": "Delete Group",
"edit_delete_group_confirm": "Are you sure you want to delete the group?",
"edit_digital_text": "Metahuman Material",
"edit_empty_group_content": "To generate a fully Metahuman video, ensure that no other media materials are included. For all other cases, both text and images must be provided in the media material section.",
"edit_empty_human_content": "Please enter meta human material",
"edit_form_search": "Please enter title keywords",
"edit_generate_again": "Regenerate",
"edit_generate_video": "Generating",
"edit_generate_video_again": "Regenerate",
"edit_notice_enter_article_content": "Please enter content",
"edit_notice_enter_article_title": "Please enter title",
"edit_notice_enter_article_title1": "Please enter news title",
"edit_notice_enter_text": "Please enter content",
"edit_notice_keep_1": "Keep at least one content block",
"edit_other_text": "Other media Material",
"edit_save_failed": "Save failed!",
"editing": "Editing",
"filter_all": "All",
"filter_source": "News source",
"generate_video": "Generating",
"get_detail": "Get news details",
"get_detail_error": "Get new details failed",
"image_count": "Images",
"materials": {
"add_group": "Add Group",
"title": "News Materials"
},
"news_all_source": "All",
"push_empty": "please select the news to edit",
"push_failed": "Failed to editing",
"push_stream_empty": "please select the news to streaming",
"push_stream_success": "Success",
"push_streaming": "Pushing...",
"push_success": "Push success",
"push_to_edit": "Editing",
"pushed": "Editing",
"save_text": "Save",
"search_key_title": "Please enter title keywords",
"source": "Source",
"title": "Title",
"title_image_count": "No. of images",
"title_operate": "",
"title_time": "Time stamp",
"title_word_count": "Word count",
"word_count": "Words"
},
"order": {
"left_time": "Remaining time",
"list": {
"consume_time": "Duration",
"cover": "Cover",
"id": "No.",
"operator": "User",
"order_time": "Time stamp",
"title": "Title"
},
"remaining_duration_warning": "Unable to generate videos due to insufficient remaining time",
"text": "Orders"
},
"page": {
"size_10": "10 per page",
"size_20": "20 per page",
"size_30": "30 per page",
"total_item": "{{total}} videos in total"
},
"recycle": {
"remove_forever": "Remove Forever",
"restore_video": "Restore"
},
"save_operation": "Save",
"select": {
"pushed": "Pushed: {{count}}",
"select_all": "Select all",
"selected": "Selected",
"selected_some": "Selected: {{count}}",
"text": "Select",
"total": "Total: {{count}}"
},
"service_error": "Service exception, please contact customer support.",
"time_filter": {
"all": "All",
"last_week": "Last week",
"past_24_hour": "Past 24 hour",
"past_30_min": "Past 30 min",
"past_4_hour": "Past 4 hour",
"past_hour": "Past 1 hour"
},
"title": "Title",
"upload": {
"delete_confirm": "Are you sure delete the picture?",
"upload_failed": "Upload failed",
"upload_file_type_error": "Only support upload image",
"upload_image": "Upload Image"
},
"user": {
"logout": "Logout"
},
"video": {
"delete_confirm": "The video will be deleted.<br/> It can be recovered from the \"News\" page. ",
"delete_confirm_count": "These videos will be deleted.<br/> They can be recovered from the \"News\" page. ",
"delete_confirm_title": "Are you sure you want to delete this video?",
"delete_description": "Are you sure you want to delete the video?",
"delete_description_count": "Are you sure you want to delete these {{count}} videos?",
"delete_empty": "Select the video you want to delete",
"delete_forever_confirm": "Do you want to permutely delete it?",
"delete_forever_confirm_count": "Do you want to permutely delete these videos?",
"download": "Download",
"generate_failed": "Generate Failed",
"generating": "Generating",
"live_rollback_confirm_title": "Are you sure you want to rollback this video?",
"playing": "Playing",
"push_confirm": "Are you sure you want to streaming these video?",
"push_empty": "Select the video you want to streaming",
"push_failed": "some video streaming failed!",
"push_success": "Streaming success,please goto \"Streaming\"!",
"push_to_live": "Streaming",
"restore_confirm": "Do you want to restore it to the generating page?",
"restore_confirm_count": "Do you want to restore these videos to the generating page?",
"rollback_confirm_title": "Are you sure you want to revert this video?",
"sort_modify_confirm": "Are you change video sequence?",
"sort_modify_failed": "Video sequence change failed",
"sort_modify_live_success": "Video sequence changed",
"sort_modify_rollback": "Exit and video sequence restored!",
"sort_modify_success": "Video sequence changed",
"title": "Title",
"title_generated_time": "Time stamp",
"title_operation": "",
"title_thumb": "Cover"
}
}

View File

@ -0,0 +1,232 @@
{
"AppTitle": "数字人直播",
"Hello": "你好",
"cancel": "取消",
"close": "关闭",
"confirm": {
"ok": "确定",
"push_title": "推流提示",
"push_video": "是否确定一键推流选中新闻视频?",
"title": "提示"
},
"confirm_text": "确定",
"delete": "删除",
"delete_batch": "批量删除",
"delete_failed": "删除失败",
"delete_success": "删除成功",
"download": "下载",
"download_fail": "下载失败",
"downloading": "下载中...",
"error_401": "您没有权限访问本页面",
"error_403": "您没有权限访问本页面",
"error_404": "访问的页面不存在",
"error_500": "服务异常,请咨询客服.",
"generating": {
"title": "预览视频 - 点击视频列表播放"
},
"go_to_home": "返回首页",
"history": {
"delete_confirm": "是否要删除该视频",
"push_success": "一键推流成功,已推流至数字人直播间,请查看!",
"search_key": "请输入视频标题关键字进行信息",
"text": "回收站"
},
"history.pushed": "已推送 {{count}} 条",
"live": {
"duration": "时长",
"edit_locked": "锁定状态不可排序",
"edit_unlock": "已解锁",
"play_first": "即将播放第一条视频",
"playlist_count": "当前播放列表共 {{count}} 条",
"title": "直播界面"
},
"loading": "加载中...",
"login": {
"code_sending": "发送中",
"invalid_username_or_pwd": "账号或密码错误",
"loading": "登录中...",
"password": "请输入验证码",
"send_sms_code": "获取验证码",
"text": "立即登录",
"title": "登录",
"username": "请输入账号",
"welcome": "欢迎登录"
},
"message": {
"save_failed": "保存失败",
"save_success": "保存成功"
},
"modal": {
"delete_item_confirm": "您确定要删除吗?",
"hot_news": {
"edit_auto": "自定义",
"edit_manual": "自定义",
"empty_notice_message": "自定义的“新闻热点”尚未填写完毕,<br/>请填写全部热点,或开启智能填充",
"empty_notice_title": "操作提示",
"title": "视频下方热点(跑马灯)"
},
"push_article": {
"action_all": "全部生成",
"action_cancel": "全部取消",
"action_skip": "跳过异常新闻",
"content_error": "已选中<span class=\"modal-count-normal\">{{count}}</span>条新闻,<span class=\"modal-count-warning\">{{error_count}}</span>条新闻数字人播报字数过少,是否生成全部<span class=\"modal-count-normal\">{{count}}</span>条视频?",
"content_error_single": "已选中<span class=\"modal-count-normal\">{{count}}</span>条新闻,<span class=\"modal-count-warning\">{{error_count}}</span>条新闻数字人播报字数过少,是否生成全部<span class=\"modal-count-normal\">{{count}}</span>条视频?",
"content_normal": "已选中<span class=\"modal-count-normal\">{{count}}</span>条新闻,是否全部生成?",
"content_normal_single": "已选中<span class=\"modal-count-normal\">{{count}}</span>条新闻,是否生成?",
"empty_notice_title": "操作提示",
"error_title": "异常新闻"
},
"warning": "操作提示"
},
"nav": {
"editing": "新闻编辑",
"generating": "视频生成",
"live": "数字人直播间",
"materials": "新闻素材"
},
"news": {
"delete_confirm": "您确定要删除吗?",
"delete_confirm_count": "你确定要删除选择的 {{count}} 条新闻吗?",
"delete_description": "删除后需从新闻素材中重新选择",
"delete_description_count": "删除后需从新闻素材中重新选择",
"delete_empty": "请选择要删除的新闻",
"delete_the_picture": "请确认删除此图片",
"download_empty": "请选择要下载的新闻",
"download_failed": "下载新闻失败,请重试!",
"edit": {
"bg": "背景",
"tag": "标签",
"tag_length_error": "标签长度不能超过4个字",
"tag_placeholder": "例:企业动态"
},
"edit_add_group": "新增分组",
"edit_delete_group": "删除此分组",
"edit_delete_group_confirm": "请确认删除此分组?",
"edit_digital_text": "数字人主播台编辑区",
"edit_empty_group_content": "如仅需数字人播报,请勿在素材融合区填写内容;如需展示图文信息,素材融合区的文本、图片均不得为空",
"edit_empty_human_content": "请先填写数字人播报内容",
"edit_form_search": "请输入新闻标题关键词进行搜索",
"edit_generate_again": "重新生成",
"edit_generate_video": "生成视频",
"edit_generate_video_again": "重新生成",
"edit_notice_enter_article_content": "请输入正文文本内容",
"edit_notice_enter_article_title": "请输入文章标题",
"edit_notice_enter_article_title1": "请输入标题内容",
"edit_notice_enter_text": "请先填写当前素材组",
"edit_notice_keep_1": "至少保留一个内容块",
"edit_other_text": "素材融合呈现编辑区",
"edit_save_failed": "保存失败,请重试!",
"editing": "新闻编辑",
"filter_all": "全部",
"filter_source": "新闻来源",
"generate_video": "生成视频",
"get_detail": "获取新闻详情",
"get_detail_error": "获取新闻详情失败",
"image_count": "图片数",
"materials": {
"add_group": "新增分组",
"title": "新闻素材"
},
"news_all_source": "全部来源",
"push_empty": "请选择要推入编辑的新闻",
"push_failed": "推送失败",
"push_stream_empty": "请选择要开播的新闻",
"push_stream_success": "推流成功",
"push_streaming": "推流中...",
"push_success": "推送成功",
"push_to_edit": "推入编辑",
"pushed": "已推送",
"save_text": "保存",
"search_key_title": "请输入新闻标题关键词进行搜索",
"source": "来源",
"title": "标题",
"title_image_count": "图片数",
"title_operate": "操作",
"title_time": "时间",
"title_word_count": "字数",
"word_count": "字数"
},
"order": {
"left_time": "当前剩余时长",
"list": {
"consume_time": "消费时长",
"cover": "缩略图",
"id": "订单编号",
"operator": "操作人",
"order_time": "下单时间",
"title": "标题"
},
"remaining_duration_warning": "视频生成剩余时长为零,将无法生成视频,请尽快充值额度。",
"text": "订单记录"
},
"page": {
"size_10": "10条/页",
"size_20": "20条/页",
"size_30": "30条/页",
"total_item": "共计{{total}}条"
},
"recycle": {
"remove_forever": "彻底删除",
"restore_video": "还原视频"
},
"save_operation": "保存操作",
"select": {
"pushed": "已推送: {{count}} 条",
"select_all": "全选",
"selected": "已选",
"selected_some": "已选 {{count}} 条",
"text": "选择",
"total": "总共 {{count}} 条"
},
"service_error": "新闻异常,无法生成,请咨询客服",
"time_filter": {
"all": "所有时间",
"last_week": "近一周",
"past_24_hour": "一天内",
"past_30_min": "半小时内",
"past_4_hour": "四小时内",
"past_hour": "一小时内"
},
"title": "标题",
"upload": {
"delete_confirm": "请确认删除此图片?",
"upload_failed": "上传图片失败,请重试",
"upload_file_type_error": "仅支持上传图片",
"upload_image": "上传图片"
},
"user": {
"logout": "退出登录"
},
"video": {
"delete_confirm": "删除后需重新生成视频",
"delete_confirm_count": "删除后需重新生成视频",
"delete_confirm_title": "你确定要删除此视频吗 ",
"delete_description": "已选择{{count}}条,确定要全部删除吗?",
"delete_description_count": "已选择{{count}}条,确定要全部删除吗?",
"delete_empty": "请选择要删除的视频",
"delete_forever_confirm": "是否彻底删除选中的视频? <br />这些视频将无法找回",
"delete_forever_confirm_count": "是否彻底删除选中的视频? <br />这些视频将无法找回!",
"download": "下载视频",
"generate_failed": "生成失败",
"generating": "生成中",
"live_rollback_confirm_title": "你确定要回退此视频吗 ",
"playing": "播放中",
"push_confirm": "是否确定一键推流选中新闻视频?",
"push_empty": "请选择要推流的新闻视频",
"push_failed": "选择视频中有部分视频还在生成中无法推送,推流成功视频前往数字人直播间页面查看!",
"push_success": "一键推流成功,已推流至数字人直播间,请前往数字人直播间页面查看!",
"push_to_live": "一键推流",
"restore_confirm": "是否将选中视频,还原到视频生成页?",
"restore_confirm_count": "是否将选中视频,还原到视频生成页",
"rollback_confirm_title": "您确定要回退此视频吗?",
"sort_modify_confirm": "是否采纳移动视频位置操作?",
"sort_modify_failed": "调整视频顺序失败,请重试!",
"sort_modify_live_success": "已完成直播队列的修改",
"sort_modify_rollback": "退出并恢复之前的直播队列!",
"sort_modify_success": "调整视频顺序成功",
"title": "标题",
"title_generated_time": "生成时间",
"title_operation": "操作",
"title_thumb": "缩略图"
}
}

View File

@ -1,5 +1,5 @@
import ReactDOM from 'react-dom/client'
import '@/i18n/config.ts'
import App from './App.tsx'
import '@/assets/index.scss'

View File

@ -1,56 +0,0 @@
import {Button, Form, Input, Select, Space} from "antd";
import {useSetState} from "ahooks";
import {PlayCircleOutlined} from "@ant-design/icons";
import {SearchListTimes} from "@/pages/news/components/news-source.ts";
type Props = {
onSearch?: (params: VideoSearchParams) => void;
onBtnStartClick?: () => Promise<void>;
loading?:boolean;
}
export default function SearchForm({onSearch, onBtnStartClick,loading}: Props) {
const [state, setState] = useSetState<{
pushing?: boolean;
}>({})
const onFinish = (values) => {
onSearch?.({
...values,
pagination: {page: 1, limit: 10}
})
//console.log(values)
}
return (<div className={'search-panel'}>
<div className="flex justify-between items-center">
<div className="search-form">
<Form<VideoSearchParams> className={""} layout="inline" onFinish={onFinish} initialValues={{title:'',time_flag:0}}>
<Form.Item name="title">
<Input className="w-[200px]" allowClear placeholder={'请输入搜索信息'}/>
</Form.Item>
<Form.Item label={'更新时间'} name="time_flag" className="w-[250px]">
<Select
options={SearchListTimes}
optionRender={(option) => (
<div className="flex items-center">
<span role="icon" className={`radio-icon`}></span>
<span role="listitem" aria-label={String(option.label)}>{option.label}</span>
</div>
)}
/>
</Form.Item>
<Form.Item>
<Space size={10}>
<Button loading={loading} type={'primary'} htmlType={'submit'}></Button>
</Space>
</Form.Item>
</Form>
</div>
<Space size={10}>
<Button
loading={state.pushing} type={'primary'}
onClick={onBtnStartClick} icon={<PlayCircleOutlined/>}
></Button>
</Space>
</div>
</div>)
}

View File

@ -1,94 +0,0 @@
import {Button, Input, Modal} from "antd";
import {saveAs} from "file-saver";
import {useEffect, useState} from "react";
import {useSetState} from "ahooks";
import {Player} from "@/components/video/player.tsx";
import ArticleGroup from "@/components/article/group";
import * as article from "@/service/api/article.ts";
import {push2room} from "@/service/api/video.ts";
import {showErrorToast, showToast} from "@/components/message.ts";
type Props = {
video?: VideoInfo;
onClose?: () => void
}
export default function VideoDetail({video, onClose}: Props) {
const [groups, setGroups] = useState<BlockContent[][]>([]);
const [state, setState] = useSetState({
exporting: false,
pushing: false,
})
// 将视频推送到数字人直播间
const pushToRoom = () => {
if (video) {
if (state.pushing) return
setState({pushing: true})
push2room([video.id]).then(() => {
showToast('一键推流成功,已推流至数字人直播间,请查看!', 'success')
}).catch(showErrorToast).finally(() => {
setState({pushing: false})
})
}
}
// 下载视频
const downloadVideo = () => {
if (video?.oss_video_url) {
const filename = video.oss_video_url.split('/').pop() || `${video.id}.flv`
saveAs(video.oss_video_url, filename)
}
}
useEffect(() => {
if (video) {
if (video.id > 0) {
article.getById(video.id).then(res => {
setGroups(res.content_group)
})
}
}
}, [video])
return (<>
<Modal open={!!video} title="新闻视频详情" width={1000} footer={null} onCancel={onClose}>
<div className="flex gap-2 my-5">
<div className="news-video w-[350px]">
<div className="video-container bg-gray-100 rounded overflow-hidden h-[640px]">
<Player url={video?.oss_video_url} poster={video?.cover} showControls={true}
className="w-[360px] h-[640px] bg-white"/>
</div>
<div className="video-info text-right text-sm text-gray-600 mt-3">
<span>创建时间: 5小时前</span>
</div>
</div>
<div className="detail flex-1 ml-5">
<div className="text-lg"></div>
<div className="article-title mt-5 items-center flex">
<span className="text text-base"></span>
<span className="ml-4 flex-1">
<Input value={video?.title}/>
</span>
</div>
<div className="aricle-body mt-3">
<div className="title">
<span className="text text-base"></span>
</div>
<div className="box mt-1">
<ArticleGroup groups={groups}/>
</div>
</div>
</div>
</div>
<div className="footer flex justify-between">
<div className="action flex gap-2">
<Button loading={state.pushing} type="primary" onClick={pushToRoom}></Button>
<Button onClick={downloadVideo}></Button>
</div>
<div className="close">
<Button onClick={onClose}></Button>
</div>
</div>
</Modal>
</>)
}

View File

@ -1,49 +0,0 @@
import {Checkbox, Image, Tag} from "antd";
import {IconDelete} from "@/components/icons";
import {useState} from "react";
import ImageCover from './cover.png'
import {formatDuration, timeFromNow} from "@/util/strings.ts";
import dayjs from "dayjs";
type VideoItemProps = {
videoInfo: VideoInfo;
onLive?: boolean;
onClick?: () => void;
onRemove?: () => void;
onCheckedChange?: (checked:boolean) => void;
}
export default function VideoItem(props: VideoItemProps) {
const [state, setState] = useState({
checked: false
})
const handleCheckedChange = (checked:boolean) => {
setState({checked})
if (props.onCheckedChange) {
props.onCheckedChange(checked)
}
}
return <div className={'video-item bg-gray-100 hover:drop-shadow-md rounded overflow-hidden relative group'}>
<div className={`controls absolute top-1 right-1 z-[2] p-1 rounded items-center gap-2 bg-white/80 ${state.checked?'flex':'hidden'} group-hover:flex`}>
<span onClick={props.onRemove} className={'cursor-pointer text-blue-500 text-2xl cursor-pointer'}><IconDelete /></span>
{!props.onLive && <Checkbox onChange={e=>handleCheckedChange(e.target.checked)} />}
</div>
<div className="cover" onClick={props.onClick}>
<img className={'w-full cursor-pointer h-[180px] object-cover'} src={props.videoInfo.cover}/>
</div>
<div className="text-sm py-2 px-3">
<div className="title my-1 cursor-pointer" onClick={props.onClick}>{props.videoInfo.title}</div>
<div className="info flex justify-between gap-2 text-sm">
<div className="video-time-info text-gray-500">
<span>: {formatDuration(Math.ceil(props.videoInfo.duration / 1000))}</span>
<span className="ml-1">{timeFromNow(props.videoInfo.publish_time)}</span>
</div>
{props.videoInfo.status == 3 && <div className="live-info">
<Tag color="processing" className="mr-0"></Tag>
</div>}
</div>
</div>
</div>
}

View File

@ -1,91 +0,0 @@
import {useState} from "react";
import {Empty, Modal, Pagination} from "antd";
import {useRequest} from "ahooks";
import VideoItem from "@/pages/library/components/video-item.tsx";
import SearchForm from "@/pages/library/components/search-form.tsx";
import VideoDetail from "@/pages/library/components/video-detail.tsx";
import {search} from "@/service/api/video.ts";
export default function LibraryIndex() {
const [modal, contextHolder] = Modal.useModal();
const [checkedIdArray, setCheckedIdArray] = useState<number[]>([])
const [params, setParams] = useState<VideoSearchParams>({
time_flag: 0,
pagination: {
page: 1,
limit: 10
}
})
const {data,loading} = useRequest(() => search(params), {
refreshDeps: [params]
})
const handleRemove = (video: VideoInfo) => {
modal.confirm({
title: '删除提示',
content: '是否要删除该视频',
onOk: () => {
console.log('OK', video);
}
})
}
const handleLive = async () => {
if (checkedIdArray.length == 0) return;
modal.confirm({
title: '推流提示',
content: '是否确定一键推流选中新闻视频?',
onOk: () => {
console.log('OK');
}
})
}
const [detailVideo, setDetailVideo] = useState<VideoInfo>()
return (<>
<div className={'container py-20'}>
{contextHolder}
<div className="search-form-container mb-5">
<SearchForm
onSearch={setParams}
onBtnStartClick={handleLive}
loading={loading}
/>
</div>
<div className="bg-white rounded p-5">
<div className={'video-list-container grid gap-5 grid-cols-4'}>
{data?.list?.map((it, idx) => (
<VideoItem
onLive={idx == 2} key={it.id}
videoInfo={it}
onRemove={() => handleRemove(it)}
onClick={() => setDetailVideo(it)}
onCheckedChange={(checked) => {
setCheckedIdArray(idArray => {
return checked ? idArray.concat(it.id) : idArray.filter(id => id != it.id);
})
}}
/>
))}
</div>
<div className="video-page-container flex justify-center mt-5">
{data?.pagination && data?.pagination.total > 0 ? <div className="flex justify-center mt-10">
<Pagination
current={params.pagination.page}
total={data?.pagination.total}
pageSize={data?.pagination.limit}
showSizeChanger={false}
simple={true}
rootClassName={'simple-pagination'}
onChange={(page) => setParams(prev=>({...prev,pagination: {page, limit: 10}}))}
/>
</div> : <div className="py-10">
<Empty />
</div>
}
{/*<Pagination defaultCurrent={1} total={50}/>*/}
</div>
</div>
</div>
{detailVideo && <VideoDetail video={detailVideo} onClose={() => setDetailVideo(undefined)}/>}
</>)
}

View File

@ -0,0 +1,18 @@
import {useMount} from "ahooks";
import React, {useState} from "react";
import {getLiveUrl} from "@/service/api/live.ts";
import {Player} from "@/components/video/player.tsx";
import './style.scss'
export default function LivePlayer() {
const [liveUrl, setLiveUrl] = useState<string>('http://fm.live.starbitech.com/fm/prod_fm.flv')
useMount(async ()=>{
getLiveUrl().then((ret)=>{
setLiveUrl(ret.flv_url)
})
})
return <div className="live-player-wrapper ">
<Player showControls url={liveUrl} className={'h-screen'} />
</div>
}

View File

@ -0,0 +1,9 @@
.live-player-wrapper {
@apply relative m-auto h-screen w-screen max-w-[480px] content-center relative;
.player-container{
@apply bg-gray-500 w-full h-screen ;
}
video {
height: 100vh !important;
}
}

View File

@ -1,41 +1,56 @@
import React, {useEffect, useMemo, useRef, useState} from "react";
import {Button, Modal} from "antd";
import React, {useCallback, useEffect, useMemo, useRef, useState} from "react";
import {Checkbox, Empty, Popconfirm, Space} from "antd";
import {SortableContext, arrayMove} from '@dnd-kit/sortable';
import {DndContext} from "@dnd-kit/core";
import FlvJs from "flv.js";
import {useTranslation} from "react-i18next";
import {useSetState} from "ahooks";
import {VideoListItem} from "@/components/video/video-list-item.tsx";
import {deleteByIds, getList, modifyOrder, playState} from "@/service/api/live.ts";
import {deleteByIds, getList, modifyOrder, playState, restoreByIds} from "@/service/api/live.ts";
import {showErrorToast, showToast} from "@/components/message.ts";
import ButtonBatch from "@/components/button-batch.tsx";
import FlvJs from "flv.js";
import {formatDuration} from "@/util/strings.ts";
import {useSetState} from "ahooks";
import {Player, PlayerInstance} from "@/components/video/player.tsx";
import {Mp4Player as Player, PlayerInstance} from "@/components/video/Mp4Player.tsx";
import {IconDelete, IconLocked, IconRollbackCircle} from "@/components/icons";
import InfiniteScroller, {InfiniteScrollerRef} from "@/components/scoller/infinite-scroller.tsx";
import ButtonToTop from "@/components/scoller/button-to-top.tsx";
import {ModalWarningIcon, ModalWarningTitle} from "@/components/icons/ModalWarning.tsx";
import styles from "./style.module.scss"
const cache: { flvPlayer?: FlvJs.Player, timerPlayNext?: any, timerLoadState?: any, prevUrl?: string } = {}
export default function LiveIndex() {
const videoRef = useRef<HTMLVideoElement | null>(null)
export default function LiveIndex() {
const {t} = useTranslation()
const player = useRef<PlayerInstance | null>(null)
const [videoData, setVideoData] = useState<LiveVideoInfo[]>([])
const [modal, contextHolder] = Modal.useModal()
const [checkedIdArray, setCheckedIdArray] = useState<number[]>([])
const [editable, setEditable] = useState<boolean>(false)
const scrollerRef = useRef<InfiniteScrollerRef | null>(null)
const [rollbackIds,setRollbackIds] = useState<Id[]>([])
const [delIds,setDelIds] = useState<Id[]>([])
const [state, setState] = useSetState({
activeIndex: -1,
playId:-1,
muted: true,
showToTop: false,
checkedAll: false,
originSort: '',
playProgress: 0,
loading:false
})
const activeIndex = useRef(state.activeIndex)
const activeIndex = useRef(-1)
useEffect(() => {
activeIndex.current = state.activeIndex
}, [state.activeIndex])
activeIndex.current = videoData.findIndex(s=>s.id == state.playId)
}, [state.playId,videoData])
const showVideoItem = (index: number) => {
// 显示当前播放视频对应 view item
const showVideoItem = (index: number,id: number) => {
// 找到对应video item 并显示在视图可见区域
const container = document.querySelector('.live-video-list-sort-container')
const item = document.querySelector(`.list-item-${index}`)
const item = document.querySelector(`.list-item-${id}`)
if (item && container) {
// 获取容器数据
const containerRect = container.getBoundingClientRect()
@ -50,25 +65,30 @@ export default function LiveIndex() {
})
}
}
const activeToNext = (index?: number) => {
// 播放下一个视频
const activeToNext = useCallback( (index?: number) => {
const endToFirst = index != undefined && index > -1 ? false : activeIndex.current >= videoData.length - 1
const _activeIndex = index != undefined && index > -1 ? index : (endToFirst ? 0 : activeIndex.current + 1)
setState({activeIndex: _activeIndex})
const playVideo = videoData[_activeIndex];
setState({playId: playVideo.id});
if (endToFirst) {
showToast('即将播放第一条视频');
showToast(t("live.play_first"));
}
// 找到对应video item 并显示在视图可见区域
showVideoItem(_activeIndex)
showVideoItem(_activeIndex,playVideo.id)
return _activeIndex;
}
}, [videoData, activeIndex])
// 播放视频
const playVideo = (video: LiveVideoInfo, liveState: LiveState) => {
if (player.current && video.video_oss_url) {
if (cache.timerPlayNext) clearTimeout(cache.timerPlayNext)
const duration = Math.ceil(video.video_duration / 1000)
// 计算已经播放时间
const playedTime = (Date.now() / 1000 >> 0) - liveState.live_start_time
if (playedTime < 0 || playedTime > duration) { // 已播放时间大于总时长了
//initPlayingState() // 重新获取播放状态
console.log('已播放时间大于总时长')
cache.timerLoadState = setTimeout(initPlayingState, 5000)
return;
}
player.current?.play(video.video_oss_url, playedTime)
@ -79,19 +99,24 @@ export default function LiveIndex() {
}
}
// 初始化播放状态
const initPlayingState = () => {
player.current?.pause();
if (cache.timerLoadState) clearTimeout(cache.timerLoadState)
if (videoData.length == 0) {
if (!videoData || videoData.length == 0) {
cache.timerLoadState = setTimeout(initPlayingState, 1000)
return;
}
playState().then(liveState => {
// 获取当前播放视频
const video = videoData.find(v => v.id === liveState.id)
if (video) {
// 开始播放
activeToNext(videoData.findIndex(v => v.id === liveState.id))
playVideo(video, liveState)
} else {
setState({activeIndex: -1})
setState({playId: -1})
cache.timerLoadState = setTimeout(initPlayingState, 5000)
}
});
@ -103,106 +128,197 @@ export default function LiveIndex() {
const loadList = () => {
clearAllTimer();
setState({loading: true})
getList().then(res => {
// console.log('origin list', res.list.map(s => s.id))
setVideoData(() => (res.list || []))
setState({
originSort: res.list ? res.list.map(s => s.id).join(',') : ''
})
setCheckedIdArray([])
}).catch(showErrorToast).finally(()=>{
setState({loading: false})
});
}
useEffect(initPlayingState, [videoData])
useEffect(() => {
loadList()
return clearAllTimer;
return ()=>{
clearAllTimer();
setTimeout(()=>{
console.log('pause all video')
try{
Array.from(document.querySelectorAll('video')).forEach(v => v.pause())
}catch (e){
console.log(e)
}
},20)
}
}, [])
// 删除视频
const processDeleteVideo = async (ids: Id[]) => {
deleteByIds(ids).then(() => {
showToast('删除成功!', 'success')
loadList()
}).catch(showErrorToast)
// 临时记录删除的id
setDelIds(_=>[...ids,..._])
// deleteByIds(ids).then(() => {
// showToast(t('delete_success'), 'success')
// loadList()
// }).catch(showErrorToast)
}
const handleConfirm = () => {
modal.confirm({
title: '提示',
content: '是否采纳移动视频位置操作?',
onOk: () => {
//showToast('编辑成功!!!', 'info');
modifyOrder(videoData.map(s => s.id)).then(() => {
setEditable(false)
loadList()
}).catch(() => {
showToast('调整视频顺序失败,请重试!')
})
// showToast('编辑成功!!!', 'info');
// console.log('origin list', videoData.map(s => s.id))
const resetState = (editable: boolean)=>{
setEditable(editable)
setCheckedIdArray([])
setRollbackIds(()=>[])
setDelIds(()=>[])
setState({checkedAll: false})
}
})
// 状态:锁定->解锁
const handleSetEditable = ()=>{
resetState(true)
}
const handleCancelConfirm = () => {
modal.confirm({
title: '提示',
content: '是否取消移动视频位置操作?',
onOk: () => {
showToast('退出并清除移动视频位置操作!', 'info');
//
const handleCancel = ()=>{
resetState(false)
}
const handleRollback = (v:LiveVideoInfo)=>{
setRollbackIds(_=>[v.id,..._])
}
const handleConfirm = async () => {
if (!editable) {
return;
}
const ids = videoData
.filter(s=>!(delIds.includes(s.id) || rollbackIds.includes(s.id)))
.map(s => s.id)
try{
// 删除
if(delIds.length > 0) {
await deleteByIds(delIds)
}
if(rollbackIds.length > 0) {
await restoreByIds(rollbackIds)
}
// 调整排序
await modifyOrder(ids);
showToast(t('message.save_success'), 'success')
}catch (e){
console.log(e)
showToast(t('message.save_failed'), 'error')
}finally {
loadList()
setEditable(false)
},
resetState(false)
}
}
const handleAllCheckedChange = () => {
if(!editable) return;
setCheckedIdArray(state.checkedAll ? [] : videoData.map(v => v.id))
setState({
checkedAll: !state.checkedAll
})
}
// 视频相关时长
const totalDuration = useMemo(() => {
if (!videoData || videoData.length == 0) return 0;
// 计算总时长
return videoData.reduce((sum, v) => sum + Math.ceil(v.video_duration / 1000), 0);
}, [videoData])
const currentVideoDuration = useMemo(()=>{
const video = videoData.find(s=>s.id == state.playId)
return (video?.video_duration || 0) / 1000;
},[state.playId, videoData])
// // 根据当前播放index计算已经播放时长
// const currentTotalDuration = useMemo(() => {
// if (state.activeIndex == -1 || !videoData || videoData.length == 0) return 0;
// // 计算总时长
// return videoData
// .filter((_, index) => (index < state.activeIndex))
// .reduce((sum, v) => sum + Math.ceil(v.video_duration / 1000), 0) + state.playProgress
// ;
// }, [state.activeIndex, state.playProgress, videoData])
//
// const currentSelectedId = useMemo(() => {
// if (state.activeIndex < 0 || state.activeIndex >= videoData.length) return [];
// const currentId = videoData[state.activeIndex];
// return checkedIdArray.filter(id => currentId.id != id)
// }, [checkedIdArray, state.activeIndex])
return (<div className="container py-10 page-live">
{contextHolder}
const currentSelectedVideoIds = useMemo(()=>{
return checkedIdArray.length == 0 ? [] : checkedIdArray.filter(id => id != state.playId)
},[checkedIdArray, state.playId])
return (<div className="container py-5 page-live">
<div className="h-[36px]"></div>
<div className="flex">
<div className="video-player-container mr-8 flex flex-col">
<div className="text-center text-base"></div>
<div className="video-player flex justify-center flex-1 mt-5">
<div className="video-player-container mr-16 flex items-center">
<div>
<div className="text-center text-base text-gray-400">{formatDuration(totalDuration)}</div>
<div className="video-player flex justify-center flex-1 mt-1">
<div className="live-player relative rounded overflow-hidden w-[360px] h-[636px]"
style={{backgroundColor: 'hsl(210, 100%, 48%)'}}>
<Player ref={player} className="w-[360px] h-[636px] bg-white" muted={true}/>
<Player
ref={player} className="w-[360px] h-[636px] bg-white"
muted={true}
onProgress={(progress) => {
setState({playProgress: progress})
}}
/>
</div>
</div>
<div className="mt-4 text-center text-sm">
<span>: {formatDuration(totalDuration)}</span>
<div className="text-center text-sm mt-4 text-gray-400">
<span>{t('live.duration')}: {formatDuration(state.playProgress)} / {formatDuration(currentVideoDuration)}</span>
</div>
</div>
<div className="video-list-container flex-1">
<div className=" bg-white py-8 px-6 rounded">
<div className="live-control flex justify-between mb-4">
{editable ? <>
<div className="flex gap-2">
<Button type="primary" onClick={handleConfirm}></Button>
<Button onClick={handleCancelConfirm}>退</Button>
</div>
</> : <div>
<Button type="primary" onClick={() => setEditable(true)}></Button>
</div>}
{!editable && <div>
<ButtonBatch
selected={checkedIdArray}
emptyMessage={`请选择要删除的新闻视频`}
confirmMessage={`是否删除当前的${checkedIdArray.length}个新闻视频?`}
onSuccess={loadList}
onProcess={processDeleteVideo}
></ButtonBatch>
</div>}
<div className="video-list-container video-list-sort-container flex flex-col flex-1 mt-2">
<div className="live-control flex justify-between mb-1 h-[30px]">
<div>
<Space>
{/*<span className={"text-blue-500"}>视频正在播放{state.activeIndex == -1 ? '' : `到 ${state.activeIndex + 1} 条`}</span>*/}
<span>{t('live.playlist_count',{count:videoData.length})}</span>
</Space>
</div>
<div className="flex items-center">
<div className={'flex items-center text-gray-400 cursor-pointer select-none'}>
{editable ? (<Space size={15}>
<button className={styles.btnDefault} onClick={handleCancel}>{t('cancel')}</button>
<button className={styles.btn} onClick={handleConfirm}>{t('save_operation')}</button>
</Space>):(<div className="flex items-center " onClick={handleSetEditable}>
{t('live.edit_locked')}
<span className="ml-2 text-sm"><IconLocked/></span>
</div>)}
</div>
<div className="check-all ml-10">
<button disabled={editable} className={`${editable?'':'hover:text-blue-300'} text-gray-400`}
onClick={handleAllCheckedChange}>
<span className="text-sm mr-2 whitespace-nowrap">{t('select.select_all')}</span>
{/*<CheckCircleFilled className={clsx({'text-blue-500': state.checkedAll})}/>*/}
</button>
<Checkbox disabled={!editable} checked={state.checkedAll} onChange={() => handleAllCheckedChange()}/>
</div>
</div>
</div>
<div className="list-header">
<div className="list-row header-row">
<div className="col number">No.</div>
<div className="col cover">{t('video.title_thumb')}</div>
<div className="col title">{t('video.title')}</div>
<div className="col generated-time">{t('video.title_generated_time')}</div>
<div className="col operation">{t('video.title_operation')}</div>
</div>
</div>
<div className="">
<div className="live-video-list-sort-container ">
<div className="flex">
<div className="sort-number-container mr-2">
{videoData.map((v, index) => (
<div key={index} className="flex items-center px-2 h-[80px] mt-3 mb-2">
<div
className="index-value w-[40px] h-[40px] flex items-center justify-center bg-gray-100 rounded-3xl">{index + 1}</div>
</div>
))}
</div>
<InfiniteScroller
ref={scrollerRef}
loading={state.loading}
onScroll={top => setState({showToTop: top > 30})}
onCallback={() => {
}}
>
{videoData.length == 0 && <div className="m-auto py-16"><Empty/></div>}
<div className="sort-list-container flex-1">
<DndContext onDragEnd={(e) => {
const {active, over} = e;
@ -216,31 +332,64 @@ export default function LiveIndex() {
}
}}>
<SortableContext items={videoData}>
{videoData.map((v, index) => (
{videoData.filter(v=>(!(delIds.includes(v.id) || rollbackIds.includes(v.id)))).map((v, index) => (
<VideoListItem
video={v}
index={index + 1}
id={v.id}
key={index}
active={state.activeIndex == index}
className={`list-item-${index} mt-3 mb-2`}
active={state.playId == v.id}
playing={state.playId == v.id}
className={`list-index-${index} list-item-${v.id} mt-3 mb-2`}
checked={checkedIdArray.includes(v.id)}
onCheckedChange={(checked) => {
setCheckedIdArray(idArray => {
return checked ? idArray.concat(v.id) : idArray.filter(id => id != v.id);
})
const newIdArray = checked ? checkedIdArray.concat(v.id) : checkedIdArray.filter(id => id != v.id);
setState({checkedAll: newIdArray.length == videoData.length})
setCheckedIdArray(newIdArray)
// setCheckedIdArray(idArray => {
// return checked ? idArray.concat(v.id) : idArray.filter(id => id != v.id);
// })
}}
onRemove={() => processDeleteVideo([v.id])}
editable={!editable}
sortable={editable}
editable={editable && state.playId != v.id}
sortable={editable && state.playId != v.id}
additionOperationBefore={<>
{editable && state.playId != v.id && <Popconfirm
rootClassName={'popconfirm-main'}
placement={'left'}
arrow={false}
icon={<ModalWarningIcon/>}
title={<ModalWarningTitle />}
description={t('video.live_rollback_confirm_title')}
onConfirm={() => handleRollback(v)}
><button className="hover:text-blue-500"><IconRollbackCircle /></button></Popconfirm>}
</>}
/>))}
</SortableContext>
</DndContext>
</div>
<div className="h-[100px]"></div>
</InfiniteScroller>
</div>
</div>
</div>
</div>
<div className="page-action">
<ButtonToTop visible={state.showToTop} onClick={() => scrollerRef.current?.scrollToPosition(0)}/>
{currentSelectedVideoIds.length > 0 && <ButtonBatch
className='bg-gray-300 hover:bg-gray-400 text-white'
selected={currentSelectedVideoIds}
emptyMessage={t('video.delete_empty')}
confirmMessage={currentSelectedVideoIds.length > 1?
t('video.delete_description_count',{count:currentSelectedVideoIds.length})
:
t('video.delete_description',{count:currentSelectedVideoIds.length})}
onSuccess={loadList}
onProcess={processDeleteVideo}
>
<span className={'text'}>{t('delete_batch')}</span>
<IconDelete/>
</ButtonBatch>}
</div>
</div>)
}

View File

@ -1,3 +1,26 @@
.videoListContainer{
}
@mixin btnDefault{
border-radius: 20px;
padding: 2px 16px;
height: auto;
}
.btn{
@include btnDefault;
background: #4096FF;
color:#fff;
border: 1px solid #4096FF;
&:hover{
background: #337acc;
}
}
.btnDefault{
@include btnDefault;
color:#00000099;
border: 1px solid #00000099;
&:hover{
background: #00000011;
}
}

View File

@ -1,5 +1,5 @@
import {Cascader} from "antd";
import React, {useEffect, useMemo} from "react";
import React, {useEffect} from "react";
const prevSelectValues: Id[][] = [];
@ -71,9 +71,6 @@ export default function ArticleCascader(props: {
// 清除上一次的选中值
prevSelectValues.length = 0;
}, [])
// const allOptionValue = useMemo(() => {
// return getAllValue(props.options)
// }, [props.options])
const setSelectValues = (value: Id[][]) => {
_setSelectValues(value)
@ -81,33 +78,6 @@ export default function ArticleCascader(props: {
props.onChange?.(value)
}
const handleChange = (values: Id[][]) => {
// const fullValues = buildValues(props.options, values)
// const diffValue = getValuesDiff(fullValues, prevSelectValues);
// const isIncrease = fullValues.length > prevSelectValues.length;
// prevSelectValues.length = 0;
//
// if(values.length == 0){
// setSelectValues([])
// return;
// }
// // 判断操作的是否是全部
// if(diffValue?.length == 1 && diffValue[0] == -1){
// if(isIncrease) prevSelectValues.push(...allOptionValue);
// setSelectValues(isIncrease ? [...allOptionValue] : [])
// return;
// }
// // if(fullValues.length != allOptionValue.length){
// // setSelectValues(fullValues.filter(s=>s.length == 1 && s[0] != -1))
// // }else{
// //
// // }
//
// if(fullValues.filter(s=>s.length > 1 || s[0] != -1).length == allOptionValue.length - 1){
// prevSelectValues.push(...allOptionValue);
// setSelectValues( [...allOptionValue])
// return;
// }
// prevSelectValues.push(...fullValues);
setSelectValues(values.filter(s=>s.length > 1 || s[0] != -1))
}

View File

@ -0,0 +1,55 @@
import {App} from "antd";
import {showToast} from "@/components/message.ts";
import React, {useState} from "react";
import {IconDelete, IconWarningCircle} from "@/components/icons";
import {deleteByIds} from "@/service/api/article.ts";
import {useTranslation} from "react-i18next";
import {divide} from "lodash";
import ModalWarning from "@/components/icons/ModalWarning.tsx";
export default function ButtonDeleteBatch(props: { ids: Id[];onSuccess?: () => void; }) {
const {t} = useTranslation()
const {modal} = App.useApp();
const [loading, setLoading] = useState(false)
const handlePush = () => {
setLoading(true)
deleteByIds(props.ids).then(() => {
props.onSuccess?.();
showToast(t('delete_success'), 'success')
}).catch(() => {
showToast(t('delete_failed'), 'error')
}).finally(() => {
setLoading(false)
})
}
const onPushClick = () => {
if(loading) return;
if (props.ids.length === 0) {
showToast(t('news.delete_empty'), 'warning')
return
}
modal.confirm({
wrapClassName:'root-modal-confirm',
icon: <ModalWarning.Icon />,
title: <ModalWarning.Title />,
content: <div>
<div>{t(props.ids.length == 1 ?'news.delete_confirm':'news.delete_confirm_count',{count:props.ids.length})}</div>
<div><span dangerouslySetInnerHTML={{__html:props.ids.length == 1 ?t('news.delete_description') :t('news.delete_description_count')}}></span></div>
</div>,
onOk: handlePush,
centered: true
})
}
return (
<div>
<button
disabled={loading}
onClick={onPushClick}
className='bg-gray-300 hover:bg-gray-400 text-white'
>
<span className={'text'}>{t('delete_batch')}</span>
<IconDelete className=""/>
</button>
</div>
)
}

View File

@ -6,6 +6,8 @@ import {useState} from "react";
import {getById} from "@/service/api/news.ts";
import {showToast} from "@/components/message.ts";
import {IconDownload} from "@/components/icons";
import {useTranslation} from "react-i18next";
/**
@ -58,18 +60,16 @@ async function downloadAsZip(list: NewsInfo[]) {
})
const content = await zip.generateAsync({type: "blob"});
saveAs(content, "news.zip");
// .then(function (content) {
//
// }).finally(() => {
// setLoading(false)
// });
}
export default function ButtonNewsDownload(props: { ids: Id[] }) {
const {t} = useTranslation()
const [loading, setLoading] = useState(false)
const onDownloadClick = async (ids: Id[]) => {
if(loading) return;
if (props.ids.length === 0) {
showToast('请选择要下载的新闻', 'warning')
showToast(t('news.download_empty'), 'warning')
return
}
setLoading(true)
@ -77,13 +77,19 @@ export default function ButtonNewsDownload(props: { ids: Id[] }) {
const list = await getAllNewsContent(ids)
await downloadAsZip(list)
} catch (e) {
showToast('下载新闻失败,请重试!', 'error')
showToast(t('news.download_failed'), 'error')
} finally {
setLoading(false)
}
}
return (
<Button loading={loading} onClick={() => onDownloadClick(props.ids)}></Button>
<button
disabled={loading}
className={'btn-action bg-[#eef5ff] text-gray-800 hover:bg-[#d2e3ff]'}
onClick={() => onDownloadClick(props.ids)}
>
<span className="text">{t('download')}</span>
<IconDownload />
</button>
)
}

View File

@ -1,36 +1,51 @@
import {Button, Modal} from "antd";
import {showToast} from "@/components/message.ts";
import {useState} from "react";
import {push2article} from "@/service/api/news.ts";
import {IconArrowRight} from "@/components/icons";
import {useNavigate} from "react-router-dom";
import {useIndexArrayCache} from "@/hooks/useCache.ts";
import {useTranslation} from "react-i18next";
export default function ButtonPushNews2Article(props: { ids: Id[] }) {
export default function ButtonPushNews2Article(props: { ids: Id[]; }) {
// const {modal} = App.useApp();
const {t}= useTranslation()
const [loading,setLoading] = useState(false)
const navigate = useNavigate();
const {set} = useIndexArrayCache();
const handlePush = () => {
setLoading(true)
push2article(props.ids).then(() => {
showToast('推送成功', 'success')
showToast(t('news.push_success'), 'success')
set([])
navigate('/edit')
}).catch(() => {
showToast('推送失败', 'error')
showToast(t('news.push_failed'), 'error')
}).finally(() => {
setLoading(false)
})
}
const onPushClick = () => {
if(loading) return;
if (props.ids.length === 0) {
showToast('请选择要推送的新闻', 'warning')
showToast(t('news.push_empty'), 'warning')
return
}
Modal.confirm({
title: '操作提示',
content: '是否确定推入素材编辑界面?',
onOk: handlePush
})
handlePush();
// modal.confirm({
// title: '操作提示',
// content: '是否确定推入素材编辑界面?',
// onOk: handlePush,
// centered: true
// })
}
return (
<Button
type={'primary'}
loading={loading}
<button
disabled={loading}
onClick={onPushClick}
></Button>
className='bg-[#4096ff] hover:bg-blue-600 text-white'
>
<span className={'text'}>{t('news.push_to_edit')}</span>
<IconArrowRight className={'text-white'} />
</button>
)
}

View File

@ -1,32 +1,180 @@
import {Button, Modal} from "antd";
import React, {useState} from "react";
import {showErrorToast, showToast} from "@/components/message.ts";
import {Modal, Space, Button} from "antd";
import {useNavigate} from "react-router-dom";
import {useTranslation} from "react-i18next";
import {useSetState} from "ahooks";
import {showToast} from "@/components/message.ts";
import {push2video} from "@/service/api/article.ts";
import {IconArrowRight} from "@/components/icons";
import ModalWarning from "@/components/icons/ModalWarning.tsx";
export enum ProcessResult {
All,
Skip,
Cancel
}
export default function ButtonPush2Video(props: { ids: Id[];onSuccess?:()=>void; }) {
type PushVideoProps = {
ids: Id[];
articles?: ListArticleItem[];
onResult?: (result: ProcessResult, errorIds: Id[]) => void;
}
export default function ButtonPush2Video(props: PushVideoProps) {
const [loading, setLoading] = useState(false)
const handlePush = () => {
// const {modal} = App.useApp()
const [state, setState] = useSetState<{
modalVisible?: boolean;
errorTitle?: string[];
errorIds: Id[]
}>({
errorIds: []
})
const {t} = useTranslation()
const navigate = useNavigate()
/**
*
* @deprecated
*/
// const checkHotNewsValid = async ()=>{
// return new Promise<string>((resolve)=>{
// const manualErrorCount = props.articles?.filter(s=>{
// return s.hot_news.replace(/,/ig,'').trim().length == 0
// })?.length || 0
// if(manualErrorCount == 0) {
// resolve('default')
// return;
// }
// modal.confirm({
// wrapClassName: 'root-modal-confirm',
// icon: <span className="anticon anticon-exclamation-circle"><IconWarningCircle/></span>,
// title: t('modal.push_article.empty_notice_title'),
// content: t('modal.push_article.empty_notice_message'),
// centered:true,
// onOk: () => {
// resolve('auto')
// },
// onCancel: () => {
// resolve('reject')
// }
// })
// })
// }
const handlePush = async (action: ProcessResult) => {
const skip = action === ProcessResult.Skip && state.errorIds.length > 0
const ids = !skip ? props.ids : props.ids.filter(id => !state.errorIds.includes(id));
if (skip && (state.errorIds.length == props.ids.length || ids.length == 0)) {
setState({modalVisible: false})
return;
}
//
// const result = await checkHotNewsValid();
// // TODO: 有热点新闻自动?
// if(result == 'reject'){ // 有热点新闻未填写 但点击取消并终止后续操作
// return;
// }
setLoading(true)
push2video(props.ids).then(() => {
showToast('一键推流成功,已成功推入数字人视频生成,请前往数字人视频生成页面查看!', 'success')
props.onSuccess?.()
}).catch(showErrorToast).finally(() => {
push2video(ids).then(() => {
setState({modalVisible: false})
if (skip) {
props.onResult?.(ProcessResult.Skip, state.errorIds || [])
return;
}
showToast(t('news.push_stream_success'), 'success')
navigate('/create?state=push-success', {
state: 'push-success'
})
// props.onSuccess?.()
}).catch(() => {
showToast(t('service_error'), 'error')
//showErrorToast
}).finally(() => {
setLoading(false)
})
}
// double check 25-02-15 https://pu7y37y121.feishu.cn/docx/FwRrddAFWotRZlxgbr5cP7b6nud
// 1.normal 数字人播报部分有内容不少于50字或者数字人播报部分无内容
// 2.error 数字人播报部分有内容但是少于50字
const checkArticleContent = () => {
const errors: string[] = [], ids: Id[] = [];
props.articles?.filter(s => {
return props.ids.includes(s.id) && (s.metahuman_text && s.metahuman_text.length < 50)
}).forEach(s => {
errors.push(s.title)
ids.push(s.id)
})
return {errors, ids}
}
const onPushClick = () => {
if (loading) return;
if (props.ids.length === 0) {
showToast('请选择要开播的新闻', 'warning')
showToast(t('news.push_stream_empty'), 'warning')
return
}
Modal.confirm({
title: '操作提示',
content: '是否确定一键开播选中新闻?',
onOk: handlePush
})
// check article content
const result = checkArticleContent()
setState({modalVisible: true, errorTitle: result.errors, errorIds: result.ids})
}
return (
<Button type="primary" loading={loading} onClick={onPushClick}></Button>
<div>
<button
className='bg-[#4096ff] hover:bg-blue-600 text-white'
onClick={onPushClick}
>
<span className={'text'}>{loading ? t('news.push_streaming') : t('news.generate_video')}</span>
<IconArrowRight className={'text-white'}/>
</button>
<Modal
open={state.modalVisible}
centered
closeIcon={false}
footer={null}
width={440}
>
<div className="modal-title flex items-center">
<div className="anticon anticon-exclamation-circle text-red-400 w-10"><ModalWarning.Icon/></div>
<div className="text-base">{t('modal.warning')}</div>
</div>
<div className="confirm-message-wrapper flex mt-2">
<div className="min-w-10"></div>
<div>
<div className="message text-gray-600" dangerouslySetInnerHTML={{
__html: t(
state.errorTitle && state.errorTitle.length > 0
? (props.ids.length == 1 ? 'modal.push_article.content_error_single' : 'modal.push_article.content_error')
: (props.ids.length == 1 ? 'modal.push_article.content_normal_single' : 'modal.push_article.content_normal'),
{count: props.ids.length, error_count: state.errorTitle?.length})
}}>
</div>
{state.errorTitle && state.errorTitle.length > 0 &&
<div className="error-list text-red-400 mt-6 w-[350px]">
<div className="title">{t('modal.push_article.error_title')}:</div>
<div className="max-h-[100px] overflow-auto" style={{lineHeight: '20px'}}>
{state.errorTitle.map((s, idx) => <div key={idx}
className="error-item overflow-hidden pr-1 text-nowrap overflow-ellipsis">{s}</div>)}
</div>
</div>}
</div>
</div>
<div className="flex justify-end mt-6">
<Space>
<Button disabled={loading} onClick={() => {
setState({modalVisible: false})
}}>{t('modal.push_article.action_cancel')}</Button>
{state.errorIds?.length > 0 && (
<Button
disabled={loading} type="primary"
onClick={() => handlePush(ProcessResult.Skip)}
>{t('modal.push_article.action_skip')}</Button>
)}
<Button
disabled={loading} type={state.errorIds.length == 0 ? 'primary' : 'default'}
onClick={() => handlePush(ProcessResult.All)}>{t('modal.push_article.action_all')}</Button>
</Space>
</div>
</Modal>
</div>
)
}

View File

@ -1,21 +1,30 @@
import {Button, Input} from "antd";
import {Input} from "antd";
import {SearchOutlined} from "@ant-design/icons";
import ArticleCascader from "@/pages/news/components/article-cascader.tsx";
import React, {useState} from "react";
import React, {useEffect, useState} from "react";
import {useSetState} from "ahooks";
import useArticleTags from "@/hooks/useArticleTags.ts";
import TagSelect from "@/components/form/tag-select.tsx";
import {useTranslation} from "react-i18next";
export default function EditSearchForm(props: {
onSubmit: (values: ApiArticleSearchParams) => void;
defaultParams?: Partial<ApiArticleSearchParams>;
}) {
const {t} = useTranslation()
const articleTags = useArticleTags()
const [tags, setTags] = useState<Id[][]>([]);
const [tags, _setTags] = useState<Id[][]>([]);
const [prevSearchName, setPrevSearchName] = useState<string>(props.defaultParams?.title || '')
const [params, setParams] = useSetState<ApiArticleSearchParams>({
pagination: {limit: 10, page: 1},
title: props.defaultParams?.title || ''
});
const handleSubmit = () => {
params.tags = tags.length == 0 ? undefined : tags.map(it => {
const handleSubmit = (_tags?: Id[][], from?: 'input') => {
if (from == 'input' && (params.title == prevSearchName || (!params.title && !prevSearchName))) return
params.title = prevSearchName;
setParams({title: prevSearchName})
const __tags = _tags || tags;
params.tags = __tags.length == 0 ? undefined : __tags.map(it => {
if (Array.isArray(it)) {
return {
level1: it[0],
@ -35,23 +44,44 @@ export default function EditSearchForm(props: {
}
})
}
useEffect(() => {
const {defaultParams} = props;
if (!defaultParams) {
return;
}
const tags: Id[][] = []
if (defaultParams.tags) {
defaultParams.tags.forEach(it => {
tags.push([it.level1, it.level2])
})
_setTags(tags)
}
}, [articleTags])
const setTags = (_tags: Id[][]) => {
console.log(_tags)
_setTags(_tags)
handleSubmit(_tags)
}
return (
<div className="search-form-input flex gap-2 items-center">
<Input
onChange={(e) => {
setParams({title: e.target.value})
}}
allowClear
type="text" className="rounded px-3 w-[250px]"
suffix={<SearchOutlined/>}
placeholder="请输入你先搜索的关键词"
value={prevSearchName}
onChange={e => setPrevSearchName(e.target.value)}
type="text" className="rounded-3xl px-3 w-[270px]"
prefix={<SearchOutlined/>}
placeholder={t('news.edit_form_search')}
onPressEnter={() => handleSubmit(undefined, 'input')}
onBlur={() => handleSubmit(undefined, 'input')}
/>
<span className="ml-5 text-sm"></span>
<ArticleCascader
options={articleTags}
onChange={setTags}
/>
<Button type="primary" onClick={handleSubmit}></Button>
{/*<span className="ml-5 text-sm">来源</span>*/}
{/*<ArticleCascader*/}
{/* options={articleTags}*/}
{/* onChange={setTags}*/}
{/*/>*/}
<TagSelect defaultSelectTags={tags} onChange={setTags} options={articleTags}/>
</div>
)
}

View File

View File

@ -1,115 +1,259 @@
import {Button, Input, Select} from "antd";
import {useSetState} from "ahooks";
import {useState} from "react";
import {Input} from "antd";
import {useLocalStorageState, useSetState, useClickAway} from "ahooks";
import React, {useCallback, useEffect, useMemo, useRef, useState} from "react";
import {clsx} from "clsx";
import useArticleTags from "@/hooks/useArticleTags.ts";
import {SearchListTimes} from "@/pages/news/components/news-source.ts";
import {UpOutlined, MenuOutlined, SearchOutlined} from "@ant-design/icons";
import TimeSelect from "@/components/form/time-select.tsx";
import styles from './style.module.scss'
import {IconPin} from "@/components/icons";
import {useTranslation} from "react-i18next";
type SearchPanelProps = {
rootClassName?: string;
onSearch?: (params: ApiArticleSearchParams) => void;
defaultParams?: Partial<ApiArticleSearchParams>;
hideNewsSource?: boolean;
rightRender?: React.ReactNode;
}
const pagination = {
limit: 10, page: 1
limit: 12, page: 1
}
const DEFAULT_STATE = {
tag_level_1_id: -1,
tag_level_2_id: -1,
subOptions: []
}
export default function SearchPanel({onSearch}: SearchPanelProps) {
export default function SearchPanel(
{
onSearch,
defaultParams,
hideNewsSource,
rightRender,
rootClassName
}: SearchPanelProps) {
const tags = useArticleTags();
const {t} = useTranslation()
const [params, setParams] = useSetState<ApiArticleSearchParams>({
pagination
pagination,
time_flag: 1,
...(defaultParams || {})
});
const [prevSearchName, setPrevSearchName] = useState<string>(defaultParams?.title || '')
const [state, setState] = useSetState<{
tag_level_1_id: number;
tag_level_2_id: number;
subOptions: (string | number)[]
}>({...DEFAULT_STATE})
}>({
...DEFAULT_STATE,
...(defaultParams && defaultParams.tag_level_1_id ? {tag_level_1_id: defaultParams.tag_level_1_id} : {}),
...(defaultParams && defaultParams.tag_level_2_id ? {tag_level_2_id: defaultParams.tag_level_2_id} : {})
})
useEffect(() => {
if (!defaultParams) {
return;
}
const _state = {
tag_level_1_id: -1,
tag_level_2_id: -1,
}
if (defaultParams.tag_level_1_id) {
_state.tag_level_1_id = defaultParams.tag_level_1_id
if (tags && tags.length > 0) {
const tag = tags.find(s => s.value == defaultParams.tag_level_1_id)
setSubOptions(tag?.children || [])
}
}
if (defaultParams.tag_level_2_id) {
_state.tag_level_2_id = defaultParams.tag_level_2_id
}
setState(_state)
}, [tags])
const [pinnedTag, setPinnedTag] = useLocalStorageState<number[]>(
'user-pinned-tag-list',
{
defaultValue: [],
},
);
// 二级分类
const [subOptions, setSubOptions] = useState<OptionItem[]>([])
const onFinish = () => {
if (params.title == prevSearchName || (!params.title && !prevSearchName)) return
params.title = prevSearchName;
setParams({title: prevSearchName})
onSearch?.({
...params,
title: prevSearchName,
tag_level_1_id: state.tag_level_1_id > 0 ? state.tag_level_1_id : undefined,
tag_level_2_id: state.tag_level_2_id > 0 ? state.tag_level_2_id : undefined,
pagination
})
}
// 重置
const onReset = () => {
setParams({pagination, title: ''})
setState({...DEFAULT_STATE})
setSubOptions([])
onSearch?.({pagination})
const handleTimeFilter = (time_flag: number) => {
const searchParams = {
...params,
time_flag,
pagination
}
setParams(searchParams)
onSearch?.(searchParams)
}
const handleFilter = (_params: Partial<ApiArticleSearchParams>) => {
const searchParams = {
...params,
..._params,
pagination
}
setParams(searchParams)
setState({
...state,
tag_level_1_id: _params.tag_level_1_id || -1,
tag_level_2_id: _params.tag_level_2_id || -1,
})
onSearch?.(searchParams)
}
const pinnedList = useMemo(() => {
if (tags?.length > 0) {
const pinnedList = pinnedTag && pinnedTag?.length > 0 ? pinnedTag : tags.map(s => s.value)
return pinnedList.filter(it => tags.findIndex(s => s.value == it) != -1)
.sort((a, b) => Number(a) - Number(b))
.map(it => (tags.find(s => s.value == it) as OptionItem))
}
return [] as OptionItem[];
}, [pinnedTag, tags])
const pinnedManagePanel = useRef<HTMLDivElement | null>(null)
return (<div className={'search-panel'}>
const togglePinnedManagePanel = useCallback((visible: boolean) => {
if (!pinnedManagePanel.current) {
return;
}
const _target = pinnedManagePanel.current!;
if (visible) {
_target.style.height = 'auto'
const {height} = _target.getBoundingClientRect()
_target.style.height = '38px'
requestAnimationFrame(() => {
_target.style.height = `${height}px`
})
} else {
requestAnimationFrame(() => {
_target.style.height = '0'
})
}
}, [pinnedManagePanel])
const setTrue = () => togglePinnedManagePanel(true)
const setFalse = () => togglePinnedManagePanel(false)
useClickAway(() => setFalse(), pinnedManagePanel)
return (<div className={`${styles.searchPanel} ${rootClassName??'pt-6 pb-2'}`}>
<div className="flex justify-between items-center">
<div className="search-form flex items-center gap-4">
<Input
value={params.title}
onChange={e => setParams({title: e.target.value})}
className="w-[240px]"
placeholder={'请输入新闻标题开始查找新闻'}
value={prevSearchName}
onChange={e => setPrevSearchName(e.target.value)}
className="w-[270px] rounded-3xl"
placeholder={t('news.search_key_title')}
onPressEnter={onFinish}
onBlur={onFinish}
prefix={<SearchOutlined/>}
/>
<div className={'flex items-center ml-2'}>
<span className="text-sm whitespace-nowrap mr-1"></span>
<Select
className="w-[150px]"
value={params.time_flag || 0}
onChange={value => setParams({time_flag: value})}
options={SearchListTimes}
optionRender={(option) => (
<div className="flex items-center">
<span role="icon" className={`radio-icon`}></span>
<span role="listitem" aria-label={String(option.label)}>{option.label}</span>
</div>
)}
<TimeSelect
className="w-[140px] ml-1"
value={typeof (params.time_flag) != "undefined" ? params.time_flag : 1}
onChange={handleTimeFilter}
/>
</div>
<Button type={'primary'} onClick={onFinish}></Button>
<Button onClick={onReset}></Button>
{rightRender && <div className="right-placeholder">{rightRender}</div>}
</div>
</div>
<div className="filter-container flex items-start mt-5">
<div className="list-container flex-1">
<div className="news-source-lv-1 flex flex-wrap">
{!hideNewsSource && <div className="filter-container mt-5">
<div className="list-container relative">
<div className="justify-between flex items-start border-b pb-2 overflow-hidden">
<div className="pinned-tag-list flex flex-wrap flex-1 min-w-0">
<div
className={`filter-item whitespace-nowrap px-2 py-1 mt-1 text-sm mr-1 cursor-pointer rounded ${state.tag_level_1_id == -1 ? 'bg-blue-500 text-white' : 'hover:bg-gray-100'}`}
className={`filter-item whitespace-nowrap px-2 py-1 mt-1 text-sm mr-1 cursor-pointer rounded ${state.tag_level_1_id == -1 ? 'selected' : ''}`}
onClick={() => {
setState({...DEFAULT_STATE})
handleFilter({tag_level_1_id: -1, tag_level_2_id: -1})
setSubOptions([])
}}></div>
{
tags.filter(s=>s.value !== 999999).map(it => (
<div
className={`filter-item whitespace-nowrap px-2 py-1 mt-1 text-sm mr-1 cursor-pointer rounded ${state.tag_level_1_id == it.value ? 'bg-blue-500 text-white' : 'hover:bg-gray-100'}`}
}}>{t('news.filter_all')}
</div>
{pinnedList.filter(s => (Number(s.value) !== 999999)).map(it => (
<span
className={`filter-item whitespace-nowrap px-2 py-1 mt-1 text-sm mr-1 cursor-pointer rounded ${state.tag_level_1_id == it.value ? 'selected' : ''}`}
key={it.value}
onClick={() => {
setState({tag_level_1_id: Number(it.value),tag_level_2_id:-1})
handleFilter({tag_level_1_id: Number(it.value), tag_level_2_id: -1})
setSubOptions(it.children || [])
}}>{it.label}</div>)
)
}}>{it.label}</span>)
)}
</div>
<div className="pinned-menu mt-2">
<span className={'cursor-pointer block hover:text-blue-500'} onClick={e => {
e.stopPropagation();
e.preventDefault();
setTrue();
}}>
<MenuOutlined style={{fontSize: 20}}/>
</span>
</div>
</div>
<div ref={pinnedManagePanel} className={clsx(styles.pinnedManagePanelContainer)}>
{/* 固定新闻来源 */}
<div className={clsx(styles.pinnedManagePanel)}>
<div className="header flex justify-between">
<div className="title font-bold">{t('news.filter_source')}</div>
<div className={'cursor-pointer block hover:text-blue-500'} onClick={setFalse}>
<UpOutlined style={{fontSize: 20}}/>
</div>
</div>
<div className="tags-list-container">
{
tags.filter(s => s.value !== 999999).map(it => {
const currentPinned = pinnedTag?.includes(Number(it.value));
return (<div
className={`filter-item border flex items-center px-2 py-1 mt-1 text-sm mr-1 cursor-pointer rounded ${currentPinned ? 'bg-gray-100' : ''} hover:border-gray-400`}
key={it.value}
onClick={() => {
const value = Number(it.value)
if (pinnedTag && pinnedTag.includes(value)) {
setPinnedTag(pinnedTag.filter(s => s != value))
} else {
setPinnedTag([...(pinnedTag || []), value])
}
}}>
<span>{it.label}</span>
{currentPinned &&
<span className={'ml-2'}><IconPin/></span>}
</div>)
})
}
</div>
{state.tag_level_1_id != -1 && subOptions.length > 0 && <div className="news-source-lv-2 bg-gray-100 p-2 rounded mt-2 flex flex-wrap">
</div>
</div>
{/* 二级目录 */}
{state.tag_level_1_id != -1 && subOptions.length > 0 &&
<div
className="absolute news-source-lv-2 flex items-center absolute left-0 right-0">
{
subOptions.map(it => (
<div
className={`filter-item whitespace-nowrap px-2 py-1 mt-1 text-sm mr-1 cursor-pointer rounded ${state.tag_level_2_id == it.value ? 'bg-blue-500 text-white' : 'hover:bg-gray-100'}`}
className={`filter-item whitespace-nowrap px-2 py-1 mt-1 text-sm mr-1 cursor-pointer rounded ${state.tag_level_2_id == it.value ? 'text-black' : ' text-gray-400 hover:text-gray-600'}`}
key={it.value}
onClick={() => {
setState({tag_level_2_id: Number(it.value)})
handleFilter({tag_level_1_id: state.tag_level_1_id, tag_level_2_id: Number(it.value)})
}}>{it.label}</div>)
)
}
</div>}
</div>
</div>
</div>}
</div>)
}

View File

@ -0,0 +1,131 @@
.searchPanel {
:global {
.filter-item {
@apply relative mr-5;
&:after {
@apply absolute bottom-1 left-0 w-full;
height: 6px;
content: ' ';
}
&.selected:after {
background: linear-gradient(to top, rgba(28, 122, 255,1),rgba(28, 122, 255,0));
}
}
}
}
.pinnedManagePanelContainer {
@apply absolute bg-white top-0 rounded shadow-md z-10;
height: 0;
overflow: hidden;
transition: height 0.2s ease-in-out;
inset-inline: -20px;
}
.pinnedManagePanel {
@apply px-4 pt-2 pb-4 grid;
:global {
.btn-panel {
@apply cursor-pointer;
}
.tags-list-container {
@apply flex flex-wrap gap-2 mt-2;
}
}
}
.newListTable{
:global{
.row{
@apply bg-white mt-2 py-2 px-4 rounded-xl gap-2 border;
border-width: 2px;
&.checked{
@apply border-primary-blue bg-primary-blue-bg;
}
}
.col{
@apply flex items-center justify-center relative pl-6 text-sm;
height: 54px;
&:after{
@apply absolute;
border-right: solid 1px #e8e8e8;
content: ' ';
top:2px;
bottom: 2px;
left:0;
}
}
.cover{
@apply pl-2;
}
.title{
@apply flex-1 pl-0;
&:after{
display: none;
}
}
.source{
width: 180px;
}
.count-picture{
width: 160px;
text-align: center;
}
.count-words{
width: 120px;
text-align: center;
}
.time{
width: 150px;
}
.operations{
@apply gap-4;
width: 100px;
}
.header{
@apply bg-primary-bg;
.col{
@apply text-base;
height: 42px;
}
.operations{
}
}
.body{}
.icon-btn{
@apply text-gray-400 hover:text-blue-500 cursor-pointer;
font-size: 18px;
}
}
}
.orderDataList{
:global {
.title{
text-align: center;
}
.id{
@apply pl-0;
width: 140px;
line-height: 1.2em;
&:after{
display: none;
}
}
.cover{
width: 140px;
//img{
// max-width: 100px;
// max-height: 56px;
//}
}
.title {
@apply flex-1 pl-4;
min-width: 100px;
&:after {
display: block;
}
}
}
}

View File

@ -1,103 +1,184 @@
import {Button, Pagination, Table, TableColumnsType, TableProps, Typography} from "antd";
import {Checkbox, Space} from "antd";
import {Card} from "@/components/card";
import React, {useState} from "react";
import React, {useRef, useState} from "react";
import {useRequest} from "ahooks";
import {formatTime} from "@/util/strings.ts";
import ArticleEditModal from "@/components/article/edit-modal.tsx";
import {getList} from "@/service/api/article.ts";
import {deleteByIds, getList} from "@/service/api/article.ts";
import EditSearchForm from "@/pages/news/components/edit-search-form.tsx";
import ButtonPush2Video from "@/pages/news/components/button-push2video.tsx";
import {Key} from "antd/es/table/interface";
import ButtonPush2Video, {ProcessResult} from "@/pages/news/components/button-push2video.tsx";
import styles from './components/style.module.scss'
import InfiniteScroller, {InfiniteScrollerRef} from "@/components/scoller/infinite-scroller.tsx";
import {IconDelete, IconEdit} from "@/components/icons";
import {clsx} from "clsx";
import ButtonToTop from "@/components/scoller/button-to-top.tsx";
import ButtonDeleteBatch from "@/pages/news/components/button-delete-batch.tsx";
import {showErrorToast, showToast} from "@/components/message.ts";
import {useTranslation} from "react-i18next";
import {DeleteItemPopoverConfirm} from "@/components/message/confirm.tsx";
const FilterCache: Partial<ApiArticleSearchParams> = {
tags: [],
}
export default function NewEdit() {
const {t} = useTranslation()
const [state, setState] = useState<{
checkAll?: boolean;
showToTop?: boolean;
}>({})
const [editId, setEditId] = useState(-1)
const [selectedRowKeys, setSelectedRowKeys] = useState<Id[]>([])
const [params, setParams] = useState<ApiArticleSearchParams>({
pagination: {page: 1, limit: 10}
pagination: {page: 1, limit: 10},
...FilterCache
})
const {data, refresh} = useRequest(() => getList(params), {refreshDeps: [params]})
const columns: TableColumnsType<ListArticleItem> = [
{
title: '标题',
minWidth: 300,
dataIndex: 'title',
},
{
title: '内容',
dataIndex: 'summary',
render: (value) => (<Typography.Paragraph style={{marginBottom: 0}} ellipsis={{
rows: 2, expandable: true, symbol: 'More'
}}>{value}</Typography.Paragraph>)
},
{
title: '来源',
minWidth: 150,
dataIndex: 'media_name',
},
{
title: '时间',
width: 150,
dataIndex: 'time',
render: (_, record) => {
return formatTime(record.publish_time, 'YYYY-MM-DD HH:mm')
const [data, setData] = useState<DataList<ListArticleItem>>()
const {refresh, loading,refreshAsync} = useRequest(() => getList(params), {
refreshDeps: [params],
onSuccess: (data) => {
FilterCache.title = params.title;
FilterCache.tags = params.tags;
setSelectedRowKeys(()=>([]))
setState({checkAll: false})
setData(prev => {
// 判断页码是否是第1页
if (data.pagination.page == 1) return data;
return {
list: [...(prev?.list || []), ...(data?.list || [])],
pagination: data.pagination
}
},
{
title: '操作',
width: 80,
align: 'center',
render: (_, record) => (<Button type="link" onClick={() => {
setEditId(record.id)
}}></Button>),
},
];
})
}
})
const rowSelection: TableProps<ListArticleItem>['rowSelection'] = {
onChange: (selectedRowKeys: Key[]) => {
setSelectedRowKeys(selectedRowKeys as Id[])
},
};
const handleItemChecked = (checked: boolean, item: ListArticleItem) => {
if (checked) {
setSelectedRowKeys(prev => [...prev, item.id])
} else {
setSelectedRowKeys(prev => prev.filter(it => it != item.id))
}
}
const handleCheckAll = (checked: boolean) => {
setState({checkAll: checked})
if(!data?.list) return;
if (checked) {
setSelectedRowKeys(data?.list?.map(item => item.id) || [])
} else {
setSelectedRowKeys([])
}
}
const scrollerRef = useRef<InfiniteScrollerRef | null>(null)
const handleDelete = (id) => {
deleteByIds([id]).then(() => {
refresh()
showToast(t('delete_success'), 'success')
}).catch(showErrorToast)
}
const handlePushProcessResult = (result: ProcessResult,errorIds: Id[])=>{
if(result == ProcessResult.Skip){
// 跳过, 加载最新并选择异常id
refreshAsync().then(()=>{
setSelectedRowKeys(errorIds)
})
}
}
return (<div className="container pb-5 news-edit">
<Card className="search-panel-container my-5">
<div className="search-form flex gap-5 justify-between">
<EditSearchForm onSubmit={setParams}/>
<Button type="primary" onClick={() => setEditId(0)}></Button>
<div className="search-panel-container my-5">
<div className="search-form flex pt-1 gap-5 justify-between">
<EditSearchForm defaultParams={params} onSubmit={setParams}/>
{/*<Button type="primary" onClick={() => setEditId(0)}>手动新增</Button>*/}
</div>
<div className="news-list-container mt-5">
<Table<ListArticleItem>
rowSelection={{type: 'checkbox', ...rowSelection}}
columns={columns}
dataSource={data?.list || []}
rowKey={'id'}
bordered
pagination={false}
<div className="news-list-container mt-2">
<div className="controls flex justify-end mb-3 gap-5">
<Space size={20}>
<span>{t('select.total',{count:data?.list?.length || 0})}</span>
<span className={'text-blue-500'}>{t('select.selected_some',{count:selectedRowKeys.length})}</span>
</Space>
<div>
<span className={'inline-block cursor-pointer mr-2'} onClick={() => {
handleCheckAll(!state.checkAll)
}}>{t('select.select_all')}</span>
<Checkbox
checked={state.checkAll && (!data?.list || selectedRowKeys.length == data?.list?.length)}
onChange={e => {
handleCheckAll(e.target.checked)
}}
/>
{data?.pagination && data?.pagination.total > 0 && <div className="footer flex justify-between items-center mt-5">
<Pagination
current={params.pagination.page}
total={data?.pagination.total}
pageSize={10}
showSizeChanger={false}
simple={true}
rootClassName={'simple-pagination'}
onChange={(page) => setParams(prev => ({
</div>
</div>
<div className={styles.newListTable}>
<div className="header row flex">
<div className="col title">{t('news.title')}</div>
<div className="col source">{t('news.source')}</div>
<div className="col count-picture">{t('news.title_image_count')}</div>
<div className="col count-words">{t('news.title_word_count')}</div>
<div className="col time">{t('news.title_time')}</div>
<div className="col operations">{t('news.title_operate')}</div>
</div>
<InfiniteScroller ref={scrollerRef} onCallback={(page) => {
setParams(prev => ({
...prev,
pagination: {page, limit: 10}
}))}
/>
<ButtonPush2Video ids={selectedRowKeys} onSuccess={refresh}/>
</div>}
}))
}} onScroll={(top) => setState(s=>({...s,showToTop: top > 30}))} loading={loading}
pagination={data?.pagination}>
<div className="body">
{data?.list?.map((item, i) => {
const checked = selectedRowKeys.includes(item.id)
return <div key={item.id} className={clsx("row flex", {checked})}>
<div className="col title cursor-pointer" onClick={() => setEditId(item.id)}>
<div className="flex-1">
<div className="text-base line-clamp-1">{item.title}</div>
<div
className="summary text-xs text-gray-400 line-clamp-1">{item.summary}</div>
</div>
</div>
<div className="col source">
<div className="text-sm line-clamp-1">{item.media_name}-{item.column_name}</div>
</div>
<div className="col count-picture">
<div className="text-sm">{item.img_num}</div>
</div>
<div className="col count-words">
<div className="text-sm">{item.content_word_count}</div>
</div>
<div className="col time">
<div
className="text-sm">{formatTime(item.publish_time, 'YYYY-MM-DD HH:mm')}</div>
</div>
<div className="col operations">
<span className="icon-btn" onClick={()=>setEditId(item.id)}><IconEdit/></span>
<DeleteItemPopoverConfirm onConfirm={() => {handleDelete(item.id)}}>
<span className="icon-btn"><IconDelete/></span>
</DeleteItemPopoverConfirm>
<Checkbox checked={checked}
onChange={e => handleItemChecked(e.target.checked, item)}/>
</div>
</div>
})}
<div className="h-[130px]"></div>
</div>
</InfiniteScroller>
</div>
<div className="page-action">
<ButtonToTop visible={state.showToTop} onClick={() => scrollerRef.current?.scrollToPosition(0)}/>
{selectedRowKeys?.length > 0 && <ButtonDeleteBatch ids={selectedRowKeys} onSuccess={refresh}/>}
<ButtonPush2Video ids={selectedRowKeys} articles={data?.list} onResult={handlePushProcessResult}/>
</div>
</div>
<ArticleEditModal
type="news" id={editId}
type="news"
id={editId}
onRefresh={refresh}
onClose={(saved) => {
setEditId(-1)
if (saved) refresh()
}}/>
</Card>
</div>
</div>)
}

View File

@ -1,137 +1,212 @@
import {useState} from "react";
import {Checkbox, Empty, Modal, Pagination, Space} from "antd";
import {useRequest} from "ahooks";
import {Card} from "@/components/card";
import React, {useMemo, useRef, useState} from "react";
import {Checkbox, Divider, Empty, Modal, Space} from "antd";
import { useRequest, useSetState } from 'ahooks';
import {CloseOutlined} from "@ant-design/icons"
import {clsx} from "clsx";
import SearchPanel from "@/pages/news/components/search-panel.tsx";
import styles from './style.module.scss'
import {getById, getList} from "@/service/api/news.ts";
import {showLoading} from "@/components/message.ts";
import {formatTime} from "@/util/strings.ts";
import ButtonPushNews2Article from "@/pages/news/components/button-push-news2article.tsx";
import ButtonNewsDownload from "@/pages/news/components/button-news-download.tsx";
import InfiniteScroller, {InfiniteScrollerRef} from "@/components/scoller/infinite-scroller.tsx";
import ButtonToTop from "@/components/scoller/button-to-top.tsx";
import {useIndexArrayCache} from "@/hooks/useCache.ts";
import {useTranslation} from "react-i18next";
export default function NewsIndex() {
const [params, setParams] = useState<ApiArticleSearchParams>({
pagination: {
page: 1,
limit: 10
const FilterCache: Partial<ApiArticleSearchParams> = {
time_flag: 1,
}
export default function NewsIndex() {
const {t} = useTranslation()
const [params, setParams] = useState<ApiArticleSearchParams>({
pagination: {page: 1, limit: 12},
...FilterCache
})
const [checkedId, setCheckedId] = useState<Id[]>([])
// const [checkedId, setCheckedId] = useState<Id[]>([])
const {cache: checkedId, set: setCheckedId} = useIndexArrayCache()
const [activeNews, setActiveNews] = useState<NewsInfo>()
const [state, setState] = useState<{
const [state, setState] = useSetState<{
checkAll?: boolean;
showToTop?: boolean;
}>({})
const {data} = useRequest(() => getList(params), {
const [data, setData] = useState<DataList<ListCrawlerNewsItem>>();
const {loading} = useRequest(() => getList(params), {
refreshDeps: [params],
onSuccess: () => {
onSuccess: (_data) => {
FilterCache.tag_level_1_id = params.tag_level_1_id;
FilterCache.tag_level_2_id = params.tag_level_2_id;
FilterCache.title = params.title;
FilterCache.time_flag = params.time_flag;
if (params.pagination.page === 1) {
setCheckedId([])
setState({checkAll:false})
setData(_data)
setState({checkAll: false,showToTop: false})
} else {
setData({
pagination: _data.pagination,
list: [...(data?.list || []), ..._data.list]
})
}
}
})
const handleViewNewsDetail = (id: number) => {
const {update, close} = showLoading('获取新闻详情...')
const handleViewNewsDetail = (id: number,internal_article_id:number) => {
const {update, close} = showLoading(`${t('news.get_detail')}...`)
getById(id).then(res => {
close()
setActiveNews(res)
setActiveNews({...res, id,internal_article_id})
}).catch(() => {
update('获取新闻详情失败', 'info')
update(t('news.get_detail_error'), 'info')
})
}
return (<div className={'container pb-5'}>
<Card className="search-panel-container my-5">
<SearchPanel onSearch={setParams}/>
</Card>
<Card className="news-list-container">
{activeNews && <Modal open={true} width={1000} footer={null} onCancel={() => setActiveNews(undefined)}>
<div className="news-detail px-3 pb-5">
<div className="new-title text-2xl">{activeNews?.title}</div>
<div className="info mt-2 mb-5 text-sm flex gap-3">
<span className="source text-blue-700">{activeNews?.media_name}</span>
<span className="create-time text-gray-400">{formatTime(activeNews?.publish_time)}</span>
</div>
<div className="overflow-auto leading-7 text-base"
style={{maxHeight: 1000}} dangerouslySetInnerHTML={{__html: activeNews?.content || ''}}></div>
</div>
</Modal>}
<div className="controls flex justify-between mb-1">
<div>
<Checkbox checked={state.checkAll} onChange={e => {
setState({checkAll: e.target.checked})
if (e.target.checked) {
setCheckedId(data?.list?.map(item => item.id) || [])
const currentEnabledList = useMemo(() => {
if (data?.list && data?.list?.length > 0) {
return data.list.filter(s => s.internal_article_id == 0)
}
return [];
}, [data?.list])
const handleCheckAll = (checked: boolean) => {
setState({checkAll: checked})
if (checked) {
setCheckedId(currentEnabledList.map(item => item.id) || [])
} else {
setCheckedId([])
}
}}></Checkbox>
</div>
<Space size={10}>
<ButtonPushNews2Article ids={checkedId}/>
<ButtonNewsDownload ids={checkedId}/>
</Space>
</div>
<div className={styles.newsList}>
{data?.list?.map(item => (
<div key={item.id} className={`py-3 flex items-start border-b border-gray-100 group`}>
<div
className={`checkbox mt-[2px] mr-2 ${checkedId.includes(item.id) ? '' : 'opacity-0'} group-hover:opacity-100`}>
{item.internal_article_id > 0 ? <span className={"inline-block w-[16px] " }></span> :<Checkbox checked={checkedId.includes(item.id)} onChange={() => {
if (checkedId.includes(item.id)) {
setCheckedId(checkedId.filter(id => id != item.id))
} else {
setCheckedId([...checkedId, item.id])
}
}}/> }
const scrollerRef = useRef<InfiniteScrollerRef | null>(null)
const handleCheckChange = (id: number) => {
if (checkedId.includes(id)) {
setCheckedId(checkedId.filter(_id => _id != id))
} else {
setCheckedId([...checkedId, id])
}
}
return (<div className={'container pb-5'}>
<SearchPanel defaultParams={params} onSearch={(params)=>{
// 滚动到顶部
scrollerRef.current?.scrollToPosition(0)
setParams(params)
}}/>
{activeNews && <Modal
rootClassName={'news-detail-modal'}
closeIcon={null} open={true} width={1000}
footer={null} onCancel={() => setActiveNews(undefined)}
>
<div className="news-detail pl-16 pr-1 flex pb-2">
<div className="px-4 py-6 bg-white flex-1">
<div className="new-title text-2xl">{activeNews?.title}</div>
<div className="info mt-2 mb-2 text-sm flex gap-3">
<span className="source text-blue-400">{activeNews?.media_name}</span>
<span className="create-time text-gray-400">{formatTime(activeNews?.publish_time)}</span>
</div>
<Divider className={'my-2'}/>
<div className="overflow-auto leading-7 text-base news-detail-content-container"
style={{maxHeight: 500}} dangerouslySetInnerHTML={{__html: activeNews?.content || ''}}></div>
</div>
<div className="actions ml-2">
<div className="close">
<CloseOutlined className="text-xl text-gray-400 hover:text-gray-800"
onClick={() => setActiveNews(undefined)}/>
</div>
<div className="whitespace-nowrap text-sm mt-2 min-w-[58px]">
{activeNews.internal_article_id <= 0 && <Checkbox
checked={checkedId.includes(activeNews!.id)}
onChange={() => handleCheckChange(activeNews!.id)}
><span className="ml-[-4px]">{t('select.text')}</span></Checkbox>}
</div>
</div>
</div>
</Modal>}
<div className="news-list-container">
<div className="controls flex justify-end mb-3 gap-5">
<Space size={20}>
<span>{t('select.total',{count:data?.list?.length || 0})}</span>
<span className={'text-blue-500'}>{t('select.selected_some',{count:checkedId.length})}</span>
</Space>
<div>
<span className={'inline-block cursor-pointer mr-2'} onClick={() => {
handleCheckAll(!state.checkAll)
}}>{t('select.select_all')}</span>
<Checkbox checked={state.checkAll && checkedId.length == currentEnabledList.length} onChange={e => {
handleCheckAll(e.target.checked)
}}></Checkbox>
</div>
</div>
<InfiniteScroller
className="grid grid-cols-2 gap-4 xl:grid-cols-4 md:grid-cols-3 pb-2"
pagination={data?.pagination}
loading={loading}
ref={scrollerRef}
onScroll={(top) => setState({showToTop: top > 30})}
onCallback={(page) => {
setParams({...params, pagination: {...params.pagination, page}})
}}
empty={<Empty/>}
>
{data?.list?.map(item => (
<div key={item.id}
className={clsx(`p-4 flex items-start group rounded border border-transparent`, {
'bg-news-to-edit': item.internal_article_id > 0,
'bg-white': !item.internal_article_id && !checkedId.includes(item.id),
'bg-blue-500/20 border-[#d9eaff]': checkedId.includes(item.id)
})}>
<div className="news-content flex-1">
<div className="flex items-center justify-between">
<div className="title text-lg cursor-pointer" onClick={() => {
handleViewNewsDetail(item.id)
<div className="title h-[60px] line-clamp-2 text-lg cursor-pointer hover:text-blue-500"
onClick={() => {
handleViewNewsDetail(item.id,item.internal_article_id)
}}>{item.title}</div>
{item.internal_article_id > 0 &&
<div className="text-sm text-blue-500"></div>}
</div>
<div className="content flex gap-3 mt-2 mb-3">
{item.cover && <div
className="cover border border-gray-100 flex items-center rounded overflow-hidden"
style={{width: 100, height: 100}}>
<img className="w-full h-full object-cover" src={item.cover}/>
</div>}
<div className="text text-gray-600 text-sm leading-6 flex-1 text-justify">
<div
className="text text-gray-600 text-sm h-[70px] line-clamp-3 overflow-hidden text-ellipsis text-justify leading-6 flex-1 text-justify break-all text-wrap">
{item.summary}
</div>
{item.cover && <div
className="cover border border-gray-100 flex items-center rounded overflow-hidden"
style={{width: 100, height: 70}}>
<img className="w-full h-full object-cover" src={item.cover}/>
</div>}
</div>
<div className="info text-gray-400 mt-4 text-sm">
<div className="line-clamp-1">{t('news.source')}: <span>{item.data_source_name}</span></div>
<div className="extras flex items-center justify-between gap-3">
<div><span>{formatTime(item.publish_time, 'min')}</span></div>
<div><span>{t('news.image_count')}: {item.img_num}</span></div>
<div><span>{t('news.word_count')}: {item.content_word_count}</span></div>
<div
className={` mt-1`}>
{item.internal_article_id > 0 ?
<span className={"inline-block text-gray-600"}>{t('news.pushed')}</span> :
<Checkbox checked={checkedId.includes(item.id)} onChange={() => {
handleCheckChange(item.id)
}}/>}
</div>
</div>
<div className="info text-gray-300 flex items-center justify-between gap-3 text-sm">
<div>: <span>{item.media_name}</span></div>
{/*<Divider type="vertical" />*/}
<div>: <span>{formatTime(item.publish_time)}</span></div>
</div>
</div>
</div>
))}
<div className="h-[100px]"></div>
</InfiniteScroller>
</div>
<div className="page-action">
<ButtonToTop visible={state.showToTop} onClick={() => scrollerRef.current?.scrollToPosition(0)}/>
<div>
<ButtonNewsDownload ids={checkedId}/>
</div>
<div>
<ButtonPushNews2Article ids={checkedId}/>
</div>
{data?.pagination && data?.pagination.total > 0 ? <div className="flex justify-center mt-10">
<Pagination
current={params.pagination.page}
total={data?.pagination.total}
pageSize={data?.pagination.limit}
showSizeChanger={false}
simple={true}
rootClassName={'simple-pagination'}
onChange={(page) => setParams(prev=>({...prev,pagination: {page, limit: 10}}))}
/>
</div> : <div className="py-10">
<Empty />
</div>
}
</Card>
</div>)
}

View File

@ -1,3 +0,0 @@
.newsList{
}

104
src/pages/order/index.tsx Normal file
View File

@ -0,0 +1,104 @@
import React, {useState} from "react";
import {useTranslation} from "react-i18next";
import {Empty, Pagination} from "antd";
import {useRequest} from "ahooks";
import SearchPanel from "@/pages/news/components/search-panel.tsx";
import styles from "@/pages/news/components/style.module.scss";
import {formatDurationToTime, formatTime} from "@/util/strings.ts";
import {getList} from "@/service/api/order.ts";
import ImageErr from "@/assets/images/error/ic_broken_image.png"
function OrderIndex() {
const {t} = useTranslation()
const [params, setParams] = useState<ApiArticleSearchParams>({
pagination: {page: 1, limit: 10},
time_flag: 0,
})
const {data} = useRequest(() => getList(params), {
refreshDeps: [params],
})
return <div className="pb-5 page-order-index">
<div className=" mb-5" style={{backgroundColor:'#dae8fc'}}>
<div className="container" style={{padding:0}}>
<SearchPanel
rootClassName="py-6"
hideNewsSource={true}
defaultParams={params}
onSearch={setParams}
rightRender={<div>{t('order.left_time')}: <span
className={`${!data?.remaining_duration || Number(data?.remaining_duration) < 3600 ? 'text-red-600' : ''}`}>{formatDurationToTime(data?.remaining_duration)}</span>
</div>}
/>
</div></div>
<div className="mt-5 container" style={{padding:"20px 0"}}>
<div className={`${styles.newListTable} ${styles.orderDataList} `}>
<div className="header row flex">
<div className="col id w-[160px]">{t('order.list.id')}</div>
<div className="col cover">{t('order.list.cover')}</div>
<div className="col title w-min-60px">{t('order.list.title')}</div>
<div className="col w-[180px]">{t('order.list.order_time')}</div>
<div className="col w-[120px]">{t('order.list.consume_time')}</div>
<div className="col w-[180px]">{t('order.list.operator')}</div>
</div>
<div>
{data?.list.length === 0 && <div style={{marginTop: 50}}>
<Empty/>
</div>}
{data?.list.map((item, i) => {
return <div key={i} className="row flex">
<div className="col id w-[160px] text-center">
<div className="flex-1">
<div className="break-all">{item.order_id}</div>
</div>
</div>
<div className="col cover">
<div
className="w-[100px] h-[56px] flex items-center rounded overflow-hidden border border-gray-50"
>
<img
src={item.img_url || ImageErr}
className="w-[100px] object-cover"
/>
</div>
</div>
<div className="col title order-title flex-1 w-min-60px">
<div className="line-clamp-2">{item.title}</div>
</div>
<div className="col w-[180px]">{formatTime(item.order_time, 'YYYY-MM-DD HH:mm')}
</div>
<div className="col w-[120px]">{formatDurationToTime(item.consumption_duration)}
</div>
<div className="col w-[180px]">{item.operator}</div>
</div>
})}
<div className="footer flex justify-end mt-10">
<Pagination
onChange={(page, limit) => {
setParams({
...params,
pagination: {page, limit}
})
}}
total={data?.pagination.total || 0}
showTotal={(total) => <div>{t('page.total_item', {total})}</div>}
showSizeChanger={{
options: [
{value: 10, label: t('page.size_10')},
{value: 20, label: t('page.size_20')},
{value: 30, label: t('page.size_30')}
]
}}
showQuickJumper={true}
/>
</div>
</div>
</div>
</div>
</div>
}
export default OrderIndex

View File

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

@ -0,0 +1,59 @@
import {Input} from "antd";
import {useSetState} from "ahooks";
import {SearchOutlined} from "@ant-design/icons";
import React from "react";
import TimeSelect from "@/components/form/time-select.tsx";
import {useTranslation} from "react-i18next";
type Props = {
onSearch?: (params: VideoSearchParams) => void;
onBtnStartClick?: () => Promise<void>;
loading?: boolean;
}
export default function SearchForm({onSearch}: Props) {
const {t} = useTranslation()
const [state, setState] = useSetState<{
pushing?: boolean;
time_flag: number;
title?: string;
}>({
time_flag: 0
})
const onFinish = (params: Partial<VideoSearchParams>) => {
onSearch?.({
time_flag: params.time_flag,
title: params.title,
pagination: {page: 1, limit: 12}
})
}
const handleTimeFilter = (time_flag: number) => {
setState({time_flag})
onFinish({
title: state.title, time_flag
})
}
return (<div className={'search-panel pt-6 pb-2'}>
<div className="search-form">
<div className="flex items-center gap-4">
<Input
className="w-[270px] rounded-3xl"
prefix={<SearchOutlined/>}
onChange={e => setState({title: e.target.value})}
onPressEnter={() => onFinish(state)}
onBlur={() => onFinish(state)}
allowClear
placeholder={t("history.search_key")}
/>
<TimeSelect
className="w-[140px] ml-1"
value={state.time_flag}
onChange={handleTimeFilter}
/>
</div>
</div>
</div>)
}

View File

@ -0,0 +1,33 @@
.videoItem {
border: solid 3px transparent;
:global {
.video-bottom {
}
.video-time-info{
min-width: 60px;
}
}
}
.videoChecked {
@apply border-blue-500;
}
.playIcon {
--size: 40px;
@apply bg-black/70 flex items-center justify-center;
border: solid 2px rgba(255, 255, 255, 0.5);
border-radius: var(--size);
width: var(--size);
height: var(--size);
color: white;
cursor: pointer;
&:hover{
@apply bg-blue-500;
}
svg{
font-size: 24px;
transform: translate(2px);
}
}

View File

@ -0,0 +1,65 @@
import {Modal} from "antd";
import {saveAs} from "file-saver";
import {useSetState} from "ahooks";
import {Player} from "@/components/video/player.tsx";
import {push2room} from "@/service/api/video.ts";
import {showErrorToast, showToast} from "@/components/message.ts";
import {useTranslation} from "react-i18next";
type Props = {
video?: VideoInfo;
autoPlay?: boolean;
onClose?: () => void
}
export default function VideoDetail({video, onClose,autoPlay}: Props) {
const {t} = useTranslation()
const [state, setState] = useSetState({
exporting: false,
pushing: false,
})
// 将视频推送到数字人直播间
const pushToRoom = () => {
if (video) {
if (state.pushing) return
setState({pushing: true})
push2room([video.id]).then(() => {
showToast(t('history.push_success'), 'success')
}).catch(showErrorToast).finally(() => {
setState({pushing: false})
})
}
}
// 下载视频
const downloadVideo = () => {
if (video?.oss_video_url) {
const filename = video.oss_video_url.split('/').pop() || `${video.id}.flv`
saveAs(video.oss_video_url, filename)
}
}
return (<>
<Modal
open={!!video} width={390} closeIcon={null} title={null} footer={null} onCancel={onClose}
rootClassName={"article-edit-modal"}
>
<div className="flex gap-2 px-6 pt-6">
<div className="news-video w-[340px]">
<div className="video-container bg-gray-100 rounded overflow-hidden">
<Player autoPlay={autoPlay} url={video?.oss_video_url} poster={video?.cover} showControls={true}
className="w-[340px] h-[600px] bg-white"/>
</div>
</div>
</div>
<div className="flex justify-end modal-control-footer">
<div className="flex gap-4">
<button disabled={state.pushing} className="text-gray-400 hover:text-gray-800 " type="button" onClick={pushToRoom}>{t('video.push_to_live')}</button>
<button disabled={state.exporting} className="text-gray-400 hover:text-gray-800 " onClick={downloadVideo}
type="button">{t('video.download')}
</button>
<button onClick={onClose} type="button" className="text-gray-800 hover:text-blue-500">{t('close')}</button>
</div>
</div>
</Modal>
</>)
}

View File

@ -0,0 +1,41 @@
import clsx from "clsx";
import {CaretRightOutlined} from "@ant-design/icons"
import {timeFromNow} from "@/util/strings.ts";
import styles from './style.module.scss'
type VideoItemProps = {
videoInfo: VideoInfo;
onLive?: boolean;
onClick?: (autoPlay: boolean) => void;
onRemove?: () => void;
onCheckedChange?: (checked: boolean) => void;
checked?: boolean;
}
export default function VideoItem(props: VideoItemProps) {
return <div
className={clsx(styles.videoItem, `rounded-lg h-[240px] overflow-hidden relative group ${props.checked ? styles.videoChecked : ''}`)}>
<div className={`controls absolute top-1 right-1 z-[2] rounded items-center gap-2`}>
{/*<span onClick={props.onRemove} className={'cursor-pointer text-blue-500 text-2xl cursor-pointer'}><IconDelete /></span>*/}
<div className={clsx("checkbox", {checked: props.checked})}
onClick={() => props.onCheckedChange?.(!props.checked)}></div>
</div>
<div className="cover">
<img className={'w-full cursor-pointer object-cover'} src={props.videoInfo.cover}/>
<div className={'absolute inset-x-0 top-0 flex items-center justify-center bottom-[36px]'}>
<div className={styles.playIcon} onClick={() => props.onClick?.(true)}><CaretRightOutlined/></div>
</div>
</div>
<div
className="video-bottom bg-black/30 backdrop-blur-[2px] text-sm absolute inset-x-0 bottom-0 text-white py-2 px-3 items-center flex justify-between">
<div className="title cursor-pointer flex-1 text-nowrap overflow-hidden text-ellipsis min-w-0 mr-4"
onClick={() => props.onClick?.(false)}>{props.videoInfo.title}</div>
<div className="video-time-info text-right">{timeFromNow(props.videoInfo.d_time)}</div>
</div>
<div
className={"absolute top-1 left-1 bg-black/50 rounded-3xl text-white px-3 py-0.5"}>{Math.ceil(props.videoInfo.duration / 1000)}s
</div>
</div>
}

216
src/pages/recycle/index.tsx Normal file
View File

@ -0,0 +1,216 @@
import React, {useEffect, useRef, useState} from "react";
import {Checkbox, Empty, Modal, Space} from "antd";
import {useRequest, useSetState} from "ahooks";
import {useTranslation} from "react-i18next";
import VideoItem from "@/pages/recycle/components/video-item.tsx";
import SearchForm from "@/pages/recycle/components/search-form.tsx";
import VideoDetail from "@/pages/recycle/components/video-detail.tsx";
import InfiniteScroller, {InfiniteScrollerRef} from "@/components/scoller/infinite-scroller.tsx";
import ButtonBatch from "@/components/button-batch.tsx";
import ButtonToTop from "@/components/scoller/button-to-top.tsx";
import {IconArrowRight, IconDelete} from "@/components/icons";
import {showToast} from "@/components/message.ts";
import {BizError} from "@/service/types.ts";
import {getList as getLiveList} from "@/service/api/live.ts";
import {getList, remove, restore} from "@/service/api/recycle.ts";
const DEFAULT_PAGE_LIMIT = {
page: 1,
limit: 12
}
export default function RecycleIndex() {
const {t} = useTranslation()
const [modal, contextHolder] = Modal.useModal();
const [checkedIdArray, setCheckedIdArray] = useState<number[]>([])
const [params, setParams] = useState<VideoSearchParams>({
time_flag: 0,
pagination: {...DEFAULT_PAGE_LIMIT}
})
const [state, setState] = useSetState({
checkedAll: false,
loading: false,
pushedCount: 0,
pushedList: [-1],
showToTop: false
})
const [data, setData] = useState<DataList<VideoInfo>>()
const scrollerRef = useRef<InfiniteScrollerRef | null>(null)
const {loading} = useRequest(() => getList(params), {
refreshDeps: [params],
onSuccess: (data) => {
setData(prev => {
// 判断页码是否是第1页
if (data.pagination.page == 1) {
setCheckedIdArray([])
return data;
}
return {
list: [...(prev?.list || []), ...(data?.list || [])],
pagination: data.pagination || {
page: 1,
limit: DEFAULT_PAGE_LIMIT.limit
}
}
})
}
})
const handleRemove = (video: VideoInfo) => {
modal.confirm({
title: t('confirm.title'),
content: t('history.delete_confirm'),
onOk: () => {
console.log('OK', video);
}
})
}
const handleLive = async () => {
if (checkedIdArray.length == 0) return;
modal.confirm({
title: t('confirm.push_title'),
content: t('confirm.push_video'),
onOk: () => {
console.log('OK');
}
})
}
const [detailVideo, setDetailVideo] = useState<{
video: VideoInfo,
autoPlay: boolean
}>()
const handleAllCheckedChange = (checked: boolean) => {
if (!data || data.pagination.total == 0) return;
setCheckedIdArray(checked ? data.list.map(v => v.id) : [])
setState({
checkedAll: !state.checkedAll
})
}
const loadPushedState = () => {
getLiveList().then((ret) => {
if (ret.list) {
setState({pushedCount: ret.list.length, pushedList: ret.list.map(s => s.id)})
}
})
}
const refresh = () => {
// loadPushedState();
setParams(prev => ({...prev, pagination: {page: 1, limit: DEFAULT_PAGE_LIMIT.limit}, request_time: Date.now()}))
}
// const pusdedCount = useMemo(() => {
// if (state.pushedCount == 0 || !data || !data.list || data.list.length == 0) return 0;
// return data.list.filter(s => state.pushedList.includes(s.id)).length
// }, [state.pushedList, state.pushedCount, data])
useEffect(loadPushedState, [])
return (<>
<div className={'container pb-5'}>
{contextHolder}
<div className="search-form-container">
<SearchForm
onSearch={(params) => {
setParams({
...params,
pagination: {...DEFAULT_PAGE_LIMIT}
})
}}
onBtnStartClick={handleLive}
loading={loading}
/>
</div>
<div className="">
<div className="live-control flex justify-between mb-2">
<div className="pl-[70px]"></div>
<div className="flex items-center">
<Space className="text-gray-400" size={20}>
<span>{t('select.total', {count: data?.list?.length || 0})}</span>
{/*<span>{t('history.pushed', {count: state.pushedCount})}</span>*/}
<span className={'text-blue-500'}>{t('select.selected_some', {count: checkedIdArray.length})}</span>
</Space>
<button className="hover:text-blue-300 text-gray-400 ml-4"
onClick={() => handleAllCheckedChange(checkedIdArray.length != data?.list.length)}>
<span className="text-sm mr-2">{t("select.select_all")}</span>
{/*<CheckCircleFilled className={clsx({'text-blue-500': state.checkedAll})}/>*/}
</button>
<Checkbox
disabled={data?.pagination.total == 0 || data?.list?.length == 0}
checked={checkedIdArray.length == data?.list?.length}
onChange={e => handleAllCheckedChange(e.target.checked)}/>
</div>
</div>
<InfiniteScroller
ref={scrollerRef} loading={loading} rootClassName="video-history-list-container"
pagination={data?.pagination} onCallback={(page) => {
setParams(prev => ({
...prev,
pagination: {page, limit: DEFAULT_PAGE_LIMIT.limit}
}))
}} onScroll={(top) => setState({showToTop: top > 30})}
>
{data?.pagination.total == 0 && !loading && <div className="mt-20">
<Empty/>
</div>}
<div className={'video-list-container grid gap-4 grid-cols-3 xl:grid-cols-4'}>
{data?.list?.map((it, idx) => (
<VideoItem
onLive={idx == 2}
key={idx}
videoInfo={it}
onRemove={() => handleRemove(it)}
onClick={(autoPlay) => setDetailVideo({video: it, autoPlay})}
checked={checkedIdArray.includes(it.id)}
onCheckedChange={(checked) => {
setCheckedIdArray(idArray => {
return checked ? idArray.concat(it.id) : idArray.filter(id => id != it.id);
})
}}
/>
))}
</div>
</InfiniteScroller>
</div>
</div>
{detailVideo && <VideoDetail video={detailVideo.video} autoPlay={detailVideo.autoPlay}
onClose={() => setDetailVideo(undefined)}/>}
<div className="page-action">
<ButtonToTop visible={state.showToTop} onClick={() => scrollerRef.current?.scrollToPosition(0)}/>
{checkedIdArray?.length > 0 && <ButtonBatch
selected={checkedIdArray}
onSuccess={refresh}
className='bg-gray-300 hover:bg-gray-400 text-white'
icon={<IconDelete className=""/>}
title={
checkedIdArray.length == 1
? t('video.delete_description', {count: checkedIdArray.length})
: t('video.delete_description_count', {count: checkedIdArray.length})
}
emptyMessage={t('video.delete_empty')}
confirmMessage={<span dangerouslySetInnerHTML={{
__html: checkedIdArray.length == 1
? t('video.delete_forever_confirm')
: t('video.delete_forever_confirm_count', {count: checkedIdArray.length})
}}></span>}
onProcess={remove}
>{t('recycle.remove_forever')}</ButtonBatch>}
{checkedIdArray?.length > 0 && <ButtonBatch
selected={checkedIdArray}
onSuccess={refresh}
className='bg-[#4096ff] hover:bg-blue-600 text-white'
icon={<IconArrowRight className={'text-white'}/>}
onProcess={restore}
confirmMessage={<span dangerouslySetInnerHTML={{
__html: checkedIdArray.length == 1
? t('video.restore_confirm')
: t('video.restore_confirm_count', {count: checkedIdArray.length})
}}></span>}
emptyMessage={t('video.push_empty')}
onError={e => {
showToast(String((e as BizError).data || e.message), 'error')
}}
>{t('recycle.restore_video')}</ButtonBatch>}
</div>
</>)
}

View File

@ -1,12 +1,13 @@
import {useState} from "react";
import {useNavigate, useSearchParams} from "react-router-dom";
import type {FormProps} from 'antd';
import {LockOutlined, UserOutlined} from '@ant-design/icons';
import {Button, Checkbox, Divider, Flex, Form, Input} from 'antd';
import {Button, Form, Input} from 'antd';
import {clsx} from "clsx";
import styles from './../style.module.scss'
import useAuth from "@/hooks/useAuth.ts";
import {useSmsCode} from "@/components/form/sms-code.tsx";
import {useTranslation} from "react-i18next";
type FieldType = {
username?: string;
@ -15,6 +16,7 @@ type FieldType = {
export default function FormLogin() {
const {t} = useTranslation()
const [disabled, setDisabled] = useState(true)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string>()
@ -24,8 +26,9 @@ export default function FormLogin() {
const [phone, setPhone] = useState<string>()
const {sending, countdown, sendCode} = useSmsCode()
const onFinish: FormProps<FieldType>['onFinish'] = (values) => {
if(disabled || loading) return
if (!values.username || !/^1\d{10}$/.test(values.username)) {
setError('账号或密码错误')
setError(t("login.invalid_username_or_pwd"))
return
}
setLoading(true)
@ -37,7 +40,7 @@ export default function FormLogin() {
};
return (<div className="form">
<Divider className=" pb-8 pt-4"><div className={'text-center text-2xl'}></div></Divider>
<div className={'text-center text-xl pb-6 pt-8'}>{t("login.welcome")}</div>
<Form<FieldType>
name="basic"
style={{maxWidth: 600}}
@ -51,34 +54,30 @@ export default function FormLogin() {
autoComplete="off"
>
<Form.Item<FieldType> name="username">
<div
className="border border-gray-300 rounded-3xl mt-2 flex items-center px-3 focus-within:border-blue-500 focus-within:shadow focus-within:shadow-blue-200">
<UserOutlined/>
<Input size={'large'} variant={'borderless'} placeholder="请输入账号"/>
<div className={styles.loginBox}>
<Input size={'large'} variant={'borderless'} placeholder={t("login.username")}/>
</div>
</Form.Item>
<Form.Item name="password">
<div
className="border border-gray-300 rounded-3xl mt-2 flex items-center px-3 focus-within:border-blue-500 focus-within:shadow focus-within:shadow-blue-200">
<LockOutlined/>
<div className={styles.loginBox}>
<Input style={{borderRadius: 20}} size={'large'} variant={'borderless'}
placeholder="请输入验证码"/>
placeholder={t("login.password")}/>
<span
className={clsx(`text-nowrap text-sm ${countdown > 0 || sending || !phone ? 'text-gray-400 cursor-not-allowed' : 'text-blue-500 cursor-pointer'}`)}
className={clsx(`text-nowrap mr-1 text-sm ${countdown > 0 || sending || !phone ? 'text-gray-400 cursor-not-allowed' : 'text-blue-500 cursor-pointer'}`)}
onClick={() => sendCode(phone)}>
{sending ? '发送中...' : (countdown > 0 ? `${Math.ceil(countdown / 1000)} s` : '获取验证码')}
{sending ? `${t('login.code_sending')}...` : (countdown > 0 ? `${Math.ceil(countdown / 1000)} s` : t('login.send_sms_code'))}
</span>
</div>
</Form.Item>
<Form.Item className={"mt-14"}>
<Form.Item className={"mt-16"}>
<div className="absolute text-red-500 text-center inset-x-0" style={{top: -30}}>{error}</div>
<Button disabled={disabled || loading} loading={loading} type="primary" size={'large'} htmlType="submit"
<div className="absolute text-red-500 text-center inset-x-0" style={{top: -34}}>{error}</div>
<Button loading={loading} type="primary" size={'large'} htmlType="submit"
block shape={'round'}>
{loading ? '登录中...' : '立即登录'}
{loading ? t("login.loading") : t('login.text')}
</Button>
</Form.Item>
</Form>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 447 KiB

View File

@ -2,7 +2,6 @@ import {useNavigate, useSearchParams} from "react-router-dom";
import {useEffect} from "react";
import useAuth from "@/hooks/useAuth.ts";
import MainBgImage from './components/bg.png'
import styles from './style.module.scss'
import FormLogin from "./components/form-login.tsx";
@ -18,7 +17,7 @@ export default function UserIndex(){
return (<div className={styles.main}>
<div className={"flex-1 ml-[15%]"}>
{/*<h2 className="text-4xl mb-10 text-white/90">数字人直播间</h2>*/}
<img className="w-[450px]" src={MainBgImage} alt=""/>
{/*<img className="w-[450px]" src={MainBgImage} alt=""/>*/}
</div>
<div className={styles.boxLogin}>
<FormLogin />

View File

@ -1,13 +1,14 @@
.main {
@apply py-10;
background-image: url(components/main-bg.jpg);
background-size: 100% 100%;
height: 100vh;
background: url(./components/main-bg.jpg) no-repeat center;
background-size: cover;
height: calc(100vh - var(--app-header-header));
min-height: 500px;
align-items: center;
display: flex;
overflow: hidden;
justify-content: right;
}
.boxLogin {
@ -25,3 +26,6 @@
//position: relative;
//width: 400px;
}
.loginBox{
@apply border border-gray-50 bg-gray-50 rounded-3xl mt-2 flex items-center pr-3 pl-1 focus-within:border-gray-300; // focus-within:shadow
}

View File

@ -1,11 +1,15 @@
import {Button, Modal} from "antd";
import {Modal} from "antd";
import React, {useState} from "react";
import {showErrorToast, showToast} from "@/components/message.ts";
import {push2room, VideoStatus} from "@/service/api/video.ts";
import {IconArrowRight} from "@/components/icons";
import {useTranslation} from "react-i18next";
import ModalWarning from "@/components/icons/ModalWarning.tsx";
export default function ButtonPush2Room(props: { ids: Id[]; list: VideoInfo[]; onSuccess?: () => void; }) {
const [loading, setLoading] = useState(false)
const {t} = useTranslation()
const handlePush = () => {
setLoading(true)
// 只需要已经生成视频的数据id
@ -13,26 +17,40 @@ export default function ButtonPush2Room(props: { ids: Id[]; list: VideoInfo[];on
push2room(vids).then(() => {
props.onSuccess?.()
if (props.ids.length == vids.length) {
showToast('一键推流成功,已推流至数字人直播间,请前往数字人直播间页面查看!', 'success')
showToast(t("video.push_success"), 'success')
} else {
showToast('选择视频中有部分视频还在生成中无法推送,推流成功视频前往数字人直播间页面查看!', 'success')
showToast(t("video.push_failed"), 'success')
}
}).catch(showErrorToast).finally(() => {
setLoading(false)
})
}
const onPushClick = () => {
if (loading) return;
if (props.ids.length === 0) {
showToast('请选择要推流的新闻', 'warning')
showToast(t("video.push_empty"), 'warning')
return
}
Modal.confirm({
title: '操作提示',
content: '是否确定一键推流选中新闻视频??',
onOk: handlePush
wrapClassName:'root-modal-confirm',
title: <ModalWarning.Title/>,
icon: <ModalWarning.Icon/>,
content: t("video.push_confirm"),
onOk: handlePush,
centered: true
})
}
return (
<Button type="primary" loading={loading} onClick={onPushClick}></Button>
<div>
<button
disabled={loading}
className='bg-[#4096ff] hover:bg-blue-600 text-white'
onClick={onPushClick}
>
<span className={'text'}>{t("video.push_to_live")}</span>
<IconArrowRight/>
</button>
</div>
)
}

View File

@ -1,115 +1,225 @@
import {Empty, Modal} from "antd";
import React, {useEffect, useMemo, useRef, useState} from "react";
import {Checkbox, Empty, Space} from "antd";
import React, {useEffect, useRef, useState} from "react";
import {DndContext} from "@dnd-kit/core";
import {arrayMove, SortableContext} from "@dnd-kit/sortable";
import {useSetState} from "ahooks";
import {CheckCircleFilled} from "@ant-design/icons";
import {clsx} from "clsx";
import {useLocation, useNavigate} from "react-router-dom";
import {useTranslation} from "react-i18next";
import {VideoListItem} from "@/components/video/video-list-item.tsx";
import ArticleEditModal from "@/components/article/edit-modal.tsx";
import {deleteByIds, getList, modifyOrder, VideoStatus} from "@/service/api/video.ts";
import {deleteFromList, getList, modifyOrder, regenerateById, VideoStatus} from "@/service/api/video.ts";
import {formatDuration} from "@/util/strings.ts";
import ButtonBatch from "@/components/button-batch.tsx";
import {showToast} from "@/components/message.ts";
import {Player, PlayerInstance} from "@/components/video/player.tsx";
import {showErrorToast, showToast} from "@/components/message.ts";
import {Mp4Player as Player, PlayerInstance} from "@/components/video/Mp4Player.tsx";
import ButtonPush2Room from "@/pages/video/components/button-push2room.tsx";
import ButtonToTop from "@/components/scoller/button-to-top.tsx";
import InfiniteScroller, {InfiniteScrollerRef} from "@/components/scoller/infinite-scroller.tsx";
import {IconDelete} from "@/components/icons";
export default function VideoIndex() {
const {t} = useTranslation()
const [editId, setEditId] = useState(-1)
const loc = useLocation()
const navigate = useNavigate()
const [videoData, setVideoData] = useState<VideoInfo[]>([])
const [modal, contextHolder] = Modal.useModal()
const player = useRef<PlayerInstance | null>(null)
const scrollerRef = useRef<InfiniteScrollerRef | null>(null)
const [state, setState] = useSetState({
checkedAll: false,
playingIndex: -1,
playingId: -1,
videoPlaying: false,
showToTop: false,
showStatePos: false,
playState: {
current: -1,
total: -1
},
loading:false,
playVideoUrl: ''
})
const [checkedIdArray, setCheckedIdArray] = useState<Id[]>([])
const [refreshTimer,setTimer] = useState(0)
// 加载列表
const loadList = (needReset = true) => {
if(state.loading) return;
if(refreshTimer) {
clearTimeout(refreshTimer)
setTimer(0)
}
setState({loading: true})
getList().then((ret) => {
const list = ret.list || []
setVideoData(list)
if (needReset) {
setCheckedIdArray([])
setState({checkedAll: false, playingIndex: -1})
setState({checkedAll: false, playingId: -1})
}
// 判断是否有生成中的视频
if (list.filter(s => s.status == VideoStatus.Generating).length > 0) {
// 每5s重新获取一次最新数据
setTimeout(() => loadList(false), 5000)
setTimer(()=>setTimeout(() => loadList(false), 5000) as any);
}
}).catch(showErrorToast)
.finally(()=>{
setState({loading: false})
})
return ()=>{
if(refreshTimer){
clearTimeout(refreshTimer)
}
console.log('go out',refreshTimer)
}
}
// 播放视频
const playVideo = (video: VideoInfo, playingIndex: number) => {
if (video.oss_video_url && video.status !== 1) {
setState({playingIndex})
player.current?.play(video.oss_video_url, 0)
const playVideo = (video: VideoInfo) => {
if (state.playingId == video.id) {
player.current?.pause();
setState({playingId: -1})
return;
}
if (video.status == VideoStatus.Generating) return;
// setState({playingIndex})
// player.current?.play('https://staticplus.gachafun.com/ai-collect/composite_video/2024-12-17/1186196465916190720.flv', 30)
//
if (video.oss_video_mp4_url && video.status !== 1) {
setState({playingId: video.id})
player.current?.play(video.oss_video_mp4_url, 0)
}
}
// 处理全选
const handleAllCheckedChange = () => {
setCheckedIdArray(state.checkedAll ? [] : videoData.map(v => v.id))
setCheckedIdArray(state.checkedAll ? [] : videoData.filter(s=>s.status == VideoStatus.Generated).map(v => v.id))
setState({
checkedAll: !state.checkedAll
})
}
const handleModifySort = () => {
setVideoData((items) => {
modifyOrder(items.map(s => s.id)).catch(() => {
showToast('调整视频顺序失败,请重试!')
}).finally(loadList)
return items;
const handleModifySort = (items: VideoInfo[]) => {
modifyOrder(items.map(s => s.id)).then(() => {
showToast(t('video.sort_modify_success'), 'success')
}).catch(() => {
loadList();
showToast(t('video.sort_modify_failed'), 'warning')
})
return ()=>{
try{
Array.from(document.querySelectorAll('video')).forEach(v => v.pause())
}catch (e){
console.log(e)
}
}
}
//
useEffect(loadList, [])
const totalDuration = useMemo(() => {
if (!videoData || videoData.length == 0) return 0;
// 计算总时长
return videoData.reduce((sum, v) => sum + Math.ceil(v.duration / 1000), 0);
}, [videoData])
// const totalDuration = useMemo(() => {
// if (!videoData || videoData.length == 0) return 0;
// const v = state.playingId == -1 ? null : videoData.find(s=>s.id == state.playingId)
// if (!v) return 0
// //const v = videoData[state.playingIndex] as VideoInfo;
// return Math.ceil(v.duration / 1000)
// // 计算总时长
// //return videoData.reduce((sum, v) => sum + Math.ceil(v.duration / 1000), 0);
// }, [videoData, state.playingId])
return (<div className="container py-10 page-live">
{contextHolder}
<div className="flex">
<div className="video-list-container bg-white p-10 rounded flex flex-col flex-1">
<div className="live-control flex justify-between mb-5">
<div className="pl-[70px]">
<span>: {formatDuration(totalDuration)}</span>
</div>
<div className="flex gap-2 items-center pr-[10px]">
<ButtonBatch
onProcess={deleteByIds}
selected={checkedIdArray}
emptyMessage={`请选择要删除的新闻视频`}
confirmMessage={`是否删除当前的${checkedIdArray.length}个新闻视频?`}
onSuccess={() => {
showToast('删除成功!', 'success')
useEffect(() => {
if (loc.state == 'push-success' && !state.showStatePos && videoData.length && scrollerRef.current) {
const generatingItem = document.querySelector(`.list-item-state-${VideoStatus.Generating}`)
if (generatingItem) {
generatingItem.scrollIntoView({behavior: 'smooth'})
setState({showStatePos: true})
}
}
}, [videoData, scrollerRef])
const processDeleteVideo = async (ids: Id[],action ?: string) => {
deleteFromList(ids).then(() => {
showToast(t('delete_success'), 'success')
if(action == 'rollback'){
navigate('/edit',{
state: {action: 'rollback',id: ids[0]},
})
}else{
loadList()
}}
></ButtonBatch>
}
}).catch(showErrorToast)
}
const processGenerateVideo = async (video: VideoInfo) => {
regenerateById(video.article_id).then(() => {
//showToast(t('delete_success'), 'success')
loadList()
}).catch(showErrorToast)
}
<button className="ml-5 hover:text-blue-300 text-gray-400 text-lg"
return (<div className="container py-5 page-live">
<div className="h-[36px]"></div>
<div className="flex">
<div className="video-player-container mr-16 w-[360px] flex items-center">
<div>
<div className="text-center text-base text-gray-400">{t("generating.title")}</div>
<div className="video-player flex items-center mt-2">
<div className=" w-[360px] h-[636px] rounded overflow-hidden">
{/*videoData[state.playingIndex]?.oss_video_url*/}
<Player
ref={player}
url={state.playVideoUrl}
onChange={(state) => {
if (state.end || state.error) setState({playingId: -1})
}}
onPause={() => {
setState({videoPlaying:false})
}}
onPlay={() => {
setState({videoPlaying:true})
}}
onProgress={(current, duration) => {
setState({
playState: {
current: current,
total: duration
}
})
}}
className="w-[360px] h-[640px] bg-white"/>
</div>
</div>
<div className="text-center text-sm mt-4 text-gray-400">{formatDuration(state.playState.current)} / {formatDuration(state.playState.total)}</div>
</div>
</div>
<div className="video-list-container rounded mt-2 flex flex-col flex-1">
<div className="live-control flex justify-between h-[30px]">
<div className="pl-[70px]"></div>
<div className="flex items-center">
<Space size={20}>
<span>{t('select.total',{count:videoData.length || 0})}</span>
<span className={'text-blue-500'}>{t('select.selected_some',{count:checkedIdArray.length})}</span>
</Space>
<button className="hover:text-blue-300 text-gray-400 ml-5"
onClick={handleAllCheckedChange}>
<CheckCircleFilled className={clsx({'text-blue-500': state.checkedAll})}/>
<span className="text-sm mr-2">{t("select.select_all")}</span>
{/*<CheckCircleFilled className={clsx({'text-blue-500': state.checkedAll})}/>*/}
</button>
<Checkbox checked={state.checkedAll} onChange={() => handleAllCheckedChange()}/>
</div>
</div>
<div className={'video-list-sort-container flex-1'}>
<div className="flex my-2">
{videoData.length == 0 ? <div className="m-auto"><Empty/></div> : <>
<div className="sort-number-container mr-2">
{videoData.map((v, index) => (
<div key={index} className="flex items-center px-2 h-[80px] mt-3 mb-2">
<div
className="index-value w-[40px] h-[40px] flex items-center justify-center bg-gray-100 rounded-3xl">{index + 1}</div>
<div className={'video-list-sort-container flex-1 mt-1'}>
<div className="list-header">
<div className="list-row header-row">
<div className="col number">No.</div>
<div className="col cover">{t('video.title_thumb')}</div>
<div className="col title">{t('video.title')}</div>
<div className="col generated-time">{t('video.title_generated_time')}</div>
<div className="col operation">{t('video.title_operation')}</div>
</div>
))}
</div>
<InfiniteScroller loading={state.loading} ref={scrollerRef} onScroll={top => setState({showToTop: top > 30})}>
{
videoData.length == 0 ? <div className="m-auto py-16"><Empty/></div> :
<div className="sort-list-container flex-1">
<DndContext onDragEnd={(e) => {
const {active, over} = e;
@ -120,16 +230,10 @@ export default function VideoIndex() {
setVideoData((items) => {
oldIndex = items.findIndex(s => s.id == active.id);
newIndex = items.findIndex(s => s.id == over.id);
return arrayMove(items, oldIndex, newIndex);
const newSorts = arrayMove(items, oldIndex, newIndex);
handleModifySort(newSorts)
return newSorts;
});
modal.confirm({
title: '提示',
content: '是否要移动到指定位置',
onOk: handleModifySort,
onCancel: () => {
setVideoData(originArr);
}
})
}
}}>
<SortableContext items={videoData}>
@ -140,9 +244,10 @@ export default function VideoIndex() {
id={v.id}
key={index}
type={'create'}
active={state.playingIndex == index}
active={checkedIdArray.includes(v.id)}
playing={state.playingId == v.id && state.videoPlaying}
checked={checkedIdArray.includes(v.id)}
className={`list-item-${index} mt-3 mb-2`}
className={`list-item-${index} mt-3 mb-2 list-item-state-${v.status} `}
onCheckedChange={(checked) => {
setCheckedIdArray(idArray => {
const newArr = checked ? idArray.concat(v.id) : idArray.filter(id => id != v.id);
@ -150,46 +255,61 @@ export default function VideoIndex() {
return newArr;
})
}}
onPlay={v.status == VideoStatus.Generating ? undefined :() => playVideo(v, index)}
onEdit={v.status == VideoStatus.Generating ? undefined : () => {
onItemClick={() => playVideo(v)}
onRemove={(action) => processDeleteVideo([v.id],action)}
onEdit={v.status == VideoStatus.Generated ? () => {
setEditId(v.article_id)
}}
}:undefined}
onRegenerate={v.status != VideoStatus.Generating && v.status != VideoStatus.Generated?()=>{
processGenerateVideo(v).catch(console.log)
}:undefined}
downloadUrl={v.oss_video_mp4_url}
hideCheckBox={v.status != VideoStatus.Generating && v.status != VideoStatus.Generated}
editable={v.status != VideoStatus.Generating}
sortable={v.status != VideoStatus.Generating}
sortable={v.status == VideoStatus.Generated}
/>))}
</SortableContext>
</DndContext>
</div>
</>}
}
<div className="h-[130px]"></div>
</InfiniteScroller>
</div>
</div>
<div className="text-right mt-5">
{/*<ButtonBatch*/}
{/* type={'primary'}*/}
{/* onProcess={push2room}*/}
{/* selected={checkedIdArray}*/}
{/* emptyMessage={`请选择要推流的新闻`}*/}
{/* confirmMessage={`是否确定一键推流选中新闻视频?`}*/}
{/* onSuccess={loadList}*/}
{/*>一键推流</ButtonBatch>*/}
<div className="page-action">
<ButtonToTop visible={state.showToTop} onClick={() => scrollerRef.current?.scrollToPosition(0)}/>
{
checkedIdArray.length > 0 && <ButtonBatch
onProcess={deleteFromList}
selected={checkedIdArray}
emptyMessage={t('video.delete_empty')}
title={
checkedIdArray.length == 1 ? t('video.delete_description', {count: checkedIdArray.length}) :
t('video.delete_description_count', {count: checkedIdArray.length})
}
className='bg-gray-300 hover:bg-gray-400 text-white'
confirmMessage={<span dangerouslySetInnerHTML={{
__html: checkedIdArray.length == 1 ?
t('video.delete_confirm', {count: checkedIdArray.length}) :
t('video.delete_confirm_count', {count: checkedIdArray.length})
}}></span>}
onSuccess={() => {
showToast(t('delete_success'), 'success')
loadList()
}}
>
<span className="text">{t('delete_batch')}</span>
<IconDelete/>
</ButtonBatch>
}
<ButtonPush2Room ids={checkedIdArray} list={videoData} onSuccess={loadList}/>
</div>
</div>
<div className="video-player-container ml-16 w-[360px] flex flex-col">
<div className="text-center text-base"></div>
<div className="video-player flex items-center mt-2">
<div className=" w-[360px] h-[630px] rounded overflow-hidden">
{/*<video ref={videoRef} poster={videoData[state.playingIndex]?.cover} preload="auto" playsinline webkit-playsinline className="w-full bg-white w-[360px] h-[640px]"></video>*/}
<Player
ref={player} url={videoData[state.playingIndex]?.oss_video_url}
onChange={(state) => {
if (state.end || state.end) setState({playingIndex: -1})
}}
className="w-[360px] h-[640px] bg-white"/>
</div>
</div>
</div>
</div>
<ArticleEditModal type={'video'} id={editId} onClose={() => setEditId(-1)}/>
<ArticleEditModal type={'video'} id={editId} onClose={(saved) =>{
setEditId(-1)
if(saved) {
loadList()
}
}}/>
</div>)
}

View File

@ -1,47 +1,53 @@
import React from "react";
import React, {useMemo} from "react";
import {isRouteErrorResponse, useNavigate, useRouteError} from 'react-router-dom';
import {Button} from "antd";
import error500 from "@/assets/images/error/Error500.png";
import {useTranslation} from "react-i18next";
// ==============================|| ELEMENT ERROR - COMMON ||============================== //
const ErrorBoundary: React.FC<{
minHeight?: string | number;
errorCode?: 401 | 404 | 503
}> = ({errorCode}) => {
const {t, i18n} = useTranslation()
const error = useRouteError() as Error;
let errorMessage = '服务异常,请稍后再试或者联系管理员.'
console.log(error)
const errorMessage = useMemo(() => {
let _message = t('error_500')
const errorConfig: {
[key: number]: string
} = {
401: '您没有权限访问本页面!',
404: '访问的页面不存在!',
503: '服务异常请联系管理员!',
401: t('error_401'),
403: t('error_403'),
404: t('error_404'),
500: t('error_500'),
}
if (isRouteErrorResponse(error)) {
if (errorConfig[error.status]) {
errorMessage = `Error ${error.status} - ${errorConfig[error.status]}`;
_message = `Error ${error.status} - ${errorConfig[error.status]}`;
}
}
if (errorCode) {
if (errorConfig[errorCode]) {
errorMessage = `Error ${errorCode} - ${errorConfig[errorCode]}`;
_message = `Error ${errorCode} - ${errorConfig[errorCode]}`;
}
}
return _message
}, [i18n, errorCode])
const navigate = useNavigate()
const handleGoBack = () => {
navigate('/')
}
return (<div className="max-w-screen-lg mx-auto flex items-center h-screen">
<div className={'flex flex-row '}>
return (<div className="max-w-screen-lg mx-auto flex justify-center items-center h-screen">
<div className={'flex flex-row items-center'}>
<div className="flex-col">
<div className="sm:w-396px">
<img src={error500} alt="error" className="w-full"/>
</div>
</div>
<div className="flex-col md:w-full ml-10">
<div className="flex-col md:w-full ml-20">
<div className="text-center">
<div className="text-4xl">
<div className="text-3xl">
Internal Server Error
</div>
<div className="text-gray-400 my-5">
@ -63,9 +69,9 @@ const ErrorBoundary: React.FC<{
}}>{error.stack}</code>
</pre>
</div>}
<div className="flex flex-grow gap-2 mt-5 justify-center">
<div className="flex flex-grow gap-2 mt-10 justify-center">
<Button type='primary' className="px-5" onClick={handleGoBack}>
<h1></h1>
<h1>{t('go_to_home')}</h1>
</Button>
</div>
</div>

View File

@ -1,18 +1,23 @@
import {createBrowserRouter, RouterProvider,} from "react-router-dom";
import {Suspense,} from "react";
import {ConfigProvider} from "antd";
import React, {Suspense, useEffect,} from "react";
import {ConfigProvider, App, Modal} from "antd";
import zhCN from 'antd/locale/zh_CN';
// for date-picker i18n
import dayjs from "dayjs";
import 'dayjs/locale/zh-cn';
import ErrorBoundary from "./error.tsx";
import Loader from "@/components/loader.tsx";
import routes from "@/routes/routes.tsx";
import {DocumentTitle} from "@/components/document.tsx";
import {useTranslation} from "react-i18next";
import useGlobalConfig from "@/hooks/useGlobalConfig.ts";
import {getRemainingDuration} from "@/service/api/order.ts";
import ModalWarning from "@/components/icons/ModalWarning.tsx";
const router = createBrowserRouter([
...routes,
{path: '*', element: <ErrorBoundary/>}
{path: '*', element: <ErrorBoundary errorCode={404}/>}
], {
basename: import.meta.env.VITE_APP_BASE_NAME,
future: {
@ -26,17 +31,56 @@ const router = createBrowserRouter([
// future={{v7_startTransition: true,v7_relativeSplatPath: true}}
const AppRouter = () => {
const {globalConfig} = useGlobalConfig();
const {t,i18n} = useTranslation();
const initRemainingDuration = () => {
getRemainingDuration().then(remain => {
if(remain <= 0){
Modal.warning({
wrapClassName:'root-modal-confirm',
title: t('confirm.title'),
icon: <ModalWarning.Icon/>,
content: t("order.remaining_duration_warning"),
okText: t('confirm.ok'),
centered: true
})
}
console.log('remain', remain)
})
}
useEffect(() => {
if(i18n.language){
if(i18n.language == 'multiple'){
const lang = localStorage.getItem('ai-human-lang') || (navigator.language.toLocaleLowerCase().indexOf('cn') != -1 ? 'zh-CN' : 'en-US')
i18n.changeLanguage(lang).catch(console.log)
return;
}
}
if (i18n && i18n.language == 'zh-CN') {
dayjs.locale('zh-cn');
}else{
dayjs.locale('en')
}
initRemainingDuration()
globalConfig.i18n = i18n.language
// i18n.changeLanguage(i18n).then(()=>console.log('change lang to ',i18n))
}, [i18n.language])
return (<ConfigProvider
locale={zhCN}
locale={i18n?.language?.toString() == 'zh-CN' ? zhCN : undefined}
theme={{
token: {
borderRadius: 4,
},
}}
>
<DocumentTitle title={t('AppTitle')}/>
<App className={`lang-${i18n.language}`}>
<Suspense fallback={<Loader/>}>
<RouterProvider future={{v7_startTransition: true}} router={router}/>
</Suspense>
</App>
</ConfigProvider>)
}

View File

@ -13,7 +13,7 @@ const AuthGuard = ({ children }:BasicComponentProps) => {
const location = useLocation();
useEffect(() => {
if (isInitialized && !isLoggedIn) {
if (isInitialized && !isLoggedIn && location.pathname !== '/user') {
navigate(`/user?from=${location.pathname}`, {
state: {
from: location.pathname

View File

@ -1,6 +1,7 @@
import {Outlet, useNavigate} from "react-router-dom";
import {Dropdown, MenuProps} from "antd";
import React from "react";
import {Outlet, useLocation, useNavigate} from "react-router-dom";
import {Divider, Dropdown, MenuProps} from "antd";
import React, {useEffect, useMemo} from "react";
import {useTranslation} from "react-i18next";
import AuthGuard from "@/routes/layout/auth-guard.tsx";
import {LogoText} from "@/components/icons/logo.tsx";
@ -10,44 +11,101 @@ import {DashboardNavigation} from "@/routes/layout/dashboard-navigation.tsx";
import useAuth from "@/hooks/useAuth.ts";
import {hidePhone} from "@/util/strings.ts";
import {defaultCache} from "@/hooks/useCache.ts";
import {IconOrderFill, IconRecycleFill} from "@/components/icons";
import LanguageSwitcher from "@/components/icons/language-switcher.tsx";
type LayoutProps = {
children: React.ReactNode
}
const NavigationUserContainer = () => {
const {t} = useTranslation()
const {logout, user} = useAuth()
const navigate = useNavigate()
const handleLogout = () => {
logout().then(() => navigate('/user'))
}
const items: MenuProps['items'] = [
{
key: 'profile',
label: '个人中心',
key: 'order',
label: <div className="nav-item" onClick={() => navigate('/order')}>
<IconOrderFill/>
<span className={"nav-text"}>{t('order.text')}</span>
</div>,
},
{
key: 'logout',
label: <div onClick={()=>{
logout().then(()=>navigate('/user'))
}}>退</div>,
key: 'recycle',
label: <div className="nav-item" onClick={() => navigate('/recycle')}>
<IconRecycleFill/>
<span className={"nav-text"}>{t('history.text')}</span>
</div>,
},
// {
// key: 'logout',
// label: <div onClick={handleLogout}>退出</div>,
// },
];
const UserButton = () => (<div
className={`flex items-center rounded-3xl ${user ? 'bg-[#e3eeff]' : 'bg-primary-blue'} p-1 pr-2 cursor-pointer rounded`}>
<UserAvatar className="user-avatar size-7"/>
{user ? <span className={"username ml-2 text-sm"}>{hidePhone(user.nickname)}</span> : (
<span className="text-sm mx-2 text-white">{t('login.title')}</span>
)}
</div>)
return (<div className={"flex items-center justify-between gap-2 ml-10"}>
<Dropdown menu={{items}} placement="bottom" arrow>
<div className="flex items-center hover:bg-gray-100 px-2 py-1 cursor-pointer rounded">
<UserAvatar className="user-avatar size-8"/>
<span className={"username ml-2 text-sm"}>{hidePhone(user?.nickname)}</span>
{user ? <Dropdown
rootClassName={'z-[999999] userinfo-drop-menu'}
menu={{items}} placement="bottomRight"
dropdownRender={(menu) => (
<div>
<div className="user-profile flex gap-4">
<div className="avatar"><UserAvatar className="user-avatar"/></div>
<div className="info">
<div>{user?.nickname}</div>
<div>ID: {user?.id}</div>
</div>
</Dropdown>
</div>
<Divider style={{margin: 0}}/>
<div className="menu-list-container">
{menu}
</div>
<Divider style={{margin: 0}}/>
<div className="logout">
<div onClick={handleLogout}>{t('user.logout')}</div>
</div>
</div>
)}
>
<div><UserButton/></div>
</Dropdown> : <UserButton/>}
</div>)
}
const ExtraNavItems = {
'/order':'order.text',
'/recycle':'history.text',
}
export const BaseLayout: React.FC<LayoutProps> = ({children}) => {
const {pathname} = useLocation()
const {t,i18n} = useTranslation()
const extraNav = useMemo(()=>{
if(!pathname || !ExtraNavItems[pathname]) return null
return t(ExtraNavItems[pathname])
},[pathname,i18n.language])
return (<div className={'dashboard-layout min-h-screen'}>
<div className="min-h-screen w-full">
<div className="app-header">
<div className="logo-container">
<LogoText/>
<div className="logo-container flex items-center">
<LogoText style={{fontSize: 30}}/>
{extraNav && <div className="extra-nav-name ml-2">
<span className="nav-item active">{extraNav}</span>
</div>}
</div>
<div className="flex items-center">
<DashboardNavigation/>
<div className="flex items-center">
<LanguageSwitcher />
<NavigationUserContainer/>
</div>
</div>
@ -62,7 +120,16 @@ export const BaseLayout: React.FC<LayoutProps> = ({children}) => {
const DashboardLayout: React.FC<{ children?: React.ReactNode }> = ({children}) => {
const loc = useLocation()
const navigate = useNavigate()
useEffect(() => {
if (!defaultCache.firstLoadPath && loc.pathname == '/live') {
defaultCache.firstLoadPath = loc.pathname;
navigate('/')
}
}, [])
return <AuthGuard>
<div className="fixed">{defaultCache.firstLoadPath}</div>
<BaseLayout>
{children ? children : <Outlet/>}
</BaseLayout>

View File

@ -1,47 +1,56 @@
import {clsx} from "clsx";
import {NavLink} from "react-router-dom";
import {IconNavigationArrow} from "@/components/icons";
import useAuth from "@/hooks/useAuth.ts";
import {useMemo} from "react";
import {useTranslation} from "react-i18next";
const NavItems = [
export function DashboardNavigation() {
const {t,i18n} = useTranslation()
const {user} = useAuth()
const NavItems = useMemo(()=>([
{
key: 'news',
name: '新闻素材',
name: t('nav.materials'),
icon: 'news',
path: '/'
},
{
key: 'video',
name: '新闻编辑',
name: t('nav.editing'),
icon: 'e',
path: '/edit'
},
{
key: 'create',
name: '视频生成',
name: t('nav.generating'),
icon: 'ai',
path: '/create'
},
{
key: 'library',
name: '视频库',
icon: '+',
path:'/library'
},
// {
// key: 'library',
// name: '视频库',
// icon: '+',
// path:'/library'
// },
{
key: 'live',
name: '数字人直播间',
name: t('nav.live'),
icon: 'v',
path: '/live'
}
]
export function DashboardNavigation() {
]),[i18n.language])
return (<div className={'flex app-main-navigation'}>
{NavItems.map((it, idx) => (
<NavLink to={it.path} key={idx} className={clsx('nav-item cursor-pointer items-center')}>
<span className="menu-text ml-1">{it.name}</span>
</NavLink>
))}
{NavItems.map((it, idx) => (<div key={idx} className={"flex items-center"}>
{user ? <NavLink to={it.path} className={clsx('nav-item cursor-pointer items-center')}>
<span className="menu-text ml-2">{it.name}</span>
</NavLink> : <div className={clsx('nav-item cursor-pointer items-center')}>
<span className="menu-text ml-2">{it.name}</span>
</div>}
{idx !== NavItems.length - 1 && <IconNavigationArrow className="ml-2" />}
</div>))}
</div>
);
}

View File

@ -1,22 +1,19 @@
import {RouteObject} from "react-router-dom";
import ErrorBoundary from "@/routes/error.tsx";
;
import DashboardLayout from "@/routes/layout/dashboard-layout.tsx";
import React from "react";
import ErrorBoundary from "@/routes/error.tsx";
import DashboardLayout from "@/routes/layout/dashboard-layout.tsx";
const UserAuth = React.lazy(() => import("@/pages/user"))
const CreateVideoIndex = React.lazy(() => import("@/pages/video"))
const LibraryIndex = React.lazy(() => import("@/pages/library"))
const RecycleIndex = React.lazy(() => import("../pages/recycle"))
const LiveIndex = React.lazy(() => import("@/pages/live"))
const NewsIndex = React.lazy(() => import("@/pages/news"))
const NewsEdit = React.lazy(() => import("@/pages/news/edit.tsx"))
const OrderIndex = React.lazy(() => import("@/pages/order/index.tsx"))
const routes: RouteObject[] = [
{
path: '/user',
element: <UserAuth/>,
},
{
path: '/',
element: <DashboardLayout/>,
@ -26,6 +23,10 @@ const routes: RouteObject[] = [
path: '',
element: <NewsIndex/>
},
{
path: 'user',
element: <UserAuth/>,
},
{
path: 'edit',
element: <NewsEdit/>
@ -35,8 +36,12 @@ const routes: RouteObject[] = [
element: <CreateVideoIndex/>
},
{
path: 'library',
element: <LibraryIndex/>
path: 'recycle',
element: <RecycleIndex/>
},
{
path: 'order',
element: <OrderIndex/>
},
{
path: 'live',

View File

@ -12,21 +12,16 @@ export function getList(data: ApiArticleSearchParams) {
*
* @param id
*/
export function deleteById(id: Id) {
throw new Error('Not implement')
return post<{ article: any }>({url: '/article/delete/' + id})
export function deleteByIds(article_ids: Id[]) {
return post('/article/remove',{article_ids})
}
export function getById(id: Id) {
return post<ArticleDetail>({url: '/article/detail/' + id})
}
export function save(title: string, content_group: BlockContent[][], id?: number) {
return post<{ content: string }>(id && id > 0 ? '/article/modify' : '/article/create/new', {
title,
content_group,
id
})
export function save(params:ArticleSaveParam) {
return post<{ content: string }>(params.id && params.id > 0 ? '/article/modify' : '/article/create/new',params)
}
export function push2video(article_ids: Id[]) {

View File

@ -1,4 +1,4 @@
import {post} from "@/service/request.ts";
import {get, post} from "@/service/request.ts";
export function playState() {
return post<LiveState>({url: '/room/playing'})
@ -15,3 +15,13 @@ export function modifyOrder(ids: Id[]) {
export function deleteByIds(ids: Id[]) {
return post('/room/remove', {ids})
}
export function restoreByIds(ids: Id[]) {
return post('/room/restore', {ids})
}
export function getLiveUrl() {
return get<{flv_url:string}>({
url: '/tencent/get_pull_url',
baseURL: '/api/v1'
})
}

15
src/service/api/order.ts Normal file
View File

@ -0,0 +1,15 @@
import {post} from "@/service/request.ts";
type OrderInfoData = DataList<OrderInfo> & {
remaining_duration: string | number;
}
export function getList(params: OrderSearchParam) {
return post<OrderInfoData>('/order/list', params)
}
export async function getRemainingDuration() {
const result = await getList({pagination: {page: 1, limit: 1}})
return Number(result.remaining_duration)
}

View File

@ -0,0 +1,11 @@
import {post} from "@/service/request.ts";
export function getList(params: NormalSearchParams) {
return post<DataList<VideoInfo>>('/recycle/list', params)
}
export function remove(ids: Id[]) {
return post('/recycle/remove', {ids})
}
export function restore(ids: Id[]) {
return post('/recycle/restore', {ids})
}

View File

@ -1,4 +1,5 @@
import {post} from "@/service/request.ts";
import {getById as getArticle} from "./article"
export function getList() {
return post<DataList<VideoInfo>>('/video/list')
@ -6,28 +7,41 @@ export function getList() {
export function search(params:VideoSearchParams) {
return post<DataList<VideoInfo>>('/video/search',params)
}
export function deleteHistories(ids: Id[]) {
return post('/video/history/remove', {ids})
}
/**
*
* @param title
* @param content_group
* @param article_id
*/
export function regenerate(title: string, content_group: BlockContent[][], article_id?: Id) {
export function regenerate(params:{title: string, metahuman_text: string, content_group: BlockContent[][], id?: Id}) {
return post<{ content: string }>({
url: '/video/regenerate',
data: {
title,
content_group,
article_id
...params,
article_id:params.id
}
})
}
// 重新生成视频
export async function regenerateById(article_id: Id) {
const article = await getArticle(article_id);
return await regenerate({
title:article.title,
metahuman_text:article.metahuman_text,
content_group:article.content_group,
id:article_id
})
}
export function getById(id: Id) {
return post<VideoInfo>({url: '/video/detail/' + id})
}
export function deleteByIds(ids: Id[]) {
export function deleteFromList(ids: Id[]) {
return post('/video/remove', {ids})
}

View File

@ -2,6 +2,7 @@ import axios from 'axios';
import {stringify} from 'qs'
import {BizError} from './types';
import {getAuthToken} from "@/hooks/useAuth.ts";
import useGlobalConfig from '@/hooks/useGlobalConfig';
const JSON_FORMAT: string = 'application/json';
const REQUEST_TIMEOUT = 300000; // 超时时长5min
@ -11,10 +12,20 @@ const Axios = axios.create({
headers: {'Content-Type': JSON_FORMAT}
})
// eslint-disable-next-line react-hooks/rules-of-hooks
const {globalConfig} = useGlobalConfig();
// 请求前拦截
Axios.interceptors.request.use(config => {
const token = getAuthToken();
if (globalConfig.i18n){
let url = config.url;
if(url){
url += (url.indexOf('?') == -1?'?':'&') + `lang=${globalConfig.i18n || ''}`
config.url = url;
}
//config.headers['language'] = globalConfig.i18n;
}
if (token) {
config.headers['Token'] = `${token}`;
}
@ -71,12 +82,13 @@ export function post<T>(params: RequestOption | string, _data?: AllType) {
})
}
export function get<T>(params: RequestOption) {
if (params.data) {
params.url += (params.url.indexOf('?') === -1 ? '?' : '&') + stringify(params.data)
export function get<T>(params: RequestOption|string) {
const options = typeof params === 'string' ? {url: params} : params;
if (options.data) {
options.url += (options.url.indexOf('?') === -1 ? '?' : '&') + stringify(options.data)
}
return request<T>({
...params,
...options,
method: 'get'
})
}

42
src/types/api.d.ts vendored
View File

@ -2,7 +2,8 @@ declare interface ApiRequestPageParams {
pagination: {
page: number;
limit: number;
}
};
request_time?: number;
}
declare interface ApiArticleSearchParams extends ApiRequestPageParams{
@ -58,10 +59,15 @@ interface BasicArticleInfo {
id: number;
title: string;
summary: string;
metahuman_text: string;
publish_time: string;
media_name: string;
column_name?: string;
img_num?: number;
content_word_count?: number;
media_id: number;
fanwen_column_id: number;
hot_news: string;
}
/**
@ -80,6 +86,11 @@ declare interface ListCrawlerNewsItem extends BasicArticleInfo {
// 内部文章关联id
internal_article_id: number;
}
declare interface NormalSearchParams extends ApiRequestPageParams{
// 标题
title?: string;
time_flag?: number;
}
declare interface VideoSearchParams extends ApiRequestPageParams{
// 标题
title?: string;
@ -91,10 +102,16 @@ declare interface VideoInfo {
title: string;
cover: string;
oss_video_url: string;
oss_video_mp4_url?: string;
duration: number;
article_id: number;
status: number;
publish_time?: number|string;
ctime?: number|string;
d_time?: number|string;
}
declare interface VideoListItem extends VideoInfo {
playing?: boolean;
}
// room live
declare interface LiveVideoInfo {
@ -104,12 +121,35 @@ declare interface LiveVideoInfo {
video_title: string;
cover: string;
video_duration: number;
oss_video_url?: string;
video_oss_url: string;
status: number;
order_no: string;
publish_time?: number|string;
ctime?: number|string;
}
declare interface LiveState{
id: number;
live_start_time: number;
}
// order
declare interface OrderSearchParam extends ApiRequestPageParams{
// 标题
title?: string;
time_flag?: number;
}
declare interface OrderInfo {
order_id: number| string;
// 缩略图
img_url: string;
// 标题
title: string;
// 下单时间
order_time: number | string;
// 消费时长
consumption_duration: number;
// 操作人
operator: string;
}

View File

@ -1,4 +1,4 @@
type I18n = 'en-US' | 'zh-CN' | 'zh-HK';
type I18n = 'en-US' | 'zh-CN' | 'zh-HK' | string;
type ConfigProps = {
fontFamily: string;

29
src/types/core.d.ts vendored
View File

@ -28,13 +28,40 @@ declare interface ArticleContentGroup {
blocks: BlockContent[];
}
interface TemplateOption {
background: string;
template_id: string;
}
interface ArticleTemplateInfo {
select: string;
options: TemplateOption[];
}
interface ArticleSaveParam {
title: string;
metahuman_text: string;
video_tag?: string;
background?: string;
content_group: BlockContent[][];
hot_news: string[];
id?: number;
}
declare interface ArticleDetail {
id: number;
title: string;
content_group: BlockContent[][]
metahuman_text: string;
video_tag: string;
template_info: ArticleTemplateInfo;
hot_news_mode?: string;
hot_news: string[]; // 4月 6 日新增
content_group: BlockContent[][];
}
declare interface NewsInfo {
id: number;
internal_article_id: number;
title: string;
content: string;
media_name: string;

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