Compare commits
No commits in common. "factory" and "factory_trust" have entirely different histories.
factory
...
factory_tr
|
|
@ -1,17 +0,0 @@
|
|||
name: Deploy API
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
env:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: https://gitlink.org.cn/actions/checkout@v4
|
||||
- name: Deploy API
|
||||
run: |
|
||||
echo "Deploying API to ${{ inputs.env }}"
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
name: Deploy Web
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
env:
|
||||
required: true
|
||||
type: string
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: https://gitlink.org.cn/actions/checkout@v4
|
||||
- name: Deploy Web
|
||||
run: |
|
||||
echo "Deploying Web to ${{ inputs.env }}"
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
name: Context testing
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
dump_contexts_to_log:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "SELECTED_COLOR=green" >> "$GITHUB_ENV"
|
||||
- run: echo "The selected color is ${{env.SELECTED_COLOR}}"
|
||||
- name: Dump GitHub context
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- name: Dump job context
|
||||
env:
|
||||
JOB_CONTEXT: ${{ toJson(job) }}
|
||||
run: |
|
||||
echo "$JOB_CONTEXT"
|
||||
echo "adsfsf=========${{ github.action }}"
|
||||
- name: env
|
||||
run: env
|
||||
- name: Dump steps context
|
||||
id: steps_context
|
||||
env:
|
||||
STEPS_CONTEXT: ${{ toJson(steps) }}
|
||||
run: |
|
||||
echo "$STEPS_CONTEXT"
|
||||
echo "adsfsf=========${{ github.action }}"
|
||||
echo "ACTIONS_RUNTIME_TOKEN====================${{ACTIONS_RUNTIME_TOKEN}}"
|
||||
- name: Dump runner context
|
||||
env:
|
||||
RUNNER_CONTEXT: ${{ toJson(runner) }}
|
||||
run: echo "$RUNNER_CONTEXT"
|
||||
- name: Dump needs context
|
||||
env:
|
||||
NEEDS_CONTEXT: ${{ toJson(needs) }}
|
||||
run: echo "$NEEDS_CONTEXT"
|
||||
- name: Dump strategy context
|
||||
env:
|
||||
STRATEGY_CONTEXT: ${{ toJson(strategy) }}
|
||||
run: echo "$STRATEGY_CONTEXT"
|
||||
- name: Dump matrix context
|
||||
env:
|
||||
MATRIX_CONTEXT: ${{ toJson(matrix) }}
|
||||
run: echo "$MATRIX_CONTEXT"
|
||||
- name: Dump steps context2
|
||||
id: steps_context2
|
||||
env:
|
||||
STEPS_CONTEXT: ${{ toJson(steps) }}
|
||||
run: |
|
||||
echo "$STEPS_CONTEXT"
|
||||
echo "adsfsf=========${{ github.action }}"
|
||||
echo "ACTIONS_RUNTIME_TOKEN====================${{ACTIONS_RUNTIME_TOKEN}}"
|
||||
- name: Dump GitHub context2
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: sleep 33 && echo "$GITHUB_CONTEXT"
|
||||
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
name: Context testing
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
run-01:
|
||||
uses: ./.gitea/workflows/上下文信息.yml
|
||||
run-02:
|
||||
uses: ./.gitea/workflows/声明流水线.yml
|
||||
run-03:
|
||||
uses: ./.gitea/workflows/deploy-api.yml
|
||||
run-04:
|
||||
uses: ./.gitea/workflows/deploy-web.yml
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
name: Context testing
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
dump_contexts_to_log:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "SELECTED_COLOR=green" >> "$GITHUB_ENV"
|
||||
- run: echo "The selected color is ${{env.SELECTED_COLOR}}"
|
||||
- name: Dump GitHub context
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- name: Dump job context
|
||||
env:
|
||||
JOB_CONTEXT: ${{ toJson(job) }}
|
||||
run: |
|
||||
echo "$JOB_CONTEXT"
|
||||
echo "adsfsf=========${{ github.action }}"
|
||||
- name: env
|
||||
run: env
|
||||
- name: Dump steps context
|
||||
id: steps_context
|
||||
env:
|
||||
STEPS_CONTEXT: ${{ toJson(steps) }}
|
||||
run: |
|
||||
echo "$STEPS_CONTEXT"
|
||||
echo "adsfsf=========${{ github.action }}"
|
||||
echo "ACTIONS_RUNTIME_TOKEN====================${{ACTIONS_RUNTIME_TOKEN}}"
|
||||
- name: Dump runner context
|
||||
env:
|
||||
RUNNER_CONTEXT: ${{ toJson(runner) }}
|
||||
run: echo "$RUNNER_CONTEXT"
|
||||
- name: Dump needs context
|
||||
env:
|
||||
NEEDS_CONTEXT: ${{ toJson(needs) }}
|
||||
run: echo "$NEEDS_CONTEXT"
|
||||
- name: Dump strategy context
|
||||
env:
|
||||
STRATEGY_CONTEXT: ${{ toJson(strategy) }}
|
||||
run: echo "$STRATEGY_CONTEXT"
|
||||
- name: Dump matrix context
|
||||
env:
|
||||
MATRIX_CONTEXT: ${{ toJson(matrix) }}
|
||||
run: echo "$MATRIX_CONTEXT"
|
||||
- name: Dump steps context2
|
||||
id: steps_context2
|
||||
env:
|
||||
STEPS_CONTEXT: ${{ toJson(steps) }}
|
||||
run: |
|
||||
echo "$STEPS_CONTEXT"
|
||||
echo "adsfsf=========${{ github.action }}"
|
||||
echo "ACTIONS_RUNTIME_TOKEN====================${{ACTIONS_RUNTIME_TOKEN}}"
|
||||
- name: Dump GitHub context2
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
|
||||
|
|
@ -56,4 +56,4 @@ export const deepRoutes = [
|
|||
]
|
||||
|
||||
// 不进行组织或个人判断
|
||||
export const specialRoute = ["ossRepo", "settings", "setting", "mulan", "wiki", "issues", "setting", "trending", "code", "projects", "pulls", "mine", "login", "register", "email", "export", "nopage", "404", "403", "500", "501", "search", "organize", "login", "register", "resetPassword", "aboutus","educoder", "glcc","bindlogin", "softbot","zone", "undefined", "admins", "oauth", "competitions", "hiagent","sf", "development", "governance","explore"]
|
||||
export const specialRoute = ["explore", "settings", "setting", "mulan", "wiki", "issues", "setting", "trending", "code", "projects", "pulls", "mine", "login", "register", "email", "export", "nopage", "404", "403", "500", "501", "search", "organize", "login", "register", "resetPassword", "aboutus","educoder", "glcc","bindlogin", "softbot","zone", "undefined", "admins", "oauth", "competitions", "hiagent","sf", "development", "governance"]
|
||||
|
|
@ -83,7 +83,6 @@
|
|||
"numeral": "^2.0.6",
|
||||
"nvm": "0.0.4",
|
||||
"object-assign": "4.1.1",
|
||||
"p-queue": "^9.2.0",
|
||||
"papaparse": "^5.3.2",
|
||||
"pinyin": "^4.0.0-alpha.0",
|
||||
"postcss-flexbugs-fixes": "3.2.0",
|
||||
|
|
@ -139,7 +138,6 @@
|
|||
"showdown": "^1.9.1",
|
||||
"showdown-katex": "^0.8.0",
|
||||
"slick-carousel": "^1.8.1",
|
||||
"spark-md5": "^3.0.2",
|
||||
"store": "^2.0.12",
|
||||
"styled-components": "^4.4.1",
|
||||
"webpack-node-externals": "^3.0.0",
|
||||
|
|
|
|||
|
|
@ -28,10 +28,6 @@
|
|||
font-family: "DouyinSansBold";
|
||||
src: url("../fonts/DouyinSansBold.ttf");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "DingTalk JinBuTi";
|
||||
src: url("../fonts/DingTalk_JinBuTi.ttf");
|
||||
}
|
||||
.header {
|
||||
width: 100%;
|
||||
height: 51px;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 2340181 */
|
||||
src: url('iconfont.woff2?t=1784688510240') format('woff2'),
|
||||
url('iconfont.woff?t=1784688510240') format('woff'),
|
||||
url('iconfont.ttf?t=1784688510240') format('truetype');
|
||||
src: url('iconfont.woff2?t=1765938619495') format('woff2'),
|
||||
url('iconfont.woff?t=1765938619495') format('woff'),
|
||||
url('iconfont.ttf?t=1765938619495') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
|
|
@ -13,302 +13,6 @@
|
|||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-zhinengti:before {
|
||||
content: "\ea0f";
|
||||
}
|
||||
|
||||
.icon-a-fabu1:before {
|
||||
content: "\ea0d";
|
||||
}
|
||||
|
||||
.icon-a-search-ai6c9d7b621:before {
|
||||
content: "\ea0e";
|
||||
}
|
||||
|
||||
.icon-remenhuati:before {
|
||||
content: "\e9df";
|
||||
}
|
||||
|
||||
.icon-chengguolingyu:before {
|
||||
content: "\ea09";
|
||||
}
|
||||
|
||||
.icon-chengguoleixing:before {
|
||||
content: "\ea0a";
|
||||
}
|
||||
|
||||
.icon-chengguolaiyuan:before {
|
||||
content: "\ea0b";
|
||||
}
|
||||
|
||||
.icon-zhinengzhushou:before {
|
||||
content: "\ea08";
|
||||
}
|
||||
|
||||
.icon-shine-search:before {
|
||||
content: "\ea07";
|
||||
}
|
||||
|
||||
.icon-zuojiantou3:before {
|
||||
content: "\ea05";
|
||||
}
|
||||
|
||||
.icon-youjiantou2:before {
|
||||
content: "\ea06";
|
||||
}
|
||||
|
||||
.icon-fanhuagongju:before {
|
||||
content: "\ea02";
|
||||
}
|
||||
|
||||
.icon-jingxuanxiangmu1:before {
|
||||
content: "\ea03";
|
||||
}
|
||||
|
||||
.icon-gengduo2:before {
|
||||
content: "\ea04";
|
||||
}
|
||||
|
||||
.icon-shengwuyiyao:before {
|
||||
content: "\e9f5";
|
||||
}
|
||||
|
||||
.icon-youjiantou1:before {
|
||||
content: "\e9fa";
|
||||
}
|
||||
|
||||
.icon-zuojiantou2:before {
|
||||
content: "\e9fd";
|
||||
}
|
||||
|
||||
.icon-daqihaiyanghuanjing:before {
|
||||
content: "\e9fe";
|
||||
}
|
||||
|
||||
.icon-tianwendiliwuli:before {
|
||||
content: "\e9ff";
|
||||
}
|
||||
|
||||
.icon-xinnengyuanxincailiao:before {
|
||||
content: "\ea00";
|
||||
}
|
||||
|
||||
.icon-gongyeshejizhizao:before {
|
||||
content: "\ea01";
|
||||
}
|
||||
|
||||
.icon-tiezi:before {
|
||||
content: "\e9f4";
|
||||
}
|
||||
|
||||
.icon-yulan:before {
|
||||
content: "\e9f1";
|
||||
}
|
||||
|
||||
.icon-arrright:before {
|
||||
content: "\e9fc";
|
||||
}
|
||||
|
||||
.icon-yunxiaogongju:before {
|
||||
content: "\e9f6";
|
||||
}
|
||||
|
||||
.icon-daimazhijian:before {
|
||||
content: "\e9f7";
|
||||
}
|
||||
|
||||
.icon-yidongkaifa:before {
|
||||
content: "\e9f8";
|
||||
}
|
||||
|
||||
.icon-qita:before {
|
||||
content: "\e9f9";
|
||||
}
|
||||
|
||||
.icon-meitichuli:before {
|
||||
content: "\e9fb";
|
||||
}
|
||||
|
||||
.icon-skill:before {
|
||||
content: "\e9f3";
|
||||
}
|
||||
|
||||
.icon-daimaku1:before {
|
||||
content: "\e9f2";
|
||||
}
|
||||
|
||||
.icon-dianzan5:before {
|
||||
content: "\e9ee";
|
||||
}
|
||||
|
||||
.icon-duomotaimoxing:before {
|
||||
content: "\e9ef";
|
||||
}
|
||||
|
||||
.icon-a-kexueyanjiuhejishufuwuye-1:before {
|
||||
content: "\e9f0";
|
||||
}
|
||||
|
||||
.icon-yinpin-1:before {
|
||||
content: "\e9e2";
|
||||
}
|
||||
|
||||
.icon-shujuji-4:before {
|
||||
content: "\e9e4";
|
||||
}
|
||||
|
||||
.icon-shujuji:before {
|
||||
content: "\e9e5";
|
||||
}
|
||||
|
||||
.icon-shujuyuanmoxing:before {
|
||||
content: "\e9e6";
|
||||
}
|
||||
|
||||
.icon-shujuji-3:before {
|
||||
content: "\e9e7";
|
||||
}
|
||||
|
||||
.icon-a-shujuji5:before {
|
||||
content: "\e9e8";
|
||||
}
|
||||
|
||||
.icon-a-shujuji-6:before {
|
||||
content: "\e9e9";
|
||||
}
|
||||
|
||||
.icon-shipinbofang-1:before {
|
||||
content: "\e9ea";
|
||||
}
|
||||
|
||||
.icon-moxingku:before {
|
||||
content: "\e9eb";
|
||||
}
|
||||
|
||||
.icon-a-shijianzhongbiao:before {
|
||||
content: "\e9ec";
|
||||
}
|
||||
|
||||
.icon-remenmoxing-2:before {
|
||||
content: "\e9ed";
|
||||
}
|
||||
|
||||
.icon-a-Dshujukukaifagongju:before {
|
||||
content: "\e9cd";
|
||||
}
|
||||
|
||||
.icon-ziranyuyanchuli-2:before {
|
||||
content: "\e9db";
|
||||
}
|
||||
|
||||
.icon-a-zu18214:before {
|
||||
content: "\e9dc";
|
||||
}
|
||||
|
||||
.icon-zizhushujuji:before {
|
||||
content: "\e9dd";
|
||||
}
|
||||
|
||||
.icon-yuyin-1:before {
|
||||
content: "\e9de";
|
||||
}
|
||||
|
||||
.icon-a-tuxiang-1:before {
|
||||
content: "\e9e0";
|
||||
}
|
||||
|
||||
.icon-xiazai3:before {
|
||||
content: "\e9e1";
|
||||
}
|
||||
|
||||
.icon-shijian1:before {
|
||||
content: "\e9cb";
|
||||
}
|
||||
|
||||
.icon-liulanliang:before {
|
||||
content: "\e9cc";
|
||||
}
|
||||
|
||||
.icon-guanbi2:before {
|
||||
content: "\e9ca";
|
||||
}
|
||||
|
||||
.icon-zhanshishangxian1:before {
|
||||
content: "\e9d3";
|
||||
}
|
||||
|
||||
.icon-tianxiexinxi:before {
|
||||
content: "\e9d8";
|
||||
}
|
||||
|
||||
.icon-tijiaoshenqing1:before {
|
||||
content: "\e9d9";
|
||||
}
|
||||
|
||||
.icon-guanliyuanshenhe1:before {
|
||||
content: "\e9da";
|
||||
}
|
||||
|
||||
.icon-sixin2:before {
|
||||
content: "\e9e3";
|
||||
}
|
||||
|
||||
.icon-dianzan4:before {
|
||||
content: "\e9d6";
|
||||
}
|
||||
|
||||
.icon-liulanshu:before {
|
||||
content: "\e9d7";
|
||||
}
|
||||
|
||||
.icon-tiezishu:before {
|
||||
content: "\e9ce";
|
||||
}
|
||||
|
||||
.icon-yanjiulingyu:before {
|
||||
content: "\e9cf";
|
||||
}
|
||||
|
||||
.icon-a-xiugai3:before {
|
||||
content: "\e9d0";
|
||||
}
|
||||
|
||||
.icon-yitijiaoderuzhuxinxi:before {
|
||||
content: "\e9d1";
|
||||
}
|
||||
|
||||
.icon-sixin:before {
|
||||
content: "\e9d2";
|
||||
}
|
||||
|
||||
.icon-gerenjianjie:before {
|
||||
content: "\e9d4";
|
||||
}
|
||||
|
||||
.icon-guanzhu1:before {
|
||||
content: "\e9d5";
|
||||
}
|
||||
|
||||
.icon-woyefatie:before {
|
||||
content: "\e9c9";
|
||||
}
|
||||
|
||||
.icon-yingyongmoban:before {
|
||||
content: "\e9c8";
|
||||
}
|
||||
|
||||
.icon-zhuangtai:before {
|
||||
content: "\e9c5";
|
||||
}
|
||||
|
||||
.icon-fuzhi-shanchu1:before {
|
||||
content: "\e9c6";
|
||||
}
|
||||
|
||||
.icon-mobanguanli:before {
|
||||
content: "\e9c7";
|
||||
}
|
||||
|
||||
.icon-svg_jiantou1:before {
|
||||
content: "\e9c3";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,524 +5,6 @@
|
|||
"css_prefix_text": "icon-",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "48129347",
|
||||
"name": "智能体",
|
||||
"font_class": "zhinengti",
|
||||
"unicode": "ea0f",
|
||||
"unicode_decimal": 59919
|
||||
},
|
||||
{
|
||||
"icon_id": "48114624",
|
||||
"name": "发布 (1)",
|
||||
"font_class": "a-fabu1",
|
||||
"unicode": "ea0d",
|
||||
"unicode_decimal": 59917
|
||||
},
|
||||
{
|
||||
"icon_id": "48114623",
|
||||
"name": "search-ai.6c9d7b62 (1)",
|
||||
"font_class": "a-search-ai6c9d7b621",
|
||||
"unicode": "ea0e",
|
||||
"unicode_decimal": 59918
|
||||
},
|
||||
{
|
||||
"icon_id": "48114629",
|
||||
"name": "热门话题",
|
||||
"font_class": "remenhuati",
|
||||
"unicode": "e9df",
|
||||
"unicode_decimal": 59871
|
||||
},
|
||||
{
|
||||
"icon_id": "47969738",
|
||||
"name": "成果领域",
|
||||
"font_class": "chengguolingyu",
|
||||
"unicode": "ea09",
|
||||
"unicode_decimal": 59913
|
||||
},
|
||||
{
|
||||
"icon_id": "47969739",
|
||||
"name": "成果类型",
|
||||
"font_class": "chengguoleixing",
|
||||
"unicode": "ea0a",
|
||||
"unicode_decimal": 59914
|
||||
},
|
||||
{
|
||||
"icon_id": "47969740",
|
||||
"name": "成果来源",
|
||||
"font_class": "chengguolaiyuan",
|
||||
"unicode": "ea0b",
|
||||
"unicode_decimal": 59915
|
||||
},
|
||||
{
|
||||
"icon_id": "45990748",
|
||||
"name": "智能助手",
|
||||
"font_class": "zhinengzhushou",
|
||||
"unicode": "ea08",
|
||||
"unicode_decimal": 59912
|
||||
},
|
||||
{
|
||||
"icon_id": "47826921",
|
||||
"name": "shine-search",
|
||||
"font_class": "shine-search",
|
||||
"unicode": "ea07",
|
||||
"unicode_decimal": 59911
|
||||
},
|
||||
{
|
||||
"icon_id": "47798947",
|
||||
"name": "左箭头",
|
||||
"font_class": "zuojiantou3",
|
||||
"unicode": "ea05",
|
||||
"unicode_decimal": 59909
|
||||
},
|
||||
{
|
||||
"icon_id": "47798946",
|
||||
"name": "右箭头",
|
||||
"font_class": "youjiantou2",
|
||||
"unicode": "ea06",
|
||||
"unicode_decimal": 59910
|
||||
},
|
||||
{
|
||||
"icon_id": "47778051",
|
||||
"name": "泛化工具",
|
||||
"font_class": "fanhuagongju",
|
||||
"unicode": "ea02",
|
||||
"unicode_decimal": 59906
|
||||
},
|
||||
{
|
||||
"icon_id": "47778050",
|
||||
"name": "精选项目",
|
||||
"font_class": "jingxuanxiangmu1",
|
||||
"unicode": "ea03",
|
||||
"unicode_decimal": 59907
|
||||
},
|
||||
{
|
||||
"icon_id": "47778049",
|
||||
"name": "更多",
|
||||
"font_class": "gengduo2",
|
||||
"unicode": "ea04",
|
||||
"unicode_decimal": 59908
|
||||
},
|
||||
{
|
||||
"icon_id": "47778058",
|
||||
"name": "生物医药",
|
||||
"font_class": "shengwuyiyao",
|
||||
"unicode": "e9f5",
|
||||
"unicode_decimal": 59893
|
||||
},
|
||||
{
|
||||
"icon_id": "47778057",
|
||||
"name": "右箭头",
|
||||
"font_class": "youjiantou1",
|
||||
"unicode": "e9fa",
|
||||
"unicode_decimal": 59898
|
||||
},
|
||||
{
|
||||
"icon_id": "47778056",
|
||||
"name": "左箭头",
|
||||
"font_class": "zuojiantou2",
|
||||
"unicode": "e9fd",
|
||||
"unicode_decimal": 59901
|
||||
},
|
||||
{
|
||||
"icon_id": "47778052",
|
||||
"name": "大气海洋环境",
|
||||
"font_class": "daqihaiyanghuanjing",
|
||||
"unicode": "e9fe",
|
||||
"unicode_decimal": 59902
|
||||
},
|
||||
{
|
||||
"icon_id": "47778055",
|
||||
"name": "天文地理物理",
|
||||
"font_class": "tianwendiliwuli",
|
||||
"unicode": "e9ff",
|
||||
"unicode_decimal": 59903
|
||||
},
|
||||
{
|
||||
"icon_id": "47778054",
|
||||
"name": "新能源新材料",
|
||||
"font_class": "xinnengyuanxincailiao",
|
||||
"unicode": "ea00",
|
||||
"unicode_decimal": 59904
|
||||
},
|
||||
{
|
||||
"icon_id": "47778053",
|
||||
"name": "工业设计制造",
|
||||
"font_class": "gongyeshejizhizao",
|
||||
"unicode": "ea01",
|
||||
"unicode_decimal": 59905
|
||||
},
|
||||
{
|
||||
"icon_id": "47756092",
|
||||
"name": "帖子",
|
||||
"font_class": "tiezi",
|
||||
"unicode": "e9f4",
|
||||
"unicode_decimal": 59892
|
||||
},
|
||||
{
|
||||
"icon_id": "47540661",
|
||||
"name": "预览",
|
||||
"font_class": "yulan",
|
||||
"unicode": "e9f1",
|
||||
"unicode_decimal": 59889
|
||||
},
|
||||
{
|
||||
"icon_id": "40883122",
|
||||
"name": "长箭头-copy",
|
||||
"font_class": "arrright",
|
||||
"unicode": "e9fc",
|
||||
"unicode_decimal": 59900
|
||||
},
|
||||
{
|
||||
"icon_id": "47487774",
|
||||
"name": "云效工具",
|
||||
"font_class": "yunxiaogongju",
|
||||
"unicode": "e9f6",
|
||||
"unicode_decimal": 59894
|
||||
},
|
||||
{
|
||||
"icon_id": "47487775",
|
||||
"name": "代码质检",
|
||||
"font_class": "daimazhijian",
|
||||
"unicode": "e9f7",
|
||||
"unicode_decimal": 59895
|
||||
},
|
||||
{
|
||||
"icon_id": "47487773",
|
||||
"name": "移动开发",
|
||||
"font_class": "yidongkaifa",
|
||||
"unicode": "e9f8",
|
||||
"unicode_decimal": 59896
|
||||
},
|
||||
{
|
||||
"icon_id": "47487772",
|
||||
"name": "其他",
|
||||
"font_class": "qita",
|
||||
"unicode": "e9f9",
|
||||
"unicode_decimal": 59897
|
||||
},
|
||||
{
|
||||
"icon_id": "47487770",
|
||||
"name": "媒体处理",
|
||||
"font_class": "meitichuli",
|
||||
"unicode": "e9fb",
|
||||
"unicode_decimal": 59899
|
||||
},
|
||||
{
|
||||
"icon_id": "47487669",
|
||||
"name": "skill",
|
||||
"font_class": "skill",
|
||||
"unicode": "e9f3",
|
||||
"unicode_decimal": 59891
|
||||
},
|
||||
{
|
||||
"icon_id": "47487670",
|
||||
"name": "代码库",
|
||||
"font_class": "daimaku1",
|
||||
"unicode": "e9f2",
|
||||
"unicode_decimal": 59890
|
||||
},
|
||||
{
|
||||
"icon_id": "47470076",
|
||||
"name": "点赞",
|
||||
"font_class": "dianzan5",
|
||||
"unicode": "e9ee",
|
||||
"unicode_decimal": 59886
|
||||
},
|
||||
{
|
||||
"icon_id": "47470075",
|
||||
"name": "多模态模型",
|
||||
"font_class": "duomotaimoxing",
|
||||
"unicode": "e9ef",
|
||||
"unicode_decimal": 59887
|
||||
},
|
||||
{
|
||||
"icon_id": "47470074",
|
||||
"name": "科学研究和技术服务业 -1",
|
||||
"font_class": "a-kexueyanjiuhejishufuwuye-1",
|
||||
"unicode": "e9f0",
|
||||
"unicode_decimal": 59888
|
||||
},
|
||||
{
|
||||
"icon_id": "47470091",
|
||||
"name": "音频-1",
|
||||
"font_class": "yinpin-1",
|
||||
"unicode": "e9e2",
|
||||
"unicode_decimal": 59874
|
||||
},
|
||||
{
|
||||
"icon_id": "47470090",
|
||||
"name": "数据集-4",
|
||||
"font_class": "shujuji-4",
|
||||
"unicode": "e9e4",
|
||||
"unicode_decimal": 59876
|
||||
},
|
||||
{
|
||||
"icon_id": "47470088",
|
||||
"name": "数据集",
|
||||
"font_class": "shujuji",
|
||||
"unicode": "e9e5",
|
||||
"unicode_decimal": 59877
|
||||
},
|
||||
{
|
||||
"icon_id": "47470089",
|
||||
"name": "数据源模型",
|
||||
"font_class": "shujuyuanmoxing",
|
||||
"unicode": "e9e6",
|
||||
"unicode_decimal": 59878
|
||||
},
|
||||
{
|
||||
"icon_id": "47470087",
|
||||
"name": "数据集-3",
|
||||
"font_class": "shujuji-3",
|
||||
"unicode": "e9e7",
|
||||
"unicode_decimal": 59879
|
||||
},
|
||||
{
|
||||
"icon_id": "47470086",
|
||||
"name": "数据集 (5)",
|
||||
"font_class": "a-shujuji5",
|
||||
"unicode": "e9e8",
|
||||
"unicode_decimal": 59880
|
||||
},
|
||||
{
|
||||
"icon_id": "47470083",
|
||||
"name": "数据集 -6",
|
||||
"font_class": "a-shujuji-6",
|
||||
"unicode": "e9e9",
|
||||
"unicode_decimal": 59881
|
||||
},
|
||||
{
|
||||
"icon_id": "47470084",
|
||||
"name": "视频播放-1",
|
||||
"font_class": "shipinbofang-1",
|
||||
"unicode": "e9ea",
|
||||
"unicode_decimal": 59882
|
||||
},
|
||||
{
|
||||
"icon_id": "47470081",
|
||||
"name": "模型库",
|
||||
"font_class": "moxingku",
|
||||
"unicode": "e9eb",
|
||||
"unicode_decimal": 59883
|
||||
},
|
||||
{
|
||||
"icon_id": "47470080",
|
||||
"name": "时间 钟表",
|
||||
"font_class": "a-shijianzhongbiao",
|
||||
"unicode": "e9ec",
|
||||
"unicode_decimal": 59884
|
||||
},
|
||||
{
|
||||
"icon_id": "47470079",
|
||||
"name": "热门模型-2",
|
||||
"font_class": "remenmoxing-2",
|
||||
"unicode": "e9ed",
|
||||
"unicode_decimal": 59885
|
||||
},
|
||||
{
|
||||
"icon_id": "47470107",
|
||||
"name": "D+数据库开发工具",
|
||||
"font_class": "a-Dshujukukaifagongju",
|
||||
"unicode": "e9cd",
|
||||
"unicode_decimal": 59853
|
||||
},
|
||||
{
|
||||
"icon_id": "47470101",
|
||||
"name": "自然语言处理-2",
|
||||
"font_class": "ziranyuyanchuli-2",
|
||||
"unicode": "e9db",
|
||||
"unicode_decimal": 59867
|
||||
},
|
||||
{
|
||||
"icon_id": "47470102",
|
||||
"name": "组 18214",
|
||||
"font_class": "a-zu18214",
|
||||
"unicode": "e9dc",
|
||||
"unicode_decimal": 59868
|
||||
},
|
||||
{
|
||||
"icon_id": "47470098",
|
||||
"name": "自助数据集",
|
||||
"font_class": "zizhushujuji",
|
||||
"unicode": "e9dd",
|
||||
"unicode_decimal": 59869
|
||||
},
|
||||
{
|
||||
"icon_id": "47470095",
|
||||
"name": "语音-1",
|
||||
"font_class": "yuyin-1",
|
||||
"unicode": "e9de",
|
||||
"unicode_decimal": 59870
|
||||
},
|
||||
{
|
||||
"icon_id": "47470094",
|
||||
"name": "图像 -1",
|
||||
"font_class": "a-tuxiang-1",
|
||||
"unicode": "e9e0",
|
||||
"unicode_decimal": 59872
|
||||
},
|
||||
{
|
||||
"icon_id": "47470092",
|
||||
"name": "下载",
|
||||
"font_class": "xiazai3",
|
||||
"unicode": "e9e1",
|
||||
"unicode_decimal": 59873
|
||||
},
|
||||
{
|
||||
"icon_id": "47464883",
|
||||
"name": "时间",
|
||||
"font_class": "shijian1",
|
||||
"unicode": "e9cb",
|
||||
"unicode_decimal": 59851
|
||||
},
|
||||
{
|
||||
"icon_id": "47464882",
|
||||
"name": "浏览量",
|
||||
"font_class": "liulanliang",
|
||||
"unicode": "e9cc",
|
||||
"unicode_decimal": 59852
|
||||
},
|
||||
{
|
||||
"icon_id": "47337195",
|
||||
"name": "关闭",
|
||||
"font_class": "guanbi2",
|
||||
"unicode": "e9ca",
|
||||
"unicode_decimal": 59850
|
||||
},
|
||||
{
|
||||
"icon_id": "47202525",
|
||||
"name": "展示上线",
|
||||
"font_class": "zhanshishangxian1",
|
||||
"unicode": "e9d3",
|
||||
"unicode_decimal": 59859
|
||||
},
|
||||
{
|
||||
"icon_id": "47202524",
|
||||
"name": "填写信息",
|
||||
"font_class": "tianxiexinxi",
|
||||
"unicode": "e9d8",
|
||||
"unicode_decimal": 59864
|
||||
},
|
||||
{
|
||||
"icon_id": "47202523",
|
||||
"name": "提交申请",
|
||||
"font_class": "tijiaoshenqing1",
|
||||
"unicode": "e9d9",
|
||||
"unicode_decimal": 59865
|
||||
},
|
||||
{
|
||||
"icon_id": "47202522",
|
||||
"name": "管理员审核",
|
||||
"font_class": "guanliyuanshenhe1",
|
||||
"unicode": "e9da",
|
||||
"unicode_decimal": 59866
|
||||
},
|
||||
{
|
||||
"icon_id": "47195847",
|
||||
"name": "私信",
|
||||
"font_class": "sixin2",
|
||||
"unicode": "e9e3",
|
||||
"unicode_decimal": 59875
|
||||
},
|
||||
{
|
||||
"icon_id": "47195594",
|
||||
"name": "点赞",
|
||||
"font_class": "dianzan4",
|
||||
"unicode": "e9d6",
|
||||
"unicode_decimal": 59862
|
||||
},
|
||||
{
|
||||
"icon_id": "47195591",
|
||||
"name": "浏览数",
|
||||
"font_class": "liulanshu",
|
||||
"unicode": "e9d7",
|
||||
"unicode_decimal": 59863
|
||||
},
|
||||
{
|
||||
"icon_id": "47195601",
|
||||
"name": "帖子数",
|
||||
"font_class": "tiezishu",
|
||||
"unicode": "e9ce",
|
||||
"unicode_decimal": 59854
|
||||
},
|
||||
{
|
||||
"icon_id": "47195600",
|
||||
"name": "研究领域",
|
||||
"font_class": "yanjiulingyu",
|
||||
"unicode": "e9cf",
|
||||
"unicode_decimal": 59855
|
||||
},
|
||||
{
|
||||
"icon_id": "47195597",
|
||||
"name": "修改 (3)",
|
||||
"font_class": "a-xiugai3",
|
||||
"unicode": "e9d0",
|
||||
"unicode_decimal": 59856
|
||||
},
|
||||
{
|
||||
"icon_id": "47195598",
|
||||
"name": "已提交的入驻信息",
|
||||
"font_class": "yitijiaoderuzhuxinxi",
|
||||
"unicode": "e9d1",
|
||||
"unicode_decimal": 59857
|
||||
},
|
||||
{
|
||||
"icon_id": "47195599",
|
||||
"name": "私信",
|
||||
"font_class": "sixin",
|
||||
"unicode": "e9d2",
|
||||
"unicode_decimal": 59858
|
||||
},
|
||||
{
|
||||
"icon_id": "47195593",
|
||||
"name": "个人简介",
|
||||
"font_class": "gerenjianjie",
|
||||
"unicode": "e9d4",
|
||||
"unicode_decimal": 59860
|
||||
},
|
||||
{
|
||||
"icon_id": "47195595",
|
||||
"name": "关注",
|
||||
"font_class": "guanzhu1",
|
||||
"unicode": "e9d5",
|
||||
"unicode_decimal": 59861
|
||||
},
|
||||
{
|
||||
"icon_id": "47139445",
|
||||
"name": "我也发帖",
|
||||
"font_class": "woyefatie",
|
||||
"unicode": "e9c9",
|
||||
"unicode_decimal": 59849
|
||||
},
|
||||
{
|
||||
"icon_id": "46808654",
|
||||
"name": "应用模板",
|
||||
"font_class": "yingyongmoban",
|
||||
"unicode": "e9c8",
|
||||
"unicode_decimal": 59848
|
||||
},
|
||||
{
|
||||
"icon_id": "46593376",
|
||||
"name": "状态",
|
||||
"font_class": "zhuangtai",
|
||||
"unicode": "e9c5",
|
||||
"unicode_decimal": 59845
|
||||
},
|
||||
{
|
||||
"icon_id": "46593375",
|
||||
"name": "复制-删除",
|
||||
"font_class": "fuzhi-shanchu1",
|
||||
"unicode": "e9c6",
|
||||
"unicode_decimal": 59846
|
||||
},
|
||||
{
|
||||
"icon_id": "46593374",
|
||||
"name": "模版管理",
|
||||
"font_class": "mobanguanli",
|
||||
"unicode": "e9c7",
|
||||
"unicode_decimal": 59847
|
||||
},
|
||||
{
|
||||
"icon_id": "46370299",
|
||||
"name": "svg_箭头1",
|
||||
|
|
|
|||
|
|
@ -1,539 +0,0 @@
|
|||
/* Logo 字体 */
|
||||
@font-face {
|
||||
font-family: "iconfont logo";
|
||||
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
|
||||
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'),
|
||||
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
|
||||
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
|
||||
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg');
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-family: "iconfont logo";
|
||||
font-size: 160px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* tabs */
|
||||
.nav-tabs {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-more {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 42px;
|
||||
line-height: 42px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
#tabs {
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
#tabs li {
|
||||
cursor: pointer;
|
||||
width: 100px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
border-bottom: 2px solid transparent;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-bottom: -1px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
|
||||
#tabs .active {
|
||||
border-bottom-color: #f00;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.tab-container .content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 页面布局 */
|
||||
.main {
|
||||
padding: 30px 100px;
|
||||
width: 960px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.main .logo {
|
||||
color: #333;
|
||||
text-align: left;
|
||||
margin-bottom: 30px;
|
||||
line-height: 1;
|
||||
height: 110px;
|
||||
margin-top: -50px;
|
||||
overflow: hidden;
|
||||
*zoom: 1;
|
||||
}
|
||||
|
||||
.main .logo a {
|
||||
font-size: 160px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.helps {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.helps pre {
|
||||
padding: 20px;
|
||||
margin: 10px 0;
|
||||
border: solid 1px #e7e1cd;
|
||||
background-color: #fffdef;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.icon_lists {
|
||||
width: 100% !important;
|
||||
overflow: hidden;
|
||||
*zoom: 1;
|
||||
}
|
||||
|
||||
.icon_lists li {
|
||||
width: 100px;
|
||||
margin-bottom: 10px;
|
||||
margin-right: 20px;
|
||||
text-align: center;
|
||||
list-style: none !important;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.icon_lists li .code-name {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.icon_lists .icon {
|
||||
display: block;
|
||||
height: 100px;
|
||||
line-height: 100px;
|
||||
font-size: 42px;
|
||||
margin: 10px auto;
|
||||
color: #333;
|
||||
-webkit-transition: font-size 0.25s linear, width 0.25s linear;
|
||||
-moz-transition: font-size 0.25s linear, width 0.25s linear;
|
||||
transition: font-size 0.25s linear, width 0.25s linear;
|
||||
}
|
||||
|
||||
.icon_lists .icon:hover {
|
||||
font-size: 100px;
|
||||
}
|
||||
|
||||
.icon_lists .svg-icon {
|
||||
/* 通过设置 font-size 来改变图标大小 */
|
||||
width: 1em;
|
||||
/* 图标和文字相邻时,垂直对齐 */
|
||||
vertical-align: -0.15em;
|
||||
/* 通过设置 color 来改变 SVG 的颜色/fill */
|
||||
fill: currentColor;
|
||||
/* path 和 stroke 溢出 viewBox 部分在 IE 下会显示
|
||||
normalize.css 中也包含这行 */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.icon_lists li .name,
|
||||
.icon_lists li .code-name {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* markdown 样式 */
|
||||
.markdown {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.markdown img {
|
||||
vertical-align: middle;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.markdown h1 {
|
||||
color: #404040;
|
||||
font-weight: 500;
|
||||
line-height: 40px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.markdown h2,
|
||||
.markdown h3,
|
||||
.markdown h4,
|
||||
.markdown h5,
|
||||
.markdown h6 {
|
||||
color: #404040;
|
||||
margin: 1.6em 0 0.6em 0;
|
||||
font-weight: 500;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.markdown h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.markdown h2 {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.markdown h3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.markdown h4 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.markdown h5 {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.markdown h6 {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.markdown hr {
|
||||
height: 1px;
|
||||
border: 0;
|
||||
background: #e9e9e9;
|
||||
margin: 16px 0;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.markdown p {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.markdown>p,
|
||||
.markdown>blockquote,
|
||||
.markdown>.highlight,
|
||||
.markdown>ol,
|
||||
.markdown>ul {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.markdown ul>li {
|
||||
list-style: circle;
|
||||
}
|
||||
|
||||
.markdown>ul li,
|
||||
.markdown blockquote ul>li {
|
||||
margin-left: 20px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.markdown>ul li p,
|
||||
.markdown>ol li p {
|
||||
margin: 0.6em 0;
|
||||
}
|
||||
|
||||
.markdown ol>li {
|
||||
list-style: decimal;
|
||||
}
|
||||
|
||||
.markdown>ol li,
|
||||
.markdown blockquote ol>li {
|
||||
margin-left: 20px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.markdown code {
|
||||
margin: 0 3px;
|
||||
padding: 0 5px;
|
||||
background: #eee;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.markdown strong,
|
||||
.markdown b {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown>table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0px;
|
||||
empty-cells: show;
|
||||
border: 1px solid #e9e9e9;
|
||||
width: 95%;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.markdown>table th {
|
||||
white-space: nowrap;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown>table th,
|
||||
.markdown>table td {
|
||||
border: 1px solid #e9e9e9;
|
||||
padding: 8px 16px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown>table th {
|
||||
background: #F7F7F7;
|
||||
}
|
||||
|
||||
.markdown blockquote {
|
||||
font-size: 90%;
|
||||
color: #999;
|
||||
border-left: 4px solid #e9e9e9;
|
||||
padding-left: 0.8em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.markdown blockquote p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.markdown .anchor {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.markdown .waiting {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.markdown h1:hover .anchor,
|
||||
.markdown h2:hover .anchor,
|
||||
.markdown h3:hover .anchor,
|
||||
.markdown h4:hover .anchor,
|
||||
.markdown h5:hover .anchor,
|
||||
.markdown h6:hover .anchor {
|
||||
opacity: 1;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.markdown>br,
|
||||
.markdown>p>br {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
background: white;
|
||||
padding: 0.5em;
|
||||
color: #333333;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-meta {
|
||||
color: #969896;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-strong,
|
||||
.hljs-emphasis,
|
||||
.hljs-quote {
|
||||
color: #df5000;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag,
|
||||
.hljs-type {
|
||||
color: #a71d5d;
|
||||
}
|
||||
|
||||
.hljs-literal,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-attribute {
|
||||
color: #0086b3;
|
||||
}
|
||||
|
||||
.hljs-section,
|
||||
.hljs-name {
|
||||
color: #63a35c;
|
||||
}
|
||||
|
||||
.hljs-tag {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-attr,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-pseudo {
|
||||
color: #795da3;
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
color: #55a532;
|
||||
background-color: #eaffea;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
color: #bd2c00;
|
||||
background-color: #ffecec;
|
||||
}
|
||||
|
||||
.hljs-link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 代码高亮 */
|
||||
/* PrismJS 1.15.0
|
||||
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
|
||||
/**
|
||||
* prism.js default theme for JavaScript, CSS and HTML
|
||||
* Based on dabblet (http://dabblet.com)
|
||||
* @author Lea Verou
|
||||
*/
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: black;
|
||||
background: none;
|
||||
text-shadow: 0 1px white;
|
||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: 1.5;
|
||||
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
pre[class*="language-"]::-moz-selection,
|
||||
pre[class*="language-"] ::-moz-selection,
|
||||
code[class*="language-"]::-moz-selection,
|
||||
code[class*="language-"] ::-moz-selection {
|
||||
text-shadow: none;
|
||||
background: #b3d4fc;
|
||||
}
|
||||
|
||||
pre[class*="language-"]::selection,
|
||||
pre[class*="language-"] ::selection,
|
||||
code[class*="language-"]::selection,
|
||||
code[class*="language-"] ::selection {
|
||||
text-shadow: none;
|
||||
background: #b3d4fc;
|
||||
}
|
||||
|
||||
@media print {
|
||||
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre[class*="language-"] {
|
||||
padding: 1em;
|
||||
margin: .5em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
:not(pre)>code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
background: #f5f2f0;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre)>code[class*="language-"] {
|
||||
padding: .1em;
|
||||
border-radius: .3em;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: slategray;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.namespace {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag,
|
||||
.token.boolean,
|
||||
.token.number,
|
||||
.token.constant,
|
||||
.token.symbol,
|
||||
.token.deleted {
|
||||
color: #905;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.builtin,
|
||||
.token.inserted {
|
||||
color: #690;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.language-css .token.string,
|
||||
.style .token.string {
|
||||
color: #9a6e3a;
|
||||
background: hsla(0, 0%, 100%, .5);
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.keyword {
|
||||
color: #07a;
|
||||
}
|
||||
|
||||
.token.function,
|
||||
.token.class-name {
|
||||
color: #DD4A68;
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important,
|
||||
.token.variable {
|
||||
color: #e90;
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
@font-face {
|
||||
font-family: "iconfont-color"; /* Project id 5169666 */
|
||||
/* Color fonts */
|
||||
src:
|
||||
url('iconfont.woff2?t=1778741571506') format('woff2'),
|
||||
url('iconfont.woff?t=1778741571506') format('woff'),
|
||||
url('iconfont.ttf?t=1778741571506') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont-color {
|
||||
font-family: "iconfont-color" !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-colorqita:before {
|
||||
content: "\e7b3";
|
||||
}
|
||||
|
||||
.icon-coloryidongkaifa:before {
|
||||
content: "\e7b4";
|
||||
}
|
||||
|
||||
.icon-colordaimazhijian:before {
|
||||
content: "\e7b5";
|
||||
}
|
||||
|
||||
.icon-coloryunxiaogongju:before {
|
||||
content: "\e7b6";
|
||||
}
|
||||
|
||||
.icon-coloryouxiyule:before {
|
||||
content: "\e7b7";
|
||||
}
|
||||
|
||||
.icon-colorskill:before {
|
||||
content: "\e7b0";
|
||||
}
|
||||
|
||||
.icon-colorshujuji:before {
|
||||
content: "\e7b1";
|
||||
}
|
||||
|
||||
.icon-colordaimaku1:before {
|
||||
content: "\e7b2";
|
||||
}
|
||||
|
||||
.icon-colormoxingku:before {
|
||||
content: "\e7af";
|
||||
}
|
||||
|
||||
.icon-coloryinpin:before {
|
||||
content: "\e7ad";
|
||||
}
|
||||
|
||||
.icon-colorremenmoxing-1:before {
|
||||
content: "\e7a3";
|
||||
}
|
||||
|
||||
.icon-colorkexueyanjiuhejishufuwuye:before {
|
||||
content: "\e7ae";
|
||||
}
|
||||
|
||||
.icon-colora-zu18210:before {
|
||||
content: "\e7a6";
|
||||
}
|
||||
|
||||
.icon-colora-zu18240:before {
|
||||
content: "\e7a5";
|
||||
}
|
||||
|
||||
.icon-colora-zuixin1:before {
|
||||
content: "\e7a7";
|
||||
}
|
||||
|
||||
.icon-colorziranyuyanchuli:before {
|
||||
content: "\e7a8";
|
||||
}
|
||||
|
||||
.icon-colorshichangtuiguang-01:before {
|
||||
content: "\e7a4";
|
||||
}
|
||||
|
||||
.icon-coloryuyin:before {
|
||||
content: "\e7a9";
|
||||
}
|
||||
|
||||
.icon-colortuxiang:before {
|
||||
content: "\e7aa";
|
||||
}
|
||||
|
||||
.icon-colora-zu18213:before {
|
||||
content: "\e7ab";
|
||||
}
|
||||
|
||||
.icon-colorshipinbofang:before {
|
||||
content: "\e7ac";
|
||||
}
|
||||
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
{
|
||||
"id": "5169666",
|
||||
"name": "newForge-color",
|
||||
"font_family": "iconfont-color",
|
||||
"css_prefix_text": "icon-color",
|
||||
"description": "gitlink/红山开源 带颜色的icon",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "47487873",
|
||||
"name": "其他",
|
||||
"font_class": "qita",
|
||||
"unicode": "e7b3",
|
||||
"unicode_decimal": 59315
|
||||
},
|
||||
{
|
||||
"icon_id": "47487872",
|
||||
"name": "移动开发",
|
||||
"font_class": "yidongkaifa",
|
||||
"unicode": "e7b4",
|
||||
"unicode_decimal": 59316
|
||||
},
|
||||
{
|
||||
"icon_id": "47487870",
|
||||
"name": "代码质检",
|
||||
"font_class": "daimazhijian",
|
||||
"unicode": "e7b5",
|
||||
"unicode_decimal": 59317
|
||||
},
|
||||
{
|
||||
"icon_id": "47487871",
|
||||
"name": "云效工具",
|
||||
"font_class": "yunxiaogongju",
|
||||
"unicode": "e7b6",
|
||||
"unicode_decimal": 59318
|
||||
},
|
||||
{
|
||||
"icon_id": "47487869",
|
||||
"name": "游戏娱乐",
|
||||
"font_class": "youxiyule",
|
||||
"unicode": "e7b7",
|
||||
"unicode_decimal": 59319
|
||||
},
|
||||
{
|
||||
"icon_id": "47487614",
|
||||
"name": "skill",
|
||||
"font_class": "skill",
|
||||
"unicode": "e7b0",
|
||||
"unicode_decimal": 59312
|
||||
},
|
||||
{
|
||||
"icon_id": "47487615",
|
||||
"name": "数据集",
|
||||
"font_class": "shujuji",
|
||||
"unicode": "e7b1",
|
||||
"unicode_decimal": 59313
|
||||
},
|
||||
{
|
||||
"icon_id": "47487613",
|
||||
"name": "代码库",
|
||||
"font_class": "daimaku1",
|
||||
"unicode": "e7b2",
|
||||
"unicode_decimal": 59314
|
||||
},
|
||||
{
|
||||
"icon_id": "47487578",
|
||||
"name": "模型库",
|
||||
"font_class": "moxingku",
|
||||
"unicode": "e7af",
|
||||
"unicode_decimal": 59311
|
||||
},
|
||||
{
|
||||
"icon_id": "47470053",
|
||||
"name": "音频",
|
||||
"font_class": "yinpin",
|
||||
"unicode": "e7ad",
|
||||
"unicode_decimal": 59309
|
||||
},
|
||||
{
|
||||
"icon_id": "47470038",
|
||||
"name": "热门模型-1",
|
||||
"font_class": "remenmoxing-1",
|
||||
"unicode": "e7a3",
|
||||
"unicode_decimal": 59299
|
||||
},
|
||||
{
|
||||
"icon_id": "47470036",
|
||||
"name": "科学研究和技术服务业",
|
||||
"font_class": "kexueyanjiuhejishufuwuye",
|
||||
"unicode": "e7ae",
|
||||
"unicode_decimal": 59310
|
||||
},
|
||||
{
|
||||
"icon_id": "47470068",
|
||||
"name": "组 18210",
|
||||
"font_class": "a-zu18210",
|
||||
"unicode": "e7a6",
|
||||
"unicode_decimal": 59302
|
||||
},
|
||||
{
|
||||
"icon_id": "47470067",
|
||||
"name": "组 18240",
|
||||
"font_class": "a-zu18240",
|
||||
"unicode": "e7a5",
|
||||
"unicode_decimal": 59301
|
||||
},
|
||||
{
|
||||
"icon_id": "47470066",
|
||||
"name": "最新 (1)",
|
||||
"font_class": "a-zuixin1",
|
||||
"unicode": "e7a7",
|
||||
"unicode_decimal": 59303
|
||||
},
|
||||
{
|
||||
"icon_id": "47470060",
|
||||
"name": "自然语言处理",
|
||||
"font_class": "ziranyuyanchuli",
|
||||
"unicode": "e7a8",
|
||||
"unicode_decimal": 59304
|
||||
},
|
||||
{
|
||||
"icon_id": "47470052",
|
||||
"name": "市场推广-01",
|
||||
"font_class": "shichangtuiguang-01",
|
||||
"unicode": "e7a4",
|
||||
"unicode_decimal": 59300
|
||||
},
|
||||
{
|
||||
"icon_id": "47470056",
|
||||
"name": "语音",
|
||||
"font_class": "yuyin",
|
||||
"unicode": "e7a9",
|
||||
"unicode_decimal": 59305
|
||||
},
|
||||
{
|
||||
"icon_id": "47470055",
|
||||
"name": "图像",
|
||||
"font_class": "tuxiang",
|
||||
"unicode": "e7aa",
|
||||
"unicode_decimal": 59306
|
||||
},
|
||||
{
|
||||
"icon_id": "47470057",
|
||||
"name": "组 18213",
|
||||
"font_class": "a-zu18213",
|
||||
"unicode": "e7ab",
|
||||
"unicode_decimal": 59307
|
||||
},
|
||||
{
|
||||
"icon_id": "47470047",
|
||||
"name": "视频播放",
|
||||
"font_class": "shipinbofang",
|
||||
"unicode": "e7ac",
|
||||
"unicode_decimal": 59308
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -42,7 +42,6 @@
|
|||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
|
||||
<link rel="stylesheet" type="text/css" href="%PUBLIC_URL%css/alex/alex.all.global.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="%PUBLIC_URL%css/iconfont.css">
|
||||
<link rel="stylesheet" type="text/css" href="%PUBLIC_URL%css/iconfont_color/iconfont.css">
|
||||
<!-- <link rel="stylesheet" type="text/css" href="%PUBLIC_URL%css/edu-purge.css"> -->
|
||||
<link rel="stylesheet" type="text/css" href="%PUBLIC_URL%css/editormd.min.css">
|
||||
<!-- <link rel="stylesheet" type="text/css" href="%PUBLIC_URL%css/merge.css"> -->
|
||||
|
|
|
|||
32
src/App.css
|
|
@ -167,38 +167,6 @@ body {
|
|||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.mt-10{
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
.df-center-between{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.df-center{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.df-between{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.df-start{
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.grid-item-top {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
grid-template-columns: max-content 1fr;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1366px) {
|
||||
body{
|
||||
zoom: 0.75;
|
||||
|
|
|
|||
59
src/App.js
|
|
@ -68,11 +68,6 @@ const SoftwareFactoryResourceDetailCommon= Loadable({
|
|||
loading: Loading,
|
||||
})
|
||||
|
||||
const SoftwareFactoryVersionDetail = Loadable({
|
||||
loader: () => import('./factory/resource/factoryVersion/detail/index'),
|
||||
loading: Loading,
|
||||
})
|
||||
|
||||
|
||||
|
||||
const SoftwareFactoryNews = Loadable({
|
||||
|
|
@ -102,11 +97,6 @@ const SoftwareFactoryScene = Loadable({
|
|||
loading: Loading,
|
||||
})
|
||||
|
||||
// 反馈中心页面
|
||||
const FeedbackCenter = Loadable({
|
||||
loader: () => import('./factory/feedback/index'),
|
||||
loading: Loading,
|
||||
})
|
||||
|
||||
//forge安全设置
|
||||
const Security = Loadable({
|
||||
|
|
@ -236,11 +226,6 @@ const GovernanceIframe = Loadable({
|
|||
loading: Loading,
|
||||
});
|
||||
|
||||
const OssRepoIframe = Loadable({
|
||||
loader: () => import("./forge/ossRepoIframe"),
|
||||
loading: Loading,
|
||||
});
|
||||
|
||||
const Forums = Loadable({
|
||||
loader: () => import("./forums"),
|
||||
loading: Loading,
|
||||
|
|
@ -251,13 +236,8 @@ const News = Loadable({
|
|||
loading: Loading,
|
||||
});
|
||||
|
||||
const AI = Loadable({
|
||||
loader: () => import("./ai"),
|
||||
loading: Loading,
|
||||
});
|
||||
|
||||
// 此处仅维护前端可能的一级路由,不用进行项目或者组织判断的字段。
|
||||
const keyWord = ["ossRepo", "settings", "setting", "mulan", "wiki", "issues", "setting", "trending", "code", "projects", "pulls", "mine", "login", "register", "email", "export", "nopage", "404", "403", "500", "501", "search", "organize", "login", "register", "resetPassword", "aboutus","educoder", "glcc","bindlogin", "softbot","zone", "hiagent","sf", "development", "governance", "explore","forums", "news", "exploreStore", "factory", "zoneAdmin", "ai"];
|
||||
const keyWord = ["explore", "settings", "setting", "mulan", "wiki", "issues", "setting", "trending", "code", "projects", "pulls", "mine", "login", "register", "email", "export", "nopage", "404", "403", "500", "501", "search", "organize", "login", "register", "resetPassword", "aboutus","educoder", "glcc","bindlogin", "softbot","zone", "hiagent","sf", "development", "governance", "forums", "news", "exploreStore", "factory", "zoneAdmin"];
|
||||
|
||||
class App extends Component {
|
||||
constructor(props) {
|
||||
|
|
@ -442,11 +422,6 @@ class App extends Component {
|
|||
{
|
||||
pathType ?
|
||||
<Switch>
|
||||
<Route path="/ai"
|
||||
render={
|
||||
(props) => (<AI {...this.props} {...props} {...this.state} />)
|
||||
}
|
||||
></Route>
|
||||
{/* 社区动态页面 */}
|
||||
<Route path="/news"
|
||||
render={
|
||||
|
|
@ -536,15 +511,6 @@ class App extends Component {
|
|||
}>
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
path={"/sf/resources/releases/:id"}
|
||||
render={
|
||||
(props) => {
|
||||
return (<SoftwareFactoryVersionDetail {...this.props} {...props} {...this.state} />)
|
||||
}
|
||||
}>
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
path={"/sf/resources"}
|
||||
render={
|
||||
|
|
@ -601,16 +567,6 @@ class App extends Component {
|
|||
}>
|
||||
</Route>
|
||||
|
||||
{/* 反馈中心页面 */}
|
||||
<Route
|
||||
path={"/sf/feedback"}
|
||||
render={
|
||||
(props) => {
|
||||
return (<FeedbackCenter {...this.props} {...props} {...this.state} />)
|
||||
}
|
||||
}>
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
path={"/sf"}
|
||||
render={
|
||||
|
|
@ -641,7 +597,7 @@ class App extends Component {
|
|||
<Route path="/search" component={Search} mygetHelmetapi={mygetHelmetapi} />
|
||||
|
||||
{/* 项目标签列表页 */}
|
||||
<Route path="/ossRepo/topic/:id/:name" component={Topic} mygetHelmetapi={mygetHelmetapi} />
|
||||
<Route path="/explore/topic/:id/:name" component={Topic} mygetHelmetapi={mygetHelmetapi} />
|
||||
|
||||
<Route path={"/:OIdentifier/enterprise"}
|
||||
render={
|
||||
|
|
@ -728,7 +684,7 @@ class App extends Component {
|
|||
<Projects {...this.props} {...props} />
|
||||
)}
|
||||
></Route>
|
||||
<Route exact path="/ossRepo"
|
||||
<Route exact path="/explore"
|
||||
render={
|
||||
(props) => (
|
||||
<Projects {...this.props} {...props} {...this.state}/>
|
||||
|
|
@ -750,15 +706,6 @@ class App extends Component {
|
|||
}>
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
path={"/explore"}
|
||||
render={
|
||||
(props) => {
|
||||
return (<OssRepoIframe {...this.props} {...props} {...this.state} />)
|
||||
}
|
||||
}>
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
path={"/factory"}
|
||||
render={
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
import fetch from './fetch';
|
||||
import fetchLogin from './fetchLogin';
|
||||
|
||||
/**智能体分类列表***/
|
||||
export function getAgentTypeList(params) {
|
||||
return fetch({
|
||||
url: `/console-api/home-page/agent-square/get-bot-type-list`,
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
/**智能体列表***/
|
||||
export function getAgentList(params) {
|
||||
return fetch({
|
||||
url: `/console-api/home-page/agent-square/get-bot-page-by-type`,
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
/**智能体登录***/
|
||||
export function agentLogin(data) {
|
||||
return fetchLogin({
|
||||
url: `/api/login/oauth/access_token`,
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
import { message, Modal } from "antd"
|
||||
import { deleteProjectInZoneById, deleteSourceById, takeDownSourceById } from "../forge/Information/api";
|
||||
import { useState } from "react";
|
||||
|
||||
export const DeleteModal = ({type, id, cancel, roleByZone, reload}) =>{
|
||||
const [loading, setLoading] = useState(false)
|
||||
return <Modal className="purpleGradient" title="删除资源" visible={!!id} onCancel={cancel} okButtonProps={{loading: loading}} onOk={()=>{
|
||||
const deleteFn = type === "codes" ? deleteProjectInZoneById : deleteSourceById;
|
||||
setLoading(true);
|
||||
deleteFn(id, roleByZone).then(res=>{
|
||||
if(res && res.data && res.data.code === 200){
|
||||
message.success(`操作成功`);
|
||||
cancel();
|
||||
reload(Math.random());
|
||||
}
|
||||
}).finally(()=>{
|
||||
setLoading(false);
|
||||
})
|
||||
}}>
|
||||
<div className="center pt20">
|
||||
<i className="iconfont icon-shanchu_tc_icon color-red mr10"></i>
|
||||
<span className="font-18">确定删除资源吗?</span>
|
||||
<div className="mt20 mb40">该操作不可恢复,请确认后再操作</div>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
|
||||
export const TakeDownModal = ({id, cancel, roleByZone, reload}) =>{
|
||||
const [loading, setLoading] = useState(false)
|
||||
return <Modal key="down" className="purpleGradient" title="下架资源" visible={!!id} onCancel={cancel} okButtonProps={{loading: loading}} onOk={()=>{
|
||||
setLoading(true);
|
||||
takeDownSourceById(id, roleByZone).then(res=>{
|
||||
if(res && res.data && res.data.code === 200){
|
||||
message.success(`操作成功`);
|
||||
cancel();
|
||||
reload(Math.random());
|
||||
}
|
||||
}).finally(()=>{
|
||||
setLoading(false);
|
||||
})
|
||||
}}>
|
||||
<div className="center pt20">
|
||||
<i className="iconfont icon-shanchu_tc_icon color-red mr10"></i>
|
||||
<span className="font-18">确定下架资源吗?</span>
|
||||
<div className="mt20 mb40">下架后AI资源广场将隐藏该资源,但可重新申请上架</div>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
|
|
@ -1,560 +0,0 @@
|
|||
import React, { useState, useEffect, Fragment, useRef } from "react";
|
||||
import { Input, Select, Upload, message, Form, Checkbox, Tree, Spin, Alert } from "antd";
|
||||
import './index.scss';
|
||||
import { aiCreateSteps, aiResouceType, aiResouceTypeKey, convertToTreeData } from "../static";
|
||||
import { Link } from "react-router-dom";
|
||||
import { AlignCenter } from "../../forge/Component/layout";
|
||||
import DorpChooseMenus from "../../forge/Component/licenseModal/dorpChooseMenus";
|
||||
import axios from "axios";
|
||||
import { addDataset, addModel, addProjectInZone, addSkill, editDataset, editModel, editSkill, getProjectsTypeLists, getAISourceById, getSourceZoneList, parseFileBySkill } from "../../forge/Information/api";
|
||||
import ChunkedUpload from "../../components/ChunkedUpload";
|
||||
const { Option } = Select;
|
||||
|
||||
function CreateResource(props) {
|
||||
const { match: { params }, form, current_user, history, aiZoneInfo, roleByZone } = props || {};
|
||||
const { getFieldDecorator, getFieldsValue, setFieldsValue, validateFields } = form;
|
||||
const type = params.type || aiResouceType[0].key;
|
||||
const sourceId = params.id; //资源id,编辑状态不允许筛选一级分类
|
||||
const { user_id, login } = current_user || {}
|
||||
const { id: aiZoneId } = aiZoneInfo || {}
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [categoryList, setCategoryList] = useState();
|
||||
const [languageList, setLanguageList] = useState();
|
||||
const [gitignoreList, setGitignoreList] = useState();
|
||||
const [licensesList, setLicensesList] = useState();
|
||||
const [ownerList, setOwnerList] = useState();
|
||||
const [cate, setCate] = useState();
|
||||
const [info, setInfo] = useState();
|
||||
const [uploadedFile, setUploadedFile] = useState(null);
|
||||
const [remark, setRemark] = useState('');
|
||||
const uploadRef = useRef(null);
|
||||
|
||||
const typeDetail = type && aiResouceType.find(i => i.key === type);
|
||||
const { ignoreFlag, licenseFlag, categoreFlag, languageFlag, file } = getFieldsValue();
|
||||
const { file: fileByupload } = file || {}
|
||||
const { response: responseByupload } = fileByupload || {}
|
||||
const { fileTreeJson } = responseByupload || {}
|
||||
|
||||
const skillTreeData = fileTreeJson && convertToTreeData(JSON.parse(fileTreeJson).children)
|
||||
|
||||
// 登录检查:如果未登录则跳转到登录页面
|
||||
useEffect(() => {
|
||||
if (!login) {
|
||||
history.push(`/login?go_page=${encodeURIComponent(window.location.pathname)}`);
|
||||
}
|
||||
}, [login]);
|
||||
|
||||
// 自动滚动动画:每 5 秒切换到下一步
|
||||
useEffect(() => {
|
||||
document.title = `发布新资源`;
|
||||
const timer = setInterval(() => {
|
||||
setCurrentStep((prev) => (prev + 1) % aiCreateSteps.length);
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
switch (type) {
|
||||
case "codes":
|
||||
axios.get(`/owners.json`).then(result => {
|
||||
if (result && result.data && result.data.owners) {
|
||||
setOwnerList(result.data.owners)
|
||||
const owner = result.data.owners.filter(item => item.id === user_id);
|
||||
owner[0] && setTimeout(() => {
|
||||
setFieldsValue({
|
||||
user_id: owner[0].id
|
||||
})
|
||||
}, 500);
|
||||
}
|
||||
})
|
||||
axios.get(`/project_categories.json`).then((result) => {
|
||||
if (result && result.data) {
|
||||
setCategoryList(result.data.project_categories)
|
||||
}
|
||||
})
|
||||
axios.get(`/project_languages.json`).then((result) => {
|
||||
if (result && result.data) {
|
||||
setLanguageList(result.data.project_languages)
|
||||
}
|
||||
})
|
||||
axios.get(`/ignores.json`).then((result) => {
|
||||
if (result && result.data) {
|
||||
setGitignoreList(result.data.ignores)
|
||||
}
|
||||
})
|
||||
axios.get(`/licenses.json`).then((result) => {
|
||||
if (result && result.data) {
|
||||
setLicensesList(result.data.licenses)
|
||||
}
|
||||
})
|
||||
aiZoneId && getProjectsTypeLists(aiZoneId).then(res => {
|
||||
if (res && res.data && res.data.rows) {
|
||||
setCate(res.data.rows)
|
||||
}
|
||||
})
|
||||
break;
|
||||
default:
|
||||
aiZoneId && getSourceZoneList(aiZoneId, {
|
||||
domainCategory: aiResouceTypeKey[type]
|
||||
}).then(res => {
|
||||
if (res && res.data && res.data.rows) {
|
||||
setCate(res.data.rows)
|
||||
}
|
||||
})
|
||||
break;
|
||||
}
|
||||
}, [type, aiZoneId])
|
||||
|
||||
useEffect(() => {
|
||||
if (sourceId) {
|
||||
getAISourceById(sourceId).then(res => {
|
||||
if (res && res.data && res.data.code === 200) {
|
||||
const { fileIds, extendData, name, summary, domainId, fileList, auditStatus, remark: remarkText } = res.data.data || {}
|
||||
// 如果被驳回(auditStatus=2),保存驳回理由
|
||||
if (auditStatus === '2') {
|
||||
setRemark(remarkText || "请修改后重新提交");
|
||||
}
|
||||
const fileInfo = {
|
||||
fileId: fileList[0]?.fileId,
|
||||
fileOriginName: fileList[0]?.fileOriginName,
|
||||
fileSize: fileList[0]?.fileSizeInfo,
|
||||
url: fileList[0]?.downloadUrl
|
||||
}
|
||||
setUploadedFile(fileInfo)
|
||||
const fileValue = {
|
||||
name,
|
||||
summary,
|
||||
domainId
|
||||
}
|
||||
type === "skills" ? fileValue.file = {
|
||||
file: {
|
||||
response: {
|
||||
fileId: fileIds,
|
||||
skillMdContent: extendData.SKILL.content,
|
||||
fileTreeJson: JSON.stringify(extendData.SKILL.fileTree)
|
||||
}
|
||||
}
|
||||
} : fileValue.fileId = fileIds
|
||||
setFieldsValue(fileValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [sourceId])
|
||||
|
||||
// 上传处理
|
||||
const beforeUpload = (file, size = 10) => {
|
||||
const limitSize = file.size / 1024 / 1024 < size;
|
||||
if (!limitSize) {
|
||||
message.error(`文件大小必须小于${size}MB!`);
|
||||
}
|
||||
return limitSize;
|
||||
};
|
||||
|
||||
function submit() {
|
||||
const compelete = (response) => {
|
||||
if (response && response.data && response.data.code === 200) {
|
||||
message.success("操作成功");
|
||||
history.push(`/ai/${type}/my`);
|
||||
}
|
||||
}
|
||||
validateFields((err, values) => {
|
||||
if (!err) {
|
||||
switch (type) {
|
||||
case "codes":
|
||||
axios.post(`/projects.json`, {
|
||||
...values,
|
||||
...info,
|
||||
}).then(res => {
|
||||
if (res && res.data && res.data.id) {
|
||||
// 关联项目到专区
|
||||
addProjectInZone([{
|
||||
gitlinkProjectId: res.data.id,
|
||||
projectTypeId: values.projectTypeId,
|
||||
zoneId: aiZoneId
|
||||
}]).then(response => { compelete(response) })
|
||||
}
|
||||
})
|
||||
break;
|
||||
case "skills":
|
||||
const { fileId, skillMdContent } = responseByupload
|
||||
const submitFn = sourceId ? editSkill : addSkill;
|
||||
const param = {
|
||||
...values,
|
||||
fileId,
|
||||
fileTreeJson,
|
||||
skillContent: skillMdContent,
|
||||
zoneId: aiZoneId
|
||||
}
|
||||
!!sourceId && (param.id = sourceId)
|
||||
submitFn(param, roleByZone).then(response => { compelete(response) })
|
||||
break;
|
||||
default:
|
||||
// models/datasets 类型:使用分片上传的 fileId
|
||||
const { fileId: formFileId } = getFieldsValue() || {};
|
||||
const submitParam = {
|
||||
...values,
|
||||
fileId: formFileId,
|
||||
zoneId: aiZoneId
|
||||
}
|
||||
if (sourceId) {
|
||||
submitParam.id = sourceId;
|
||||
}
|
||||
const submitFn1 = type === "models" ? sourceId ? editModel : addModel : sourceId ? editDataset : addDataset;
|
||||
submitFn1(submitParam).then(response => { compelete(response) })
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const codeCheckBox = (checkKey, checkName, value, selectKey, list) => {
|
||||
return <Fragment>
|
||||
<Form.Item
|
||||
className="privatePart"
|
||||
>
|
||||
{getFieldDecorator(checkKey)(
|
||||
<Checkbox>{checkName}</Checkbox>
|
||||
)}
|
||||
</Form.Item>
|
||||
{value && <Form.Item>
|
||||
{getFieldDecorator(selectKey, {
|
||||
rules: [{
|
||||
required: value, message: `请选择${checkName}`
|
||||
}],
|
||||
})(
|
||||
<DorpChooseMenus idname={"newPanel"} name={`请选择${checkName}${checkName === "gitignore" ? ",用来定义哪些文件不需要添加到版本管理中" : ""}`} list={list} getValue={(value) => {
|
||||
const value2 = {}
|
||||
value2[selectKey] = value.name;
|
||||
setFieldsValue(value2);
|
||||
const value1 = { ...info }
|
||||
value1[selectKey + "_id"] = value.id;
|
||||
setInfo(value1)
|
||||
}} />
|
||||
)}
|
||||
</Form.Item>
|
||||
}
|
||||
</Fragment>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ai_create_page">
|
||||
{/* 顶部导航 */}
|
||||
<div className="ai_create_header center pt40">
|
||||
<div className="ai_banner_title mb5">
|
||||
发布
|
||||
<span className="carousel_text mr20">新资源</span>
|
||||
全新智算共享
|
||||
</div>
|
||||
<div className="font-16 mb40">
|
||||
将您的模型、数据集、skill或代码库分享给社区
|
||||
</div>
|
||||
<div className="ai_create_banner_tabs df">
|
||||
{aiResouceType.map((item) => (
|
||||
<Link
|
||||
to={`/ai/${item.key}/create`}
|
||||
key={item.key}
|
||||
className={`ai_create_banner_tab pointer ${type === item.key ? 'active' : ''} ${!!sourceId && "disabled"}`}
|
||||
onClick={(e) => {
|
||||
if (!!sourceId) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
form.resetFields();
|
||||
if (uploadRef.current) {
|
||||
uploadRef.current.cancelUpload();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<i className={`${type === item.key ? 'iconfont icon-' : 'iconfont-color icon-color'}${item.icon} font-20`}></i>
|
||||
<div className="font-16 font-bd mb4">{item.name}</div>
|
||||
<div className="font-13 color3f4">{item.intro}</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 主体内容 */}
|
||||
<div className="ai_create_content ai_1600 df-start">
|
||||
<div className="ai_create_content_left">
|
||||
<div className="font-bd font-15 mb30">如何开始?</div>
|
||||
<div className="steps-container">
|
||||
{aiCreateSteps.map((step, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`step-item df-start pb40 ${currentStep === index ? 'active' : ''}`}
|
||||
>
|
||||
<div className="step-number mr15">{index + 1}</div>
|
||||
<div className="step-content flex1">
|
||||
<div className="font-bd font-15">{step.title}</div>
|
||||
<div className="font-13 color768">{step.description}</div>
|
||||
</div>
|
||||
{index < aiCreateSteps.length - 1 && (
|
||||
<div className="step-line"></div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ai_create_form_wrapper flex1" id="newPanel">
|
||||
<div className="ai_create_form_header mb30">
|
||||
<div className="font-20 font-bd mb5">基本信息</div>
|
||||
<div className="font-15 color768">{typeDetail.createIntro}</div>
|
||||
{remark && (
|
||||
<div className="audit-rejected-wrapper mt20">
|
||||
<Alert
|
||||
message="审核驳回"
|
||||
description={remark}
|
||||
type="error"
|
||||
showIcon
|
||||
closable
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
className="ai_form"
|
||||
colon={false}
|
||||
>
|
||||
{type === "codes" ? <Fragment>
|
||||
<AlignCenter>
|
||||
<Form.Item
|
||||
label="拥有者"
|
||||
style={{ width: "260px" }}
|
||||
>
|
||||
{getFieldDecorator('user_id', {
|
||||
rules: [{
|
||||
required: true, message: '请选择拥有者'
|
||||
}],
|
||||
})(
|
||||
<Select
|
||||
showSearch
|
||||
style={{ width: "260px", height: "33px" }}
|
||||
placeholder="请选择拥有者"
|
||||
className="plateAutoComplete"
|
||||
optionFilterProp="children"
|
||||
filterOption={(input, option) =>
|
||||
option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
>
|
||||
{ownerList && ownerList.length > 0 &&
|
||||
ownerList.map((i, k) => {
|
||||
return (
|
||||
<Option value={i.id}>{i.name}</Option>
|
||||
)
|
||||
})
|
||||
}
|
||||
</Select>
|
||||
)}
|
||||
</Form.Item>
|
||||
<span className="ml10 mr10 mt10 font-18">/</span>
|
||||
<Form.Item
|
||||
label="项目名称"
|
||||
className="flex1"
|
||||
>
|
||||
{getFieldDecorator('name', {
|
||||
rules: [{
|
||||
required: true, message: '请填写项目名称'
|
||||
}],
|
||||
})(
|
||||
<Input placeholder="例如:团队协作方法与研究" maxLength={50} />// onBlur={this.changeName}
|
||||
)}
|
||||
</Form.Item>
|
||||
</AlignCenter>
|
||||
<Form.Item
|
||||
label={<span>项目标识 <span className="color-grey-9">(项目url标识部分)</span></span>}
|
||||
>
|
||||
{getFieldDecorator('repository_name', {
|
||||
rules: [{
|
||||
required: true, message: '请填写项目标识'
|
||||
}],
|
||||
})(
|
||||
<Input placeholder="项目标识请使用与项目相关的英文关键字" maxLength={100} />
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="项目分类"
|
||||
>
|
||||
{getFieldDecorator('projectTypeId', {
|
||||
rules: [{
|
||||
required: true, message: '请选择项目分类'
|
||||
}],
|
||||
})(
|
||||
<Select
|
||||
placeholder="请选择项目分类"
|
||||
optionFilterProp="children"
|
||||
>
|
||||
{cate && cate.length > 0 &&
|
||||
cate.map((i, k) => {
|
||||
return (
|
||||
<Option value={i.id}>{i.name}</Option>
|
||||
)
|
||||
})
|
||||
}
|
||||
</Select>
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="简介"
|
||||
>
|
||||
{getFieldDecorator('description', {
|
||||
rules: [],
|
||||
})(
|
||||
<Input.TextArea maxLength={200} placeholder="请输入简介内容不超过200字" autoSize={{ minRows: 2, maxRows: 6 }} />
|
||||
)}
|
||||
</Form.Item>
|
||||
{codeCheckBox("ignoreFlag", ".gitignore", ignoreFlag, "ignore", gitignoreList)}
|
||||
{codeCheckBox("licenseFlag", "开源许可证", licenseFlag, "license", licensesList)}
|
||||
{/* <Form.Item
|
||||
className="privatePart"
|
||||
>
|
||||
{getFieldDecorator('private')(
|
||||
<Checkbox value="limit">将项目设为私有<span className="font-13 color-grey-9">(只有项目所有人或拥有权限的项目成员才能看到)</span></Checkbox>
|
||||
)}
|
||||
</Form.Item > */}
|
||||
{codeCheckBox("categoreFlag", "项目类别", categoreFlag, "project_category", categoryList)}
|
||||
{codeCheckBox("languageFlag", "项目语言", languageFlag, "project_language", languageList)}
|
||||
</Fragment> : <Fragment>
|
||||
{type === "skills" && <Form.Item label={`${typeDetail.name}文件`}>
|
||||
{getFieldDecorator('file', {
|
||||
rules: [{ required: true, message: "请上传文件" }],
|
||||
})(
|
||||
responseByupload ? <Spin spinning={loading}>
|
||||
<div className="edu-txt-right mb10"><a onClick={() => {
|
||||
form.setFieldsValue({
|
||||
file: undefined
|
||||
})
|
||||
}}>重新上传(清空)</a></div>
|
||||
<Tree.DirectoryTree
|
||||
className="ai_tree_file"
|
||||
treeData={skillTreeData}
|
||||
selectable={false}
|
||||
/>
|
||||
</Spin> : <Upload
|
||||
accept={".zip"}
|
||||
beforeUpload={(file) => beforeUpload(file, 10)}
|
||||
showUploadList={false}
|
||||
customRequest={(options) => {
|
||||
const { file, onSuccess } = options;
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
setLoading(true);
|
||||
parseFileBySkill(form, roleByZone).then((response) => {
|
||||
if (response && response.data && response.data.code === 200) {
|
||||
onSuccess(response.data.data)
|
||||
const { summary, name } = response.data.data;
|
||||
setFieldsValue({
|
||||
summary,
|
||||
name
|
||||
})
|
||||
}
|
||||
}).finally(() => {
|
||||
setLoading(false);
|
||||
})
|
||||
}}
|
||||
>
|
||||
<div className="ai_create_upload center cursor">
|
||||
<span className="ai_create_upload_icon">
|
||||
<i className="iconfont icon-shangchuanicon color091"></i>
|
||||
</span>
|
||||
<div className="color091 mt15 mb15">请上传您的{typeDetail.name}文件(.zip)</div>
|
||||
<div className="coloraeb">请确保压缩包中包含SKILL.md,大小限制 10MB</div>
|
||||
</div>
|
||||
</Upload>
|
||||
)}
|
||||
</Form.Item>}
|
||||
<Form.Item label="名称">
|
||||
{getFieldDecorator('name', {
|
||||
rules: [{
|
||||
required: true, message: '请填写名称'
|
||||
}],
|
||||
})(
|
||||
<Input placeholder="请输入名称" maxLength={50} />
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item label="简介">
|
||||
{getFieldDecorator('summary', {
|
||||
rules: [{
|
||||
required: false, message: '请填写简介内容'
|
||||
}],
|
||||
})(
|
||||
<Input.TextArea maxLength={500} placeholder="请输入简介内容不超过500字" autoSize={{ minRows: 2 }} />
|
||||
)}
|
||||
</Form.Item>
|
||||
{/* 资源类型 */}
|
||||
<Form.Item label="类别">
|
||||
{getFieldDecorator('domainId', {
|
||||
rules: [{
|
||||
required: true, message: '请选择类别'
|
||||
}],
|
||||
})(
|
||||
<Select placeholder="请选择类别" className="font-15">
|
||||
{cate && cate.length > 0 &&
|
||||
cate.map((i, k) => {
|
||||
return (
|
||||
<Option value={i.id}>{i.name}</Option>
|
||||
)
|
||||
})
|
||||
}
|
||||
</Select>
|
||||
)}
|
||||
</Form.Item>
|
||||
{/* models/datasets 类型使用分片上传 */}
|
||||
{type !== "skills" && <Form.Item label={`${typeDetail.name}文件`} required>
|
||||
{getFieldDecorator('fileId', {
|
||||
rules: [{
|
||||
required: true, message: `请上传${typeDetail.name}文件`
|
||||
}],
|
||||
})(
|
||||
<div>
|
||||
<ChunkedUpload
|
||||
hierarchy={`${type}`}
|
||||
accept=".zip"
|
||||
defaultFile={uploadedFile}
|
||||
ref={uploadRef}
|
||||
fileType={"ai-resource"}
|
||||
disabled={loading}
|
||||
maxSize={1024 * 1024 * 1024 * 1024} // 1TB
|
||||
onUploadComplete={(fileInfo) => {
|
||||
// 设置表单字段,保存 fileId
|
||||
setFieldsValue({
|
||||
fileId: fileInfo.fileId
|
||||
});
|
||||
}}
|
||||
onCancel={() => {
|
||||
setUploadedFile(null);
|
||||
setFieldsValue({
|
||||
fileId: undefined
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span className="ai_create_upload_icon">
|
||||
<i className="iconfont icon-shangchuanicon color091"></i>
|
||||
</span>
|
||||
<div className="color091 mt15 mb15">请上传{typeDetail.name}压缩包(支持 .zip 格式)</div>
|
||||
<div className="coloraeb mb10">请确保压缩包内包含{typeDetail.name}文件及 README.md,系统将自动解析说明文档</div>
|
||||
<div className="coloraeb">单文件最大支持 1TB,系统支持断点续传</div>
|
||||
</ChunkedUpload>
|
||||
</div>
|
||||
)}
|
||||
</Form.Item>}
|
||||
</Fragment>}
|
||||
|
||||
{/* 提交按钮 */}
|
||||
<Form.Item className="mt36">
|
||||
<a className="btn-outline mr25" href={sourceId ? `/ai/${type}/my` : `/ai/${type}`}>取 消</a>
|
||||
<a className="btn-black" loading={loading} onClick={submit}>提 交</a>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Form.create()(CreateResource);
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
.ai_create_page {
|
||||
background: #f8f9fc url('../image/bk2.png');
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% auto;
|
||||
|
||||
.ai_create_banner_tabs {
|
||||
justify-content: center;
|
||||
gap: 36px;
|
||||
|
||||
.ai_create_banner_tab {
|
||||
background: linear-gradient(87.55deg, #eef3fd 3.07%, #ffffff 98.94%);
|
||||
border: 1px solid rgba(22, 76, 228, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 34px 92px;
|
||||
|
||||
&:nth-child(4n+2) {
|
||||
background: linear-gradient(87.55deg, #fdf5fd 3.07%, #ffffff 98.94%);
|
||||
}
|
||||
|
||||
&:nth-child(4n+3) {
|
||||
background: linear-gradient(87.55deg, #eef3fd 3.07%, #ffffff 98.94%);
|
||||
}
|
||||
|
||||
&:nth-child(4n) {
|
||||
background: linear-gradient(87.55deg, #ececff 3.07%, #f8f9fd 98.94%);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-image: url('../image/card_bk2.png');
|
||||
background-size: 100% 100%;
|
||||
border-color: white;
|
||||
|
||||
&,
|
||||
& .color3f4 {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover:not(.active):not(.disabled) {
|
||||
transition: all 0.3s ease;
|
||||
transform: translateY(-10px);
|
||||
border-color: #164ce4;
|
||||
}
|
||||
|
||||
&:not(.active),
|
||||
&:not(.active):hover,
|
||||
&:not(.active):focus {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 主体内容
|
||||
.ai_create_content {
|
||||
padding: 68px 0;
|
||||
}
|
||||
|
||||
// 步骤引导组件样式
|
||||
.ai_create_content_left {
|
||||
flex: 0 0 380px;
|
||||
margin-right: 30px;
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(22, 76, 228, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 30px 35px;
|
||||
|
||||
.steps-container {
|
||||
.step-number {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: #eef4fe;
|
||||
border: 2px solid rgba(22, 76, 228, 0.1);
|
||||
line-height: 36px;
|
||||
text-align: center;
|
||||
color: #aebbd0;
|
||||
font-size: 16px;
|
||||
font-family: "DouyinSansBold";
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.step-item.active {
|
||||
.step-number {
|
||||
background-image: url('../image/icon4.png');
|
||||
background-size: 100% 100%;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.step-item:not(:last-of-type) {
|
||||
.step-number::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
left: 15px;
|
||||
width: 0px;
|
||||
height: 58px;
|
||||
border-left: 1px dashed rgba(174, 187, 208, 0.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ai_create_form_wrapper {
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(22, 76, 228, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 55px 60px;
|
||||
}
|
||||
|
||||
.btn-black,
|
||||
.btn-outline {
|
||||
font-size: 15px;
|
||||
padding: 11px 54px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.ai_form {
|
||||
width: 68.5%;
|
||||
|
||||
.ant-upload.ant-upload-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// 审核驳回提示样式
|
||||
.audit-rejected-wrapper {
|
||||
.ant-alert-error {
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ai_create_upload {
|
||||
padding: 30px 0;
|
||||
border: 1px dashed rgba(22, 76, 228, 0.17);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.ai_create_upload_icon {
|
||||
display: inline-block;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
background: #f3f4f8;
|
||||
border-radius: 50%;
|
||||
line-height: 45px;
|
||||
}
|
||||
.ai_tree_file{
|
||||
border: 1px solid #D9D9D9;
|
||||
border-radius: 6px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { withRouter } from "react-router";
|
||||
import { httpUrl } from '../../ai/fetch';
|
||||
import cookie from 'react-cookies';
|
||||
import { aiAgentLogin } from "../../forge/Information/api";
|
||||
import { agentLogin } from "../api";
|
||||
|
||||
function Agent(props) {
|
||||
const { match, current_user, history, showLoginDialog } = props;
|
||||
const { id } = match.params;
|
||||
const { login } = current_user || {};
|
||||
const [iframeUrl, setIframeUrl] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
document.title = "智能体-AI资源";
|
||||
checkLoginAndGetIframeUrl();
|
||||
}, []);
|
||||
|
||||
// 检查登录状态并获取iframe URL
|
||||
const checkLoginAndGetIframeUrl = async () => {
|
||||
try {
|
||||
// 判断用户是否登录
|
||||
if (!login) {
|
||||
history.push(`/login?go_page=/ai/agents/${id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 已登录,检查Cookie中agent是否有值
|
||||
const agentCookie = cookie.load('agent');
|
||||
let currentTokenData = null;
|
||||
|
||||
if (!agentCookie) {
|
||||
// Cookie中agent没有值,调用登录接口
|
||||
const res = await agentLogin({
|
||||
grant_type: 'password',
|
||||
client_id: '4b25ca036879216a020a',
|
||||
username: 'hnxjy',
|
||||
password: '123456',
|
||||
scope: 'profile'
|
||||
});
|
||||
|
||||
if (res && res.data && res.data) {
|
||||
const { access_token, expires_in } = res.data;
|
||||
// 用当前时间 + expires_in(秒)计算过期时间
|
||||
const expiryDate = new Date(Date.now() + (expires_in) * 1000);
|
||||
|
||||
// 将完整的token数据存储在agent Cookie中
|
||||
currentTokenData = access_token;
|
||||
cookie.save('agent', currentTokenData, { expires: expiryDate, path: '/' });
|
||||
}
|
||||
} else {
|
||||
// Cookie中有值,从Cookie获取保存的token数据
|
||||
currentTokenData = agentCookie;
|
||||
}
|
||||
|
||||
// 获取iframe URL(带上token参数)
|
||||
const url = getIframeUrl(id, currentTokenData);
|
||||
setIframeUrl(url);
|
||||
} catch (err) {
|
||||
console.error('检查登录状态失败:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// 构建iframe URL,将token作为URL参数传递
|
||||
const getIframeUrl = (agentId, accessToken) => {
|
||||
if (!httpUrl || !agentId) return '';
|
||||
|
||||
try {
|
||||
const urlObj = new URL(httpUrl);
|
||||
const baseOrigin = `${urlObj.protocol}//${urlObj.host}`;
|
||||
let url = `${baseOrigin}/chat/${agentId}`;
|
||||
|
||||
// 如果有token,添加到URL参数中
|
||||
if (accessToken) {
|
||||
const separator = url.includes('?') ? '&' : '?';
|
||||
url += `${separator}token=${encodeURIComponent(accessToken)}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="agent-container" style={{ width: '100%', height: '100vh', overflow: 'hidden' }}>
|
||||
{iframeUrl ? (
|
||||
<iframe
|
||||
src={iframeUrl}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
border: 'none',
|
||||
}}
|
||||
allow="clipboard-read; clipboard-write"
|
||||
/>
|
||||
) : (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
|
||||
加载中...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(Agent);
|
||||
|
|
@ -1,299 +0,0 @@
|
|||
import React, { useState, useEffect, Fragment } from "react";
|
||||
import { Tabs, Tag, Spin, message, Divider, Tree, Tooltip, Button, Badge } from "antd";
|
||||
import RenderHtml from "../../components/render-html";
|
||||
import "./index.scss";
|
||||
import { getlikedSource, getAISourceById, likeAISource, editModel, editDataset } from "../../forge/Information/api";
|
||||
import { aiResouceStatus, aiResouceType, aiResouceTypeKey, convertToTreeData } from "../static";
|
||||
import skillIcon from '../image/icon7.png';
|
||||
import CommentsList from "../../components/commentsByZone";
|
||||
import TpmMdEditor from "../../modules/tpm/challengesnew/tpm-md-editor";
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
function Detail(props) {
|
||||
// 路由参数
|
||||
const { history, match: { params }, current_user: { login }, current_user, showLoginDialog, showNotification } = props;
|
||||
const { id, type } = params || {};
|
||||
|
||||
// 状态管理
|
||||
const [reload, setReload] = useState(undefined);
|
||||
const [detail, setDetail] = useState({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState("introduction");
|
||||
const [liked, setLiked] = useState(false);
|
||||
const [isEditor, setIsEditor] = useState(false);
|
||||
const [editContent, setEditContent] = useState("");
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const { extendData={}, createBy, auditStatus } = detail;
|
||||
const status = aiResouceStatus[auditStatus] || {};
|
||||
|
||||
const typeDetail = type && aiResouceType.find(i => i.key === type);
|
||||
const name = typeDetail && typeDetail.name
|
||||
|
||||
const dataByType = extendData[aiResouceTypeKey[type]] || {}
|
||||
const content = dataByType.content || dataByType.readmeContent
|
||||
const introContent = String(content || `<div className="ai_detail_nodata_text">当前${name}未提供详细介绍</div>`).replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '');
|
||||
const treeData = dataByType && dataByType.fileTree && convertToTreeData(dataByType.fileTree.children)
|
||||
|
||||
// 判断当前用户是否为发布者
|
||||
const isPublisher = login && createBy && login === createBy;
|
||||
|
||||
// 组件挂载和 ID 变化时加载数据
|
||||
useEffect(() => {
|
||||
document.title = "AI资源";
|
||||
setLoading(true);
|
||||
getDetail()
|
||||
getIsLikedSource()
|
||||
}, [id, reload]); // 依赖 id,当 id 变化时重新加载
|
||||
|
||||
|
||||
const getDetail = () => {
|
||||
id && getAISourceById(id).then(response => {
|
||||
if (response && response.data && response.data.code === 200) {
|
||||
const { name } = response.data.data
|
||||
document.title = `${name}-AI资源`;
|
||||
setDetail(response.data.data);
|
||||
setLoading(false);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getIsLikedSource = () => {
|
||||
id && getlikedSource(id).then(response => {
|
||||
if (response && response.data && response.data.code === 200) {
|
||||
const { isLike } = response.data.rows[0] || {};
|
||||
setLiked(isLike);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 切换编辑模式
|
||||
const handleToggleEdit = () => {
|
||||
if (isEditor) {
|
||||
// 取消编辑,恢复原内容
|
||||
setEditContent(content || "");
|
||||
setIsEditor(false);
|
||||
} else {
|
||||
// 进入编辑模式
|
||||
setEditContent(content || "");
|
||||
setIsEditor(true);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存编辑内容
|
||||
const handleSave = () => {
|
||||
setSaving(true);
|
||||
const editFun = type === "models" ? editModel : editDataset
|
||||
editFun({
|
||||
id: +id,
|
||||
readmeContent: editContent
|
||||
}).then(response => {
|
||||
if (response && response.data && response.data.code === 200) {
|
||||
message.success("保存成功");
|
||||
setIsEditor(false);
|
||||
setReload(Date.now());
|
||||
} else {
|
||||
message.error(response?.data?.msg || "保存失败");
|
||||
}
|
||||
}).catch(() => {
|
||||
message.error("保存失败,请重试");
|
||||
}).finally(() => {
|
||||
setSaving(false);
|
||||
});
|
||||
}
|
||||
|
||||
// 格式化下载量
|
||||
const formatDownloads = (num) => {
|
||||
if (num >= 10000) {
|
||||
return (num / 10000).toFixed(1) + "万";
|
||||
}
|
||||
return num;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="ai_detail_loading">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!detail) {
|
||||
return (
|
||||
<div className="ai_detail_nodata">
|
||||
<div className="font-16 color768">暂无数据</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ai_detail_page">
|
||||
{/* 顶部信息卡片 */}
|
||||
<div className="ai_detail_header ai_1600 mb40">
|
||||
<div className="ai_detail_header_inner">
|
||||
<div className="df-start">
|
||||
<img src={skillIcon} width={40} className="mr15" />
|
||||
<div className="mt-10 flex1">
|
||||
<div className="df-start">
|
||||
<div className="ai_detail_header_info mrat">
|
||||
<div className="font-20 font-bd">{detail.name}</div>
|
||||
<div className="color3f4">{detail.summary || "暂无简介"}</div>
|
||||
<div className="ai_detail_header_tags mt10 df-center">
|
||||
{detail.tags && detail.tags.map((tag, index) => (
|
||||
<Tag key={index} className="ai_detail_header_tag">
|
||||
{tag}
|
||||
</Tag>
|
||||
))}
|
||||
{detail.license && (
|
||||
<Tag className="ai_detail_header_tag">
|
||||
开源协议:{detail.license}
|
||||
</Tag>
|
||||
)}
|
||||
{detail.domainName && (
|
||||
<Tag className="ai_detail_header_tag">
|
||||
{detail.domainName}
|
||||
</Tag>
|
||||
)}
|
||||
{dataByType && dataByType.fileSize && (
|
||||
<Tag className="ai_detail_header_tag">
|
||||
{dataByType.fileSize}
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink0 mt20 ml20">
|
||||
{detail.fileList && detail.fileList[0] && <a className="btn-black font-15 mr20" href={detail.fileList[0].downloadUrl} download onClick={(e) => {
|
||||
if (!login) {
|
||||
showLoginDialog();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
}}>下载</a>}
|
||||
<a className="btn-outline font-15" onClick={() => history.goBack()}>返回</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Divider dashed className="mt20 mb20" />
|
||||
<div className="df-center color828">
|
||||
{auditStatus !== "1" && <Fragment>
|
||||
<Badge color={status.color} text={status.label} className='status-dot' />
|
||||
<Divider type="vertical" style={{ height: '7px', margin: '0 20px' }} />
|
||||
</Fragment>}
|
||||
<i className="iconfont icon-yonghuming font-14 mr8"></i>
|
||||
{detail.createUser && detail.createUser.userName || "未知作者"}
|
||||
<Divider type="vertical" style={{ height: '7px', margin: '0 20px' }} />
|
||||
<i className="iconfont icon-a-shijianzhongbiao font-14 mr8"></i>
|
||||
{moment(detail.updateTime).format('YYYY.MM.DD')}
|
||||
<Divider type="vertical" style={{ height: '7px', margin: '0 20px' }} />
|
||||
<i className="iconfont icon-xiazai3 font-14 mr8"></i>
|
||||
{formatDownloads(detail.downloadCount)}
|
||||
<Divider type="vertical" style={{ height: '7px', margin: '0 20px' }} />
|
||||
<Tooltip title={liked ? "取消点赞" : "点赞"}>
|
||||
<a className="likeBtn" onClick={() => {
|
||||
if (!login) {
|
||||
showLoginDialog()
|
||||
}
|
||||
likeAISource(id).then(res => {
|
||||
if (res && res.data && res.data.code === 200) {
|
||||
message.success("操作成功")
|
||||
getDetail();
|
||||
getIsLikedSource();
|
||||
}
|
||||
})
|
||||
}}><i className="iconfont icon-dianzan5 font-14 mr8"></i></a>
|
||||
</Tooltip>
|
||||
{detail.likeCount}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab 导航 */}
|
||||
<div className="ai_detail_tabs_wrapper ai_1600">
|
||||
<Tabs
|
||||
className="ai_detail_tabs"
|
||||
activeKey={activeTab}
|
||||
onChange={(tab) => setActiveTab(tab)}
|
||||
centered
|
||||
>
|
||||
<TabPane
|
||||
tab={`${name}介绍`}
|
||||
key="introduction"
|
||||
/>
|
||||
<TabPane
|
||||
tab={`${name}文件`}
|
||||
key="files"
|
||||
/>
|
||||
<TabPane
|
||||
tab={`交流反馈`}
|
||||
key="comments"
|
||||
/>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<div className="ai_detail_content ai_1600">
|
||||
{activeTab === "introduction" && (
|
||||
<div className="ai_detail_tab_panel">
|
||||
{/* 编辑按钮区域 - 仅发布者可见 */}
|
||||
{isPublisher && ["models", "datasets"].includes(type) && !isEditor && <div className="ai_detail_edit_toolbar mb15">
|
||||
{auditStatus === "1" ? (
|
||||
<Tooltip getPopupContainer={trigger => trigger.parentNode} title={<span className="color-grey-98">当前资源是上架状态,请前往<br/><a href={`/ai/${type}/my`} className="color-white">我的资源池</a>页面先进行下架</span>}>
|
||||
<a className="btn-outline pt5 pb5" style={{ cursor: 'not-allowed', color: '#999', borderColor: '#999' }}>编辑</a>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<a className="btn-outline pt5 pb5" onClick={handleToggleEdit}>编辑</a>
|
||||
)}
|
||||
</div>}
|
||||
{isEditor ? (
|
||||
<Fragment>
|
||||
<div className="ai_detail_readme_editor">
|
||||
<TpmMdEditor
|
||||
initValue={editContent}
|
||||
onChange={(value) => setEditContent(value)}
|
||||
height={600}
|
||||
mdID={`ai_detail_readme_editor_${id}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="edu-txt-right mt40">
|
||||
<Button className="mr10" onClick={handleToggleEdit}>取消</Button>
|
||||
<Button type="primary" loading={saving} onClick={handleSave}>保存</Button>
|
||||
</div>
|
||||
</Fragment>
|
||||
) : (
|
||||
<div className="ai_detail_readme">
|
||||
<RenderHtml value={introContent} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "files" && (
|
||||
<div className="ai_detail_tab_panel">
|
||||
<Tree.DirectoryTree
|
||||
className="ai_detail_tree_node"
|
||||
defaultExpandAll={false}
|
||||
treeData={treeData}
|
||||
selectable={false}
|
||||
blockNode={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "comments" && (
|
||||
<div className="ai_detail_tab_panel ai_detail_comments">
|
||||
<CommentsList
|
||||
type={"resource"}
|
||||
id={id}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Detail;
|
||||
|
|
@ -1,385 +0,0 @@
|
|||
.ai_detail_page {
|
||||
background: #f8f9fc url('../image/bk2.png');
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% auto;
|
||||
padding-top: 45px;
|
||||
padding-bottom: 50px;
|
||||
min-height: 89vh;
|
||||
}
|
||||
|
||||
// 加载状态
|
||||
.ai_detail_loading,
|
||||
.ai_detail_nodata {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.ai_detail_nodata_text {
|
||||
color: #76849b;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
// 顶部信息卡片
|
||||
.ai_detail_header {
|
||||
background-image: url('../image/bk3.png');
|
||||
background-size: 100% 100%;
|
||||
padding: 14px 13px;
|
||||
border-radius: 6px;
|
||||
|
||||
.ai_detail_header_inner {
|
||||
background: linear-gradient(90.39deg, #ffffff 1.15%, #ebf1ff 18.37%, #f4f3fd 98.25%);
|
||||
border-radius: 6px;
|
||||
padding: 30px;
|
||||
|
||||
.btn-black, .btn-outline {
|
||||
padding: 7px 32px;
|
||||
border-radius:10px;
|
||||
}
|
||||
.btn-black{
|
||||
border: 1px solid #091221;
|
||||
}
|
||||
|
||||
.ai_detail_header_tag{
|
||||
color: #091221;
|
||||
border-color:#d3e1fd;
|
||||
background:#f5f9fd;
|
||||
padding: 6px 16px 5px 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.likeBtn{
|
||||
color: inherit;
|
||||
&:hover{
|
||||
color:#164ce4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ai_detail_tabs {
|
||||
.ant-tabs-bar {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.ant-tabs-ink-bar {
|
||||
background: #164ce4;
|
||||
border-radius: 10px;
|
||||
height: 3px;
|
||||
width: 20px !important;
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
.ant-tabs-tab {
|
||||
padding: 12px 0px;
|
||||
font-size: 15px;
|
||||
color: #091221;
|
||||
margin-right: 52px;
|
||||
|
||||
&:hover {
|
||||
color: #164ce4;
|
||||
}
|
||||
|
||||
&.ant-tabs-tab-active {
|
||||
color: #164ce4;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tab 内容
|
||||
.ai_detail_tab_panel {
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(22, 76, 228, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
min-height: 365px;
|
||||
|
||||
// 模型介绍
|
||||
// .ai_detail_readme {
|
||||
// .markdown-body {
|
||||
|
||||
// h1,
|
||||
// h2,
|
||||
// h3,
|
||||
// h4,
|
||||
// h5,
|
||||
// h6 {
|
||||
// color: #091221;
|
||||
// margin-top: 24px;
|
||||
// margin-bottom: 16px;
|
||||
// font-weight: 600;
|
||||
// line-height: 1.25;
|
||||
// }
|
||||
|
||||
// h1 {
|
||||
// font-size: 28px;
|
||||
// }
|
||||
|
||||
// h2 {
|
||||
// font-size: 24px;
|
||||
// }
|
||||
|
||||
// h3 {
|
||||
// font-size: 20px;
|
||||
// }
|
||||
|
||||
// h4 {
|
||||
// font-size: 18px;
|
||||
// }
|
||||
|
||||
// p {
|
||||
// font-size: 15px;
|
||||
// line-height: 1.8;
|
||||
// color: #333;
|
||||
// margin: 10px 0;
|
||||
// }
|
||||
|
||||
// a {
|
||||
// color: #0e4aef;
|
||||
// text-decoration: none;
|
||||
|
||||
// &:hover {
|
||||
// text-decoration: underline;
|
||||
// }
|
||||
// }
|
||||
|
||||
// ul,
|
||||
// ol {
|
||||
// margin: 15px 0;
|
||||
// padding-left: 30px;
|
||||
|
||||
// li {
|
||||
// font-size: 15px;
|
||||
// line-height: 1.8;
|
||||
// color: #333;
|
||||
// }
|
||||
// }
|
||||
|
||||
// code {
|
||||
// background: #f5f7fa;
|
||||
// padding: 2px 6px;
|
||||
// border-radius: 4px;
|
||||
// font-family: "Courier New", monospace;
|
||||
// font-size: 14px;
|
||||
// color: #d63384;
|
||||
// }
|
||||
|
||||
// pre {
|
||||
// background: #282c34;
|
||||
// padding: 16px;
|
||||
// border-radius: 8px;
|
||||
// overflow-x: auto;
|
||||
|
||||
// code {
|
||||
// background: none;
|
||||
// padding: 0;
|
||||
// color: #abb2bf;
|
||||
// }
|
||||
// }
|
||||
|
||||
// blockquote {
|
||||
// border-left: 4px solid #0e4aef;
|
||||
// padding: 12px 16px;
|
||||
// margin: 16px 0;
|
||||
// background: #f5f7fa;
|
||||
// border-radius: 4px;
|
||||
|
||||
// p {
|
||||
// margin: 0;
|
||||
// color: #666;
|
||||
// }
|
||||
// }
|
||||
|
||||
// table {
|
||||
// width: 100%;
|
||||
// margin: 16px 0;
|
||||
// border-collapse: collapse;
|
||||
|
||||
// th,
|
||||
// td {
|
||||
// padding: 12px 16px;
|
||||
// border: 1px solid #e1e4e8;
|
||||
// text-align: left;
|
||||
// }
|
||||
|
||||
// th {
|
||||
// background: #f5f7fa;
|
||||
// font-weight: 600;
|
||||
// color: #091221;
|
||||
// }
|
||||
|
||||
// td {
|
||||
// color: #333;
|
||||
// }
|
||||
// }
|
||||
|
||||
// img {
|
||||
// max-width: 100%;
|
||||
// height: auto;
|
||||
// border-radius: 8px;
|
||||
// margin: 16px 0;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// 模型文件
|
||||
.ai_detail_files_header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
|
||||
.ai_detail_branch_select {
|
||||
.ant-select-selector {
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.ai_detail_commit_info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
|
||||
.ai_detail_commit_message {
|
||||
color: #091221;
|
||||
font-size: 14px;
|
||||
max-width: 400px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ai_detail_commit_author {
|
||||
color: #667eea;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ai_detail_commit_sha {
|
||||
font-family: "Courier New", monospace;
|
||||
}
|
||||
|
||||
.ai_detail_commit_time {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.ai_detail_commit_count {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.ai_detail_files_list {
|
||||
.ai_detail_file_item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 15px 20px;
|
||||
margin-bottom: 10px;
|
||||
background: #f8f9fd;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #f1f3ff;
|
||||
border: 1px solid #667eea;
|
||||
}
|
||||
|
||||
.ai_detail_file_info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.ai_detail_file_name {
|
||||
color: #091221;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ai_detail_file_size {
|
||||
color: #76849b;
|
||||
}
|
||||
}
|
||||
|
||||
.ai_detail_file_meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
margin-right: 20px;
|
||||
|
||||
.ai_detail_file_message {
|
||||
color: #76849b;
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ai_detail_file_time {
|
||||
color: #76849b;
|
||||
}
|
||||
}
|
||||
|
||||
.ai_detail_file_download {
|
||||
color: #0e4aef;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #0e4aef;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 交流反馈
|
||||
&.ai_detail_comments {
|
||||
--primary-color: #466aff;
|
||||
--light-color: rgba(70,106,255,0.85);
|
||||
--more-light-color: rgba(70, 106, 255, 0.05);
|
||||
--linear-back-color: linear-gradient(89.9deg,rgba(70, 106, 255, 0) 0%,rgba(70, 106, 255, 0.09) 100%);
|
||||
--tag-back: rgba(70, 106, 255, 0.12);
|
||||
--light-color-2: rgba(70,106,255,0.23);
|
||||
--light-color-3: rgba(70,106,255,0.55);
|
||||
}
|
||||
|
||||
// 文件树节点样式
|
||||
.ai_detail_tree_node {
|
||||
.ant-tree-title{
|
||||
width: calc(100% - 42px);
|
||||
display: inline-flex;
|
||||
justify-content: space-between;
|
||||
font-size: 15px;
|
||||
color:#27264d;
|
||||
}
|
||||
li:not(.ant-tree-treenode-switcher-close):not(.ant-tree-treenode-switcher-open) .ant-tree-node-content-wrapper{
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑工具栏
|
||||
.ai_detail_edit_toolbar {
|
||||
padding: 0 4px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
// Markdown 编辑器容器
|
||||
.ai_detail_readme_editor {
|
||||
// min-height: 500px;
|
||||
|
||||
// 覆盖 TpmMdEditor 的默认样式,使其与页面风格一致
|
||||
.markdown-editor {
|
||||
border-radius: 8px;
|
||||
|
||||
.editor-textarea {
|
||||
font-family: "Courier New", monospace;
|
||||
font-size: 15px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
import axios from 'axios';
|
||||
import { message , notification } from 'antd';
|
||||
import cookie from 'react-cookies';
|
||||
|
||||
function beforeFetch(actionUrl){
|
||||
if (window.location.href.indexOf('localhost') < 0) {
|
||||
axios.defaults.withCredentials = true;
|
||||
}
|
||||
const config = {
|
||||
// headers:{Authorization:"Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjo0NCwidXNlcl9rZXkiOiI3NWY3MmJmYmE2NGU4N2U3NWVjNWMwZDk2OThkMDFiN2EyODE0YjYwIiwidXNlcm5hbWUiOiJpbm5vdiJ9.IviAyZQjPFc4rT3ejAKfJYx3_QgS78RCKHx1XS-A4uI_mrc7_l6wK76HVIoPCEvP0Qimzyc6dpeVW4cE2fILZw"},
|
||||
baseURL: actionUrl,
|
||||
timeout: 1800000, // 请求超时时间
|
||||
}
|
||||
|
||||
const service = axios.create(config);
|
||||
|
||||
service.interceptors.request.use(request => {
|
||||
// let deptId = sessionStorage.getItem('deptId')
|
||||
// request.headers.Authorization = cookie.load('autologin_trustie')
|
||||
// if (deptId) {
|
||||
// // 用于权限区分
|
||||
// request.headers['Dept-Id'] = deptId
|
||||
// }
|
||||
return request
|
||||
})
|
||||
|
||||
service.interceptors.response.use(
|
||||
response => {
|
||||
const res = response||{};
|
||||
if(res && res.data && res.data.code === 404){
|
||||
message.error("错误链接!");
|
||||
window.location.href = '/nopage';
|
||||
return Promise.reject('error');
|
||||
}else if(res && res.data && res.data.code === 500){
|
||||
message.error(res.data.msg);
|
||||
return Promise.reject('error');
|
||||
}else{
|
||||
return response;
|
||||
}
|
||||
},
|
||||
error => {
|
||||
console.log(error);
|
||||
// notification.open({
|
||||
// message: "提示",
|
||||
// description: error.message,
|
||||
// });
|
||||
// return Promise.reject(error);
|
||||
}
|
||||
)
|
||||
return service;
|
||||
}
|
||||
let settings = localStorage.chromesetting&& localStorage.chromesetting !=="undefined"&& JSON.parse(localStorage.chromesetting);
|
||||
// 根据当前IP判断使用哪个zone配置
|
||||
const isSpecificIP = window.location.hostname.startsWith('27.');
|
||||
// let actionUrl = settings && (isSpecificIP ? settings.common.zone : settings.common.zone_local) + '/zoneAdmin/api';
|
||||
|
||||
let actionUrl = settings && settings.common && settings.common.agent
|
||||
const service = beforeFetch(actionUrl);
|
||||
export const httpUrl = actionUrl;
|
||||
export default service;
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
import axios from 'axios';
|
||||
import { message , notification } from 'antd';
|
||||
import cookie from 'react-cookies';
|
||||
|
||||
function beforeFetch(actionUrl){
|
||||
if (window.location.href.indexOf('localhost') < 0) {
|
||||
axios.defaults.withCredentials = true;
|
||||
}
|
||||
const config = {
|
||||
// headers:{Authorization:"Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjo0NCwidXNlcl9rZXkiOiI3NWY3MmJmYmE2NGU4N2U3NWVjNWMwZDk2OThkMDFiN2EyODE0YjYwIiwidXNlcm5hbWUiOiJpbm5vdiJ9.IviAyZQjPFc4rT3ejAKfJYx3_QgS78RCKHx1XS-A4uI_mrc7_l6wK76HVIoPCEvP0Qimzyc6dpeVW4cE2fILZw"},
|
||||
baseURL: actionUrl,
|
||||
timeout: 1800000, // 请求超时时间
|
||||
}
|
||||
|
||||
const service = axios.create(config);
|
||||
|
||||
service.interceptors.request.use(request => {
|
||||
// let deptId = sessionStorage.getItem('deptId')
|
||||
// request.headers.Authorization = cookie.load('autologin_trustie')
|
||||
// if (deptId) {
|
||||
// // 用于权限区分
|
||||
// request.headers['Dept-Id'] = deptId
|
||||
// }
|
||||
return request
|
||||
})
|
||||
|
||||
service.interceptors.response.use(
|
||||
response => {
|
||||
const res = response||{};
|
||||
if(res && res.data && res.data.code === 404){
|
||||
message.error("错误链接!");
|
||||
window.location.href = '/nopage';
|
||||
return Promise.reject('error');
|
||||
}else if(res && res.data && res.data.code === 500){
|
||||
message.error(res.data.msg);
|
||||
return Promise.reject('error');
|
||||
}else{
|
||||
return response;
|
||||
}
|
||||
},
|
||||
error => {
|
||||
console.log(error);
|
||||
// notification.open({
|
||||
// message: "提示",
|
||||
// description: error.message,
|
||||
// });
|
||||
// return Promise.reject(error);
|
||||
}
|
||||
)
|
||||
return service;
|
||||
}
|
||||
let settings = localStorage.chromesetting&& localStorage.chromesetting !=="undefined"&& JSON.parse(localStorage.chromesetting);
|
||||
// 根据当前IP判断使用哪个zone配置
|
||||
const isSpecificIP = window.location.hostname.startsWith('27.');
|
||||
// let actionUrl = settings && (isSpecificIP ? settings.common.zone : settings.common.zone_local) + '/zoneAdmin/api';
|
||||
|
||||
let actionUrl = settings && settings.common && settings.common.agent_login;
|
||||
|
||||
const service = beforeFetch(actionUrl);
|
||||
export const httpUrl = actionUrl;
|
||||
export default service;
|
||||
|
Before Width: | Height: | Size: 587 KiB |
|
Before Width: | Height: | Size: 412 KiB |
|
Before Width: | Height: | Size: 300 KiB |
|
Before Width: | Height: | Size: 625 KiB |
|
Before Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 93 KiB |
113
src/ai/index.jsx
|
|
@ -1,113 +0,0 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { ImageLayerOfCommentHOC } from "../modules/page/layers/ImageLayerOfCommentHOC";
|
||||
import { Route, Switch, withRouter } from "react-router";
|
||||
import { SnackbarHOC } from "educoder";
|
||||
import { CNotificationHOC } from "../modules/courses/common/CNotificationHOC";
|
||||
import { TPMIndexHOC } from "../modules/tpm/TPMIndexHOC";
|
||||
import Loadable from 'react-loadable';
|
||||
import Loading from "../Loading";
|
||||
import './index.scss';
|
||||
import { getCurrentRole, getMainInfos } from "../forge/Information/api";
|
||||
|
||||
const Detail = Loadable({
|
||||
loader: () => import("./detail"),
|
||||
loading: Loading,
|
||||
})
|
||||
const List = Loadable({
|
||||
loader: () => import("./list"),
|
||||
loading: Loading,
|
||||
})
|
||||
const Create = Loadable({
|
||||
loader: () => import("./create"),
|
||||
loading: Loading,
|
||||
})
|
||||
const MyResources = Loadable({
|
||||
loader: () => import("./my-resources"),
|
||||
loading: Loading,
|
||||
})
|
||||
const Agents = Loadable({
|
||||
loader: () => import("./detail/agent"),
|
||||
loading: Loading,
|
||||
})
|
||||
|
||||
function AI(props) {
|
||||
const { current_user: { login } } = props
|
||||
|
||||
const [aiZoneInfo, setAiZoneInfo] = useState(undefined);
|
||||
const [roleByZone, setRoleByZone] = useState("Member");
|
||||
const {id} = aiZoneInfo || {}
|
||||
|
||||
useEffect(() => {
|
||||
getMainInfos("AI").then(res => {
|
||||
if (res && res.data && res.data.data) {
|
||||
setAiZoneInfo(res.data.data)
|
||||
// 存储deptId,所有专区接口header带上
|
||||
sessionStorage.setItem('deptId', res.data.data.deptId)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(()=>{
|
||||
if(id && login){
|
||||
getCurrentRole(id);
|
||||
// login && getCurrentRole(res.data.data.id).then(res1 => {
|
||||
// if (res1 && res1.data && res1.data.data) {
|
||||
// setRoleByZone(res1.data.data.role)
|
||||
// }
|
||||
// })
|
||||
}
|
||||
}, [id, login])
|
||||
|
||||
return <div className="ai_resouce_page">
|
||||
<Switch {...props}>
|
||||
<Route
|
||||
path="/ai/agents/:id"
|
||||
render={(p) => (
|
||||
<Agents {...props} {...p} aiZoneInfo={aiZoneInfo} roleByZone={roleByZone}/>
|
||||
)}
|
||||
></Route>
|
||||
<Route
|
||||
path="/ai/:type/my"
|
||||
render={(p) => (
|
||||
<MyResources {...props} {...p} aiZoneInfo={aiZoneInfo} roleByZone={roleByZone}/>
|
||||
)}
|
||||
></Route>
|
||||
<Route
|
||||
path="/ai/:type/create"
|
||||
render={(p) => (
|
||||
<Create {...props} {...p} aiZoneInfo={aiZoneInfo} roleByZone={roleByZone}/>
|
||||
)}
|
||||
></Route>
|
||||
<Route
|
||||
path="/ai/:type/edit/:id"
|
||||
render={(p) => (
|
||||
<Create {...props} {...p} aiZoneInfo={aiZoneInfo} roleByZone={roleByZone}/>
|
||||
)}
|
||||
></Route>
|
||||
<Route
|
||||
path="/ai/:type/:id"
|
||||
render={(p) => (
|
||||
<Detail {...props} {...p} />
|
||||
)}
|
||||
></Route>
|
||||
<Route
|
||||
path="/ai/:type"
|
||||
render={(p) => (
|
||||
<List {...props} {...p} aiZoneInfo={aiZoneInfo} />
|
||||
)}
|
||||
></Route>
|
||||
<Route
|
||||
path="/ai"
|
||||
render={(p) => (
|
||||
<List {...props} {...p} aiZoneInfo={aiZoneInfo} />
|
||||
)}
|
||||
></Route>
|
||||
</Switch>
|
||||
</div>
|
||||
}
|
||||
export default withRouter(
|
||||
ImageLayerOfCommentHOC({
|
||||
imgSelector: ".imageLayerParent img, .imageLayerParent .imageTarget",
|
||||
parentSelector: ".newMain",
|
||||
})(CNotificationHOC()(SnackbarHOC()(TPMIndexHOC(AI))))
|
||||
);
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
@import url('../animations.scss');
|
||||
|
||||
.ai_resouce_page {
|
||||
font-family: "alibabaReg";
|
||||
color: #091221;
|
||||
|
||||
.ai_1600 {
|
||||
width: 1600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.color091 {
|
||||
color: #091221;
|
||||
}
|
||||
|
||||
.color768 {
|
||||
color: #76849b;
|
||||
}
|
||||
|
||||
.color3f4 {
|
||||
color: #3f4f69;
|
||||
}
|
||||
|
||||
.color828 {
|
||||
color: #8285a5;
|
||||
}
|
||||
|
||||
.coloraeb{
|
||||
color:#aebbd0;
|
||||
}
|
||||
|
||||
.mlat {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.mrat {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.shrink0{
|
||||
flex-shrink: 0
|
||||
}
|
||||
|
||||
.ai_banner_title {
|
||||
font-size: 52px;
|
||||
font-family: "DouyinSansBold";
|
||||
|
||||
.carousel_text {
|
||||
background-image: linear-gradient(0.27deg, #0e4aef 1.35%, #a74eff 87.93%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
border: 1px solid #091221;
|
||||
border-radius: 22px;
|
||||
color: #091221;
|
||||
font-size: 15px;
|
||||
padding: 11px 20px;
|
||||
}
|
||||
|
||||
.btn-black {
|
||||
background: #091221;
|
||||
border-radius: 9px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.arrowRight{
|
||||
.iconfont {
|
||||
display: inline-block;
|
||||
animation: arrow-move 1.2s ease-in-out infinite;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,456 +0,0 @@
|
|||
import { Fragment, useEffect, useState } from "react";
|
||||
import './index.scss';
|
||||
|
||||
// 预置智能体 Mock 数据(从预置12个智能体.md 提炼)
|
||||
const agentMockCate = [
|
||||
{ id: "work", name: "工作" },
|
||||
{ id: "study", name: "学习" },
|
||||
{ id: "writing", name: "写作" },
|
||||
{ id: "programming", name: "编程" },
|
||||
{ id: "life", name: "生活" },
|
||||
{ id: "health", name: "健康" }
|
||||
];
|
||||
|
||||
const agentMockList = [
|
||||
// 工作类
|
||||
{
|
||||
id: "meeting-minutes",
|
||||
name: "会议纪要助手",
|
||||
image: "",
|
||||
desc: "专注于会议纪要的整理与提炼,帮助用户快速生成结构清晰、重点突出的会议纪要。",
|
||||
cate: "work"
|
||||
},
|
||||
{
|
||||
id: "project-management",
|
||||
name: "项目管理助手",
|
||||
image: "",
|
||||
desc: "协助项目管理者进行任务拆解、进度跟踪、风险识别与资源协调,提升项目管理效率。",
|
||||
cate: "work"
|
||||
},
|
||||
// 学习类
|
||||
{
|
||||
id: "knowledge-combination",
|
||||
name: "知识梳理助手",
|
||||
image: "",
|
||||
desc: "帮助用户将零散的知识点系统化整理,构建知识框架,提升学习效率与记忆效果。",
|
||||
cate: "study"
|
||||
},
|
||||
{
|
||||
id: "training-planning",
|
||||
name: "培训规划助手",
|
||||
image: "",
|
||||
desc: "根据岗位需求与员工能力现状,协助制定科学合理的培训计划与课程体系。",
|
||||
cate: "study"
|
||||
},
|
||||
// 写作类
|
||||
{
|
||||
id: "official-writing",
|
||||
name: "公文写作助手",
|
||||
image: "",
|
||||
desc: "专注于各类公文的撰写与润色,确保格式规范、用语准确、逻辑严密。",
|
||||
cate: "writing"
|
||||
},
|
||||
{
|
||||
id: "report-writing",
|
||||
name: "报告撰写助手",
|
||||
image: "",
|
||||
desc: "协助用户撰写各类工作报告、调研报告、总结报告,提升报告的专业性与可读性。",
|
||||
cate: "writing"
|
||||
},
|
||||
// 编程类
|
||||
{
|
||||
id: "code-review",
|
||||
name: "代码审查助手",
|
||||
image: "",
|
||||
desc: "协助开发人员进行代码审查,发现潜在问题,提升代码质量与可维护性。",
|
||||
cate: "programming"
|
||||
},
|
||||
{
|
||||
id: "tech-documentation",
|
||||
name: "技术文档助手",
|
||||
image: "",
|
||||
desc: "协助技术人员撰写 API 文档、系统设计文档、操作手册等技术文档,确保文档清晰准确。",
|
||||
cate: "programming"
|
||||
},
|
||||
// 生活类
|
||||
{
|
||||
id: "schedule-planning",
|
||||
name: "日程规划助手",
|
||||
image: "",
|
||||
desc: "协助用户合理安排日常工作与生活日程,提升时间利用效率,平衡工作与生活。",
|
||||
cate: "life"
|
||||
},
|
||||
{
|
||||
id: "meeting-arrangement",
|
||||
name: "会议安排助手",
|
||||
image: "",
|
||||
desc: "协助用户高效安排会议时间、协调与会人员、准备会议材料,减少会议组织成本。",
|
||||
cate: "life"
|
||||
},
|
||||
// 健康类
|
||||
{
|
||||
id: "schedule-management",
|
||||
name: "作息管理助手",
|
||||
image: "",
|
||||
desc: "协助用户建立科学的作息规律,提供睡眠改善建议与作息调整方案,促进身心健康。",
|
||||
cate: "health"
|
||||
},
|
||||
{
|
||||
id: "exercise-planning",
|
||||
name: "运动计划助手",
|
||||
image: "",
|
||||
desc: "根据用户的身体状况与运动目标,协助制定科学合理的运动计划,促进身体健康。",
|
||||
cate: "health"
|
||||
}
|
||||
];
|
||||
import { aiResouceType, aiResouceTypeKey, iconByCateIndex, iconByCateName } from "../static";
|
||||
import icon1 from '../image/icon1.png';
|
||||
import icon2 from '../image/icon2.png';
|
||||
import icon3 from '../image/icon3.png';
|
||||
import icon5 from '../image/icon5.png';
|
||||
import icon6 from '../image/icon6.png';
|
||||
import { Divider, Input, Pagination, Spin } from "antd";
|
||||
import ActionItem from "../../factory/resource/components/ActionItem";
|
||||
import { Link } from "react-router-dom";
|
||||
import { getAISourceList, getProjectsLists, getProjectsTypeLists, getSourceZoneList } from "../../forge/Information/api";
|
||||
import moment from "moment";
|
||||
import Nodata from "../../forge/Nodata";
|
||||
import { getAgentList, getAgentTypeList } from "../api";
|
||||
import { httpUrl } from "../fetch";
|
||||
|
||||
function List(props) {
|
||||
|
||||
const { match: { params }, history, aiZoneInfo, current_user, showLoginDialog } = props || {};
|
||||
const type = params.type || aiResouceType[0].key
|
||||
const [activeCategory, setActiveCategory] = useState();
|
||||
const [page, setPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [cate, setCate] = useState([]);
|
||||
const [list, setList] = useState();
|
||||
const [search, setSearch] = useState();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const limit = 12;
|
||||
const { id: aiZoneId } = aiZoneInfo || {}
|
||||
const aiTypeInList = [
|
||||
...aiResouceType,
|
||||
{ key: "agents", name: "智能体", icon: "icon-zhinengti", intro: "驱动行业数智化", desc: "矩阵覆盖全场景需求,极速响应精准输出,低成本释放 AI 算力,赋能每一项任务。", createIntro: "上传您的智能体文件" }
|
||||
]
|
||||
const typeDetail = type && aiTypeInList.find(i => i.key === type);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `AI资源广场`;
|
||||
}, []);
|
||||
|
||||
// 获取分类列表:agents 类型不依赖 aiZoneId,单独处理避免重复请求
|
||||
useEffect(() => {
|
||||
if (type === "agents") {
|
||||
if(!httpUrl){
|
||||
setCate(agentMockCate);
|
||||
return;
|
||||
}
|
||||
getAgentTypeList().then(res => {
|
||||
if (res && res.data && res.data.data) {
|
||||
const data = res.data.data
|
||||
setCate(data.map(item=>{
|
||||
return {
|
||||
...item,
|
||||
name: item.typeName,
|
||||
id: item.typeKey
|
||||
}
|
||||
}));
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [type])
|
||||
|
||||
// 其他类型需要 aiZoneId,当 aiZoneId 变化时获取分类
|
||||
useEffect(() => {
|
||||
if (type === "agents") return; // agents 类型已在上方的 useEffect 中处理
|
||||
|
||||
if (!aiZoneId) return;
|
||||
|
||||
switch (type) {
|
||||
case "codes":
|
||||
getProjectsTypeLists(aiZoneId).then(res => {
|
||||
if (res && res.data && res.data.rows) {
|
||||
setCate(res.data.rows);
|
||||
}
|
||||
})
|
||||
break;
|
||||
default:
|
||||
getSourceZoneList(aiZoneId, {
|
||||
domainCategory: aiResouceTypeKey[type]
|
||||
}).then(res => {
|
||||
if (res && res.data && res.data.rows) {
|
||||
setCate(res.data.rows);
|
||||
}
|
||||
})
|
||||
break;
|
||||
}
|
||||
}, [type, aiZoneId])
|
||||
|
||||
useEffect(() => {
|
||||
if (type === "agents") {
|
||||
setLoading(true);
|
||||
if(!httpUrl){
|
||||
let filteredList = [...agentMockList];
|
||||
if (activeCategory) {
|
||||
filteredList = filteredList.filter(item => item.cate === activeCategory);
|
||||
}
|
||||
if (search) {
|
||||
filteredList = filteredList.filter(item =>
|
||||
item.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
item.desc.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
}
|
||||
const start = (page - 1) * limit;
|
||||
setList(filteredList.slice(start, start + limit));
|
||||
setTotal(filteredList.length);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
getAgentList({
|
||||
page: page,
|
||||
pageSize: limit,
|
||||
search: search,
|
||||
type: activeCategory
|
||||
}).then(res => {
|
||||
if (res && res.data && res.data.data) {
|
||||
const data = res.data.data;
|
||||
setList(data.pageData.map(row => {
|
||||
const { botId, botCoverUrl, botName, botDesc } = row;
|
||||
return {
|
||||
id: botId,
|
||||
name: botName,
|
||||
image: botCoverUrl,
|
||||
desc: botDesc,
|
||||
}}
|
||||
));
|
||||
setTotal(data.totalCount);
|
||||
}
|
||||
}).finally(() => {
|
||||
setLoading(false);
|
||||
})
|
||||
}
|
||||
}, [type, activeCategory, page, search])
|
||||
|
||||
useEffect(() => {
|
||||
if (type === "agents" || !aiZoneId) return
|
||||
setLoading(true);
|
||||
switch (type) {
|
||||
case "codes":
|
||||
getProjectsLists(aiZoneId, {
|
||||
pageNum: page,
|
||||
pageSize: limit,
|
||||
projectTypeId: activeCategory,
|
||||
name: search
|
||||
}).then(res => {
|
||||
if (res && res.data && res.data.rows) {
|
||||
setList(res.data.rows.map(row => {
|
||||
const { projectProperties, id } = row;
|
||||
const imageUrl = projectProperties.authorImageUrl.split(':4000/') && projectProperties.authorImageUrl.split(':4000/')[1]
|
||||
const authorImageUrl = imageUrl ? `${window.location.origin}/${imageUrl}` : projectProperties.authorImageUrl
|
||||
return {
|
||||
id: id,
|
||||
name: projectProperties.fromOwner === "AI-RESOURCE" ? projectProperties.name.split("/").pop() : projectProperties.name,
|
||||
image: authorImageUrl,
|
||||
isNew: false,
|
||||
url: projectProperties.fullName,
|
||||
desc: projectProperties.description,
|
||||
date: projectProperties.timeAgo,
|
||||
forkedCount: projectProperties.forkedCount,
|
||||
likes: projectProperties.praisesCount,
|
||||
language: projectProperties.language ? projectProperties.language.split(",") : []
|
||||
}
|
||||
}))
|
||||
setTotal(res.data.total)
|
||||
}
|
||||
}).finally(() => {
|
||||
setLoading(false);
|
||||
})
|
||||
break;
|
||||
default:
|
||||
getAISourceList(aiZoneId, {
|
||||
domainId: activeCategory,
|
||||
pageNum: page,
|
||||
pageSize: limit,
|
||||
name: search,
|
||||
resourceCategory: aiResouceTypeKey[type]
|
||||
}).then(res => {
|
||||
if (res && res.data && res.data.rows) {
|
||||
setList(res.data.rows.map(row => {
|
||||
const { name, id, summary, updateTime, downloadCount, likeCount, domainName } = row;
|
||||
return {
|
||||
id: id,
|
||||
name: name,
|
||||
isNew: false,
|
||||
desc: summary,
|
||||
date: moment(updateTime).format('YYYY.MM.DD'),
|
||||
downloads: downloadCount,
|
||||
likes: likeCount,
|
||||
domainName
|
||||
}
|
||||
}))
|
||||
setTotal(res.data.total)
|
||||
}
|
||||
}).finally(() => {
|
||||
setLoading(false);
|
||||
})
|
||||
break;
|
||||
}
|
||||
}, [type, aiZoneId, activeCategory, page, search])
|
||||
|
||||
const aiCard = (model) => {
|
||||
return <Fragment>
|
||||
<i className="iconfont icon-a-shijianzhongbiao font-13 mr8"></i>
|
||||
<span>{model.date}</span>
|
||||
<Divider type="vertical" style={{ height: '7px', margin: '0 18px' }} />
|
||||
{Object.prototype.hasOwnProperty.call(model, "downloads") && <Fragment>
|
||||
<i className="iconfont icon-xiazai3 font-12 mr8"></i>
|
||||
<span>{model.downloads}</span>
|
||||
<Divider type="vertical" style={{ height: '7px', margin: '0 18px' }} />
|
||||
</Fragment>}
|
||||
{Object.prototype.hasOwnProperty.call(model, "forkedCount") && <Fragment>
|
||||
<i className="iconfont icon-fork font-14 mr8"></i>
|
||||
<span>{model.forkedCount}</span>
|
||||
<Divider type="vertical" style={{ height: '7px', margin: '0 18px' }} />
|
||||
</Fragment>}
|
||||
<i className="iconfont icon-dianzan5 font-12 mr8"></i>
|
||||
<span>{model.likes}</span>
|
||||
</Fragment>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ai_home_page">
|
||||
{/* Banner 区域 */}
|
||||
<div className="ai_home_banner center">
|
||||
<div className="ai_home_banner_title mb10 ai_banner_title">
|
||||
<span className="carousel_text mr40">AI资源广场</span>
|
||||
<span>汇聚无限创意</span>
|
||||
<div className="ai_home_banner_stars">
|
||||
<img src={icon2} className="star_1" />
|
||||
<img src={icon3} className="star_2" />
|
||||
<img src={icon1} width={49} className="star_3" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="font-16 mb40">
|
||||
面向开发者提供可信、合规、高效的 AI 资产分发中心。涵盖百亿级模型权重库、千万量级专业数据集、托管代码仓及<br />智能skill中心。实现从资源检索、安全审计到业务集成的一体化研发生态。
|
||||
</div>
|
||||
<div className="ai_home_banner_tabs">
|
||||
{aiTypeInList.map((item) => (
|
||||
<Link
|
||||
to={`/ai/${item.key}`}
|
||||
onClick={()=>{setActiveCategory(undefined); setList([])}}
|
||||
key={item.key}
|
||||
className={`ai_home_banner_tab font-15 pointer ${type === item.key ? 'active' : ''}`}
|
||||
>
|
||||
<i className={`iconfont ${item.icon} font-14 mr10`}></i>
|
||||
<i className={`${type === item.key ? 'iconfont icon-' : 'iconfont-color icon-color'}${item.icon} font-14 mr10`}></i>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 主体内容区域 */}
|
||||
<div className="ai_home_content ai_1600 pb100">
|
||||
{/* 左侧分类筛选 */}
|
||||
<div className="ai_home_sidebar">
|
||||
<div className="font-22 font-bd">全部{typeDetail.name}</div>
|
||||
<div className="font-16 color768 mt10 mb30">
|
||||
{typeDetail.desc}
|
||||
</div>
|
||||
<ul className="ai_home_sidebar_list">
|
||||
<li
|
||||
className={`ai_home_sidebar_item mb30 ${!activeCategory ? 'active' : ''}`}
|
||||
onClick={() => setActiveCategory(undefined)}
|
||||
>
|
||||
{type !== "agents" && <i className={`${!activeCategory ? "iconfont icon-remenmoxing-2" : "iconfont-color icon-colorremenmoxing-1"} font-14 mr10`}></i>}
|
||||
全部
|
||||
</li>
|
||||
{cate.map((category, index) => {
|
||||
const icons = iconByCateName[category.name] ? iconByCateName[category.name] : (iconByCateIndex[type] && iconByCateIndex[type][index]) || ["iconfont-color icon-colorkexueyanjiuhejishufuwuye", "iconfont icon-a-kexueyanjiuhejishufuwuye-1"]
|
||||
return <li
|
||||
key={category.id}
|
||||
className={`ai_home_sidebar_item mb30 ${activeCategory === category.id ? 'active' : ''}`}
|
||||
onClick={() => setActiveCategory(category.id)}
|
||||
>
|
||||
{type !== "agents" && <i className={`${icons[Number(activeCategory === category.id)]} font-14 mr10`}></i>}
|
||||
{category.name}
|
||||
</li>
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* 右侧内容区 */}
|
||||
<div className="ai_home_main flex1">
|
||||
{/* 搜索和操作栏 */}
|
||||
<div className="ai_home_toolbar">
|
||||
<Input.Search className="ai_home_search flex1" placeholder={`输入关键词搜索您想要的${typeDetail.name}(共${total}个)`} enterButton={<i className="iconfont icon-sousuo5 font-15"></i>} onSearch={(value) => { setPage(1); setSearch(value) }} />
|
||||
{current_user && current_user.login && type !== "agents" && <div className="">
|
||||
<a className="btn-outline font-bd mr10" href={`/ai/${type}/create`}> 新建 </a>
|
||||
<a className="btn-outline font-bd ai_home_action_my" href={`/ai/${type}/my`}>我的{typeDetail.name}<i className="iconfont icon-arrright ml5"></i></a>
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
{/* 模型列表 */}
|
||||
<Spin spinning={loading}>
|
||||
{list && !!list.length ? <div className={`ai_home_list ${type}_pattern`}>
|
||||
{list.map((model) => (
|
||||
<ActionItem key={`${type}-${model.id}`} className="ai_home_card" padding="1px" borderRadius="12px">
|
||||
{type === "agents" ? <div>
|
||||
<div className="df">
|
||||
<img src={model.image ? model.image : require(`../image/${model.id}.png`).default} width={46} className="mr15" style={{ borderRadius: '8px' }} />
|
||||
<span className="font-18 font-bd task-hide">{model.name}</span>
|
||||
</div>
|
||||
<div className="color3f4 mt15 task-hide-2" style={{minHeight: 56}}>{model.desc}</div>
|
||||
<div className="btn-Box">
|
||||
{httpUrl ? <a className="btn-black" href={`/ai/${type}/${model.id}`} onClick={(e) => { if (!current_user || !current_user.login) { e.preventDefault(); e.stopPropagation(); showLoginDialog(); } }}>立即使用</a> : <a className="unClickBtn">暂未开放</a>}
|
||||
</div>
|
||||
</div>:<div>
|
||||
<div className="df-center">
|
||||
{model.image && <img src={model.image} width={38} className="mr15" style={{ borderRadius: '8px' }} />}
|
||||
{model.icon && type !== "skills" && <i className={`iconfont ${model.icon} font-30 mr15`}></i>}
|
||||
<div className="flex1 task-hide mr10" style={{minHeight: '40px'}}>
|
||||
<div className="font-16 font-bd">{model.name}{model.isNew && <i className="iconfont-color icon-colora-zuixin1 font-20 ml10"></i>}</div>
|
||||
{model.desc && <div className="color3f4">{model.desc}</div>}
|
||||
</div>
|
||||
<a className="btn-black arrowRight" href={type === "codes" ? `/${model.url}` : `/ai/${type}/${model.id}`}>查看<i className="iconfont icon-arrright"></i></a>
|
||||
</div>
|
||||
{type === "skills" ? <div className="df-center skills_bottom color828">
|
||||
<img src={icon6} width={18} />
|
||||
<span className="mrat ml10 color091 font-13">{model.domainName}</span>
|
||||
{aiCard(model)}
|
||||
</div> : <Fragment>
|
||||
{type === "codes" ? <div className="mt15"></div> : <Divider dashed />}
|
||||
<div className="df-center color828">
|
||||
{aiCard(model)}
|
||||
{type === "codes" ? <div className="df-center mlat">
|
||||
{model.language && !!model.language.length && <Fragment>
|
||||
<img src={icon5} className="mr8 mt1" width={18} />
|
||||
{model.language.map((topic, index) => {
|
||||
return <Fragment>
|
||||
<span className="color091">{topic}</span>
|
||||
{index < model.language.length - 1 && <Divider type="vertical" style={{ height: '7px' }} className="mt5" />}
|
||||
</Fragment>
|
||||
})}
|
||||
</Fragment>}
|
||||
</div> : <span className="ai_home_card_tag mlat">{model.domainName}</span>}
|
||||
</div>
|
||||
</Fragment>}
|
||||
</div>}
|
||||
</ActionItem>
|
||||
))}
|
||||
</div> : <div className="pt80"><Nodata _html="暂无数据"/></div>}
|
||||
</Spin>
|
||||
|
||||
{/* 分页 */}
|
||||
<Pagination className="edu-txt-right ai_list_Pagination mb70 black_active_Pagination" current={page} onChange={(p) => { setPage(p) }} pageSize={limit} total={total} showQuickJumper hideOnSinglePage />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default List;
|
||||
|
|
@ -1,323 +0,0 @@
|
|||
.ai_home_page {
|
||||
background: linear-gradient(180deg, #f8f8fe 0%, #f1f6fd 100%);
|
||||
|
||||
// Banner 区域
|
||||
.ai_home_banner {
|
||||
background-image: url('../image/bk1.png');
|
||||
background-size: 100% 100%;
|
||||
padding: 50px 20px 80px 20px;
|
||||
|
||||
.ai_home_banner_title {
|
||||
|
||||
// 星星装饰
|
||||
.ai_home_banner_stars {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-left: 20px;
|
||||
|
||||
.star_1,
|
||||
.star_2 {
|
||||
position: absolute;
|
||||
animation: twinkle 2s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.star_1 {
|
||||
width: 35px;
|
||||
top: -35px;
|
||||
}
|
||||
|
||||
.star_2 {
|
||||
width: 20px;
|
||||
animation-delay: 0.5s;
|
||||
top: -46px;
|
||||
left: 38px;
|
||||
}
|
||||
|
||||
.star_3 {
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
left: 25px;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Tab 切换
|
||||
.ai_home_banner_tabs {
|
||||
border: 1px solid #091221;
|
||||
border-radius: 24px;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ai_home_banner_tab {
|
||||
font-weight: 600;
|
||||
padding: 7px 36px;
|
||||
|
||||
&,
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: #091221;
|
||||
}
|
||||
|
||||
i {
|
||||
display: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #091221;
|
||||
border-radius: 22px;
|
||||
color: white;
|
||||
|
||||
i {
|
||||
display: inline-block;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-left: -18px;
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
background: rgba(9, 18, 33, 0.05);
|
||||
border-radius: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 主体内容区域
|
||||
.ai_home_content {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
// 左侧边栏
|
||||
.ai_home_sidebar {
|
||||
flex: 0 0 340px;
|
||||
height: fit-content;
|
||||
|
||||
.ai_home_sidebar_list {
|
||||
.ai_home_sidebar_item {
|
||||
width: fit-content;
|
||||
padding: 7px 33px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 1px solid rgba(22, 76, 228, 0.1);
|
||||
border-radius: 22px;
|
||||
|
||||
&:hover {
|
||||
border-color: #091221;
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: #091221;
|
||||
background: #091221;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 工具栏
|
||||
.ai_home_toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.ai_home_search {
|
||||
background: linear-gradient(89.82deg, #ffffff 1.89%, #f8f9fd 99.99%);
|
||||
border: 1px solid rgba(22, 76, 228, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 8px 15px;
|
||||
|
||||
.ant-input-search-button {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: #091221;
|
||||
border-radius: 16px;
|
||||
padding: 0;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
input {
|
||||
background: none !important;
|
||||
border: none;
|
||||
font-size: 15px;
|
||||
|
||||
&::placeholder {
|
||||
color: #76849b;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ai_home_action_my{
|
||||
i{
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
&:hover{
|
||||
i{
|
||||
animation: arrow-move 1.2s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 模型列表
|
||||
.ai_home_list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.ai_home_list.skills_pattern .ai_home_card {
|
||||
&:nth-child(4n+1) {
|
||||
.action-item_content {
|
||||
background: linear-gradient(88.92deg, #ececff 2.11%, #f8f9fd 99.79%);
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(4n) {
|
||||
.action-item_content {
|
||||
background: linear-gradient(88.92deg, #fdf5fd 2.11%, #f8f9fd 99.79%);
|
||||
}
|
||||
}
|
||||
|
||||
.action-item_content {
|
||||
background: linear-gradient(88.92deg, #eef3fd 2.11%, #f8f9fd 99.79%);
|
||||
}
|
||||
}
|
||||
|
||||
.codes_pattern {
|
||||
.ai_home_card {
|
||||
.action-item_content {
|
||||
padding: 13px 14px;
|
||||
background: linear-gradient(88.92deg, #E3E9FE 2.11%, #ECF1FD 99.79%);
|
||||
|
||||
&>div {
|
||||
padding: 20px 26px;
|
||||
background: linear-gradient(90.61deg, #ffffff 1.22%, rgba(255, 255, 255, 0.44) 18.41%, rgba(255, 255, 255, 0.66) 98.18%);
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(4n) .action-item_content {
|
||||
background: linear-gradient(88.92deg, #eaeeff 2.11%, #F5F7FE 99.79%);
|
||||
}
|
||||
|
||||
&:nth-child(4n+1) .action-item_content {
|
||||
background: linear-gradient(88.92deg, #ebecff 2.11%, #f1f1fd 99.79%);
|
||||
}
|
||||
|
||||
&:nth-child(4n+2) .action-item_content {
|
||||
background: linear-gradient(88.92deg, #E8F4F9 2.11%, #F5F9FE 99.79%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.agents_pattern{
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
.ai_home_card{
|
||||
position: relative;
|
||||
.action-item_content{
|
||||
padding: 27px 36px;
|
||||
background-image: url('../image/card_bk3.png');
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
&:hover{
|
||||
.btn-Box{
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 17px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.btn-Box{
|
||||
display: none;
|
||||
}
|
||||
.btn-black{
|
||||
padding: 4px 48px 4px 48px;
|
||||
}
|
||||
.unClickBtn{
|
||||
padding: 8px 47px 8px 47px;
|
||||
background:#9ea2a8;
|
||||
border-radius:10px;
|
||||
color:#ffffff;
|
||||
font-size:15px;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 卡片
|
||||
.ai_home_card {
|
||||
width: auto;
|
||||
|
||||
&:hover {
|
||||
.btn-black {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
&.action-item:hover:before {
|
||||
background: conic-gradient(from 180deg at 50% 50%,
|
||||
#587fee 0deg,
|
||||
#9467e3 45deg,
|
||||
transparent 90deg,
|
||||
transparent 180deg,
|
||||
#587fee 180deg,
|
||||
#9467e3 225deg,
|
||||
transparent 270deg);
|
||||
}
|
||||
|
||||
.action-item_content {
|
||||
border: 1px solid rgba(22, 76, 228, 0.1);
|
||||
background: white url('../image/card_bk1.png');
|
||||
background-blend-mode: multiply;
|
||||
padding: 20px 32px;
|
||||
width: auto;
|
||||
border-radius: 11px;
|
||||
}
|
||||
|
||||
.ai_home_card_tag {
|
||||
background: #f5f9fd;
|
||||
border: 1px solid #d3e1fd;
|
||||
border-radius: 4px;
|
||||
color: #091221;
|
||||
font-size: 13px;
|
||||
line-height: 24px;
|
||||
padding: 1px 9px;
|
||||
}
|
||||
|
||||
.icon-colora-zuixin1 {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.btn-black {
|
||||
padding: 0px 19px 4px 19px;
|
||||
display: none;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
.skills_bottom {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border-radius: 6px;
|
||||
padding: 13px 16px;
|
||||
margin-top: 17px;
|
||||
}
|
||||
|
||||
.ai_list_Pagination {
|
||||
margin-top: 45px;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,234 +0,0 @@
|
|||
import { Fragment, useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Table, Pagination, Badge, message } from "antd";
|
||||
import './index.scss';
|
||||
import { aiResouceStatus, aiResouceType, aiResouceTypeKey } from "../static";
|
||||
import { drawSourceById, getAIProjectStatistics, getAIResourceStatistics, getMyCodeList, getMyResourceList, takeUpSourceById } from "../../forge/Information/api";
|
||||
import { DeleteModal, TakeDownModal } from "../component";
|
||||
import moment from "moment";
|
||||
|
||||
function MyResources(props) {
|
||||
const pageSize = 10;
|
||||
const { match: { params }, history, aiZoneInfo, roleByZone, current_user } = props || {};
|
||||
const type = params.type;
|
||||
const { id: aiZoneId } = aiZoneInfo || {}
|
||||
const { login } = current_user || {}
|
||||
const [current, setCurrent] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [reload, setReload] = useState(undefined);
|
||||
const [list, setList] = useState([]);
|
||||
const [deleteId, setDeleteId] = useState();
|
||||
const [downId, setDownId] = useState();
|
||||
const [stats, setStats] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `我的资源池`
|
||||
}, [])
|
||||
|
||||
// 登录检查:如果未登录则跳转到登录页面
|
||||
useEffect(() => {
|
||||
if (!login) {
|
||||
history.push(`/login?go_page=${encodeURIComponent(window.location.pathname)}`);
|
||||
}
|
||||
}, [login]);
|
||||
|
||||
useEffect(() => {
|
||||
if (aiZoneId && type) {
|
||||
if(type === "codes") {
|
||||
getAIProjectStatistics({
|
||||
zoneId: aiZoneId,
|
||||
}).then(res => {
|
||||
if (res && res.data && res.data.code === 200) {
|
||||
const { likeCount, publishedCount, pendingCount } = res.data.data || {};
|
||||
setStats([
|
||||
{ key: 'likes', label: '累计获得点赞', value: likeCount || 0 },
|
||||
{ key: 'published', label: '已发布', value: publishedCount || 0 },
|
||||
{ key: 'reviewing', label: '审核中', value: pendingCount || 0 },
|
||||
]);
|
||||
}
|
||||
})
|
||||
return;
|
||||
}
|
||||
getAIResourceStatistics({
|
||||
isMine: true,
|
||||
resourceCategory: aiResouceTypeKey[type],
|
||||
zoneId: aiZoneId,
|
||||
}).then(res => {
|
||||
if (res && res.data && res.data.code === 200) {
|
||||
const { likeCount, publishedCount, pendingCount } = res.data.data || {};
|
||||
setStats([
|
||||
{ key: 'likes', label: '累计获得点赞', value: likeCount || 0 },
|
||||
{ key: 'published', label: '已发布', value: publishedCount || 0 },
|
||||
{ key: 'reviewing', label: '审核中', value: pendingCount || 0 },
|
||||
]);
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [aiZoneId, type, reload])
|
||||
|
||||
useEffect(() => {
|
||||
if(type === "codes" && aiZoneId) {
|
||||
// 获取聚合的代码库列表
|
||||
getMyCodeList({
|
||||
zoneId: aiZoneId,
|
||||
pageNum: current,
|
||||
pageSize: pageSize,
|
||||
}, roleByZone).then(res => {
|
||||
if(res && res.data && res.data.code === 200){
|
||||
setList(res.data.rows);
|
||||
setTotal(res.data.total)
|
||||
}
|
||||
});
|
||||
return
|
||||
}
|
||||
aiZoneId && getMyResourceList({
|
||||
auditStatus: "all",
|
||||
zoneId: aiZoneId,
|
||||
resourceCategory: aiResouceTypeKey[type],
|
||||
pageNum: current,
|
||||
pageSize: pageSize,
|
||||
}, roleByZone).then(res => {
|
||||
if(res && res.data && res.data.code === 200){
|
||||
setList(res.data.rows);
|
||||
setTotal(res.data.total)
|
||||
}
|
||||
})
|
||||
}, [type, aiZoneId, current, reload, roleByZone])
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: <span className="pl25">资源名称</span>,
|
||||
dataIndex: type === "codes" ? "projectProperties.name" : 'name',
|
||||
key: 'name',
|
||||
render: (text, record) => {
|
||||
return <a href={type === "codes" ? `/${record.projectProperties && record.projectProperties.fullName}` : `/ai/${type}/${record.id}`}>{text}</a>
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '提交时间',
|
||||
dataIndex: 'updateTime',
|
||||
key: 'updateTime',
|
||||
render: (text) => {
|
||||
return moment(text).format('YYYY-MM-DD HH:mm')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'auditStatus',
|
||||
dataIndex: 'auditStatus',
|
||||
render: (text, record) => {
|
||||
const status = aiResouceStatus[text] || {};
|
||||
return <Badge color={status.color} text={status.label} className='status-dot' />
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
className: 'col-actions',
|
||||
render: (text, record) => {
|
||||
const { auditStatus, id } = record;
|
||||
return type === "codes" ? <a onClick={()=>{setDeleteId(id)}}>删除</a> : auditStatus === "1" ? <a onClick={()=>{setDownId(id)}}>下架</a> : auditStatus === "0" ? <a onClick={()=>{
|
||||
drawSourceById(id).then(res=>{
|
||||
if(res && res.data && res.data.code === 200){
|
||||
message.success(`操作成功`);
|
||||
setReload(Math.random());
|
||||
}
|
||||
})
|
||||
}}>撤回</a> :
|
||||
<Fragment>
|
||||
{auditStatus === "3" && <a className="mr20" onClick={()=>{
|
||||
takeUpSourceById(id, roleByZone).then(res=>{
|
||||
if(res && res.data && res.data.code === 200){
|
||||
message.success(`操作成功`);
|
||||
setReload(Math.random());
|
||||
}
|
||||
})
|
||||
}}>上架</a>}
|
||||
<a className="mr20" href={`/ai/${type}/edit/${id}`}>修改</a>
|
||||
<a onClick={()=>{setDeleteId(id)}}>删除</a>
|
||||
</Fragment>
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// 分页变化处理
|
||||
const handlePageChange = (page) => {
|
||||
setCurrent(page);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="my-resources-page">
|
||||
<div className="ai_1600 container">
|
||||
{/* 左侧区域 */}
|
||||
<div className="left-panel pt40">
|
||||
{/* 标题区域 */}
|
||||
<div className="header-section ai_banner_title">
|
||||
<Link to={`/ai${type === "all" ? '' : `/${type}`}`} className="back-btn">
|
||||
<i className="iconfont icon-jiantou2 font-20"></i>
|
||||
</Link>
|
||||
<span className="font-40">我的资源池</span>
|
||||
<div className="carousel_text mt5">栈筑 AI 全能力</div>
|
||||
</div>
|
||||
<div className="font-15 color768 mt20">
|
||||
聚合数据集、模型、Skill 场景能力与工程代码,模块化灵活编排,一站式搭建全链路智能 AI 应用
|
||||
</div>
|
||||
|
||||
{/* 分类标签 */}
|
||||
<div className="category-tabs mt30">
|
||||
{aiResouceType.map((category) => (
|
||||
<Link
|
||||
to={`/ai/${category.key}/my`}
|
||||
key={category.key}
|
||||
className={`category-tab ${type === category.key ? 'active' : ''}`}
|
||||
>
|
||||
{category.name}
|
||||
</Link>
|
||||
))}
|
||||
<Link
|
||||
to={`/ai/${type}/create`}
|
||||
className={`category-tab-add`}
|
||||
>
|
||||
<i className="iconfont icon-jia font-12"></i>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* 统计数据 */}
|
||||
<div className="stats-section mt60">
|
||||
{stats.map((stat) => (
|
||||
<div key={stat.key} className="stat-card">
|
||||
<span>{stat.value}</span>
|
||||
<div className="font-15">{stat.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧区域 */}
|
||||
<div className="right-panel">
|
||||
<div className="table-container">
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={list}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
/>
|
||||
{/* 分页 */}
|
||||
<Pagination
|
||||
current={current}
|
||||
pageSize={pageSize}
|
||||
total={total}
|
||||
onChange={handlePageChange}
|
||||
showSizeChanger={false}
|
||||
showTotal={(total) => `共 ${total} 条记录`}
|
||||
className="black_active_Pagination edu-txt-right mt36"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DeleteModal type={type} id={deleteId} cancel={()=>{setDeleteId(undefined)}} roleByZone={roleByZone} reload={setReload}/>
|
||||
<TakeDownModal id={downId} cancel={()=>{setDownId(undefined)}} roleByZone={roleByZone} reload={setReload}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MyResources;
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
.my-resources-page {
|
||||
background-image: url('../image/bk5.png');
|
||||
background-size: 100% auto;
|
||||
background-repeat: no-repeat;
|
||||
padding-top: 55px;
|
||||
min-height: 91vh;
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
gap: 75px;
|
||||
}
|
||||
|
||||
// 左侧区域
|
||||
.left-panel {
|
||||
flex: 0 0 400px;
|
||||
height: fit-content;
|
||||
|
||||
// 返回按钮
|
||||
.back-btn {
|
||||
display: inline-block;
|
||||
transform: scaleX(-1) translateY(-7px);
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
// 标题区域
|
||||
.header-section {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
// 分类标签
|
||||
.category-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
border: 1px solid #091221;
|
||||
border-radius: 24px;
|
||||
|
||||
.category-tab {
|
||||
flex: 1;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
border-radius: 22px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
color: inherit;
|
||||
background-color: rgba(9, 18, 33, 0.05);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #091221;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
.category-tab-add{
|
||||
width:34px;
|
||||
height:34px;
|
||||
line-height: 34px;
|
||||
text-align: center;
|
||||
background:#091221;
|
||||
border-radius:17px;
|
||||
color: white;
|
||||
margin-right: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
// 统计数据
|
||||
.stats-section {
|
||||
display: flex;
|
||||
|
||||
.stat-card {
|
||||
margin-right: auto;
|
||||
|
||||
span {
|
||||
font-size: 44px;
|
||||
font-family: "YouSheBiaoTiHei";
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 右侧区域
|
||||
.right-panel {
|
||||
flex: 1;
|
||||
width: 0;
|
||||
background-image: url('../image/bk4.png');
|
||||
background-size: 100% 100%;
|
||||
padding: 20px;
|
||||
border: 2px solid #ffffff;
|
||||
border-radius: 12px;
|
||||
|
||||
.table-container .ant-table {
|
||||
min-height: 650px;
|
||||
background: white;
|
||||
border: 1px solid;
|
||||
border-color: rgba(22, 76, 228, 0.1);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
|
||||
table {
|
||||
color: #091221;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.ant-table-tbody>tr>td {
|
||||
border-bottom: none
|
||||
}
|
||||
|
||||
.ant-table-tbody>tr {
|
||||
background: linear-gradient(89.78deg, rgba(236, 236, 255, 0.58) 1.89%, rgba(248, 249, 253, 0) 99.99%);
|
||||
|
||||
&:nth-child(2n) {
|
||||
background: linear-gradient(89.78deg, #eef3fd 1.89%, rgba(248, 249, 253, 0) 99.99%);
|
||||
}
|
||||
|
||||
td {
|
||||
border-top: 25px solid white;
|
||||
|
||||
&:first-of-type {
|
||||
border-left: 12px solid white;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-thead>tr>th {
|
||||
background: #f5f6ff;
|
||||
color: #091221;
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
border-bottom-color: rgba(22, 76, 228, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
.ant-badge-status-text {
|
||||
color: #091221;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.ant-badge-status-dot {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: inherit;
|
||||
z-index: 1;
|
||||
opacity: 0.2;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.col-actions a{
|
||||
color:#164ce4;
|
||||
}
|
||||
}
|
||||
}
|
||||
140
src/ai/static.js
|
|
@ -1,140 +0,0 @@
|
|||
import { Fragment } from "react";
|
||||
|
||||
export const aiResouceType = [
|
||||
{ key: "models", name: "模型库", icon: "moxingku", intro: "行业算法模型", desc: "收录多模态原生大模型矩阵,开箱即用灵活调度,全方位支撑企业 AI 业务研发与智能化升级", createIntro: "上传您的模型文件,审核通过后将同步展示在AI广场" },
|
||||
{ key: "datasets", name: "数据集", icon: "shujuji", intro: "高精训练数据", desc: "精心策划的多模态数据集,覆盖 NLP、CV、语音、多模态等核心方向,助力模型快速迭代。", createIntro: "上传您的数据集文件,审核通过后将同步展示在AI广场" },
|
||||
{ key: "skills", name: "Skills", icon: "skill", intro: "场景智能能力", desc: "标准化封装多维场景 Skill 能力,灵活组合调用,低成本快速搭建各类智能化 AI 业务应用。", createIntro: "上传您的skill文件,审核通过后将同步展示在AI广场" },
|
||||
{ key: "codes", name: "代码库", icon: "daimaku1", intro: "通用开发源码", desc: "模块化沉淀全场景通用代码能力,积木式灵活编排复用,高效轻量化落地各类研发业务需求", createIntro: "上传您的代码库,审核通过后将同步展示在AI广场" }
|
||||
]
|
||||
|
||||
export const aiResouceTypeKey = {
|
||||
models: "MODEL",
|
||||
datasets: "DATASET",
|
||||
skills: "SKILL"
|
||||
}
|
||||
|
||||
// 已发布:审核通过/上架
|
||||
export const aiResouceStatus = {
|
||||
"0": {label: "待审核", color: "#ff9140"},
|
||||
"1": {label: "已发布", color: "#164ce4"},
|
||||
"2": {label: "已驳回", color: "#d82944"},
|
||||
"3": {label: "已下架", color: "#5C5A59"},
|
||||
"4": {label: "草稿", color: "#ff5e00"},
|
||||
}
|
||||
|
||||
// 步骤引导数据
|
||||
export const aiCreateSteps = [
|
||||
{
|
||||
title: "注册/登录账号",
|
||||
description: "首先需要注册或登录您的账号,才能开始发布资源"
|
||||
},
|
||||
{
|
||||
title: "选择资源类型",
|
||||
description: "选择您要发布的资源类型:模型、数据集、Skill 或代码库"
|
||||
},
|
||||
{
|
||||
title: "填写信息并发布",
|
||||
description: "填写资源的详细信息,包括名称、简介、标签等,然后提交发布"
|
||||
}
|
||||
];
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (size) => {
|
||||
if (!size || size === 0) return "";
|
||||
const num = parseFloat(size);
|
||||
if (isNaN(num)) return "";
|
||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||
let unitIndex = 0;
|
||||
let fileSize = num;
|
||||
while (fileSize >= 1024 && unitIndex < units.length - 1) {
|
||||
fileSize /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
const formatted = fileSize.toFixed(2);
|
||||
// 如果两位小数都是0,则显示整数
|
||||
return formatted.endsWith('.00') ? `${parseInt(formatted, 10)}${units[unitIndex]}` : `${formatted}${units[unitIndex]}`;
|
||||
};
|
||||
|
||||
export const convertToTreeData = (data) => {
|
||||
return data.map(node => {
|
||||
const isLeaf = !node.children || node.children.length === 0;
|
||||
let title = node.name;
|
||||
if (isLeaf && node.size) {
|
||||
const sizeStr = formatFileSize(node.size);
|
||||
title = <Fragment><span>{node.name}</span><span className="fileSize">{sizeStr}</span></Fragment>;
|
||||
}
|
||||
return {
|
||||
key: node.name,
|
||||
title,
|
||||
isLeaf,
|
||||
size: isLeaf ? node.size : undefined,
|
||||
children: node.children && node.children.length > 0 ? convertToTreeData(node.children) : undefined
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// 分类图标
|
||||
export const iconByCateName = {
|
||||
"开发工具": ["iconfont-color icon-colora-zu18240", "iconfont icon-a-Dshujukukaifagongju"],
|
||||
"多模态": ["iconfont-color icon-colora-zu18210", "iconfont icon-duomotaimoxing"],
|
||||
"自然语言处理": ["iconfont-color icon-colorziranyuyanchuli", "iconfont icon-ziranyuyanchuli-2"],
|
||||
"语音": ["iconfont-color icon-coloryuyin", "iconfont icon-yuyin-1"],
|
||||
"计算机视觉": ["iconfont-color icon-colora-zu18213", "iconfont icon-a-zu18214"],
|
||||
"科学计算": ["iconfont-color icon-colorkexueyanjiuhejishufuwuye", "iconfont icon-a-kexueyanjiuhejishufuwuye-1"],
|
||||
"文本": ["iconfont-color icon-colorziranyuyanchuli", "iconfont icon-ziranyuyanchuli-2"],
|
||||
"图像": ["iconfont-color icon-colortuxiang", "iconfont icon-a-tuxiang-1"],
|
||||
"音频": ["iconfont-color icon-coloryinpin", "iconfont icon-yinpin-1"],
|
||||
"视频": ["iconfont-color icon-colorshipinbofang", "iconfont icon-shipinbofang-1"],
|
||||
"开发工具": ["iconfont-color icon-colora-zu18240", "iconfont icon-a-Dshujukukaifagongju"],
|
||||
"市场推广": ["iconfont-color icon-colora-zu18213", "iconfont icon-a-zu18214"],
|
||||
"前端开发": ["iconfont-color icon-colordaimaku1", "iconfont icon-daimaku1"],
|
||||
"媒体处理": ["iconfont-color icon-coloryouxiyule", "iconfont icon-meitichuli"],
|
||||
"代码质检": ["iconfont-color icon-colordaimazhijian", "iconfont icon-daimazhijian"],
|
||||
"移动开发": ["iconfont-color icon-coloryidongkaifa", "iconfont icon-yidongkaifa"],
|
||||
"云效工具": ["iconfont-color icon-coloryunxiaogongju", "iconfont icon-yunxiaogongju"],
|
||||
"其他": ["iconfont-color icon-colorqita", "iconfont icon-qita"],
|
||||
"程序开发": ["iconfont-color icon-colora-zu18240", "iconfont icon-a-Dshujukukaifagongju"],
|
||||
"操作系统": ["iconfont-color icon-colora-zu18213", "iconfont icon-a-zu18214"],
|
||||
"人工智能": ["iconfont-color icon-colordaimaku1", "iconfont icon-daimaku1"],
|
||||
"游戏娱乐": ["iconfont-color icon-coloryouxiyule", "iconfont icon-meitichuli"],
|
||||
"程序设计": ["iconfont-color icon-colordaimazhijian", "iconfont icon-daimazhijian"],
|
||||
"云原生": ["iconfont-color ", "iconfont "]
|
||||
}
|
||||
|
||||
export const iconByCateIndex = {
|
||||
"models": [
|
||||
["iconfont-color icon-colora-zu18210", "iconfont icon-duomotaimoxing"],
|
||||
["iconfont-color icon-colorziranyuyanchuli", "iconfont icon-ziranyuyanchuli-2"],
|
||||
["iconfont-color icon-coloryuyin", "iconfont icon-yuyin-1"],
|
||||
["iconfont-color icon-colora-zu18213", "iconfont icon-a-zu18214"],
|
||||
["iconfont-color icon-colorkexueyanjiuhejishufuwuye", "iconfont icon-a-kexueyanjiuhejishufuwuye-1"]
|
||||
],
|
||||
"datasets": [
|
||||
["iconfont-color icon-colora-zu18210", "iconfont icon-duomotaimoxing"],
|
||||
["iconfont-color icon-colorziranyuyanchuli", "iconfont icon-ziranyuyanchuli-2"],
|
||||
["iconfont-color icon-colortuxiang", "iconfont icon-a-tuxiang-1"],
|
||||
["iconfont-color icon-coloryinpin", "iconfont icon-yinpin-1"],
|
||||
["iconfont-color icon-colorkexueyanjiuhejishufuwuye", "iconfont icon-a-kexueyanjiuhejishufuwuye-1"],
|
||||
["iconfont-color icon-colorshipinbofang", "iconfont icon-shipinbofang-1"]
|
||||
],
|
||||
"skills": [
|
||||
["iconfont-color icon-colora-zu18240", "iconfont icon-a-Dshujukukaifagongju"],
|
||||
["iconfont-color icon-colora-zu18213", "iconfont icon-a-zu18214"],
|
||||
["iconfont-color icon-colordaimaku1", "iconfont icon-daimaku1"],
|
||||
["iconfont-color icon-coloryouxiyule", "iconfont icon-meitichuli"],
|
||||
["iconfont-color icon-colordaimazhijian", "iconfont icon-daimazhijian"],
|
||||
["iconfont-color icon-coloryidongkaifa", "iconfont icon-yidongkaifa"],
|
||||
["iconfont-color icon-coloryunxiaogongju", "iconfont icon-yunxiaogongju"],
|
||||
["iconfont-color icon-colorqita", "iconfont icon-qita"]
|
||||
],
|
||||
"codes": [
|
||||
["iconfont-color icon-colora-zu18240", "iconfont icon-a-Dshujukukaifagongju"],
|
||||
["iconfont-color icon-colora-zu18213", "iconfont icon-a-zu18214"],
|
||||
["iconfont-color icon-colordaimaku1", "iconfont icon-daimaku1"],
|
||||
["iconfont-color icon-coloryouxiyule", "iconfont icon-meitichuli"],
|
||||
["iconfont-color icon-colordaimazhijian", "iconfont icon-daimazhijian"],
|
||||
["iconfont-color icon-coloryidongkaifa", "iconfont icon-yidongkaifa"],
|
||||
["iconfont-color icon-coloryunxiaogongju", "iconfont icon-yunxiaogongju"],
|
||||
["iconfont-color icon-colorqita", "iconfont icon-qita"]
|
||||
]
|
||||
}
|
||||
|
|
@ -50,25 +50,4 @@
|
|||
to {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 左右摆动动画
|
||||
@keyframes arrow-move {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
50% { transform: translateX(4px); }
|
||||
}
|
||||
|
||||
// 星星闪烁动画
|
||||
@keyframes twinkle {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
|
|
@ -124,10 +124,10 @@ export function setImagesUrl(path){
|
|||
}
|
||||
|
||||
export function getUrl(path, goTest) {
|
||||
const local = 'http://172.20.32.203:4000'
|
||||
if (isDev) {
|
||||
return `${local}${path?path:''}`
|
||||
}
|
||||
// const local = 'http://172.20.32.202:8080'
|
||||
// if (isDev) {
|
||||
// return `${local}${path?path:''}`
|
||||
// }
|
||||
return `${path ? path: ''}`;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,849 +0,0 @@
|
|||
import React, { useState, useRef, useCallback, useEffect, useImperativeHandle, forwardRef } from 'react';
|
||||
import { Button, Progress, message, Spin } from 'antd';
|
||||
import SparkMD5 from 'spark-md5';
|
||||
import './index.scss';
|
||||
|
||||
/**
|
||||
* 分片上传组件
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Function} props.onUploadComplete - 上传完成回调 (fileInfo) => void
|
||||
* @param {Function} props.onUploadProgress - 上传进度回调 (progress, uploaded, total) => void
|
||||
* @param {Function} props.onUploadError - 上传错误回调 (error) => void
|
||||
* @param {Function} props.onCancel - 取消上传回调 () => void
|
||||
* @param {string} props.fileType - 文件类型 (cms/dms/resource/pms等)
|
||||
* @param {string} props.hierarchy - 层级结构
|
||||
* @param {number} props.chunkSize - 分片大小(字节)
|
||||
* @param {boolean} props.disabled - 是否禁用
|
||||
* @param {string} props.accept - 接受的文件类型
|
||||
* @param {number} props.maxSize - 最大文件大小(字节)
|
||||
* @param {Object} props.defaultFile - 默认文件信息(已上传的文件),用于回显
|
||||
* - {string} defaultFile.fileId - 文件ID
|
||||
* - {string} defaultFile.fileOriginName - 文件名
|
||||
* - {number} defaultFile.fileSize - 文件大小(字节)
|
||||
* - {string} defaultFile.url - 文件下载地址
|
||||
*/
|
||||
|
||||
/**
|
||||
* 暴露给父组件的方法(通过 ref)
|
||||
*
|
||||
* @method cancelUpload - 移除上传的文件(重置为待上传状态)
|
||||
*/
|
||||
const ChunkedUpload = forwardRef(({
|
||||
onUploadComplete,
|
||||
onUploadProgress,
|
||||
onUploadError,
|
||||
onCancel,
|
||||
fileType = 'resource',
|
||||
hierarchy = '',
|
||||
chunkSize = 10 * 1024 * 1024, // 默认 10MB
|
||||
disabled = false,
|
||||
accept = '', // 空字符串表示接受所有文件类型,这是 HTML5 标准写法
|
||||
maxSize = 1024 * 1024 * 1024, // 默认 1GB
|
||||
children, // 支持自定义未选择文件状态的内容
|
||||
defaultFile, // 默认文件信息(已上传的文件),用于回显
|
||||
}, ref) => {
|
||||
// 暴露方法给父组件
|
||||
useImperativeHandle(ref, () => ({
|
||||
/**
|
||||
* 移除上传的文件(重置为待上传状态)
|
||||
*/
|
||||
cancelUpload: () => {
|
||||
handleCancel();
|
||||
},
|
||||
}));
|
||||
const DEFAULT_SIZE = 10 * 1024 * 1024; // 10MB,与 ruoyi-react 保持一致
|
||||
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [file, setFile] = useState(null);
|
||||
const [uploadStatus, setUploadStatus] = useState('idle'); // idle, checking, initializing, uploading, merging, completed, error, cancelled, instant
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [uploadedChunks, setUploadedChunks] = useState(0);
|
||||
const [totalChunks, setTotalChunks] = useState(0);
|
||||
const [uploadId, setUploadId] = useState(null);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [uploadSpeed, setUploadSpeed] = useState(0);
|
||||
const [instantFileInfo, setInstantFileInfo] = useState(null); // 秒传成功时的文件信息
|
||||
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
// 初始化默认文件(已上传的文件回显)
|
||||
useEffect(() => {
|
||||
if (defaultFile && Object.keys(defaultFile).length > 0) {
|
||||
// 设置已完成状态
|
||||
const fileInfo = {
|
||||
name: defaultFile.fileOriginName || 'unknown',
|
||||
size: defaultFile.fileSize || 0,
|
||||
};
|
||||
setFile(fileInfo);
|
||||
setUploadStatus('completed');
|
||||
setProgress(100);
|
||||
setUploadedChunks(1);
|
||||
setTotalChunks(1);
|
||||
}
|
||||
}, [defaultFile]);
|
||||
|
||||
const uploadRef = useRef({
|
||||
isCancelled: false,
|
||||
uploadId: null,
|
||||
abortController: null,
|
||||
currentFile: null,
|
||||
fileSignature: null, // 文件签名
|
||||
});
|
||||
|
||||
// 清理上传状态
|
||||
const cleanupUpload = useCallback(() => {
|
||||
uploadRef.current.isCancelled = true;
|
||||
|
||||
// 取消进行中的请求
|
||||
if (uploadRef.current.abortController) {
|
||||
uploadRef.current.abortController.abort();
|
||||
}
|
||||
|
||||
setUploadStatus('cancelled');
|
||||
setUploading(false);
|
||||
}, []);
|
||||
|
||||
// 移除上传(仅前端移除,重置为待上传样式)
|
||||
const handleCancel = useCallback(() => {
|
||||
uploadRef.current.isCancelled = true;
|
||||
|
||||
// 取消进行中的请求
|
||||
if (uploadRef.current.abortController) {
|
||||
uploadRef.current.abortController.abort();
|
||||
}
|
||||
|
||||
setUploadStatus('idle');
|
||||
setUploading(false);
|
||||
setUploadId(null);
|
||||
setFile(null);
|
||||
setProgress(0);
|
||||
setUploadedChunks(0);
|
||||
setTotalChunks(0);
|
||||
setErrorMessage('');
|
||||
onCancel?.();
|
||||
}, [onCancel]);
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = useCallback((bytes) => {
|
||||
if (!bytes || bytes === 0) return '0 B';
|
||||
if(typeof bytes === "string") return bytes
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}, []);
|
||||
|
||||
// 格式化速度
|
||||
const formatSpeed = useCallback((bytesPerSecond) => {
|
||||
return formatFileSize(bytesPerSecond) + '/s';
|
||||
}, [formatFileSize]);
|
||||
|
||||
// 更新进度
|
||||
const updateProgress = useCallback((uploaded, total, fileSize, elapsedMs) => {
|
||||
const percent = Math.round((uploaded / total) * 100);
|
||||
setProgress(percent);
|
||||
setUploadedChunks(uploaded);
|
||||
setTotalChunks(total);
|
||||
|
||||
// 计算速度
|
||||
const speed = elapsedMs > 0 ? Math.round((uploaded * chunkSize) / (elapsedMs / 1000)) : 0;
|
||||
setUploadSpeed(speed);
|
||||
|
||||
onUploadProgress?.(percent, uploaded, total);
|
||||
}, [chunkSize, onUploadProgress]);
|
||||
|
||||
// 上传单个分片(带重试)
|
||||
const uploadSingleChunk = useCallback(async (chunkNumber, chunkFile, onChunkProgress) => {
|
||||
const { uploadChunk: uploadChunkApi } = await import('../../factory/api');
|
||||
let retryCount = 0;
|
||||
const maxRetries = 3;
|
||||
|
||||
while (retryCount < maxRetries) {
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
uploadRef.current.abortController = abortController;
|
||||
|
||||
await uploadChunkApi(
|
||||
{
|
||||
uploadId: uploadRef.current.uploadId,
|
||||
chunkNumber,
|
||||
chunk: chunkFile,
|
||||
},
|
||||
onChunkProgress
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error.name === 'AbortError') {
|
||||
return false;
|
||||
}
|
||||
|
||||
retryCount++;
|
||||
if (retryCount >= maxRetries) {
|
||||
console.error(`分片 ${chunkNumber} 上传失败,已达最大重试次数:`, error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// 等待后重试
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * retryCount));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}, []);
|
||||
|
||||
// 并发上传分片
|
||||
const uploadChunksConcurrently = useCallback(async (chunks, fileToUpload) => {
|
||||
const uploaded = new Set();
|
||||
const chunksToUpload = chunks.filter(c => !uploaded.has(c.index));
|
||||
|
||||
if (chunksToUpload.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
let uploadedCount = 0;
|
||||
const maxConcurrent = 4;
|
||||
|
||||
// 并发控制
|
||||
const processChunk = async (chunk) => {
|
||||
if (uploadRef.current.isCancelled) return;
|
||||
|
||||
try {
|
||||
const start = chunk.start;
|
||||
const end = chunk.end;
|
||||
const blob = fileToUpload.slice(start, end);
|
||||
// 将 Blob 包装为 File 对象,与 ruoyi-react 保持一致
|
||||
const chunkFile = new File([blob], `chunk-${chunk.index}-${Date.now()}`);
|
||||
|
||||
await uploadSingleChunk(chunk.index, chunkFile, (loaded, total) => {
|
||||
// 分片级别进度
|
||||
});
|
||||
|
||||
uploaded.add(chunk.index);
|
||||
uploadedCount++;
|
||||
|
||||
updateProgress(uploadedCount, chunks.length, fileToUpload.size, Date.now() - startTime);
|
||||
} catch (error) {
|
||||
if (error.name !== 'AbortError') {
|
||||
setErrorMessage(`分片 ${chunk.index} 上传失败: ${error.message}`);
|
||||
setUploadStatus('error');
|
||||
onUploadError?.(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 分批并发上传
|
||||
for (let i = 0; i < chunksToUpload.length; i += maxConcurrent) {
|
||||
if (uploadRef.current.isCancelled) break;
|
||||
|
||||
const batch = chunksToUpload.slice(i, i + maxConcurrent);
|
||||
await Promise.all(batch.map(processChunk));
|
||||
}
|
||||
}, [uploadSingleChunk, updateProgress, onUploadError]);
|
||||
|
||||
// 初始化上传(参照 ruoyi-react 的流程)
|
||||
const initUpload = useCallback(async (fileToUpload) => {
|
||||
setUploadStatus('initializing');
|
||||
|
||||
try {
|
||||
const { initChunkedUpload, checkInstantUpload } = await import('../../factory/api');
|
||||
|
||||
// 1. 初始化分片上传
|
||||
const initResponse = await initChunkedUpload({
|
||||
fileName: fileToUpload.name,
|
||||
fileSize: fileToUpload.size,
|
||||
type: fileType,
|
||||
hierarchy: hierarchy || undefined,
|
||||
});
|
||||
|
||||
const { data: initData } = initResponse.data || initResponse;
|
||||
const { uploadId: newUploadId, totalChunks, chunkSize: serverChunkSize, hashType, sampleChunks } = initData;
|
||||
|
||||
uploadRef.current.uploadId = newUploadId;
|
||||
setUploadId(newUploadId);
|
||||
|
||||
// 2. 根据 hashType 和 sampleChunks 计算文件签名(参照 ruoyi-react)
|
||||
setUploadStatus('calculating');
|
||||
const fileSignature = await computeFileSignature(fileToUpload, hashType, sampleChunks, serverChunkSize || chunkSize);
|
||||
uploadRef.current.fileSignature = fileSignature;
|
||||
|
||||
// 3. 检查秒传/断点续传(参照 ruoyi-react 的参数格式)
|
||||
setUploadStatus('checking');
|
||||
const checkResponse = await checkInstantUpload({
|
||||
uploadId: String(newUploadId),
|
||||
fileSignature: fileSignature,
|
||||
});
|
||||
|
||||
const { data: checkData } = checkResponse.data || checkResponse;
|
||||
const { canInstantUpload, canResumingUpload, completedUploadChunkedNumberList, existingFileId, existingFileName, existingFileSize, existingFileUrl } = checkData || {};
|
||||
|
||||
// 秒传成功(参照 ruoyi-react 的实现)
|
||||
if (canInstantUpload) {
|
||||
setUploadStatus('instant');
|
||||
const fileInfo = {
|
||||
fileId: existingFileId,
|
||||
fileOriginName: existingFileName,
|
||||
fileSize: existingFileSize,
|
||||
url: existingFileUrl,
|
||||
};
|
||||
setInstantFileInfo(fileInfo);
|
||||
message.success('秒传成功,文件已存在');
|
||||
onUploadComplete?.(fileInfo);
|
||||
setUploading(false); // 重置 uploading 状态,隐藏 Spin
|
||||
return null; // 返回 null 表示不需要上传
|
||||
}
|
||||
|
||||
// 断点续传(参照 ruoyi-react 的实现)
|
||||
let uploadedChunksSet = new Set();
|
||||
if (canResumingUpload && completedUploadChunkedNumberList && completedUploadChunkedNumberList.length > 0) {
|
||||
const resumingUploadId = checkData.resumingUploadId || newUploadId;
|
||||
uploadRef.current.uploadId = resumingUploadId;
|
||||
setUploadId(resumingUploadId);
|
||||
|
||||
// 处理会话状态为合并中的情况(参照 ruoyi-react)
|
||||
if (checkData.resumingUploadStatus === 'MERGING' || checkData.resumingUploadStatus === '合并中') {
|
||||
// message.info('文件正在合并中,请稍候...');
|
||||
// 直接轮询进度
|
||||
const mergeResult = await pollMergeStatus(resumingUploadId, (mergeProgress) => {
|
||||
setProgress(mergeProgress);
|
||||
});
|
||||
setUploadStatus('completed');
|
||||
setProgress(100);
|
||||
setUploading(false);
|
||||
message.success('文件上传完成');
|
||||
|
||||
// 确保传递 fileId,从轮询结果中获取
|
||||
onUploadComplete?.({
|
||||
fileId: mergeResult?.fileId || mergeResult?.data?.fileId,
|
||||
fileOriginName: mergeResult?.fileOriginName || mergeResult?.data?.fileOriginName,
|
||||
fileSize: mergeResult?.fileSize || mergeResult?.data?.fileSize,
|
||||
url: mergeResult?.url || mergeResult?.data?.url,
|
||||
uploadId: newUploadId,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
// 将已上传的分片索引存入 Set,方便后续过滤
|
||||
uploadedChunksSet = new Set(completedUploadChunkedNumberList);
|
||||
const uploadedCount = uploadedChunksSet.size;
|
||||
setUploadedChunks(uploadedCount);
|
||||
setTotalChunks(totalChunks);
|
||||
|
||||
// message.info(`断点续传,已上传 ${uploadedCount} 个分片,将从第 ${uploadedCount + 1} 个分片继续上传`);
|
||||
|
||||
// 如果所有分片都已上传,直接合并
|
||||
if (uploadedCount >= totalChunks) {
|
||||
const mergeResult = await mergeUpload();
|
||||
setUploadStatus('completed');
|
||||
setProgress(100);
|
||||
setUploading(false);
|
||||
message.success('上传完成');
|
||||
// 确保传递 fileId,从合并结果中获取
|
||||
onUploadComplete?.({
|
||||
fileId: mergeResult?.fileId || mergeResult?.data?.fileId,
|
||||
fileOriginName: mergeResult?.fileOriginName || mergeResult?.data?.fileOriginName,
|
||||
fileSize: mergeResult?.fileSize || mergeResult?.data?.fileSize,
|
||||
url: mergeResult?.url || mergeResult?.data?.url,
|
||||
uploadId: uploadRef.current.uploadId,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
setTotalChunks(totalChunks);
|
||||
|
||||
// 如果服务端返回的分片大小不同,重新计算分片
|
||||
const finalChunkSize = serverChunkSize || chunkSize;
|
||||
const chunks = [];
|
||||
|
||||
for (let i = 0; i < totalChunks; i++) {
|
||||
// 跳过已上传的分片
|
||||
if (uploadedChunksSet.has(i)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
chunks.push({
|
||||
index: i, // 分片从 0 开始
|
||||
start: i * finalChunkSize,
|
||||
end: Math.min((i + 1) * finalChunkSize, fileToUpload.size),
|
||||
size: Math.min(fileToUpload.size - (i * finalChunkSize), finalChunkSize),
|
||||
});
|
||||
}
|
||||
|
||||
// message.info(`实际需要上传 ${chunks.length} 个分片`);
|
||||
|
||||
return chunks;
|
||||
} catch (error) {
|
||||
setErrorMessage(`初始化上传失败: ${error.message}`);
|
||||
setUploadStatus('error');
|
||||
onUploadError?.(error);
|
||||
throw error;
|
||||
}
|
||||
}, [fileType, hierarchy, chunkSize, onUploadComplete, onUploadError]);
|
||||
|
||||
// 轮询检查合并状态(递归方式)
|
||||
const pollMergeStatus = useCallback(async (uploadId, onMergeProgress, maxRetries = 60, interval = 3000) => {
|
||||
const { getProgress: getProgressApi } = await import('../../factory/api');
|
||||
|
||||
let retryCount = 0;
|
||||
|
||||
const checkStatus = async () => {
|
||||
try {
|
||||
const response = await getProgressApi(uploadId);
|
||||
const { data: progressData } = response.data || response;
|
||||
|
||||
// 检查合并状态
|
||||
if (progressData.status === 'COMPLETED' || progressData.status === 'SUCCESS') {
|
||||
// 合并成功
|
||||
onMergeProgress?.(100);
|
||||
return progressData;
|
||||
} else if (progressData.status === 'FAILED' || progressData.status === 'ERROR') {
|
||||
// 合并失败
|
||||
throw new Error(progressData.message || '合并失败');
|
||||
}
|
||||
|
||||
// 继续轮询(合并中或其他中间状态)
|
||||
retryCount++;
|
||||
// 不要做最大次数限制
|
||||
// if (retryCount >= maxRetries) {
|
||||
// throw new Error('合并超时,请稍后查看结果');
|
||||
// }
|
||||
|
||||
// 等待指定间隔后继续轮询
|
||||
await new Promise(resolve => setTimeout(resolve, interval));
|
||||
return await checkStatus();
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 开始轮询
|
||||
return checkStatus();
|
||||
}, []);
|
||||
|
||||
// 合并分片
|
||||
const mergeUpload = useCallback(async () => {
|
||||
setUploadStatus('merging');
|
||||
|
||||
try {
|
||||
const { mergeChunks: mergeChunksApi } = await import('../../factory/api');
|
||||
|
||||
// 1. 提交合并任务
|
||||
const response = await mergeChunksApi(uploadRef.current.uploadId);
|
||||
const { data: mergeData } = response.data || response;
|
||||
|
||||
// 如果返回的消息提示需要轮询进度
|
||||
if (mergeData?.message && mergeData.message.includes('请通过进度接口查询结果')) {
|
||||
// message.info('合并任务已提交,正在处理中...');
|
||||
|
||||
// 2. 轮询检查合并状态
|
||||
const result = await pollMergeStatus(uploadRef.current.uploadId, (mergeProgress) => {
|
||||
setProgress(mergeProgress);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 如果直接返回成功(不需要轮询)
|
||||
return mergeData;
|
||||
} catch (error) {
|
||||
setErrorMessage(`合并分片失败: ${error.message}`);
|
||||
setUploadStatus('error');
|
||||
onUploadError?.(error);
|
||||
throw error;
|
||||
}
|
||||
}, [onUploadError, pollMergeStatus]);
|
||||
|
||||
// 主上传流程
|
||||
const startUpload = useCallback(async (selectedFile) => {
|
||||
if (disabled) return;
|
||||
|
||||
// 验证文件大小
|
||||
if (selectedFile.size > maxSize) {
|
||||
message.error(`文件大小不能超过 ${formatFileSize(maxSize)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
setFile(selectedFile);
|
||||
uploadRef.current.currentFile = selectedFile;
|
||||
setUploading(true);
|
||||
setUploadStatus('checking');
|
||||
setProgress(0);
|
||||
setUploadedChunks(0);
|
||||
setErrorMessage('');
|
||||
setInstantFileInfo(null);
|
||||
|
||||
uploadRef.current.isCancelled = false;
|
||||
|
||||
try {
|
||||
// 1. 初始化分片上传(内部会计算文件签名并检查秒传)
|
||||
const chunks = await initUpload(selectedFile);
|
||||
|
||||
// 如果 chunks 为 null,说明秒传成功或无需上传
|
||||
if (chunks === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 上传分片
|
||||
setUploadStatus('uploading');
|
||||
await uploadChunksConcurrently(chunks, selectedFile);
|
||||
|
||||
if (uploadRef.current.isCancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 合并分片
|
||||
const mergeResult = await mergeUpload();
|
||||
|
||||
setUploadStatus('completed');
|
||||
setProgress(100);
|
||||
setUploading(false); // 重置 uploading 状态,隐藏 Spin
|
||||
|
||||
message.success('上传成功');
|
||||
// 确保传递 fileId,从合并结果中获取
|
||||
onUploadComplete?.({
|
||||
fileId: mergeResult?.fileId || mergeResult?.data?.fileId,
|
||||
fileOriginName: mergeResult?.fileOriginName || mergeResult?.data?.fileOriginName,
|
||||
fileSize: mergeResult?.fileSize || mergeResult?.data?.fileSize,
|
||||
url: mergeResult?.url || mergeResult?.data?.url,
|
||||
uploadId: uploadRef.current.uploadId,
|
||||
});
|
||||
} catch (error) {
|
||||
if (!uploadRef.current.isCancelled) {
|
||||
setUploadStatus('error');
|
||||
setUploading(false); // 重置 uploading 状态,隐藏 Spin
|
||||
onUploadError?.(error);
|
||||
}
|
||||
}
|
||||
}, [disabled, maxSize, formatFileSize, onUploadComplete, onUploadError, initUpload, uploadChunksConcurrently, mergeUpload]);
|
||||
|
||||
// 计算文件签名(参照 ruoyi-react 的实现)
|
||||
const computeFileSignature = useCallback((fileToUpload, hashType = 'FULL', sampleChunks = [], chunkSize = DEFAULT_SIZE) => {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
if (hashType === 'FULL') {
|
||||
// 计算完整文件MD5
|
||||
computeFullMD5(fileToUpload).then(resolve);
|
||||
} else if (hashType === 'SAMPLE' && sampleChunks.length > 0) {
|
||||
// 计算采样点哈希
|
||||
computeSampleHash(fileToUpload, sampleChunks, chunkSize).then(resolve);
|
||||
} else {
|
||||
// 默认使用UUID
|
||||
resolve(generateUUID());
|
||||
}
|
||||
} catch (error) {
|
||||
resolve(generateUUID());
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 计算完整文件MD5
|
||||
const computeFullMD5 = useCallback((fileToUpload) => {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const fileReader = new FileReader();
|
||||
const spark = new SparkMD5.ArrayBuffer();
|
||||
|
||||
fileReader.onload = (e) => {
|
||||
if (e.target?.result) {
|
||||
spark.append(e.target.result);
|
||||
const md5 = spark.end();
|
||||
resolve(md5);
|
||||
} else {
|
||||
resolve(generateUUID());
|
||||
}
|
||||
};
|
||||
|
||||
fileReader.onerror = function () {
|
||||
resolve(generateUUID());
|
||||
};
|
||||
|
||||
fileReader.readAsArrayBuffer(fileToUpload);
|
||||
} catch (error) {
|
||||
resolve(generateUUID());
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 计算采样点哈希(参照 ruoyi-react 的实现)
|
||||
const computeSampleHash = useCallback((fileToUpload, sampleChunks, chunkSize = DEFAULT_SIZE) => {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
// 对采样分片排序(与后端保持一致)
|
||||
const sortedChunks = [...sampleChunks].sort((a, b) => a - b);
|
||||
const totalChunks = Math.ceil(fileToUpload.size / chunkSize);
|
||||
|
||||
if (sortedChunks.length === 0) {
|
||||
resolve(generateUUID());
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建固定长度的数组,按sortedChunks顺序存储MD5
|
||||
const chunkMd5s = new Array(sortedChunks.length);
|
||||
let processedCount = 0;
|
||||
|
||||
sortedChunks.forEach((chunkIndex, idx) => {
|
||||
if (chunkIndex >= totalChunks) {
|
||||
// 分片索引超出范围,填充占位符
|
||||
chunkMd5s[idx] = 'error';
|
||||
processedCount++;
|
||||
if (processedCount === sortedChunks.length) {
|
||||
calculateFinalSignature(chunkMd5s, resolve);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const start = chunkIndex * chunkSize;
|
||||
const end = Math.min(start + chunkSize, fileToUpload.size);
|
||||
const blob = fileToUpload.slice(start, end);
|
||||
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = (e) => {
|
||||
try {
|
||||
if (e.target?.result) {
|
||||
// 计算单个分片的MD5
|
||||
const spark = new SparkMD5.ArrayBuffer();
|
||||
spark.append(e.target.result);
|
||||
const chunkMd5 = spark.end();
|
||||
chunkMd5s[idx] = chunkMd5;
|
||||
}
|
||||
} catch (error) {
|
||||
chunkMd5s[idx] = 'error';
|
||||
}
|
||||
processedCount++;
|
||||
if (processedCount === sortedChunks.length) {
|
||||
// 所有分片处理完成,计算最终签名
|
||||
calculateFinalSignature(chunkMd5s, resolve);
|
||||
}
|
||||
};
|
||||
|
||||
fileReader.onerror = function () {
|
||||
chunkMd5s[idx] = 'error';
|
||||
processedCount++;
|
||||
if (processedCount === sortedChunks.length) {
|
||||
calculateFinalSignature(chunkMd5s, resolve);
|
||||
}
|
||||
};
|
||||
|
||||
fileReader.readAsArrayBuffer(blob);
|
||||
});
|
||||
|
||||
// 辅助函数:计算最终签名
|
||||
function calculateFinalSignature(md5List, resolveCallback) {
|
||||
try {
|
||||
const spark = new SparkMD5.ArrayBuffer();
|
||||
const md5String = md5List.join('');
|
||||
const encoder = new TextEncoder();
|
||||
spark.append(encoder.encode(md5String));
|
||||
const finalSignature = spark.end();
|
||||
resolveCallback(finalSignature);
|
||||
} catch (error) {
|
||||
resolveCallback(generateUUID());
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
resolve(generateUUID());
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 生成 UUID
|
||||
const generateUUID = useCallback(() => {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 文件选择处理
|
||||
const handleFileSelect = useCallback((e) => {
|
||||
const selectedFile = e.target.files?.[0];
|
||||
if (selectedFile) {
|
||||
startUpload(selectedFile);
|
||||
}
|
||||
}, [startUpload]);
|
||||
|
||||
// 拖拽上传处理
|
||||
const handleDrop = useCallback((e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const droppedFile = e.dataTransfer?.files?.[0];
|
||||
if (droppedFile) {
|
||||
startUpload(droppedFile);
|
||||
}
|
||||
}, [startUpload]);
|
||||
|
||||
const handleDragOver = useCallback((e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}, []);
|
||||
|
||||
// 重新上传
|
||||
const handleRetry = useCallback(() => {
|
||||
setUploadStatus('checking');
|
||||
setProgress(0);
|
||||
setUploadedChunks(0);
|
||||
setErrorMessage('');
|
||||
|
||||
const fileToRetry = file || uploadRef.current.currentFile;
|
||||
if (fileToRetry) {
|
||||
startUpload(fileToRetry);
|
||||
}
|
||||
}, [file, startUpload]);
|
||||
|
||||
// 组件卸载时清理
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cleanupUpload();
|
||||
};
|
||||
}, [cleanupUpload]);
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = () => {
|
||||
switch (uploadStatus) {
|
||||
case 'idle':
|
||||
return '等待上传';
|
||||
case 'checking':
|
||||
return '检查中...';
|
||||
case 'initializing':
|
||||
return '初始化中...';
|
||||
case 'calculating':
|
||||
return '计算文件签名中...';
|
||||
case 'uploading':
|
||||
return `上传中... (${uploadedChunks}/${totalChunks})`;
|
||||
case 'merging':
|
||||
return '合并中...';
|
||||
case 'completed':
|
||||
return '上传完成';
|
||||
case 'instant':
|
||||
return '秒传成功';
|
||||
case 'error':
|
||||
return '上传失败';
|
||||
case 'cancelled':
|
||||
return '已取消';
|
||||
default:
|
||||
return '未知状态';
|
||||
}
|
||||
};
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = () => {
|
||||
switch (uploadStatus) {
|
||||
case 'completed':
|
||||
return '#52c41a';
|
||||
case 'instant':
|
||||
return '#52c41a';
|
||||
case 'error':
|
||||
return '#ff4d4f';
|
||||
case 'cancelled':
|
||||
return '#d9d9d9';
|
||||
default:
|
||||
return '#1890ff';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="chunked-upload-wrapper">
|
||||
<Spin spinning={uploading}>
|
||||
<div
|
||||
className={`chunked-upload-area ${uploading ? 'uploading' : ''} ${disabled ? 'disabled' : ''}`}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
>
|
||||
{/* 文件选择输入 */}
|
||||
<input
|
||||
type="file"
|
||||
ref={(ref) => {
|
||||
fileInputRef.current = ref;
|
||||
}}
|
||||
style={{ display: 'none' }}
|
||||
accept={accept}
|
||||
onChange={handleFileSelect}
|
||||
disabled={uploading || disabled}
|
||||
/>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<div className="chunked-upload-content" onClick={() => {
|
||||
if (!uploading && !disabled && fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
}}>
|
||||
{uploadStatus === 'idle' && !uploading ? (
|
||||
// 未选择文件状态
|
||||
children ? (
|
||||
children
|
||||
) : (
|
||||
<div className="chunked-upload-placeholder">
|
||||
<i className="iconfont icon-shangchuanicon chunked-upload-icon"></i>
|
||||
<div className="chunked-upload-text">
|
||||
<span className="chunked-upload-main">点击或拖拽文件到此处</span>
|
||||
<span className="chunked-upload-hint">支持分片上传,最大 {formatFileSize(maxSize)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
// 上传中/已完成状态
|
||||
<div className="chunked-upload-info">
|
||||
{file && (
|
||||
<div className="chunked-upload-file-info">
|
||||
<div className="chunked-upload-file-name">{file.name}</div>
|
||||
<div className="chunked-upload-file-size">{formatFileSize(file.size)}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="chunked-upload-progress">
|
||||
<Progress
|
||||
percent={progress}
|
||||
status={uploadStatus === 'error' ? 'exception' : uploadStatus === 'cancelled' ? 'normal' : 'success'}
|
||||
strokeColor={getStatusColor()}
|
||||
size="small"
|
||||
/>
|
||||
<div className="chunked-upload-status">
|
||||
<span className="chunked-upload-status-text">{getStatusText()}</span>
|
||||
{uploadStatus === 'uploading' && uploadSpeed > 0 && (
|
||||
<span className="chunked-upload-speed">{formatSpeed(uploadSpeed)}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{uploadStatus === 'error' && errorMessage && (
|
||||
<div className="chunked-upload-error">{errorMessage}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
{(uploadStatus === 'completed' || uploadStatus === 'instant' || uploadStatus === 'error' || uploadStatus === 'cancelled') && (
|
||||
<div className="chunked-upload-actions">
|
||||
{uploadStatus === 'error' && (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={handleRetry}
|
||||
>
|
||||
<i className="iconfont icon-refresh mr5"></i>重试
|
||||
</Button>
|
||||
)}
|
||||
{(uploadStatus === 'completed' || uploadStatus === 'instant' || uploadStatus === 'cancelled') && (
|
||||
<Button
|
||||
size="small"
|
||||
danger
|
||||
onClick={handleCancel}
|
||||
>
|
||||
<i className="iconfont icon-delete mr5"></i>移除
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Spin>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
ChunkedUpload.displayName = 'ChunkedUpload';
|
||||
|
||||
export default ChunkedUpload;
|
||||
|
|
@ -1,181 +0,0 @@
|
|||
/**
|
||||
* 分片上传组件样式
|
||||
*/
|
||||
|
||||
.chunked-upload-wrapper {
|
||||
width: 100%;
|
||||
|
||||
.chunked-upload-area {
|
||||
border: 2px dashed rgba(22, 76, 228, 0.17);
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
border-color: #164ce4;
|
||||
|
||||
.chunked-upload-placeholder {
|
||||
.chunked-upload-icon {
|
||||
color: #164ce4;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.uploading {
|
||||
cursor: default;
|
||||
border-color: #52c41a;
|
||||
background-color: #f6ffed;
|
||||
|
||||
&:hover {
|
||||
border-color: #52c41a;
|
||||
background-color: #f6ffed;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
|
||||
&:hover {
|
||||
border-color: #d9d9d9;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
}
|
||||
|
||||
.chunked-upload-content {
|
||||
padding: 40px 20px;
|
||||
|
||||
.chunked-upload-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.chunked-upload-icon {
|
||||
font-size: 48px;
|
||||
color: #8c8c8c;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.chunked-upload-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.chunked-upload-main {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.chunked-upload-hint {
|
||||
font-size: 13px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chunked-upload-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.chunked-upload-file-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.chunked-upload-file-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chunked-upload-file-size {
|
||||
font-size: 13px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
}
|
||||
|
||||
.chunked-upload-progress {
|
||||
.chunked-upload-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 8px;
|
||||
|
||||
.chunked-upload-status-text {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.chunked-upload-speed {
|
||||
font-size: 12px;
|
||||
color: #164ce4;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chunked-upload-error {
|
||||
padding: 8px 12px;
|
||||
background-color: #fff2f0;
|
||||
border: 1px solid #ffccc7;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chunked-upload-actions {
|
||||
padding: 12px 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// 覆盖 Ant Design Progress 组件样式
|
||||
:global {
|
||||
.ant-progress-status-normal {
|
||||
.ant-progress-bg {
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-progress-exception {
|
||||
.ant-progress-text {
|
||||
color: #ff4d4f !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-progress-success-circle {
|
||||
.ant-progress-circle-trail {
|
||||
stroke: #f7f7f7 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 按钮样式优化
|
||||
.ant-btn {
|
||||
font-size: 13px;
|
||||
|
||||
&.ant-btn-dangerous {
|
||||
&:hover {
|
||||
background-color: #ff4d4f !important;
|
||||
border-color: #ff4d4f !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,260 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Popconfirm, Input, message, Spin, Pagination } from 'antd';
|
||||
import { getImageUrl, timeAgo, isPhone } from 'educoder';
|
||||
import RenderHtml from '../render-html';
|
||||
import Attachment from '../../forge/Upload/attachment';
|
||||
import EditComment from './EditComment';
|
||||
import CheckProfile from '../../forge/Component/ProfileModal/Profile';
|
||||
import './comments.scss';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { normalizeUrl } from '../../forge/common/util';
|
||||
import { COMMENT_PAGE_SIZE, COMMENT_CATEGORY } from './constants';
|
||||
import * as commentApi from './api';
|
||||
|
||||
/**
|
||||
* 通用评论列表组件
|
||||
* @param {Object} props - 组件属性
|
||||
* @property {string} type - 评论类型:'news' | 'project' | 'resource' | 'skill'
|
||||
* @property {string|number} id - 资源ID(文章ID、项目ID等)
|
||||
* @property {Object} [extraParams] - 额外的业务参数
|
||||
* @property {string} [owner] - 所有者/组织名(用于 Markdown 渲染)
|
||||
* @property {string} [projectsId] - 项目 ID(用于 Markdown 渲染)
|
||||
* @property {Object} current_user - 当前用户信息
|
||||
* @property {string} current_user.login - 用户登录名
|
||||
* @property {boolean} current_user.admin - 是否平台管理员
|
||||
* @property {string} current_user.image_url - 用户头像URL
|
||||
* @property {string} current_user.username - 用户姓名
|
||||
* @property {boolean} [isManager] - 是否为仓库/项目管理员
|
||||
* @property {Function} [showNotification] - 显示通知
|
||||
* @property {Function} [showLoginDialog] - 显示登录弹窗
|
||||
* @property {string} [author] - 评论作者(用于权限判断)
|
||||
*/
|
||||
function CommentsList(props){
|
||||
const {type, id, extraParams, owner, projectsId, current_user, isManager, showLoginDialog, author, showNotification} = props;
|
||||
const {login, admin, image_url, username} = current_user;
|
||||
|
||||
const [category, setCategory] = useState('comment');
|
||||
const [journals, setJournals] = useState(undefined);
|
||||
// 是否展示新建/编辑评论markdown部分新建评论1 编辑父级评论2 回复评论3 编辑回复内容4 回复子机评论5
|
||||
const [showEdit, setShowEdit] = useState(false);
|
||||
const [parentId, setParentId] = useState(undefined);
|
||||
const [replyId, setReplyId] = useState(undefined);
|
||||
// 修改评论id
|
||||
const [updateId, setUpdateId] = useState(undefined);
|
||||
// 操作日志展开效果
|
||||
const [open, setOpen] = useState(undefined);
|
||||
// 加载中效果(缓冲css 线条效果)
|
||||
const [spin, setSpin] = useState(false);
|
||||
// 评论总数
|
||||
const [journalsCount, setJournalsCount] = useState(undefined);
|
||||
// 分页
|
||||
const limit = COMMENT_PAGE_SIZE;
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalCount, setTotalCount] = useState(undefined);
|
||||
const [reload, setReload] = useState(undefined);
|
||||
|
||||
// 根据 type 加载评论列表
|
||||
useEffect(()=>{
|
||||
setSpin(true);
|
||||
let params = {
|
||||
limit: limit,
|
||||
page: page
|
||||
};
|
||||
commentApi.getCommentList(type, id, page, limit).then(res=>{
|
||||
if(res && res.data && res.data.data){
|
||||
const {journals: journalList, total_comment_journals_count, total_count} = res.data.data;
|
||||
if(category === COMMENT_CATEGORY.ALL){
|
||||
const array = [];
|
||||
let start = undefined;
|
||||
let isJournalCount = 0;
|
||||
journalList.map((item, index)=>{
|
||||
!isJournalCount && (start = index);
|
||||
item.is_journal_detail && (isJournalCount++);
|
||||
if(!item.is_journal_detail && isJournalCount < 10){
|
||||
isJournalCount = 0;
|
||||
return;
|
||||
}
|
||||
if(isJournalCount >= 10 && (!item.is_journal_detail || journalList.length-1 === index)){
|
||||
array.push({start, count: isJournalCount});
|
||||
isJournalCount = 0;
|
||||
}
|
||||
});
|
||||
array.map((item, i)=>{
|
||||
journalList[item.start].numCount = item.count-1;
|
||||
for (let index = 0; index < item.count; index++) {
|
||||
journalList[item.start+index].start = journalList[item.start].id;
|
||||
journalList[item.start+index].closeAndSpan = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
setTotalCount(total_count);
|
||||
setJournalsCount(total_comment_journals_count);
|
||||
setJournals(journalList);
|
||||
setSpin(false);
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('获取评论列表失败:', err);
|
||||
setSpin(false);
|
||||
message.error('获取评论列表失败');
|
||||
});
|
||||
}, [type, id, reload, category, page]);
|
||||
|
||||
// 刷新评论列表的回调
|
||||
const reloadComment = () => {
|
||||
setReload(Math.random());
|
||||
};
|
||||
|
||||
function commentCtx(v){
|
||||
return <RenderHtml owner={owner} projectsId={projectsId} className="break_word_comments imageLayerParent commentRenderHtml" value={v} url={{pathname: ''}}/>;
|
||||
};
|
||||
|
||||
// 删除评论内容
|
||||
function deleteCommentFunc(commentId){
|
||||
commentApi.deleteComment(type, id, commentId).then(res=>{
|
||||
if(res && res.data && res.data.code === 200){
|
||||
reloadComment();
|
||||
message.success('删除成功');
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('删除评论失败:', err);
|
||||
message.error('删除评论失败');
|
||||
});
|
||||
};
|
||||
|
||||
// markdown 取消操作
|
||||
function cancelMd(){
|
||||
setShowEdit(false);
|
||||
};
|
||||
|
||||
// 创建不包含已解构属性的 props 对象传递给子组件
|
||||
const childProps = {
|
||||
...props,
|
||||
type,
|
||||
id,
|
||||
extraParams,
|
||||
owner,
|
||||
projectsId,
|
||||
current_user,
|
||||
isManager,
|
||||
showLoginDialog,
|
||||
author,
|
||||
showNotification,
|
||||
reloadComment,
|
||||
cancelMd
|
||||
};
|
||||
|
||||
return(
|
||||
(isPhone() && journalsCount === 0) ? '' : <div className="commentListBox info_text">
|
||||
{/* 评论快速入口-仅有评论且登录时展示 */}
|
||||
{login && category !== COMMENT_CATEGORY.OPERATE && journalsCount > 0 && !isPhone() && <div className={`pb30 ${journalsCount > 0 && 'pt15'}`}><div className='gotoComment'>点击<a className='ml5' href='#addComments'>添加评论</a></div></div>}
|
||||
{/* 评论/操作日志 展示列表 */}
|
||||
<Spin spinning={spin}>
|
||||
<div className={`issueCommentsBox ${category === COMMENT_CATEGORY.COMMENT && !spin && 'justComment'}`}>
|
||||
{journals && (journals.length > 0 && journals.map(item => {return <div key={item.id} className='commentContentBox pb30'>
|
||||
{/* 评论 */}
|
||||
<div className='commentOperationBor'></div>
|
||||
{
|
||||
item.user.userName ?
|
||||
<Link to={`/${item.user.userName}`}><img src={normalizeUrl(item.user.avatar)} alt="" className='commentUserImg mr15'/></Link>
|
||||
:
|
||||
<span><img src={normalizeUrl(item.user.avatar)} alt="" className='commentUserImg mr15'/></span>
|
||||
}
|
||||
<div className='commentContentRight'>
|
||||
{/* 判断是否是编辑状态 */}
|
||||
{(showEdit === 2 && updateId === item.id) ? <div className='mt15 mr20'><EditComment extraParams={extraParams} {...childProps} updateId={updateId} content={item.notes} defaultFileList={item.attachments} showUserImg={false}/></div> : <div>
|
||||
<div className='commentContent'>
|
||||
<div className='flexCenter font-14'>
|
||||
<div>
|
||||
{item.user.userName ? <Link to={`/${item.user.userName}`}>{item.user.nickName}</Link>:<span>{item.user.nickName}</span>}
|
||||
<span className='ml15 timeAgo font-14'>{timeAgo(item.created_at)}</span>
|
||||
</div>
|
||||
{login && !isPhone() && <div>
|
||||
{/* 平台管理员/仓库管理员/发布评论者/issue创建者 */}
|
||||
{(admin || isManager || login === item.user.userName || username === author) && <Popconfirm
|
||||
placement="bottom"
|
||||
title={`确定要删除此条评论吗?${item.children_journals.length > 0 ? '子评论也将被一起删除。' : ''}`}
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
onConfirm={() => deleteCommentFunc(item.id)}
|
||||
>
|
||||
<Button type='link' className='color-grey-89'><i className='iconfont icon-fuzhi-shanchu font-14 mr8'></i>删除</Button>
|
||||
</Popconfirm>}
|
||||
{/* 仅评论者可修改 */}
|
||||
{login === item.user.userName && <Button type='link' className='color-grey-89' onClick={()=>{setUpdateId(item.id); setShowEdit(2)}}><i className='iconfont icon-a-bianji12 font-14 mr8'></i>修改</Button>}
|
||||
<CheckProfile {...props} sureFunc={()=>{setParentId(item.id); setReplyId(item.id); setShowEdit(3)}}><Button type='link' className='color-grey-89'><i className='iconfont icon-a-xiaoxi1 font-14 mr8'></i>回复</Button></CheckProfile>
|
||||
</div>}
|
||||
</div>
|
||||
<div className='contentHtml mb5'>{commentCtx(item.notes)}</div>
|
||||
{item && item.attachments && item.attachments.length > 0 && <div className='attachmentBox mb5'><Attachment
|
||||
attachments={item.attachments}
|
||||
showNotification={showNotification}
|
||||
canDelete={false}
|
||||
/></div>}
|
||||
</div>
|
||||
</div>}
|
||||
{showEdit === 3 && replyId === item.id && <div className='contentHtml mr20'><EditComment extraParams={extraParams} {...childProps} parentId={parentId} replyId={replyId}/></div>}
|
||||
{/* 评论回复部分 */}
|
||||
{item.children_journals.map(i =>{
|
||||
return <div className='commentReply' key={i.id}>
|
||||
{(showEdit === 4 && updateId === i.id) ? <div className='mr20'><EditComment extraParams={extraParams} {...childProps} updateId={updateId} content={i.notes} defaultFileList={i.attachments}/></div> : <div>
|
||||
<div className='flexCenter'>
|
||||
<div>
|
||||
<Link to={`/${i.user.userName}`}><img src={normalizeUrl(i.user.avatar)} alt="" className='commentUserImg mr8'/></Link>
|
||||
<Link to={`/${i.user.userName}`}>{i.user.nickName}</Link>
|
||||
{i.reply_user && <span className='ml5 timeAgo mr3'>回复</span>}
|
||||
<span>{i.reply_user && i.reply_user.name}</span>
|
||||
<span className='ml15 timeAgo font-14'>{timeAgo(i.created_at)}</span>
|
||||
</div>
|
||||
{login && !isPhone() && <div>
|
||||
{/* 平台管理员/仓库管理员/发布评论者/回复评论者/issue创建者 */}
|
||||
{(admin || isManager || login === i.user.userName || (i.reply_user && login === i.reply_user.login) || username === author) && <Popconfirm
|
||||
placement="bottom"
|
||||
title={"确定要删除当前回复吗?"}
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
onConfirm={() => deleteCommentFunc(i.id)}
|
||||
>
|
||||
<Button type='link' className='color-grey-89'><i className='iconfont icon-fuzhi-shanchu font-14 mr8'></i>删除</Button>
|
||||
</Popconfirm>}
|
||||
{/* 仅回复评论者可修改 */}
|
||||
{login === i.user.userName && <Button type='link' className='color-grey-89' onClick={()=>{setUpdateId(i.id); setShowEdit(4)}}><i className='iconfont icon-a-bianji12 font-14 mr8'></i>修改</Button>}
|
||||
<CheckProfile {...props} sureFunc={()=>{setParentId(item.id);setReplyId(i.id); setShowEdit(5)}}>
|
||||
<Button type='link' className='color-grey-89'><i className='iconfont icon-a-xiaoxi1 font-14 mr8'></i>回复</Button>
|
||||
</CheckProfile>
|
||||
</div>}
|
||||
</div>
|
||||
<div className='contentHtml mt5 mb10'>{commentCtx(i.notes)}</div>
|
||||
{i && i.attachments && i.attachments.length > 0 && <div className='attachmentBox'><Attachment
|
||||
attachments={i.attachments}
|
||||
showNotification={showNotification}
|
||||
canDelete={false}
|
||||
/></div>}
|
||||
</div>}
|
||||
{showEdit === 5 && replyId === i.id && <div className='contentHtml mr20'><EditComment extraParams={extraParams} {...childProps} parentId={parentId} replyId={replyId}/></div>}
|
||||
</div>})}
|
||||
</div>
|
||||
</div>}))}
|
||||
</div>
|
||||
</Spin>
|
||||
{totalCount > limit && <div className='mt20 paginationIssueComment mb20'>
|
||||
<Pagination simple current={page} pageSize={limit} total={totalCount} onChange={(page)=>setPage(page)}/>
|
||||
</div>}
|
||||
{/* 添加评论 */}
|
||||
{category !== COMMENT_CATEGORY.OPERATE && !isPhone() && <div className="pb30">
|
||||
{login ? showEdit === 1 ? <EditComment extraParams={extraParams} {...childProps}/> : <div className="addComments" id='addComments'>
|
||||
<img src={getImageUrl(image_url)} alt="" />
|
||||
<div style={{flex:1}}>
|
||||
<CheckProfile {...props} sureFunc={()=>{setShowEdit(1);}}>
|
||||
<Input className='addCommentBox' placeholder="添加评论"/>
|
||||
</CheckProfile>
|
||||
</div>
|
||||
</div> : <div className='unLoginComment font-15 pl20'>
|
||||
{/* 未登录用户 */}
|
||||
<a className='mr5 loginBtn' onClick={()=>{showLoginDialog()}}>登录</a>并参与评论与回复
|
||||
</div>}
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommentsList;
|
||||
|
|
@ -1,185 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Button, Icon, message, Upload } from 'antd';
|
||||
import { getImageUrl, getZoneUrl } from 'educoder';
|
||||
import MDEditor from "../../modules/tpm/challengesnew/tpm-md-editor";
|
||||
import Attachments from "../../forge/Upload/attachment";
|
||||
import './comments.scss';
|
||||
import { Link } from 'react-router-dom';
|
||||
import * as commentApi from './api';
|
||||
import cookie from 'react-cookies';
|
||||
|
||||
/**
|
||||
* 通用评论编辑组件
|
||||
* @param {Object} props - 组件属性
|
||||
* @property {string} type - 评论类型:'news' | 'project' | 'resource' | 'skill'
|
||||
* @property {string|number} id - 资源ID
|
||||
* @property {Object} [extraParams] - 额外的业务参数
|
||||
* @property {string} [owner] - 所有者/组织名(用于 Markdown 编辑器 @ 功能)
|
||||
* @property {string} [projectsId] - 项目 ID(用于 Markdown 编辑器 @ 功能)
|
||||
* @property {Object} current_user - 当前用户信息
|
||||
* @property {string} current_user.login - 用户登录名
|
||||
* @property {string} current_user.image_url - 用户头像URL
|
||||
* @property {Function} cancelMd - 取消编辑回调
|
||||
* @property {Function} reloadComment - 刷新评论列表回调
|
||||
* @property {Function} showNotification - 显示通知
|
||||
* @property {string} [content] - 编辑时的原始内容
|
||||
* @property {Array} [defaultFileList] - 默认附件列表
|
||||
* @property {string} [parentId] - 父评论ID
|
||||
* @property {string} [replyId] - 回复的评论ID
|
||||
* @property {string} [updateId] - 正在编辑的评论ID
|
||||
* @property {boolean} [showUserImg=true] - 是否显示用户头像
|
||||
*/
|
||||
function EditComment(props) {
|
||||
const { type, id, extraParams, current_user, current_user: { login, image_url }, owner, projectsId, cancelMd, reloadComment, showNotification, parentId, replyId, updateId, showUserImg = true, content: propContent, defaultFileList: propDefaultFileList } = props;
|
||||
|
||||
const [content, setContent] = useState(propContent);
|
||||
const [quillFlag, setQuillFlag] = useState(false);
|
||||
// 确认评论按钮loading效果
|
||||
const [journalSpin, setJournalSpin] = useState(false);
|
||||
const [fileList, setFileList] = useState(propDefaultFileList || []);
|
||||
|
||||
// 评论点击函数
|
||||
function addJournals() {
|
||||
if (!content) {
|
||||
setQuillFlag(true);
|
||||
return;
|
||||
}
|
||||
setJournalSpin(true);
|
||||
const fileIds = fileList.map(file=>file.fileIdentifier || file.identifier).toString()
|
||||
if (updateId) {
|
||||
// 修改评论
|
||||
const params = {
|
||||
id: updateId,
|
||||
notes: content,
|
||||
attachment_identifiers: fileIds,
|
||||
};
|
||||
commentApi.editComment(type, id, params).then(res => {
|
||||
if (res && res.data && res.data.code === 200) {
|
||||
// 刷新评论列表
|
||||
reloadComment();
|
||||
message.success('编辑成功');
|
||||
cancelMd();
|
||||
setContent(undefined);
|
||||
}
|
||||
setJournalSpin(false);
|
||||
}).catch(err => {
|
||||
console.error('编辑评论失败:', err);
|
||||
message.error('编辑评论失败');
|
||||
setJournalSpin(false);
|
||||
});
|
||||
} else {
|
||||
// 新建评论
|
||||
const params = {
|
||||
parent_id: parentId,
|
||||
reply_id: replyId,
|
||||
notes: content,
|
||||
attachment_identifiers: fileIds,
|
||||
};
|
||||
commentApi.addComment(type, id, params).then(res => {
|
||||
if (res && res.data && res.data.code === 200) {
|
||||
// 刷新评论列表
|
||||
reloadComment();
|
||||
message.success('评论成功');
|
||||
cancelMd();
|
||||
setContent(undefined);
|
||||
}
|
||||
setJournalSpin(false);
|
||||
}).catch(err => {
|
||||
console.error('添加评论失败:', err);
|
||||
message.error('添加评论失败');
|
||||
setJournalSpin(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function beforeUpload(file) {
|
||||
const isLt100M = file.size / 1024 / 1024 < 100;
|
||||
if (!isLt100M) {
|
||||
message.error(`文件大小必须小于${100}MB!`);
|
||||
}
|
||||
return isLt100M;
|
||||
}
|
||||
|
||||
function changeFileList(info) {
|
||||
const { file } = info;
|
||||
const { status, response } = file || {}
|
||||
if (status === 'done' || status === 'removed') {
|
||||
setFileList([
|
||||
...fileList,
|
||||
response
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid-item-top pb10">
|
||||
<Link
|
||||
to={`/${current_user && current_user.login}`}
|
||||
className="show-user-link mr10"
|
||||
>
|
||||
<img
|
||||
className="radius"
|
||||
src={getImageUrl(`/${current_user && current_user.image_url}`)}
|
||||
alt=""
|
||||
width="30"
|
||||
height="30"
|
||||
style={{ display: showUserImg ? '' : 'none' }}
|
||||
/>
|
||||
</Link>
|
||||
<div style={{ position: "relative" }}>
|
||||
<MDEditor
|
||||
placeholder={"添加评论..."}
|
||||
height={300}
|
||||
mdID={"orderdetail-add-descriptions" + replyId}
|
||||
initValue={content}
|
||||
onChange={(value) => { setQuillFlag(false); setContent(value); }}
|
||||
owner={owner}
|
||||
projectsId={projectsId}
|
||||
></MDEditor>
|
||||
<p className="quillFlagBox">
|
||||
{quillFlag && <span>请输入评论内容</span>}
|
||||
</p>
|
||||
<Upload.Dragger
|
||||
showUploadList={false}
|
||||
data={commentApi.uploadCommentFileData(type)}
|
||||
action={getZoneUrl(`/api/file/common/upload`)}
|
||||
beforeUpload={beforeUpload}
|
||||
onChange={changeFileList}
|
||||
withCredentials={true}
|
||||
headers={{
|
||||
Authorization: cookie.load('autologin_trustie'),
|
||||
}}
|
||||
>
|
||||
<i className='iconfont icon-shangchuan font-30'></i>
|
||||
<p className="mt10">点击或拖拽文件到此区域上传,文件大小不超过 100MB</p>
|
||||
</Upload.Dragger>
|
||||
{fileList && fileList.length > 0 && <div className='mt10'>
|
||||
{fileList.map(file => {
|
||||
return <div className='df-center'>
|
||||
<i className="iconfont icon-fujian mr8 paper-clip-color font-14"></i>
|
||||
<span className='font-16'>{file.fileOriginName || file.title}</span>
|
||||
<span className='ml20 mrat'>({file.fileSize || file.filesize})</span>
|
||||
<a onClick={() => {
|
||||
const deletedList = fileList.filter(i => i.fileId !== file.fileId || i.id !== file.id);
|
||||
setFileList(deletedList);
|
||||
}}><i className='iconfont icon-shanchuicon3'></i></a>
|
||||
</div>
|
||||
})}
|
||||
</div>}
|
||||
<p className="clearfix mt20">
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={addJournals}
|
||||
loading={journalSpin}
|
||||
className="mr15 colorButton"
|
||||
>
|
||||
评论
|
||||
</Button>
|
||||
<Button className="colorButton2" onClick={() => { cancelMd() }}>取消</Button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditComment;
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
/**
|
||||
* 通用评论 API 调用器
|
||||
* 根据 type 参数调用不同的业务接口
|
||||
*/
|
||||
|
||||
import { COMMENT_TYPE } from './constants';
|
||||
import {
|
||||
getJournalList,
|
||||
addJournal,
|
||||
editJournal,
|
||||
deleteJournal,
|
||||
uploadFileBySourceJournal
|
||||
} from '../../forge/Information/api';
|
||||
|
||||
/**
|
||||
* 获取评论列表
|
||||
* @param {string} type - 评论类型
|
||||
* @param {string|number} id - 资源 ID
|
||||
* @param {Object} params - 分页参数
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export async function getCommentList(type, id, page, limit) {
|
||||
switch (type) {
|
||||
case COMMENT_TYPE.RESOURCE:
|
||||
return await getJournalList({
|
||||
journalized_id: id,
|
||||
journalized_type: type,
|
||||
pageNum: page,
|
||||
pageSize: limit
|
||||
})
|
||||
default:
|
||||
throw new Error(`Unsupported comment type: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增评论
|
||||
* @param {string} type - 评论类型
|
||||
* @param {string|number} id - 资源 ID
|
||||
* @param {Object} data - 评论数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export async function addComment(type, id, data) {
|
||||
switch (type) {
|
||||
case COMMENT_TYPE.RESOURCE:
|
||||
return await addJournal({
|
||||
journalized_id: id,
|
||||
journalized_type: type,
|
||||
...data
|
||||
})
|
||||
default:
|
||||
throw new Error(`Unsupported comment type: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑评论
|
||||
* @param {string} type - 评论类型
|
||||
* @param {string|number} id - 资源 ID
|
||||
* @param {Object} data - 评论数据(包含 id 字段)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export async function editComment(type, id, data) {
|
||||
switch (type) {
|
||||
case COMMENT_TYPE.RESOURCE:
|
||||
return await editJournal({
|
||||
id: id,
|
||||
...data
|
||||
})
|
||||
default:
|
||||
throw new Error(`Unsupported comment type: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除评论
|
||||
* @param {string} type - 评论类型
|
||||
* @param {string|number} id - 资源 ID
|
||||
* @param {string|number} commentId - 评论 ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export async function deleteComment(type, id, commentId) {
|
||||
switch (type) {
|
||||
case COMMENT_TYPE.RESOURCE:
|
||||
return await deleteJournal(commentId)
|
||||
default:
|
||||
throw new Error(`Unsupported comment type: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @获取文件上传路径
|
||||
* pms-gitlink会传到ruby的文件服务
|
||||
* contentType: 如果文件上传路径为/api/file/common/upload,需要指定内容属于哪个模块,eg:products/projects/docs/ docs/{projectId}
|
||||
*/
|
||||
export async function uploadCommentFile(type, data) {
|
||||
switch (type) {
|
||||
case COMMENT_TYPE.RESOURCE:
|
||||
return await uploadFileBySourceJournal(data)
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
export function uploadCommentFileData(type) {
|
||||
switch (type) {
|
||||
case COMMENT_TYPE.RESOURCE:
|
||||
return {
|
||||
hierarchy: "Journals",
|
||||
type: "ai-resource"
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,205 +0,0 @@
|
|||
.typeActionBox{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
.line {
|
||||
width: 3px;
|
||||
height: 14px;
|
||||
background: var(--primary-color);
|
||||
margin-right: 13px;
|
||||
}
|
||||
}
|
||||
.commentUserImg{
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.issueCommentsBox{
|
||||
.operationLog, .commentContentBox{
|
||||
position: relative;
|
||||
min-height: 62px;
|
||||
display: flex;
|
||||
>.flexCenter, >a{
|
||||
z-index: 2;
|
||||
}
|
||||
&::before, &::after{
|
||||
content: '';
|
||||
width: 1px;
|
||||
height: 50%;
|
||||
position: absolute;
|
||||
background: #eee;
|
||||
left: 12px;
|
||||
z-index: 1;
|
||||
}
|
||||
&::after{
|
||||
background: #eee;
|
||||
top: 50%;
|
||||
}
|
||||
}
|
||||
.commentContentBox+.operationLog .operationCommentBor, .operationLog+.commentContentBox .commentOperationBor{
|
||||
border-top: 1px solid #eee;
|
||||
width: 98.5%;
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
}
|
||||
.operationLog+.commentContentBox{
|
||||
&>a, &>.commentContentRight{
|
||||
margin-top: 25px;
|
||||
}
|
||||
}
|
||||
.timeAgo{
|
||||
color:#acb0bf;
|
||||
}
|
||||
.operationLog:last-child::after, .operationLog:first-child::before, .commentContentBox:last-child::after, &.justComment .commentContentBox::before, &.justComment .commentContentBox::after{
|
||||
display: none;
|
||||
}
|
||||
.operationLog:first-child, .commentContentBox+.operationLog{
|
||||
margin-top: -15px;
|
||||
}
|
||||
.commentContentBox:last-child::before{
|
||||
height: 35%;
|
||||
}
|
||||
.flexCenter{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.commentRenderHtml.markdown-body p{
|
||||
font-size: 14px !important;
|
||||
}
|
||||
.commentReply{
|
||||
padding: 15px 0 0 20px;
|
||||
background-color: hsla(225, 31%, 95%, 0.41);
|
||||
&>div{
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
&+.commentReply>div{
|
||||
margin-top: -15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px dashed #eee;
|
||||
}
|
||||
}
|
||||
.opBox{
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.commentContentBox{
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.commentContentRight{
|
||||
flex: 1;
|
||||
background-color:#fafafc;
|
||||
border:1px solid rgba(42, 97, 255, 0.23);
|
||||
border-radius:6px;
|
||||
position: relative;
|
||||
top: -6px;
|
||||
padding: 10px 15px 16px;
|
||||
width: 0;
|
||||
&::before{
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 13px;
|
||||
left: -14px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
border: 7px;
|
||||
border-style: solid;
|
||||
border-color: transparent rgba(42, 97, 255, 0.23) transparent transparent;
|
||||
}
|
||||
&::after{
|
||||
content: '';
|
||||
display: block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
border: 6px;
|
||||
border-style: solid;
|
||||
border-color: transparent #fafafc transparent transparent;
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
left: -11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.attachmentBox{
|
||||
margin-top: -8px;
|
||||
}
|
||||
.color-grey-89{
|
||||
color: #898d9d;
|
||||
}
|
||||
// 添加评论样式
|
||||
.unLoginComment{
|
||||
height:58px;
|
||||
line-height: 58px;
|
||||
background-color:var(--more-light-color);
|
||||
border-radius:4px;
|
||||
color: rgba(84, 87, 103, 1);
|
||||
.loginBtn{
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
.quillFlagBox{
|
||||
color: red;
|
||||
margin-bottom: 5px !important;
|
||||
height: 28px;
|
||||
}
|
||||
.addComments{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color:var(--more-light-color);
|
||||
border-radius:4px;
|
||||
padding:7px 10px;
|
||||
&>img{
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
border-radius: 50%;
|
||||
margin-right: 13px;
|
||||
}
|
||||
}
|
||||
.addCommentBox{
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
border: none;
|
||||
&:focus {
|
||||
border-color: var(--primary-color) !important;
|
||||
}
|
||||
}
|
||||
.paginationIssueComment{
|
||||
text-align: center;
|
||||
}
|
||||
.gotoComment{
|
||||
background-color:rgba(241, 243, 252, 0.55);
|
||||
border-radius:4px;
|
||||
color: rgba(172, 176, 191, 1);
|
||||
padding: 8px 20px;
|
||||
margin-top: -10px;
|
||||
&>a{color: var(--primary-color);}
|
||||
}
|
||||
.colorButton {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
&:hover {
|
||||
background-color: var(--light-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
&:focus {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
.colorButton2 {
|
||||
&:hover {
|
||||
border-color: var(--primary-color);
|
||||
color: var(--light-color);
|
||||
}
|
||||
&:focus {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
/**
|
||||
* 评论组件类型枚举
|
||||
*/
|
||||
export const COMMENT_TYPE = {
|
||||
NEWS: 'news', // 资讯/文章评论
|
||||
PROJECT: 'project', // 项目评论
|
||||
RESOURCE: 'resource', // 资源评论
|
||||
SKILL: 'skill' // AI 技能评论
|
||||
};
|
||||
|
||||
/**
|
||||
* 评论组件 Props 类型定义(JSDoc 用于 TypeScript/IDE 提示)
|
||||
* @typedef {Object} CommentsListProps
|
||||
* @property {string} type - 评论类型:'news' | 'project' | 'resource' | 'skill'
|
||||
* @property {string|number} id - 资源 ID(文章 ID、项目 ID、资源 ID 等)
|
||||
* @property {Object} [extraParams] - 额外的业务参数(根据不同 type 传递不同参数)
|
||||
* @property {string} [owner] - 所有者/组织名(用于 Markdown 渲染)
|
||||
* @property {string} [projectsId] - 项目 ID(用于 Markdown 渲染)
|
||||
* @property {Object} current_user - 当前用户信息
|
||||
* @property {string} current_user.login - 用户登录名
|
||||
* @property {boolean} current_user.admin - 是否管理员
|
||||
* @property {string} current_user.image_url - 用户头像 URL
|
||||
* @property {string} current_user.username - 用户姓名
|
||||
* @property {boolean} [isManager] - 是否为仓库/项目管理员
|
||||
* @property {string} [author] - 评论作者(用于权限判断)
|
||||
* @property {Function} [reload] - 触发重新加载
|
||||
* @property {Function} [showNotification] - 显示通知
|
||||
* @property {Function} [showLoginDialog] - 显示登录弹窗
|
||||
* @property {Function} [onCommentAdded] - 评论添加回调
|
||||
* @property {Function} [onCommentDeleted] - 评论删除回调
|
||||
*/
|
||||
|
||||
/**
|
||||
* 评论列表分页大小
|
||||
*/
|
||||
export const COMMENT_PAGE_SIZE = 50;
|
||||
|
||||
/**
|
||||
* 评论分类
|
||||
*/
|
||||
export const COMMENT_CATEGORY = {
|
||||
COMMENT: 'comment', // 仅评论
|
||||
ALL: 'all', // 评论 + 操作日志
|
||||
OPERATE: 'operate' // 仅操作日志
|
||||
};
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
/**
|
||||
* 通用评论组件入口
|
||||
*
|
||||
* 基于 src/forge/Information/Component/comments 组件重构
|
||||
* 支持通过 type 参数区分不同的业务场景
|
||||
*
|
||||
* 使用示例:
|
||||
*
|
||||
* import CommentsList, { COMMENT_TYPE } from '@/components/comments';
|
||||
*
|
||||
* <CommentsList
|
||||
* type={COMMENT_TYPE.NEWS}
|
||||
* id={docId}
|
||||
* owner={owner}
|
||||
* projectsId={projectsId}
|
||||
* current_user={currentUser}
|
||||
* isManager={isManager}
|
||||
* author={author}
|
||||
* showNotification={showNotification}
|
||||
* showLoginDialog={showLoginDialog}
|
||||
* />
|
||||
*
|
||||
* 不同业务模块传参示例:
|
||||
*
|
||||
* // News 模块
|
||||
* <CommentsList
|
||||
* type={COMMENT_TYPE.NEWS}
|
||||
* id={articleId}
|
||||
* owner={owner}
|
||||
* projectsId={projectsId}
|
||||
* current_user={currentUser}
|
||||
* author={articleAuthor}
|
||||
* ...
|
||||
* />
|
||||
*
|
||||
* // Project 模块(待实现对应 API)
|
||||
* <CommentsList
|
||||
* type={COMMENT_TYPE.PROJECT}
|
||||
* id={projectId}
|
||||
* owner={owner}
|
||||
* projectsId={projectId}
|
||||
* current_user={currentUser}
|
||||
* author={projectCreator}
|
||||
* ...
|
||||
* />
|
||||
*/
|
||||
|
||||
import CommentsList from './CommentsList';
|
||||
import EditComment from './EditComment';
|
||||
import { COMMENT_TYPE, COMMENT_PAGE_SIZE, COMMENT_CATEGORY } from './constants';
|
||||
import * as commentApi from './api';
|
||||
|
||||
export {
|
||||
CommentsList,
|
||||
EditComment,
|
||||
COMMENT_TYPE,
|
||||
COMMENT_PAGE_SIZE,
|
||||
COMMENT_CATEGORY,
|
||||
commentApi
|
||||
};
|
||||
|
||||
export default CommentsList;
|
||||
|
|
@ -135,14 +135,6 @@ export default ({
|
|||
}
|
||||
}
|
||||
|
||||
// 图片加载失败处理 - 使用事件委托统一监听
|
||||
const handleImageError = (e) => {
|
||||
const img = e.target;
|
||||
// 避免循环调用:如果当前已经是裂图,不再替换
|
||||
if (img.src === imgError) return;
|
||||
img.src = imgError;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (el.current && html) {
|
||||
if (html.match(preRegex)) {
|
||||
|
|
@ -151,11 +143,8 @@ export default ({
|
|||
}
|
||||
if (el.current) {
|
||||
el.current.addEventListener('click', onAncherHandler)
|
||||
// 使用事件委托,在父容器上统一监听图片错误事件
|
||||
el.current.addEventListener('error', handleImageError, true);
|
||||
return () => {
|
||||
el.current.removeEventListener('click', onAncherHandler);
|
||||
el.current.removeEventListener('error', handleImageError, true);
|
||||
el.current.removeEventListener('click', onAncherHandler)
|
||||
resetMathExpressions()
|
||||
cleanToc()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,23 +40,12 @@ export function getProjectsLists(id,params){
|
|||
* @param {*} params
|
||||
* @returns
|
||||
*/
|
||||
export async function getSourceList(params){
|
||||
const res = await fetch({
|
||||
export function getSourceList(params){
|
||||
return fetch({
|
||||
url:`/zone/open/${params.id}/resource/list`,
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
|
||||
if (res && res.data && res.data.rows) {
|
||||
res.data.rows = res.data.rows.map(row => {
|
||||
if (row && row.extendData) {
|
||||
return { ...row, extendData: JSON.stringify(row.extendData) };
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
|
||||
return res;
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -85,17 +74,11 @@ export function getNewsDetail(id){
|
|||
})
|
||||
}
|
||||
|
||||
export async function getSourceDetail(id){
|
||||
const res = await fetch({
|
||||
export function getSourceDetail(id){
|
||||
return fetch({
|
||||
url:`/zone/open/resource/detail/${id}`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
if (res && res.data && res.data.data && res.data.data.extendData) {
|
||||
res.data.data.extendData = JSON.stringify(res.data.data.extendData);
|
||||
}
|
||||
|
||||
return res;
|
||||
})
|
||||
}
|
||||
|
||||
export function getMainInfos(deptId, params) {
|
||||
|
|
@ -104,113 +87,4 @@ export function getMainInfos(deptId, params) {
|
|||
method: 'get',
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 分片上传相关 API ====================
|
||||
|
||||
/**
|
||||
* 初始化分片上传
|
||||
* @param {Object} data - 初始化请求参数
|
||||
* @param {string} data.fileName - 文件名
|
||||
* @param {number} data.fileSize - 文件大小(字节)
|
||||
* @param {string} [data.type] - 文件类型(cms/dms/resource/pms等)
|
||||
* @param {string} [data.hierarchy] - 层级结构
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function initChunkedUpload(data) {
|
||||
return fetch({
|
||||
url: '/file/chunked/init',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传分片
|
||||
* @param {Object} data - 分片上传参数
|
||||
* @param {string} data.uploadId - 上传ID
|
||||
* @param {number} data.chunkNumber - 分片序号
|
||||
* @param {File} data.chunk - 分片文件
|
||||
* @param {Function} [onUploadProgress] - 上传进度回调
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function uploadChunk(data, onUploadProgress) {
|
||||
// 使用 FormData 包装数据,让浏览器自动设置 boundary
|
||||
const formData = new FormData();
|
||||
formData.append('chunk', data.chunk);
|
||||
formData.append('uploadId', data.uploadId);
|
||||
formData.append('chunkNumber', data.chunkNumber);
|
||||
|
||||
return fetch({
|
||||
url: `/file/chunked/upload/${data.uploadId}/${data.chunkNumber}`,
|
||||
method: 'post',
|
||||
data: formData,
|
||||
timeout: 600000,
|
||||
onUploadProgress
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并分片
|
||||
* @param {string} uploadId - 上传ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function mergeChunks(uploadId) {
|
||||
return fetch({
|
||||
url: `/file/chunked/merge/${uploadId}`,
|
||||
method: 'post'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消上传
|
||||
* @param {string} uploadId - 上传ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function cancelUpload(uploadId) {
|
||||
return fetch({
|
||||
url: `/file/chunked/cancel/${uploadId}`,
|
||||
method: 'delete'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查分片是否已上传
|
||||
* @param {string} uploadId - 上传ID
|
||||
* @param {number} chunkNumber - 分片序号
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function checkChunk(uploadId, chunkNumber) {
|
||||
return fetch({
|
||||
url: `/file/chunked/check/${uploadId}/${chunkNumber}`,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上传进度
|
||||
* @param {string} uploadId - 上传ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getProgress(uploadId) {
|
||||
return fetch({
|
||||
url: `/file/chunked/progress/${uploadId}`,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查秒传/断点续传
|
||||
* @param {Object} data - 检查参数
|
||||
* @param {string} data.uploadId - 上传ID
|
||||
* @param {string} data.fileSignature - 文件签名
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function checkInstantUpload(data) {
|
||||
return fetch({
|
||||
url: '/file/chunked/checkInstantUpload',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import React, { useState } from "react";
|
||||
import "./index.scss";
|
||||
import { Icon } from "antd";
|
||||
|
||||
export default function ContactFloat({ visible = false }) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<div className={`contact-float ${isExpanded ? "expanded" : ""}`}>
|
||||
<div className="contact-float-toggle" onClick={() => setIsExpanded(!isExpanded)}>
|
||||
<Icon type={isExpanded ? "close" : "customer-service"} />
|
||||
<span className="contact-float-text">{isExpanded ? "收起" : "服务渠道"}</span>
|
||||
</div>
|
||||
|
||||
<div className="contact-float-content">
|
||||
<div className="contact-float-tip">菌用手机蓝信-工作台-“网信通小程序”-软件工厂页面,反馈软件问题和需求;</div>
|
||||
<div className="contact-float-tip">7*24小时反馈热线:0901-748916;15337908492;</div>
|
||||
<div className="contact-float-tip">我们将及时推送至研发团队,尽快迭代</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
.contact-float {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 20%;
|
||||
// transform: translateY(-50%);
|
||||
transform: translateX(100%);
|
||||
z-index: 1000;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.contact-float-toggle {
|
||||
position: absolute;
|
||||
// right: 0;
|
||||
transform: translateX(-100%);
|
||||
background: linear-gradient(132.09deg, #4689ff 0%, #5182ff 28.18%, #b546ff 100%);
|
||||
color: white;
|
||||
padding: 12px 8px;
|
||||
border-radius: 8px 0 0 8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
writing-mode: vertical-lr;
|
||||
font-size: 14px;
|
||||
box-shadow: -2px 0 8px rgba(70, 106, 255, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.anticon {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.contact-float-content {
|
||||
width: 200px;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
border-radius: 0 0 0 12px;
|
||||
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&.expanded {
|
||||
transform: translatex(0);
|
||||
}
|
||||
|
||||
.contact-float-tip {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.contact-float-qrcode {
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
img {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.contact-float-wechat,
|
||||
.contact-float-email {
|
||||
text-align: center;
|
||||
padding-top: 12px;
|
||||
width: 100%;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.contact-float-wechat-label {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.contact-float-wechat-id {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import './index.scss';
|
||||
import featureLeft1 from '../../../images/factory/feature-left-1.png';
|
||||
import featureLeft1 from '../../../images/factory/feature-left-1.webp';
|
||||
import featureLeft2 from '../../../images/factory/feature-left-2.jpg';
|
||||
import featureright1 from '../../../images/factory/feature-right-1.webp';
|
||||
import featureright2 from '../../../images/factory/feature-right-2.jpg';
|
||||
|
|
@ -53,7 +53,6 @@ function Feature(props) {
|
|||
const getStandard = () => {
|
||||
getSourceList({ id: factoryZoneId, keywords: standardId }).then(res => {
|
||||
const rows = []
|
||||
if (!res || !res.data || !res.data.rows) return
|
||||
res.data.rows.forEach(e => {
|
||||
let extendData = {}
|
||||
if (e.extendData) {
|
||||
|
|
@ -107,8 +106,8 @@ function Feature(props) {
|
|||
<div className='df-item' key={index}>
|
||||
<img src={logolist[index]} alt={item.title} />
|
||||
<div>
|
||||
<h4 title={item.name}>{item.name}</h4>
|
||||
<p title={item.summary}>{item.summary}</p>
|
||||
<h4>{item.name}</h4>
|
||||
<p>{item.summary}</p>
|
||||
<button onClick={() => {
|
||||
if (item.fileIds) {
|
||||
downloadFunc(`${httpUrl}/file/open/download/${item.fileIds}`)
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@
|
|||
background-image: url('../../../images/factory/feature-left-bg2.webp');
|
||||
img {
|
||||
width: 620px;
|
||||
// margin-top: 32px;
|
||||
margin-top: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
import React, { useEffect, useRef, useState, forwardRef, useImperativeHandle } from 'react';
|
||||
import React, { useEffect, useRef, useState, forwardRef } from 'react';
|
||||
import './index.scss';
|
||||
import gsap from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
import { ScrollToPlugin } from 'gsap/ScrollToPlugin';
|
||||
import KFIcon from '../../../components/KFIcon';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { getProjectsLists, getSourceList, httpUrl } from '../../api';
|
||||
import { factoryZoneId, manualId, versionId, toolId, commontoolId } from '../../../constants/factory'
|
||||
import { downloadFunc } from '../../../forge/Utils/utils'
|
||||
import '../../common.scss'
|
||||
import { Modal } from 'antd';
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger, ScrollToPlugin);
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
const resourceInfo = [
|
||||
{ name: '项目', title: '工厂开源项目', icon: 'icon-xiangmu', desc: '基于标准化生产线开发的项目源码、依赖包源码、标准化生产线源码等', path: '/sf/resources/project' },
|
||||
|
|
@ -20,15 +18,13 @@ const resourceInfo = [
|
|||
{ name: '软件', title: '开源社区软件', icon: 'icon-gongju', desc: '来自开源社区,可按授权许可使用和复用的各类软件', path: '/sf/resources/devtools' },
|
||||
]
|
||||
|
||||
const Resource = forwardRef(({ zoneId, history }, ref) => {
|
||||
const Resource = forwardRef(({ zoneId }, ref) => {
|
||||
const r1Ref = useRef(null);
|
||||
const r2Ref = useRef(null);
|
||||
const r3Ref = useRef(null);
|
||||
const r4Ref = useRef(null);
|
||||
const containerRef = useRef(null);
|
||||
const timelineRef = useRef(null);
|
||||
const scrollTriggerRef = useRef(null);
|
||||
const wrapperRef = useRef(null); // 用于 IntersectionObserver 的内部 ref
|
||||
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [resourceList1, setResourceList1] = useState([]);
|
||||
|
|
@ -37,14 +33,6 @@ const Resource = forwardRef(({ zoneId, history }, ref) => {
|
|||
const [resourceList4, setResourceList4] = useState([]);
|
||||
const [manualList, setManualList] = useState([]);
|
||||
|
||||
// 调试参数:调整每个面板的滚动偏移量(单位:像素)
|
||||
const scrollOffsetConfig = useRef({
|
||||
0: 0, // resource-1 偏移量
|
||||
1: 0, // resource-2 偏移量
|
||||
2: 0, // resource-3 偏移量
|
||||
3: 300, // resource-4 偏移量
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!zoneId) return;
|
||||
getToolsList();
|
||||
|
|
@ -66,152 +54,61 @@ const Resource = forwardRef(({ zoneId, history }, ref) => {
|
|||
}
|
||||
}, { threshold: 0.1 });
|
||||
|
||||
if (wrapperRef.current) {
|
||||
observer.observe(wrapperRef.current);
|
||||
if (ref.current) {
|
||||
observer.observe(ref.current);
|
||||
}
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
// 初始化GSAP动画
|
||||
useEffect(() => {
|
||||
// 检查元素是否在 DOM 中并且有正确的 offsetParent
|
||||
const checkAndInit = () => {
|
||||
const container = containerRef.current;
|
||||
const panels = [r1Ref.current, r2Ref.current, r3Ref.current, r4Ref.current];
|
||||
|
||||
// 确保所有元素都存在且在 DOM 中有位置信息
|
||||
if (!container || !container.offsetParent || panels.some(p => !p || !p.offsetParent)) {
|
||||
return false;
|
||||
if (!containerRef.current) return;
|
||||
|
||||
// 获取面板元素
|
||||
const panels = [r1Ref.current, r2Ref.current, r3Ref.current, r4Ref.current];
|
||||
|
||||
// 创建时间轴动画
|
||||
timelineRef.current = gsap.timeline({
|
||||
scrollTrigger: {
|
||||
trigger: containerRef.current,
|
||||
start: "top top",
|
||||
end: "+=800",
|
||||
scrub: 1,
|
||||
pin: false,
|
||||
anticipatePin: 1,
|
||||
markers: false,
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
// 创建时间轴动画
|
||||
timelineRef.current = gsap.timeline({
|
||||
scrollTrigger: {
|
||||
trigger: container,
|
||||
start: "top top",
|
||||
end: "+=800",
|
||||
scrub: 1,
|
||||
pin: false,
|
||||
anticipatePin: 1,
|
||||
markers: false,
|
||||
onUpdate: (self) => {
|
||||
scrollTriggerRef.current = self;
|
||||
}
|
||||
}
|
||||
});
|
||||
// 为每个面板创建动画并添加回调
|
||||
panels.forEach((panel, index) => {
|
||||
const animations = [
|
||||
{ y: "-5%", scale: 0.85, duration: 1 },
|
||||
{ y: "-50%", scale: 0.9, duration: 1 },
|
||||
{ y: "-265%", scale: 0.95, duration: 1.2 },
|
||||
{ y: "-415%", scale: 0.98, duration: 2 }
|
||||
];
|
||||
|
||||
// 为每个面板创建动画
|
||||
const animations = [
|
||||
{ y: "-5%", scale: 0.85, duration: 1 },
|
||||
{ y: "-50%", scale: 0.9, duration: 1 },
|
||||
{ y: "-265%", scale: 0.95, duration: 1.2 },
|
||||
{ y: "-415%", scale: 0.98, duration: 2 }
|
||||
];
|
||||
|
||||
panels.forEach((panel, index) => {
|
||||
if (panel && animations[index]) {
|
||||
timelineRef.current.to(panel, {
|
||||
...animations[index],
|
||||
onComplete: () => {
|
||||
panel.classList.add('animated-panel');
|
||||
},
|
||||
onReverseComplete: () => {
|
||||
panel.classList.remove('animated-panel');
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('GSAP animation init error:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 尝试初始化,如果失败则每 100ms 重试一次
|
||||
let attempts = 0;
|
||||
const maxAttempts = 100; // 最多等待 10 秒
|
||||
|
||||
const tryInit = () => {
|
||||
if (checkAndInit()) {
|
||||
console.log('Resource GSAP animation initialized');
|
||||
} else if (attempts < maxAttempts) {
|
||||
attempts++;
|
||||
setTimeout(tryInit, 100);
|
||||
} else {
|
||||
console.error('Failed to initialize Resource GSAP animation');
|
||||
}
|
||||
};
|
||||
|
||||
// 立即开始尝试
|
||||
tryInit();
|
||||
timelineRef.current.to(panel, {
|
||||
...animations[index],
|
||||
onComplete: () => {
|
||||
panel.classList.add('animated-panel');
|
||||
},
|
||||
onReverseComplete: () => {
|
||||
panel.classList.remove('animated-panel');
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// 清理函数
|
||||
return () => {
|
||||
if (timelineRef.current) {
|
||||
timelineRef.current.kill();
|
||||
}
|
||||
// 只清理当前组件创建的 ScrollTrigger
|
||||
ScrollTrigger.getAll().forEach(trigger => {
|
||||
if (trigger.trigger === containerRef.current) {
|
||||
trigger.kill();
|
||||
}
|
||||
});
|
||||
ScrollTrigger.getAll().forEach(trigger => trigger.kill());
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 点击面板跳转到对应滚动位置
|
||||
// progressValues: 每个面板对应的动画进度值 (0-1)
|
||||
const progressValues = [0, 0.25, 0.5, 0.75];
|
||||
|
||||
const handlePanelClick = (index, event) => {
|
||||
// 如果点击的是子元素(按钮、链接、内容项等),不触发面板滚动
|
||||
if (event) {
|
||||
const target = event.target;
|
||||
// 检查是否点击了按钮、链接、内容项或它们的子元素
|
||||
if (target.closest('button') ||
|
||||
target.closest('a') ||
|
||||
target.closest('.item-btn') ||
|
||||
target.closest('.content-item1') ||
|
||||
target.closest('.content-item2') ||
|
||||
target.closest('.content-item3')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!scrollTriggerRef.current) return;
|
||||
|
||||
const st = scrollTriggerRef.current;
|
||||
const targetProgress = progressValues[index];
|
||||
|
||||
// 计算目标滚动位置
|
||||
// start 是动画开始的滚动位置,end 是动画结束的滚动位置
|
||||
const start = st.start;
|
||||
const end = st.end;
|
||||
const targetScroll = start + (end - start) * targetProgress + scrollOffsetConfig.current[index];
|
||||
|
||||
// 平滑滚动到目标位置
|
||||
gsap.to(window, {
|
||||
scrollTo: {
|
||||
y: targetScroll,
|
||||
autoKill: false
|
||||
},
|
||||
duration: 0.8,
|
||||
ease: "power2.inOut"
|
||||
});
|
||||
};
|
||||
|
||||
// 暴露方法给父组件(可选)
|
||||
useImperativeHandle(ref, () => ({
|
||||
scrollToPanel: handlePanelClick,
|
||||
// 获取/设置调试偏移量
|
||||
getScrollOffset: (index) => scrollOffsetConfig.current[index],
|
||||
setScrollOffset: (index, value) => { scrollOffsetConfig.current[index] = value; },
|
||||
getAllOffsets: () => ({ ...scrollOffsetConfig.current }),
|
||||
}));
|
||||
|
||||
const getLeftContent = (index) => {
|
||||
const item = resourceInfo[index];
|
||||
return <div className="content-left">
|
||||
|
|
@ -228,17 +125,13 @@ const Resource = forwardRef(({ zoneId, history }, ref) => {
|
|||
|
||||
const getVersionList = () => {
|
||||
getSourceList({ id: factoryZoneId, keywords: versionId }).then(res => {
|
||||
if (!res || !res.data || !res.data.rows) return
|
||||
// 过滤掉前缀是"历史-"的数据
|
||||
const filteredRows = res.data.rows.filter(item => !item.name || !item.name.startsWith('历史-'));
|
||||
setResourceList2(filteredRows.slice(0, 1));
|
||||
setResourceList2(res.data.rows.slice(0, 1));
|
||||
});
|
||||
}
|
||||
|
||||
const getToolsList = () => {
|
||||
getSourceList({ id: factoryZoneId, keywords: toolId }).then(res => {
|
||||
const rows = []
|
||||
if (!res || !res.data || !res.data.rows) return
|
||||
res.data.rows.forEach(e => {
|
||||
let extendData = {}
|
||||
if (e.extendData) {
|
||||
|
|
@ -254,18 +147,12 @@ const Resource = forwardRef(({ zoneId, history }, ref) => {
|
|||
const getCommonToolsList = () => {
|
||||
getSourceList({ id: factoryZoneId, keywords: commontoolId }).then(res => {
|
||||
const rows = []
|
||||
if (!res || !res.data || !res.data.rows) return
|
||||
res.data.rows.forEach(e => {
|
||||
let extendData = {}
|
||||
if (e.extendData) {
|
||||
extendData = JSON.parse(e.extendData)
|
||||
}
|
||||
rows.push({ ...e,
|
||||
extendDataUrl: extendData.url,
|
||||
extendDataHeadImg: extendData.headImg,
|
||||
isOpen: extendData.isOpen || false,
|
||||
sharer: extendData.sharer
|
||||
})
|
||||
rows.push({ ...e, extendDataUrl: extendData.url, extendDataHeadImg: extendData.headImg })
|
||||
});
|
||||
|
||||
setResourceList3(rows.slice(0, 6));
|
||||
|
|
@ -275,7 +162,6 @@ const Resource = forwardRef(({ zoneId, history }, ref) => {
|
|||
const getManualList = () => {
|
||||
getSourceList({ id: factoryZoneId, keywords: manualId }).then(res => {
|
||||
const rows = []
|
||||
if (!res || !res.data || !res.data.rows) return
|
||||
res.data.rows.forEach(e => {
|
||||
let extendData = {}
|
||||
if (e.extendData) {
|
||||
|
|
@ -290,22 +176,14 @@ const Resource = forwardRef(({ zoneId, history }, ref) => {
|
|||
return (
|
||||
<div
|
||||
className={`factory-resource factory-module ${isVisible ? 'animate-active' : ''}`}
|
||||
ref={(node) => {
|
||||
// 合并内部 ref 和外部 forwarded ref
|
||||
wrapperRef.current = node;
|
||||
if (typeof ref === 'function') {
|
||||
ref(node);
|
||||
} else if (ref) {
|
||||
ref.current = node;
|
||||
}
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<div className="resource-container" ref={containerRef}>
|
||||
<h1 className="module-title">共建共用共享,为软件工厂赋能</h1>
|
||||
|
||||
<div className="panels-stack">
|
||||
|
||||
<div ref={r1Ref} className="resource-1" onClick={(e) => handlePanelClick(0, e)} style={{ cursor: 'pointer' }}>
|
||||
<div ref={r1Ref} className="resource-1">
|
||||
<div className="panel-content panel-content-1">
|
||||
{
|
||||
getLeftContent(1)
|
||||
|
|
@ -317,8 +195,10 @@ const Resource = forwardRef(({ zoneId, history }, ref) => {
|
|||
<div className="item-name" title={item.name}>{item.name}</div>
|
||||
<div className="item-desc" title={item.summary}>{item.summary}</div>
|
||||
<button onClick={() => {
|
||||
history.push(`/sf/resources/releases`)
|
||||
}}>查看</button>
|
||||
if (item.fileIds) {
|
||||
downloadFunc(`${httpUrl}/file/open/download/${item.fileIds}`)
|
||||
}
|
||||
}} disabled={!item.fileIds} className={!item.fileIds && 'disabled'} >{item.fileIds ? '下载' : '稍后下载'}</button>
|
||||
</div>
|
||||
})
|
||||
}
|
||||
|
|
@ -344,7 +224,7 @@ const Resource = forwardRef(({ zoneId, history }, ref) => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div ref={r2Ref} className="resource-2" onClick={(e) => handlePanelClick(1, e)} style={{ cursor: 'pointer' }}>
|
||||
<div ref={r2Ref} className="resource-2">
|
||||
<div className="panel-content panel-content-2">
|
||||
{
|
||||
getLeftContent(0)
|
||||
|
|
@ -384,7 +264,7 @@ const Resource = forwardRef(({ zoneId, history }, ref) => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div ref={r3Ref} className="resource-3" onClick={(e) => handlePanelClick(2, e)} style={{ cursor: 'pointer' }}>
|
||||
<div ref={r3Ref} className="resource-3">
|
||||
<div className="panel-content panel-content-3">
|
||||
{
|
||||
getLeftContent(2)
|
||||
|
|
@ -399,14 +279,7 @@ const Resource = forwardRef(({ zoneId, history }, ref) => {
|
|||
<Link to={`/sf/resources/commontools/${item.id}`}><button className="shiny-button">查看</button></Link>
|
||||
<button onClick={() => {
|
||||
if (item.fileIds) {
|
||||
if (item.isOpen != 1) {
|
||||
downloadFunc(`${httpUrl}/file/open/download/${item.fileIds}`)
|
||||
} else {
|
||||
Modal.info({
|
||||
title: '下载工具',
|
||||
content: `下载请与软件工厂社区管理员联系`
|
||||
})
|
||||
}
|
||||
downloadFunc(`${httpUrl}/file/open/download/${item.fileIds}`)
|
||||
}
|
||||
}} disabled={!item.fileIds} className={!item.fileIds && 'disabled'} >{item.fileIds ? '下载' : '稍后下载'}</button>
|
||||
</div>
|
||||
|
|
@ -416,7 +289,7 @@ const Resource = forwardRef(({ zoneId, history }, ref) => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ref={r4Ref} className="resource-4" onClick={(e) => handlePanelClick(3, e)} style={{ cursor: 'pointer' }}>
|
||||
<div ref={r4Ref} className="resource-4">
|
||||
<div className="panel-content panel-content-4">
|
||||
{
|
||||
getLeftContent(3)
|
||||
|
|
|
|||
|
|
@ -73,7 +73,6 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-right: 15px;
|
||||
cursor: default;
|
||||
|
||||
.item-head {
|
||||
display: flex;
|
||||
|
|
@ -163,7 +162,6 @@
|
|||
align-items: center;
|
||||
padding: 30px 35px;
|
||||
margin-right: 18px;
|
||||
cursor: default;
|
||||
|
||||
.item-name {
|
||||
max-width: 210px;
|
||||
|
|
@ -200,7 +198,6 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-right: 15px;
|
||||
cursor: default;
|
||||
|
||||
.item-desc {
|
||||
margin-top: 27px;
|
||||
|
|
@ -246,7 +243,6 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-right: 15px;
|
||||
cursor: default;
|
||||
|
||||
.item-desc {
|
||||
margin-top: 27px;
|
||||
|
|
@ -388,7 +384,7 @@
|
|||
font-family: alibaba;
|
||||
font-weight: 500;
|
||||
color: #091221;
|
||||
font-size: 20px;
|
||||
font-size: 24px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
|
|
|||
|
|
@ -1,259 +0,0 @@
|
|||
import React, { useState, useCallback } from 'react';
|
||||
import './FeedbackModal.scss';
|
||||
import { createMemo } from '../../forums/api';
|
||||
import { message, Upload, Icon, Button } from 'antd';
|
||||
import { httpUrl } from '../../forums/fetch';
|
||||
import RichEditor from '../../forge/Component/RichEditor';
|
||||
import cookie from 'react-cookies';
|
||||
import '../common.scss'
|
||||
import { feedBackId } from '../../forums/static'
|
||||
|
||||
// 导入图片资源
|
||||
import modalBg from '../../images/factory/feedback/modal-bg.webp';
|
||||
import iconClose from '../../images/factory/feedback/icon-close-new.webp';
|
||||
import iconRequired from '../../images/factory/feedback/icon-required-new.webp';
|
||||
|
||||
/**
|
||||
* 反馈中心-新增弹窗组件
|
||||
* @param {boolean} visible - 弹窗显示状态
|
||||
* @param {function} onClose - 关闭弹窗回调
|
||||
* @param {function} onSuccess - 提交成功回调
|
||||
*/
|
||||
function FeedbackModal({ visible, onClose, onSuccess }) {
|
||||
const [formData, setFormData] = useState({
|
||||
title: '',
|
||||
description: '',
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [fileList, setFileList] = useState([]);
|
||||
|
||||
// 处理输入变化
|
||||
const handleInputChange = useCallback((field, value) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}));
|
||||
// 清除对应字段的错误
|
||||
if (errors[field]) {
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
[field]: null,
|
||||
}));
|
||||
}
|
||||
}, [errors]);
|
||||
|
||||
// 文件上传前校验
|
||||
const beforeUpload = (file) => {
|
||||
const isLt100M = file.size / 1024 / 1024 < 100;
|
||||
if (!isLt100M) {
|
||||
message.error('文件大小必须小于100MB!');
|
||||
}
|
||||
return isLt100M;
|
||||
};
|
||||
|
||||
// 文件列表变化
|
||||
const handleFileChange = (info) => {
|
||||
if (info.file.status === 'uploading' || info.file.status === 'done' || info.file.status === 'removed') {
|
||||
let fileList = info.fileList || [];
|
||||
|
||||
// 处理后端返回格式与Ant Design Upload预期格式不匹配的问题
|
||||
// 后端返回 { status: 1, message: "成功", id: 123 } 格式
|
||||
// Ant Design 期望 response 中包含能识别的成功标识
|
||||
fileList = fileList.map(file => {
|
||||
if (file.response && file.response.status === 1 && file.status === 'uploading') {
|
||||
return {
|
||||
...file,
|
||||
status: 'done',
|
||||
};
|
||||
}
|
||||
return file;
|
||||
});
|
||||
|
||||
setFileList(fileList);
|
||||
}
|
||||
};
|
||||
|
||||
// 验证表单
|
||||
const validateForm = useCallback(() => {
|
||||
const newErrors = {};
|
||||
if (!formData.title.trim()) {
|
||||
newErrors.title = '请输入标题';
|
||||
}
|
||||
|
||||
if (!formData.description) {
|
||||
newErrors.description = '请输入问题描述';
|
||||
} else {
|
||||
if (formData.description === '<p><br></p>') {
|
||||
newErrors.description = '请输入问题描述';
|
||||
}
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
}, [formData]);
|
||||
|
||||
// 处理提交
|
||||
const handleSubmit = useCallback(async () => {
|
||||
if (!validateForm()) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
// 构建附件ID数组,参考forums/edit的实现
|
||||
const attachmentIdArray = fileList.map(item =>
|
||||
item.response && item.response.id ? item.response.id : item.id ? item.id : item
|
||||
);
|
||||
|
||||
// 构建接口参数,主题板块默认填12
|
||||
const params = {
|
||||
forum_id: feedBackId,
|
||||
attachments: attachmentIdArray,
|
||||
memo: {
|
||||
subject: formData.title,
|
||||
content: formData.description,
|
||||
is_original: 1,
|
||||
tag_id: 1
|
||||
},
|
||||
};
|
||||
|
||||
const ret = await createMemo(params);
|
||||
|
||||
if (ret.status === 1) {
|
||||
message.success(ret.message || '提交成功');
|
||||
// 提交成功后重置表单并关闭
|
||||
setFormData({ title: '', description: '' });
|
||||
setFileList([]);
|
||||
onClose?.();
|
||||
onSuccess?.(ret);
|
||||
} else {
|
||||
message.error(ret.message || '提交失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交反馈失败:', error);
|
||||
message.error('提交失败,请稍后重试');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}, [formData, validateForm, fileList, onClose, onSuccess]);
|
||||
|
||||
// 处理取消
|
||||
const handleCancel = useCallback(() => {
|
||||
setFormData({ title: '', description: '' });
|
||||
setErrors({});
|
||||
setFileList([]);
|
||||
onClose?.();
|
||||
}, [onClose]);
|
||||
|
||||
// 点击蒙层关闭(已禁用,仅通过X按钮和取消按钮关闭)
|
||||
const handleMaskClick = useCallback((e) => {
|
||||
// 点击蒙层不关闭弹窗
|
||||
}, []);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<div className="feedback-modal-wrap">
|
||||
<div className="feedback-modal-mask" onClick={handleMaskClick}>
|
||||
<div className="feedback-modal">
|
||||
{/* 弹窗背景 */}
|
||||
<div className="feedback-modal-bg">
|
||||
<img src={modalBg} alt="" />
|
||||
</div>
|
||||
|
||||
{/* 弹窗头部 */}
|
||||
<div className="feedback-modal-header">
|
||||
<h2 className="feedback-modal-title">意见反馈</h2>
|
||||
<button
|
||||
className="feedback-modal-close"
|
||||
onClick={handleCancel}
|
||||
aria-label="关闭"
|
||||
>
|
||||
<img src={iconClose} alt="关闭" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 弹窗内容 */}
|
||||
<div className="feedback-modal-content">
|
||||
{/* 标题输入 */}
|
||||
<div className="feedback-form-item">
|
||||
<div className="feedback-form-label">
|
||||
<img src={iconRequired} alt="必填" className="required-icon" />
|
||||
<span>标题</span>
|
||||
</div>
|
||||
<div className={`feedback-form-input-wrapper ${errors.title ? 'error' : ''}`}>
|
||||
<input
|
||||
type="text"
|
||||
className="feedback-form-input"
|
||||
placeholder="请输入标题"
|
||||
value={formData.title}
|
||||
onChange={(e) => handleInputChange('title', e.target.value)}
|
||||
maxLength={200}
|
||||
/>
|
||||
</div>
|
||||
{errors.title && <span className="feedback-form-error">{errors.title}</span>}
|
||||
</div>
|
||||
|
||||
{/* 问题描述输入 */}
|
||||
<div className="feedback-form-item">
|
||||
<div className="feedback-form-label">
|
||||
<img src={iconRequired} alt="必填" className="required-icon" />
|
||||
<span>问题描述</span>
|
||||
</div>
|
||||
<div className="feedback-form-rich">
|
||||
<RichEditor httpUrl={httpUrl} uploadUrl={`${httpUrl}/api/attachments.json`} videoUploadUrl={`${httpUrl}/api/attachments.json`} height={'270px'} setValue={(value) => { handleInputChange('description', value) }} />
|
||||
</div>
|
||||
{errors.description && <span className="feedback-form-error">{errors.description}</span>}
|
||||
</div>
|
||||
|
||||
{/* 附件上传 - 参考forums/edit配置 */}
|
||||
<div className="feedback-form-item">
|
||||
<div className="feedback-form-label">
|
||||
<span>附件</span>
|
||||
</div>
|
||||
<div className="feedback-form-upload">
|
||||
<Upload
|
||||
action={`${httpUrl}/api/attachments.json`}
|
||||
beforeUpload={beforeUpload}
|
||||
onChange={handleFileChange}
|
||||
fileList={fileList}
|
||||
className="feedback-upload-dragger"
|
||||
withCredentials={true}
|
||||
headers={{ Authorization: cookie.load('autologin_trustie') }}
|
||||
>
|
||||
{
|
||||
fileList.length < 3 && <>
|
||||
<Button className="uploadBtn">
|
||||
<Icon type="upload" /> 选择文件
|
||||
</Button>
|
||||
<span className={"ml10 color-ooo"}>(单个文件最大100M,最多3个附件)</span></>
|
||||
}
|
||||
|
||||
</Upload>
|
||||
</div>
|
||||
</div>
|
||||
<div className="feedback-modal-footer">
|
||||
<button
|
||||
className="feedback-btn feedback-btn-cancel"
|
||||
onClick={handleCancel}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
className="feedback-btn feedback-btn-submit shiny-button"
|
||||
onClick={handleSubmit}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? '提交中...' : '提交'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FeedbackModal;
|
||||
|
|
@ -1,449 +0,0 @@
|
|||
// 反馈中心-新增弹窗样式
|
||||
// 遵循 factory 目录下的 SCSS 规范
|
||||
// 使用嵌套写法并添加命名空间避免影响其他页面
|
||||
|
||||
// 颜色变量
|
||||
$primary-color: #4953e6;
|
||||
$primary-hover: #3a42d1;
|
||||
$text-primary: #091221;
|
||||
$text-secondary: #223452;
|
||||
$text-muted: #9494b4;
|
||||
$border-color: #cfd5e8;
|
||||
$border-focus: #4953e6;
|
||||
$bg-white: #ffffff;
|
||||
$bg-mask: rgba(0, 0, 0, 0.4);
|
||||
$error-color: #ff4d4f;
|
||||
|
||||
// 尺寸变量
|
||||
$modal-width: 900px;
|
||||
$modal-height: 810px;
|
||||
$content-width: 860px;
|
||||
$content-height: 700px;
|
||||
$border-radius-modal: 30px;
|
||||
$border-radius-content: 30px;
|
||||
$border-radius-input: 8px;
|
||||
$border-radius-btn: 9px;
|
||||
|
||||
// 动画变量
|
||||
$transition-duration: 0.3s;
|
||||
|
||||
// 动画定义
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 命名空间包裹所有样式
|
||||
.feedback-modal-wrap {
|
||||
// 蒙层
|
||||
.feedback-modal-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: $bg-mask;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
animation: fadeIn $transition-duration ease;
|
||||
|
||||
// 弹窗容器
|
||||
.feedback-modal {
|
||||
position: relative;
|
||||
width: $modal-width;
|
||||
// height: $modal-height;
|
||||
border-radius: $border-radius-modal;
|
||||
overflow: hidden;
|
||||
animation: slideUp $transition-duration ease;
|
||||
padding-bottom: 20px;
|
||||
|
||||
// 弹窗背景
|
||||
.feedback-modal-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
// 添加渐变叠加层以增强文字可读性
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(73, 83, 230, 0.1) 0%,
|
||||
rgba(73, 83, 230, 0.05) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗头部
|
||||
.feedback-modal-header {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px 30px;
|
||||
height: 81px;
|
||||
|
||||
// 弹窗标题
|
||||
.feedback-modal-title {
|
||||
font-family: alibabaReg;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: $bg-white;
|
||||
text-align: center;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
// 关闭按钮
|
||||
.feedback-modal-close {
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform $transition-duration ease;
|
||||
|
||||
img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-50%) rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗内容区域
|
||||
.feedback-modal-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: $content-width;
|
||||
// height: $content-height;
|
||||
margin: 0 auto;
|
||||
background-color: $bg-white;
|
||||
border-radius: $border-radius-content;
|
||||
padding: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
|
||||
// 表单项
|
||||
.feedback-form-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
// 表单标签
|
||||
.feedback-form-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-family: alibabaReg;
|
||||
font-size: 14px;
|
||||
color: $text-primary;
|
||||
line-height: 1.5;
|
||||
|
||||
.required-icon {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
// 输入框容器
|
||||
.feedback-form-input-wrapper,
|
||||
.feedback-form-textarea-wrapper {
|
||||
width: 100%;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: $border-radius-input;
|
||||
transition: border-color $transition-duration ease, box-shadow $transition-duration ease;
|
||||
|
||||
&:hover {
|
||||
border-color: darken($border-color, 10%);
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border-color: $border-focus;
|
||||
box-shadow: 0 0 0 2px rgba($border-focus, 0.1);
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-color: $error-color;
|
||||
box-shadow: 0 0 0 2px rgba($error-color, 0.1);
|
||||
}
|
||||
|
||||
// 输入框
|
||||
.feedback-form-input {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-family: alibabaReg;
|
||||
font-size: 14px;
|
||||
color: $text-primary;
|
||||
line-height: 1.5;
|
||||
outline: none;
|
||||
|
||||
&::placeholder {
|
||||
color: $text-muted;
|
||||
}
|
||||
}
|
||||
|
||||
// 文本域
|
||||
.feedback-form-textarea {
|
||||
width: 100%;
|
||||
min-height: 80px;
|
||||
padding: 12px 16px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-family: alibabaReg;
|
||||
font-size: 14px;
|
||||
color: $text-primary;
|
||||
line-height: 1.6;
|
||||
outline: none;
|
||||
resize: vertical;
|
||||
|
||||
&::placeholder {
|
||||
color: $text-muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 富文本编辑器容器
|
||||
.feedback-form-rich {
|
||||
// height: 400px;
|
||||
}
|
||||
|
||||
// 错误提示
|
||||
.feedback-form-error {
|
||||
font-family: alibabaReg;
|
||||
font-size: 12px;
|
||||
color: $error-color;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
// 上传区域
|
||||
.feedback-form-upload {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
// Upload.Dragger 样式
|
||||
.feedback-upload-dragger {
|
||||
width: 100%;
|
||||
|
||||
.ant-upload {
|
||||
&.ant-upload-drag {
|
||||
background: #fafafa;
|
||||
border: 1px dashed $border-color;
|
||||
border-radius: $border-radius-input;
|
||||
padding: 16px;
|
||||
|
||||
&:hover {
|
||||
border-color: $primary-color;
|
||||
}
|
||||
|
||||
.ant-upload-drag-icon {
|
||||
margin-bottom: 8px;
|
||||
|
||||
.anticon {
|
||||
font-size: 32px;
|
||||
color: $primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: alibabaReg;
|
||||
font-size: 13px;
|
||||
color: $text-muted;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 文件列表样式
|
||||
.ant-upload-list {
|
||||
margin-top: 8px;
|
||||
|
||||
.ant-upload-list-item {
|
||||
font-family: alibabaReg;
|
||||
font-size: 13px;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($primary-color, 0.02);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 上传按钮
|
||||
.uploadBtn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 16px;
|
||||
border: 1px solid $primary-color;
|
||||
border-radius: 4px;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
transition: all $transition-duration ease;
|
||||
color: $primary-color;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($primary-color, 0.05);
|
||||
color: $primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗底部
|
||||
.feedback-modal-footer {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
|
||||
// 按钮基础样式
|
||||
.feedback-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 101px;
|
||||
height: 36px;
|
||||
padding: 0 24px;
|
||||
border-radius: $border-radius-btn;
|
||||
font-family: alibabaReg;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
cursor: pointer;
|
||||
transition: all $transition-duration ease;
|
||||
border: none;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
// 取消按钮
|
||||
.feedback-btn-cancel {
|
||||
background-color: transparent;
|
||||
border: 1px solid $text-primary;
|
||||
color: $text-primary;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: rgba($text-primary, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
// 提交按钮
|
||||
.feedback-btn-submit {
|
||||
background-color: $text-primary;
|
||||
color: $bg-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式适配
|
||||
@media screen and (max-width: 992px) {
|
||||
.feedback-modal-mask {
|
||||
.feedback-modal {
|
||||
width: 90vw;
|
||||
max-width: 600px;
|
||||
height: auto;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
|
||||
.feedback-modal-content {
|
||||
width: auto;
|
||||
height: auto;
|
||||
margin: 0 20px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.feedback-modal-header {
|
||||
padding: 20px 24px;
|
||||
height: auto;
|
||||
|
||||
.feedback-modal-close {
|
||||
right: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 576px) {
|
||||
.feedback-modal-mask {
|
||||
.feedback-modal {
|
||||
.feedback-modal-content {
|
||||
padding: 20px;
|
||||
gap: 16px;
|
||||
|
||||
.feedback-modal-footer {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
|
||||
.feedback-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.feedback-form-item {
|
||||
.feedback-form-upload {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,302 +0,0 @@
|
|||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import './index.scss';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { TPMIndexHOC } from "../../modules/tpm/TPMIndexHOC";
|
||||
import FeedbackModal from './FeedbackModal';
|
||||
import { getForumsById } from '../../forums/api';
|
||||
import InfiniteScroll from 'react-infinite-scroller';
|
||||
import '../common.scss'
|
||||
import { feedBackId } from '../../forums/static'
|
||||
|
||||
// 导入图片资源
|
||||
import headerBg from '../../images/factory/feedback/header-bg.webp';
|
||||
import arrowRight from '../../images/factory/feedback/arrow-right.webp';
|
||||
import defaultAvatar from '../../images/factory/feedback/default-avatar.webp';
|
||||
import iconComment from '../../images/factory/feedback/icon-comment.webp';
|
||||
import iconLike from '../../images/factory/feedback/icon-like.webp';
|
||||
import iconView from '../../images/factory/feedback/icon-view.webp';
|
||||
import tagNew from '../../images/factory/feedback/tag-new-main.webp';
|
||||
import rocketImg from '../../images/factory/feedback/rocket.svg';
|
||||
import ActionItem from '../resource/components/ActionItem';
|
||||
|
||||
function FeedbackCenter(props) {
|
||||
const [hotFeedback, setHotFeedback] = useState([]);
|
||||
const [allFeedback, setAllFeedback] = useState([]);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const contentRef = useRef(null);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [animationPhase, setAnimationPhase] = useState('initial'); // initial: 球形状态, expanded: 展开状态
|
||||
|
||||
// 分页相关状态
|
||||
const pageSize = 10;
|
||||
const [pageNum, setPageNum] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [total, setTotal] = useState(0);
|
||||
const hasMore = useRef(true);
|
||||
|
||||
// 获取反馈列表数据
|
||||
const getFeedbackList = (isMore = false) => {
|
||||
if (!hasMore.current && !isMore) return;
|
||||
setLoading(true);
|
||||
getForumsById(feedBackId, {
|
||||
page: isMore || 1,
|
||||
limit: pageSize,
|
||||
sort: 'published_at',
|
||||
select_type: 'all',
|
||||
}).then(res => {
|
||||
setLoading(false);
|
||||
if (res && res.memos) {
|
||||
const rows = res.memos;
|
||||
if (isMore) {
|
||||
setPageNum(isMore);
|
||||
setAllFeedback([...allFeedback, ...rows]);
|
||||
} else {
|
||||
// pageNum=1 时,取前3个作为热门反馈,其余作为全部反馈
|
||||
setHotFeedback(rows.slice(0, 3).map((item) => ({
|
||||
...item,
|
||||
isNew: true,
|
||||
avatar: item.image_url ? item.image_url : defaultAvatar,
|
||||
author: item.username,
|
||||
date: item.published_time,
|
||||
viewed_count: item.viewed_count || 0,
|
||||
praises_count: item.praises_count || 0,
|
||||
replies_count: item.replies_count || 0,
|
||||
})));
|
||||
setAllFeedback(rows.slice(3));
|
||||
}
|
||||
hasMore.current = res.memos_count > (isMore ? pageSize * pageNum + rows.length : rows.length);
|
||||
setTotal(res.memos_count || 0);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.title = '反馈中心';
|
||||
|
||||
// 页面可见性动画
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
const entry = entries[0];
|
||||
if (entry.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
observer.disconnect();
|
||||
}
|
||||
}, {
|
||||
threshold: 0.1
|
||||
});
|
||||
|
||||
if (contentRef.current) {
|
||||
observer.observe(contentRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (contentRef.current) observer.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 初始加载数据
|
||||
useEffect(() => {
|
||||
getFeedbackList();
|
||||
}, []);
|
||||
|
||||
// 页面渲染完毕后触发动画
|
||||
useEffect(() => {
|
||||
// 延迟一段时间确保球形状态可见后再展开
|
||||
const timer = setTimeout(() => {
|
||||
setAnimationPhase('expanded');
|
||||
}, 800);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
// 打开弹窗
|
||||
const handleFeedbackClick = () => {
|
||||
const { user } = props;
|
||||
if (user && user.login) {
|
||||
setModalVisible(true);
|
||||
} else {
|
||||
const { history } = props;
|
||||
history.push(`/login?go_page=/sf/feedback`);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭弹窗
|
||||
const handleModalClose = () => {
|
||||
setModalVisible(false);
|
||||
};
|
||||
|
||||
// 提交成功回调
|
||||
const handleModalSuccess = (ret) => {
|
||||
// 提交成功后关闭弹窗并刷新列表
|
||||
setModalVisible(false);
|
||||
// 重置hasMore后刷新列表
|
||||
hasMore.current = true;
|
||||
getFeedbackList();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="feedback-center" ref={contentRef}>
|
||||
{/* 顶部横幅区域 */}
|
||||
<div className="feedback-header">
|
||||
<div className="feedback-header-bg">
|
||||
<img src={headerBg} alt="" />
|
||||
</div>
|
||||
<div className="feedback-header-content">
|
||||
<h1 className="feedback-title">反馈中心,用心聆听</h1>
|
||||
<p className="feedback-subtitle">
|
||||
汇聚每一份声音,持续优化产品与服务,与开发者共建高效可靠的软件工厂社区
|
||||
</p>
|
||||
<div className="feedback-button-wrapper">
|
||||
<button
|
||||
className={`feedback-btn ${animationPhase === 'initial' ? 'ball-state' : 'expand-state'}`}
|
||||
onClick={handleFeedbackClick}
|
||||
>
|
||||
{animationPhase === 'initial' ? (
|
||||
<img src={rocketImg} alt="" className="rocket-icon" />
|
||||
) : (
|
||||
<>
|
||||
<span>意见反馈</span>
|
||||
<img src={arrowRight} alt="" className="btn-arrow" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 热门反馈卡片区域 */}
|
||||
<div className={`feedback-hot-section ${isVisible ? 'animate-active' : ''}`}>
|
||||
<div className="feedback-container">
|
||||
<div className="feedback-cards">
|
||||
{hotFeedback.map((item, index) => (
|
||||
<ActionItem className="content-bottom-wrapper" key={item.id} backgroundColor="#ffffff" padding="2px" borderRadius="16px">
|
||||
<div
|
||||
className="feedback-card"
|
||||
style={{ animationDelay: `${index * 0.1}s` }}
|
||||
>
|
||||
|
||||
{/* NEW标签 */}
|
||||
{item.isNew && (
|
||||
<div className="card-tag">
|
||||
<img src={tagNew} alt="NEW" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 卡片内容 */}
|
||||
<div className="card-content">
|
||||
<h3 className="card-title" title={item.subject}>{item.subject}</h3>
|
||||
|
||||
<div className="card-author">
|
||||
<img src={item.avatar} alt={item.author} className="author-avatar" />
|
||||
<span className="author-name">{item.author}</span>
|
||||
<span className="author-date">{item.date}</span>
|
||||
</div>
|
||||
|
||||
<div className="card-actions">
|
||||
<Link to={`/forums/${item.id}`} className="detail-btn shiny-button" >
|
||||
查看详情
|
||||
</Link>
|
||||
|
||||
<div className="card-stats">
|
||||
<span className="stat-item">
|
||||
<img src={iconView} alt="浏览" />
|
||||
{item.viewed_count}
|
||||
</span>
|
||||
<span className="stat-item">
|
||||
<img src={iconLike} alt="点赞" />
|
||||
{item.praises_count}
|
||||
</span>
|
||||
<span className="stat-item">
|
||||
<img src={iconComment} alt="评论" />
|
||||
{item.replies_count}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ActionItem>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 全部反馈区域 - 2列网格布局 */}
|
||||
{
|
||||
allFeedback.length > 0 && <div className="feedback-all-section">
|
||||
<div className="feedback-container">
|
||||
<div className="feedback-list-wrapper">
|
||||
<h2 className="section-title">全部反馈</h2>
|
||||
|
||||
<InfiniteScroll
|
||||
threshold={10}
|
||||
initialLoad={false}
|
||||
pageStart={1}
|
||||
loadMore={(e) => { getFeedbackList(e) }}
|
||||
hasMore={!loading && hasMore.current}
|
||||
>
|
||||
{/* 2列网格布局 */}
|
||||
<div className="feedback-grid">
|
||||
{allFeedback.map((item) => (
|
||||
<ActionItem className="content-bottom-wrapper" key={item.id} backgroundColor="#ffffff" padding="1px" borderRadius="24px">
|
||||
<div className="feedback-grid-item">
|
||||
<div className="grid-item-header">
|
||||
<img src={item.image_url} alt={item.username} className="grid-avatar" />
|
||||
<div className="grid-info">
|
||||
<div className="grid-author-info">
|
||||
<span className="grid-author-name">{item.username}</span>
|
||||
{
|
||||
props && props.user && props.user.login === item.user_login && <span
|
||||
className="grid-tag"
|
||||
style={{ backgroundColor: '#4953e6' }}
|
||||
>
|
||||
我
|
||||
</span>
|
||||
}
|
||||
|
||||
</div>
|
||||
<div className="grid-item-date">{item.published_at}</div>
|
||||
</div>
|
||||
<Link to={`/forums/${item.id}`} className="grid-detail-btn">
|
||||
查看详情
|
||||
<span className="arrow"></span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
|
||||
<h4 className="grid-item-title">{item.subject}</h4>
|
||||
|
||||
<div className="grid-item-stats">
|
||||
<span className="stat-item">
|
||||
<img src={iconView} alt="浏览" />
|
||||
{item.viewed_count || 0}
|
||||
</span>
|
||||
<span className="stat-item">
|
||||
<img src={iconLike} alt="点赞" />
|
||||
{item.praises_count || 0}
|
||||
</span>
|
||||
<span className="stat-item">
|
||||
<img src={iconComment} alt="评论" />
|
||||
{item.replies_count || 0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</ActionItem>
|
||||
))}
|
||||
</div>
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
{/* 反馈弹窗 */}
|
||||
<FeedbackModal
|
||||
visible={modalVisible}
|
||||
onClose={handleModalClose}
|
||||
onSuccess={handleModalSuccess}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TPMIndexHOC(FeedbackCenter);
|
||||
|
|
@ -1,674 +0,0 @@
|
|||
// 反馈中心页面样式
|
||||
// 遵循 factory 目录下的 SCSS 规范
|
||||
|
||||
// 颜色变量
|
||||
$primary-color: #5b47f2;
|
||||
$secondary-color: #3a49eb;
|
||||
$text-primary: #091221;
|
||||
$text-secondary: #223452;
|
||||
$text-muted: #687082;
|
||||
$bg-color: #f8f9ff;
|
||||
$card-bg: #ffffff;
|
||||
$card-bg-light: rgba(239, 243, 255, 0.7);
|
||||
$border-color: #e8eaf0;
|
||||
$tag-bg: #091221;
|
||||
$tag-new-bg: #ce325c;
|
||||
$tag-me-bg: #4953e6;
|
||||
|
||||
// 动画变量
|
||||
$animation-duration: 0.6s;
|
||||
|
||||
// 按钮球形展开动画关键帧
|
||||
@keyframes rocketShrink {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes expandBtn {
|
||||
0% {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
padding: 0;
|
||||
}
|
||||
100% {
|
||||
width: 180px;
|
||||
height: 60px;
|
||||
border-radius: 29px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInText {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInArrow {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(10px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.feedback-center {
|
||||
min-height: 100vh;
|
||||
background-color: $bg-color;
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
// 顶部横幅区域
|
||||
.feedback-header {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 364px;
|
||||
overflow: hidden;
|
||||
|
||||
.feedback-header-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.feedback-header-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
.feedback-title {
|
||||
line-height: normal;
|
||||
font-family: DouyinSansBold;
|
||||
color: #27264e;
|
||||
font-size: 44px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.feedback-subtitle {
|
||||
line-height: normal;
|
||||
font-family: alibabaReg;
|
||||
color: $text-primary;
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.feedback-button-wrapper {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.feedback-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
width: 180px;
|
||||
height: 60px;
|
||||
background:linear-gradient(90deg,#5b47f2 1.89%,#3a49eb 47.17%,#164ce4 97.65%);
|
||||
border: 2px solid #ffffff;
|
||||
border-radius: 29px;
|
||||
box-shadow:0px -3px 12px rgba(255, 255, 255, 0.45) inset;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
// 球形状态 - 初始显示为60x60的球
|
||||
&.ball-state {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.rocket-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
animation: rocketShrink 0.5s ease forwards;
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
// 展开状态 - 恢复正常按钮样式
|
||||
&.expand-state {
|
||||
width: 180px;
|
||||
height: 60px;
|
||||
border-radius: 29px;
|
||||
padding: 0 24px;
|
||||
animation: expandBtn 0.5s ease forwards;
|
||||
|
||||
span {
|
||||
font-size: 18px;
|
||||
color: #ffffff;
|
||||
line-height: 25px;
|
||||
font-family: alibaba;
|
||||
animation: fadeInText 0.3s ease forwards;
|
||||
animation-delay: 0.3s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.btn-arrow {
|
||||
width: 14px;
|
||||
height: 6px;
|
||||
transition: transform 0.3s ease;
|
||||
animation: fadeInArrow 0.3s ease forwards;
|
||||
animation-delay: 0.4s;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 18px;
|
||||
color: #ffffff;
|
||||
line-height: 25px;
|
||||
font-family: alibaba;
|
||||
}
|
||||
|
||||
.btn-arrow {
|
||||
width: 14px;
|
||||
height: 6px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
// transform: translateY(-2px);
|
||||
// box-shadow: 0 4px 20px rgba(91, 71, 242, 0.4), 0 0 12px rgba(255, 255, 255, 0.39), inset 0 -3px 12px rgba(255, 255, 255, 0.45);
|
||||
|
||||
.btn-arrow {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 内容容器
|
||||
.feedback-container {
|
||||
width: 1600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
// 热门反馈区域
|
||||
.feedback-hot-section {
|
||||
margin-top: -30px;
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: all $animation-duration ease;
|
||||
|
||||
&.animate-active {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
|
||||
.feedback-card {
|
||||
animation: fadeInUp 0.6s ease forwards;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 反馈卡片网格 - 3列
|
||||
.feedback-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
// 单个反馈卡片 - 热门
|
||||
.feedback-card {
|
||||
position: relative;
|
||||
width: 513px;
|
||||
height: 210px;
|
||||
background: linear-gradient(89.47deg,#ffffff 1.22%,#f7f8ff 50.79%,#ebf0ff 99.87%);
|
||||
border-radius: 16px;
|
||||
padding: 34px 28px;
|
||||
|
||||
&:nth-child(1) {
|
||||
background: url('../../images/factory/feedback/card-bg-1.webp'), linear-gradient(89.47deg,#ffffff 1.22%,#f7f8ff 50.79%,#ebf0ff 99.87%);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 17px;
|
||||
right: 0;
|
||||
width: 223px;
|
||||
height: 174px;
|
||||
background: url('../../images/factory/feedback/card-bg-2.webp') no-repeat;
|
||||
background-size: 223px 174px;
|
||||
opacity: 0.5; // 在这里调整透明度,0-1之间
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
// 确保内容在背景之上
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background: linear-gradient(89.47deg, #ffffff 1.22%, rgba(247, 249, 255, 0.5) 50.79%, #ebf0ff 99.87%);
|
||||
// background-repeat: no-repeat;
|
||||
background-position-x: 100%;
|
||||
background-position-y: 17px;
|
||||
}
|
||||
|
||||
|
||||
&:nth-child(3) {
|
||||
background: url('../../images/factory/feedback/card-bg-3.webp'), linear-gradient(89.47deg,#ffffff 1.22%,#f8fffa 50.79%,#eafeff 99.87%);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
}
|
||||
|
||||
.card-tag {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 13px;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
width: 41px;
|
||||
}
|
||||
|
||||
span {
|
||||
position: absolute;
|
||||
font-family: 'Douyin Sans', sans-serif;
|
||||
font-size: 12px;
|
||||
color: #ffffff;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
flex-shrink: 0;
|
||||
max-width: 454px;
|
||||
line-height: 30px;
|
||||
height: 30px;
|
||||
font-family: alibaba;
|
||||
color: $text-primary;
|
||||
font-size: 22px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
margin-top: 14px;
|
||||
|
||||
.author-avatar {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.author-name {
|
||||
font-family: alibabaReg;
|
||||
font-size: 15px;
|
||||
color: $text-secondary;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.author-date {
|
||||
font-family: alibabaReg;
|
||||
font-size: 15px;
|
||||
color: $text-secondary;
|
||||
line-height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: auto;
|
||||
|
||||
.detail-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 104px;
|
||||
height: 36px;
|
||||
background-color: $tag-bg;
|
||||
border-radius: 9px;
|
||||
font-family: alibabaReg;
|
||||
font-size: 14px;
|
||||
color: #ffffff;
|
||||
line-height: 28px;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
// &:hover {
|
||||
// background-color: lighten($tag-bg, 10%);
|
||||
// transform: scale(1.02);
|
||||
// }
|
||||
}
|
||||
|
||||
.card-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-family: 'Alibaba PuHuiTi', sans-serif;
|
||||
font-size: 14px;
|
||||
color: $text-secondary;
|
||||
line-height: 40px;
|
||||
img {
|
||||
width: 12px;
|
||||
}
|
||||
&:nth-child(1) {
|
||||
img {
|
||||
width: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 全部反馈区域
|
||||
.feedback-all-section {
|
||||
padding: 40px 0;
|
||||
|
||||
.feedback-list-wrapper {
|
||||
background-color: $card-bg;
|
||||
border-radius: 24px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 0 6px rgba(0, 74, 168, 0.03);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-family: alibaba;
|
||||
font-size: 22px;
|
||||
font-weight: normal;
|
||||
color: $text-primary;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
// 全部反馈 - 2列网格布局
|
||||
.feedback-grid {
|
||||
margin-top: 33px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.content-bottom-wrapper {
|
||||
&:nth-child(4n),
|
||||
&:nth-child(4n+1) {
|
||||
.feedback-grid-item {
|
||||
background-color: $card-bg-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 全部反馈卡片
|
||||
.feedback-grid-item {
|
||||
position: relative;
|
||||
width:740px;
|
||||
height:220px;
|
||||
border-radius: 24px;
|
||||
padding: 24px;
|
||||
transition: all 0.3s ease;
|
||||
background: rgba(234, 246, 255, 0.31);
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ffffff;
|
||||
border-radius: 24px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
// background-color: rgba(239, 243, 255, 1);
|
||||
// transform: translateY(-2px);
|
||||
// box-shadow: 0 4px 12px rgba(0, 74, 168, 0.08);
|
||||
}
|
||||
|
||||
.grid-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.grid-avatar {
|
||||
width: 71px;
|
||||
height: 71px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.grid-info {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.grid-author-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.grid-author-name {
|
||||
font-family: alibabareg;
|
||||
font-size: 17px;
|
||||
color: $text-secondary;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.grid-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px 8px;
|
||||
width: 30px;
|
||||
height: 20px;
|
||||
background: #4953e6;
|
||||
border-radius: 4px 8px 4px 8px;
|
||||
font-family: alibabareg;
|
||||
font-size: 12px;
|
||||
color: #ffffff;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.grid-item-date {
|
||||
font-family: alibabareg;
|
||||
font-size: 14px;
|
||||
color: $text-muted;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.grid-detail-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
width: 104px;
|
||||
height: 36px;
|
||||
border: 1px solid;
|
||||
border-color: $text-primary;
|
||||
border-radius: 9px;
|
||||
transition: all 0.3s ease;
|
||||
line-height:28px;
|
||||
font-family: alibaba;
|
||||
color:$text-primary;
|
||||
font-size:15px;
|
||||
.arrow {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: url('../../images/factory/feedback/tag-new-3.webp');
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: $primary-color;
|
||||
color: $primary-color;
|
||||
|
||||
.arrow {
|
||||
background: url('../../images/factory/feedback/tag-new-2.webp');
|
||||
transform: translateX(2px);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.grid-item-title {
|
||||
margin-top: 20px;
|
||||
max-width: 650px;
|
||||
line-height: 25px;
|
||||
font-family: alibaba;
|
||||
color: $text-primary;
|
||||
font-size: 18px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.grid-item-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
margin-top: 16px;
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-family: alibabareg;
|
||||
font-size: 14px;
|
||||
color: $text-muted;
|
||||
line-height: 1.5;
|
||||
|
||||
img {
|
||||
width: 12px;
|
||||
}
|
||||
&:nth-child(1) {
|
||||
img {
|
||||
width: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 动画定义
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式适配
|
||||
@media screen and (max-width: 1200px) {
|
||||
.feedback-cards {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.feedback-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.feedback-header {
|
||||
height: auto;
|
||||
min-height: 300px;
|
||||
padding: 40px 20px;
|
||||
|
||||
.feedback-title {
|
||||
font-size: 28px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.feedback-subtitle {
|
||||
font-size: 14px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.feedback-container {
|
||||
width: 1600px;
|
||||
// padding: 0 20px;
|
||||
}
|
||||
|
||||
.feedback-cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.feedback-all-section {
|
||||
.feedback-list-wrapper {
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.feedback-grid-item {
|
||||
.grid-item-date,
|
||||
.grid-item-title,
|
||||
.grid-item-stats {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,6 @@ import bg2 from '../images/factory/bg-2.mp4'
|
|||
import mouse from '../images/factory/mouse.png'
|
||||
import DelayedScroll from '../forge/Utils/delayScroll'
|
||||
import { getMainInfos } from './api'
|
||||
import ContactFloat from './components/ContactFloat'
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
const info = {
|
||||
|
|
@ -37,7 +36,6 @@ function Index(props) {
|
|||
const [banner2isVisible, setBanner2IsVisible] = useState(false);
|
||||
const [firstV, setFirstV] = useState(true)
|
||||
const [ bannerInfo, setBannerInfo ] = useState(info)
|
||||
const [showContactFloat, setShowContactFloat] = useState(false)
|
||||
const isAnimating = useRef(false)
|
||||
const lastScrollY = useRef(false)
|
||||
const firstScreenRef = useRef(false)
|
||||
|
|
@ -102,60 +100,26 @@ function Index(props) {
|
|||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const resource = resourceRef.current;
|
||||
const resource2 = resource2Ref.current;
|
||||
if (!resource || !resource2) return;
|
||||
const cleanup = [];
|
||||
|
||||
// 检查元素是否存在的函数
|
||||
const checkAndInit = () => {
|
||||
// 通过选择器获取 DOM 元素(因为 useImperativeHandle 使 ref 不再是 DOM 元素)
|
||||
const resourceEl = document.querySelector('.factory-resource');
|
||||
const endTriggerEl = document.querySelector('.factory-resource .panels-stack');
|
||||
|
||||
if (!resourceEl || !resourceEl.offsetParent || !endTriggerEl || !endTriggerEl.offsetParent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const pinContainer = ScrollTrigger.create({
|
||||
trigger: resourceEl,
|
||||
start: "top top",
|
||||
endTrigger: endTriggerEl,
|
||||
end: "bottom top",
|
||||
pin: resourceEl,
|
||||
pinSpacing: false,
|
||||
scrub: true
|
||||
});
|
||||
cleanup.push(() => pinContainer.kill());
|
||||
console.log('pinContainer initialized');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error creating pinContainer ScrollTrigger:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 立即尝试初始化,如果失败则重试
|
||||
let attempts = 0;
|
||||
const maxAttempts = 50;
|
||||
|
||||
const tryInit = () => {
|
||||
if (checkAndInit()) {
|
||||
// 成功
|
||||
} else if (attempts < maxAttempts) {
|
||||
attempts++;
|
||||
setTimeout(tryInit, 100);
|
||||
} else {
|
||||
console.warn('Failed to initialize pinContainer after max attempts');
|
||||
}
|
||||
};
|
||||
|
||||
// 稍微延迟启动,让 Resource 组件先开始初始化
|
||||
const timeoutId = setTimeout(tryInit, 200);
|
||||
cleanup.push(() => clearTimeout(timeoutId));
|
||||
|
||||
const pinContainer = ScrollTrigger.create({
|
||||
trigger: resource,
|
||||
start: "top top",
|
||||
endTrigger: ".factory-resource .panels-stack", // 使用Resource内部的堆叠区域
|
||||
end: "bottom top", // 当堆叠区域滚动到顶部时结束固定
|
||||
pin: resource,
|
||||
pinSpacing: false,
|
||||
scrub: true
|
||||
});
|
||||
cleanup.push(() => pinContainer.kill());
|
||||
|
||||
return () => {
|
||||
cleanup.forEach(fn => fn());
|
||||
};
|
||||
}, []);
|
||||
}, [resourceRef, resource2Ref]);
|
||||
|
||||
const setupScrollListeners = () => {
|
||||
window.addEventListener('scroll', helpScroll);
|
||||
|
|
@ -178,24 +142,6 @@ function Index(props) {
|
|||
// observer.observe(firstScreenRef.current);
|
||||
}
|
||||
|
||||
// 监听滚动,控制 ContactFloat 显示
|
||||
useEffect(() => {
|
||||
const handleContactFloatScroll = () => {
|
||||
const firstScreenHeight = window.innerHeight;
|
||||
// 滑出第一屏时显示 ContactFloat
|
||||
if (window.scrollY > firstScreenHeight - 100) {
|
||||
setShowContactFloat(true);
|
||||
} else {
|
||||
setShowContactFloat(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleContactFloatScroll);
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleContactFloatScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const helpScroll = () => {
|
||||
if (!ticking.current) {
|
||||
requestAnimationFrame(() => {
|
||||
|
|
@ -252,7 +198,6 @@ function Index(props) {
|
|||
|
||||
return (
|
||||
<div className="factory">
|
||||
<ContactFloat visible={showContactFloat} />
|
||||
<div className="factory-banner fullscreen-element">
|
||||
<div className="video-wpapper" ref={firstScreenRef}>
|
||||
<video className={`factory-banner-video ${firstV ? 'video-show' : ''}`} src={bg1} autoPlay muted onEnded={() => { setFirstV(false) }}></video>
|
||||
|
|
@ -263,7 +208,7 @@ function Index(props) {
|
|||
<div className="p-wrapper delayed-element"><p>{bannerInfo.desc}</p></div>
|
||||
|
||||
<div className="b-wrapper delayed-element">
|
||||
<a onClick={() => { window.open(bannerInfo.link) }}>
|
||||
<a onClick={() => { window.location.href = bannerInfo.link }}>
|
||||
<div className="banner-content-button">立即体验</div>
|
||||
</a>
|
||||
<Link to={{ pathname: `${bannerInfo.forums}`, state: { name: bannerInfo.forumsName } }}>
|
||||
|
|
@ -298,7 +243,7 @@ function Index(props) {
|
|||
|
||||
<News zoneId={factoryZoneId} />
|
||||
<Feature />
|
||||
<Resource zoneId={factoryZoneId} history={ props.history } ref={resourceRef} />
|
||||
<Resource zoneId={factoryZoneId} ref={resourceRef} />
|
||||
<Resource2 zoneId={factoryZoneId} ref={resource2Ref} />
|
||||
{/* <Scene zoneId={factoryZoneId} /> */}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import imageBack1 from '../../../images/factory/resource/tools-detail-head-left.
|
|||
import imageGif from '../../../images/factory/resource/tools-detail-head-left2.webp'
|
||||
import { Link } from 'react-router-dom';
|
||||
import RenderHtml from '../../../components/render-html';
|
||||
import { Breadcrumb, Tooltip } from 'antd';
|
||||
import { Breadcrumb } from 'antd';
|
||||
import { getSourceDetail, httpUrl, getSourceList } from '../../api';
|
||||
import { downloadFunc } from '../../../forge/Utils/utils'
|
||||
import { docId, standardId } from '../../../constants/factory'
|
||||
|
|
@ -49,7 +49,7 @@ function Index(props) {
|
|||
|
||||
|
||||
return (
|
||||
<div className="lessons-detail">
|
||||
<div className="news-detail">
|
||||
|
||||
<div className="news-detail-header">
|
||||
<Breadcrumb separator=">">
|
||||
|
|
@ -58,12 +58,8 @@ function Index(props) {
|
|||
<Breadcrumb.Item> <span className="breacrumb-name" title={detail.name}>{detail.name}</span> </Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
<div className="header-content">
|
||||
<Tooltip title={ detail.name } placement="topLeft">
|
||||
<h3>{detail.name}</h3>
|
||||
</Tooltip>
|
||||
<Tooltip title={ detail.summary } placement="topLeft">
|
||||
<p>{detail.summary}</p>
|
||||
</Tooltip>
|
||||
<h3 title={detail.name}>{detail.name}</h3>
|
||||
<p title={detail.summary}>{detail.summary}</p>
|
||||
{
|
||||
(detail.keywords === docId || detail.keywords===standardId) &&
|
||||
<button onClick={() => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.lessons-detail {
|
||||
.news-detail {
|
||||
min-height: calc(100vh - 58px);
|
||||
|
||||
.news-detail-header {
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
color: #091221;
|
||||
|
||||
.breacrumb-name {
|
||||
max-width: 300px;
|
||||
max-width: 200px;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ function Index(props) {
|
|||
</Breadcrumb>
|
||||
<div className="news-detail-content-wrapper">
|
||||
<h4 className="news-detail-content-title">{detail.name}</h4>
|
||||
<p className="news-detail-content-time">更新于: {detail.publishTime}</p>
|
||||
{
|
||||
content && <RenderHtml className="informations_detail imageLayerParent" value={content} url={props.history.location} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,10 +105,6 @@
|
|||
color: #091221;
|
||||
font-size: 24px;
|
||||
}
|
||||
.news-detail-content-time {
|
||||
margin-top: 10px;
|
||||
color: #445a7a;
|
||||
}
|
||||
.informations_detail {
|
||||
margin-top: 23px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import imageGif from '../../../../images/factory/resource/tools-detail-head-left
|
|||
import headIcon from '../../../../images/factory/resource/tools-detail-head-icon.webp'
|
||||
import { Link } from 'react-router-dom';
|
||||
import RenderHtml from '../../../../components/render-html';
|
||||
import { Breadcrumb, Modal, Tooltip } from 'antd';
|
||||
import { Breadcrumb } from 'antd';
|
||||
import { getSourceDetail, httpUrl, getSourceList } from '../../../api';
|
||||
import { downloadFunc } from '../../../../forge/Utils/utils'
|
||||
import { factoryZoneId, } from '../../../../constants/factory'
|
||||
|
|
@ -61,7 +61,7 @@ function Index(props) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="tool-detail">
|
||||
<div className="news-detail">
|
||||
|
||||
<div className="news-detail-header">
|
||||
<Breadcrumb separator=">">
|
||||
|
|
@ -70,12 +70,8 @@ function Index(props) {
|
|||
<Breadcrumb.Item> <span className="breacrumb-name">{detail.name}</span> </Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
<div className="header-content">
|
||||
<Tooltip title={ detail.name } placement="topLeft">
|
||||
<h3>{detail.name}</h3>
|
||||
</Tooltip>
|
||||
<Tooltip title={ detail.summary } placement="topLeft">
|
||||
<p>{detail.summary}</p>
|
||||
</Tooltip>
|
||||
<h3>{detail.name}</h3>
|
||||
<p title={detail.summary}>{detail.summary}</p>
|
||||
<div className="tools-info">
|
||||
<div>
|
||||
<span>{detail.downloadCount}</span>
|
||||
|
|
@ -92,14 +88,7 @@ function Index(props) {
|
|||
</div>
|
||||
<button onClick={() => {
|
||||
if (detail.fileIds) {
|
||||
if (detail.isOpen != 1) {
|
||||
downloadFunc(`${httpUrl}/file/open/download/${detail.fileIds}`)
|
||||
} else {
|
||||
Modal.info({
|
||||
title: '下载工具',
|
||||
content: `下载请与软件工厂社区管理员联系`
|
||||
})
|
||||
}
|
||||
downloadFunc(`${httpUrl}/file/open/download/${detail.fileIds}`)
|
||||
}
|
||||
}} disabled={!detail.fileIds} className={`shiny-button ${!detail.fileIds && 'disabled'}`} >{detail.fileIds ? '立即下载' : '稍后下载'}</button>
|
||||
<h6 className="more-title">更多热门工具</h6>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.tool-detail {
|
||||
.news-detail {
|
||||
min-height: calc(100vh - 58px);
|
||||
|
||||
.news-detail-header {
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
color: #091221;
|
||||
|
||||
.breacrumb-name {
|
||||
max-width: 300px;
|
||||
max-width: 200px;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import imageGif from '../../../../images/factory/resource/tools-detail-head-left
|
|||
import headIcon from '../../../../images/factory/resource/tools-detail-head-icon.webp'
|
||||
import { Link } from 'react-router-dom';
|
||||
import RenderHtml from '../../../../components/render-html';
|
||||
import { Breadcrumb, Tooltip } from 'antd';
|
||||
import { Breadcrumb } from 'antd';
|
||||
import { getSourceDetail, httpUrl, getSourceList } from '../../../api';
|
||||
import { downloadFunc } from '../../../../forge/Utils/utils'
|
||||
import { factoryZoneId, } from '../../../../constants/factory'
|
||||
|
|
@ -70,12 +70,8 @@ function Index(props) {
|
|||
<Breadcrumb.Item> <span className="breacrumb-name">{detail.name}</span> </Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
<div className="header-content">
|
||||
<Tooltip title={ detail.name } placement="topLeft">
|
||||
<h3>{detail.name}</h3>
|
||||
</Tooltip>
|
||||
<Tooltip title={ detail.summary } placement="topLeft">
|
||||
<p>{detail.summary}</p>
|
||||
</Tooltip>
|
||||
<h3>{detail.name}</h3>
|
||||
<p title={detail.summary}>{detail.summary}</p>
|
||||
<div className="tools-info">
|
||||
<div>
|
||||
<span>{detail.downloadCount}</span>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
color: #091221;
|
||||
|
||||
.breacrumb-name {
|
||||
max-width: 300px;
|
||||
max-width: 200px;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
|
|
|||
|
|
@ -1,347 +0,0 @@
|
|||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import './index.scss';
|
||||
import { TPMIndexHOC } from '../../../../modules/tpm/TPMIndexHOC';
|
||||
import fileDownload from '../../../../images/factory/resource/version-file-download.webp'
|
||||
import fileDownloadHover from '../../../../images/factory/resource/version-file-download-hover.webp'
|
||||
import icon1 from '../../../../images/factory/resource/version-file-icon-1.webp'
|
||||
import icon2 from '../../../../images/factory/resource/version-file-icon-2.webp'
|
||||
import icon3 from '../../../../images/factory/resource/version-file-icon-3.webp'
|
||||
import icon4 from '../../../../images/factory/resource/version-file-icon-4.webp'
|
||||
import icon5 from '../../../../images/factory/resource/version-file-icon-5.webp'
|
||||
import icon6 from '../../../../images/factory/resource/version-file-icon-6.webp'
|
||||
import buttonLeft from '../../../../images/factory/resource/version-list-left.webp'
|
||||
import buttonRight from '../../../../images/factory/resource/version-list-right.webp'
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
import RenderHtml from '../../../../components/render-html';
|
||||
import { Breadcrumb, Button, Tooltip, Modal, Table, Pagination } from 'antd';
|
||||
import { getSourceDetail, httpUrl, getSourceList } from '../../../api';
|
||||
import { downloadFunc } from '../../../../forge/Utils/utils'
|
||||
import { factoryZoneId, versionId } from '../../../../constants/factory'
|
||||
import '../../../common.scss'
|
||||
import { Base64 } from 'js-base64';
|
||||
|
||||
|
||||
function Index(props) {
|
||||
const id = props.match.params.id
|
||||
const [detail, setDetail] = useState({})
|
||||
const [content, setContent] = useState();
|
||||
const [totalFileSize, setTotalFileSize] = useState(0);
|
||||
const [hoveredFileId, setHoveredFileId] = useState(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const itemsPerPage = 6;
|
||||
// 下载用户 Modal 相关状态
|
||||
const [isDownloadUserModalVisible, setIsDownloadUserModalVisible] = useState(false);
|
||||
const [downloadUserPage, setDownloadUserPage] = useState(1);
|
||||
const downloadUsersPerPage = 10;
|
||||
const {current_user} = props;
|
||||
const {admin} = current_user || {}
|
||||
const {downloadUserSummary = []} = detail || {}
|
||||
|
||||
useEffect(() => {
|
||||
document.title = '工具详情-软件工厂专区';
|
||||
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (id !== detail.id) {
|
||||
getDetail()
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
// 计算文件总大小
|
||||
const calculateTotalFileSize = (fileList) => {
|
||||
if (!fileList || fileList.length === 0) return 0;
|
||||
let totalBytes = 0;
|
||||
fileList.forEach(file => {
|
||||
const sizeInfo = file.fileSizeInfo || '';
|
||||
// 解析文件大小字符串,如 "2.31 kB", "181.72 kB"
|
||||
const match = sizeInfo.match(/([\d.]+)\s*(kB|MB|GB|B)/i);
|
||||
if (match) {
|
||||
const value = parseFloat(match[1]);
|
||||
const unit = match[2].toUpperCase();
|
||||
switch (unit) {
|
||||
case 'B':
|
||||
totalBytes += value;
|
||||
break;
|
||||
case 'KB':
|
||||
totalBytes += value * 1024;
|
||||
break;
|
||||
case 'MB':
|
||||
totalBytes += value * 1024 * 1024;
|
||||
break;
|
||||
case 'GB':
|
||||
totalBytes += value * 1024 * 1024 * 1024;
|
||||
break;
|
||||
default:
|
||||
totalBytes += value;
|
||||
}
|
||||
}
|
||||
});
|
||||
// 转换为合适的单位显示
|
||||
if (totalBytes >= 1024 * 1024 * 1024) {
|
||||
return (totalBytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
|
||||
} else if (totalBytes >= 1024 * 1024) {
|
||||
return (totalBytes / (1024 * 1024)).toFixed(2) + ' MB';
|
||||
} else if (totalBytes >= 1024) {
|
||||
return (totalBytes / 1024).toFixed(2) + ' kB';
|
||||
} else {
|
||||
return totalBytes + ' B';
|
||||
}
|
||||
};
|
||||
|
||||
const getDetail = () => {
|
||||
getSourceDetail(id).then(response => {
|
||||
if (response) {
|
||||
let extendData = {}
|
||||
if (response.data.data.extendData) {
|
||||
extendData = JSON.parse(response.data.data.extendData)
|
||||
}
|
||||
document.title = response.data.data.name
|
||||
if (extendData.content) {
|
||||
const base64content = Base64.decode(extendData.content)
|
||||
setContent(base64content !== '<p><br></p>' ? base64content : '')
|
||||
}
|
||||
const detailData = { ...response.data.data, ...extendData, name: response.data.data.name.replace("历史-", "") };
|
||||
setDetail(detailData);
|
||||
// 计算文件总大小
|
||||
if (detailData.fileList && detailData.fileList.length > 0) {
|
||||
setTotalFileSize(calculateTotalFileSize(detailData.fileList));
|
||||
}
|
||||
// 重置分页
|
||||
setCurrentPage(1);
|
||||
}
|
||||
}).catch(error => { })
|
||||
}
|
||||
|
||||
const iconList = [icon1, icon2, icon3, icon4, icon5, icon6]
|
||||
|
||||
// 计算当前页的文件列表
|
||||
const getCurrentPageFiles = () => {
|
||||
if (!detail.fileList || detail.fileList.length <= itemsPerPage) {
|
||||
return detail.fileList || [];
|
||||
}
|
||||
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
return detail.fileList.slice(startIndex, endIndex);
|
||||
}
|
||||
|
||||
// 总页数
|
||||
const totalPages = detail.fileList ? Math.ceil(detail.fileList.length / itemsPerPage) : 1;
|
||||
|
||||
// 上一页
|
||||
const goToPreviousPage = () => {
|
||||
if (currentPage > 1) {
|
||||
setCurrentPage(currentPage - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 下一页
|
||||
const goToNextPage = () => {
|
||||
if (currentPage < totalPages) {
|
||||
setCurrentPage(currentPage + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前页的下载用户数据(前端分页)
|
||||
const getCurrentPageDownloadUsers = () => {
|
||||
if (!downloadUserSummary || downloadUserSummary.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const startIndex = (downloadUserPage - 1) * downloadUsersPerPage;
|
||||
const endIndex = startIndex + downloadUsersPerPage;
|
||||
return downloadUserSummary.slice(startIndex, endIndex);
|
||||
};
|
||||
|
||||
// 表格列定义
|
||||
const downloadUserColumns = [
|
||||
{
|
||||
title: '用户昵称',
|
||||
dataIndex: 'nickName',
|
||||
key: 'nickName',
|
||||
align: 'center',
|
||||
render: (text, record) => text || record.userName || '-'
|
||||
},
|
||||
{
|
||||
title: '手机号',
|
||||
dataIndex: 'phonenumber',
|
||||
key: 'phonenumber',
|
||||
align: 'center',
|
||||
render: (text) => text || '-'
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
dataIndex: 'email',
|
||||
key: 'email',
|
||||
align: 'center',
|
||||
render: (text) => text || '-'
|
||||
},
|
||||
{
|
||||
title: 'IP 地址',
|
||||
dataIndex: 'ipAddr',
|
||||
key: 'ipAddr',
|
||||
align: 'center',
|
||||
render: (text) => text || '-'
|
||||
},
|
||||
{
|
||||
title: '下载文件',
|
||||
dataIndex: 'fileOriginName',
|
||||
align: 'center',
|
||||
key: 'fileOriginName'
|
||||
},
|
||||
{
|
||||
title: '下载时间',
|
||||
dataIndex: 'downloadTime',
|
||||
align: 'center',
|
||||
key: 'downloadTime'
|
||||
}
|
||||
];
|
||||
|
||||
// 打开下载用户 Modal
|
||||
const handleOpenDownloadUserModal = () => {
|
||||
setDownloadUserPage(1);
|
||||
setIsDownloadUserModalVisible(true);
|
||||
};
|
||||
|
||||
// 关闭下载用户 Modal
|
||||
const handleCloseDownloadUserModal = () => {
|
||||
setIsDownloadUserModalVisible(false);
|
||||
};
|
||||
|
||||
// 处理分页变化
|
||||
const handlePaginationChange = (page) => {
|
||||
setDownloadUserPage(page);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="release-detail">
|
||||
|
||||
<div className="news-detail-header">
|
||||
<Breadcrumb separator=">">
|
||||
<Breadcrumb.Item href={`/sf`}>首页</Breadcrumb.Item>
|
||||
<Breadcrumb.Item href={`/sf/resources/releases`}>工厂版本</Breadcrumb.Item>
|
||||
<Breadcrumb.Item> <span className="breacrumb-name">{detail.name}</span> </Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
<div className="header-content">
|
||||
<div className="header-info">
|
||||
<Tooltip title={ detail.name } placement="topLeft">
|
||||
<h3>{detail.name}</h3>
|
||||
</Tooltip>
|
||||
<Tooltip title={ detail.summary } placement="topLeft">
|
||||
<p>{detail.summary}</p>
|
||||
</Tooltip>
|
||||
<div className="tools-info">
|
||||
<div>
|
||||
<span>{detail.downloadCount}</span>
|
||||
<span>下载量</span>
|
||||
{/* {admin && <Button className='downloadUser' onClick={handleOpenDownloadUserModal}>查看下载用户</Button>} */}
|
||||
</div>
|
||||
<div>
|
||||
<span>{totalFileSize || (detail.fileList && detail.fileList[0] ? detail.fileList[0].fileSizeInfo : 0)}</span>
|
||||
<span>文件大小</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{detail.fileList ? detail.fileList.length : 0}</span>
|
||||
<span>文件数量</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{detail.domainName}</span>
|
||||
<span>工具类型</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 文件列表展示 */}
|
||||
{detail.fileList && detail.fileList.length > 0 && (
|
||||
<div className="file-list">
|
||||
{/* 左侧翻页按钮 - 始终显示,但在不需要时作为占位符 */}
|
||||
<div
|
||||
className={`pagination-button left ${currentPage > 1 ? 'clickable' : 'placeholder'}`}
|
||||
onClick={currentPage > 1 ? goToPreviousPage : undefined}
|
||||
>
|
||||
<img src={buttonLeft} alt="上一页" />
|
||||
</div>
|
||||
|
||||
<div className="file-list-content">
|
||||
{getCurrentPageFiles().map((file, index) => (
|
||||
<div
|
||||
key={file.fileId || index}
|
||||
className="file-item"
|
||||
onMouseEnter={() => setHoveredFileId(file.fileId || index)}
|
||||
onMouseLeave={() => setHoveredFileId(null)}
|
||||
>
|
||||
<div className="file-icon">
|
||||
<img src={ iconList[ index % 6 ] }></img>
|
||||
</div>
|
||||
<div className="file-info">
|
||||
<span className="file-name">{file.fileOriginName}</span>
|
||||
<span className="file-size">{file.fileSizeInfo}</span>
|
||||
</div>
|
||||
<button
|
||||
className="file-download-btn"
|
||||
onClick={() => {
|
||||
if (file.fileId) {
|
||||
downloadFunc(`${httpUrl}/file/open/download/${file.fileId}`)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<img src={ (hoveredFileId === (file.fileId || index)) ? fileDownloadHover : fileDownload } alt="" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 右侧翻页按钮 - 始终显示,但在不需要时作为占位符 */}
|
||||
<div
|
||||
className={`pagination-button right ${currentPage < totalPages ? 'clickable' : 'placeholder'}`}
|
||||
onClick={currentPage < totalPages ? goToNextPage : undefined}
|
||||
>
|
||||
<img src={buttonRight} alt="下一页" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="news-detail-content">
|
||||
{
|
||||
content &&
|
||||
<div className="news-detail-content-wrapper">
|
||||
<RenderHtml className="informations_detail imageLayerParent" value={content} url={props.history.location} />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* 下载用户 Modal */}
|
||||
<Modal
|
||||
title="下载用户列表"
|
||||
visible={isDownloadUserModalVisible}
|
||||
onCancel={handleCloseDownloadUserModal}
|
||||
footer={null}
|
||||
width={1000}
|
||||
className="download-user-modal"
|
||||
>
|
||||
<Table
|
||||
columns={downloadUserColumns}
|
||||
dataSource={getCurrentPageDownloadUsers()}
|
||||
pagination={false}
|
||||
size="middle"
|
||||
scroll={{ y: 400 }}
|
||||
/>
|
||||
|
||||
{/* 分页控件 */}
|
||||
<div className="download-user-pagination edu-txt-right mt20">
|
||||
<Pagination
|
||||
current={downloadUserPage}
|
||||
total={downloadUserSummary.length}
|
||||
pageSize={downloadUsersPerPage}
|
||||
onChange={handlePaginationChange}
|
||||
showSizeChanger={false}
|
||||
hideOnSinglePage
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default TPMIndexHOC(Index)
|
||||