From fff42ae685d9fa526cfec58ecb80adbaaeb13f1f Mon Sep 17 00:00:00 2001 From: chenhao <2917815974@qq.com> Date: Fri, 8 May 2026 14:08:51 +0800 Subject: [PATCH 01/11] =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +++- package.json | 4 ++-- .../WorkBench/components/Banner/Common/bannerDefaultConfig.ts | 4 ++-- src/ts/core/public/consts.ts | 1 + 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 30b5dd605..9456e061f 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,6 @@ test pnpm-lock.yaml -public/domain.json \ No newline at end of file +public/domain.json + +dist.tar \ No newline at end of file diff --git a/package.json b/package.json index 68c5dade9..1f174c244 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "license": "MIT", "scripts": { "dev": "vite --mode development", - "build": "node --max_old_space_size=20480 ./node_modules/.bin/vite build", + "build": "node --max_old_space_size=20480 ./node_modules/vite/bin/vite.js build", "build:beta": "vite build --mode beta", "build:release": "vite build --mode release", "build:legacy": "vite build --mode legacy ", @@ -162,4 +162,4 @@ "vite-plugin-style-import": "^2.0.0" }, "type": "commonjs" -} \ No newline at end of file +} diff --git a/src/components/DataPreview/session/WorkBench/components/Banner/Common/bannerDefaultConfig.ts b/src/components/DataPreview/session/WorkBench/components/Banner/Common/bannerDefaultConfig.ts index 450b3a4e8..0f4511934 100644 --- a/src/components/DataPreview/session/WorkBench/components/Banner/Common/bannerDefaultConfig.ts +++ b/src/components/DataPreview/session/WorkBench/components/Banner/Common/bannerDefaultConfig.ts @@ -1,8 +1,8 @@ import orgCtrl from '@/ts/controller'; export const LoadBanner = (type: string) => { - if (type in orgCtrl.domain.theme.banner) { + if (orgCtrl.domain?.theme?.banner && type in orgCtrl.domain.theme.banner) { return orgCtrl.domain.theme.banner[type]; } return []; -}; +}; \ No newline at end of file diff --git a/src/ts/core/public/consts.ts b/src/ts/core/public/consts.ts index 92ef5afdd..bde7a1370 100644 --- a/src/ts/core/public/consts.ts +++ b/src/ts/core/public/consts.ts @@ -294,6 +294,7 @@ export const DefaultPlatformSetting = (id: string): XPlatformSetting => { barBgColor: 'linear-gradient(45deg, #358bff, #506cfa)', posters: ['/img/orginone/passport/page1.png'], background: '/img/orginone/passport/background.png', + banner: {}, }, toolBars: [], enabled: false, -- Gitee From 05c34fcd708f7e2b65fbbafc1f835581953667bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=AE=97=E9=98=B3?= <1627320840@qq.com> Date: Fri, 8 May 2026 14:31:31 +0800 Subject: [PATCH 02/11] =?UTF-8?q?refactor:=E6=8A=BD=E8=B1=A1=E6=8E=A5?= =?UTF-8?q?=E6=94=B6=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C=E7=AE=80?= =?UTF-8?q?=E5=8C=96=E5=90=84=E6=8E=A5=E6=94=B6=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/homePage/index.module.less | 264 ++++++++++++++++++ .../plaza/components/homePage/index.tsx | 92 ++++++ src/components/DataPreview/plaza/editForm.tsx | 4 +- src/components/DataPreview/plaza/index.tsx | 11 + .../session/info/accoutSetting.tsx | 4 +- src/ts/core/thing/directory.ts | 3 +- 6 files changed, 374 insertions(+), 4 deletions(-) create mode 100644 src/components/DataPreview/plaza/components/homePage/index.module.less create mode 100644 src/components/DataPreview/plaza/components/homePage/index.tsx diff --git a/src/components/DataPreview/plaza/components/homePage/index.module.less b/src/components/DataPreview/plaza/components/homePage/index.module.less new file mode 100644 index 000000000..24681013c --- /dev/null +++ b/src/components/DataPreview/plaza/components/homePage/index.module.less @@ -0,0 +1,264 @@ +:root { + --logo-size: 180px; + --logo-ring-gap: 20px; +} + +@media (max-width: 768px) { + :root { + --logo-size: 120px; + --logo-ring-gap: 15px; + } +} + +.container { + position: relative; + width: 100%; + height: 100%; + background: #ffffff; + overflow: hidden; + font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif; +} + +.sector { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + cursor: pointer; + transition: background-color 0.5s ease; + text-decoration: none; + color: #333; + overflow: visible; +} + +.sectorTopLeft { + background: linear-gradient(160deg, #f0f4f8 0%, #e2e8f0 100%); + clip-path: polygon(0% 0%, 50% 0%, 50% 50%, 0% 100%); + + &:hover { + background: linear-gradient(160deg, #e2e8f0 0%, #d1d8e0 100%); + + .title { + color: #1a252f; + transform: scale(1.05); + } + + .desc { + color: #3a4c5d; + } + } +} + +.sectorTopRight { + background: linear-gradient(200deg, #f8f0f0 0%, #f0e2e2 100%); + clip-path: polygon(50% 0%, 100% 0%, 100% 100%, 50% 50%); + + &:hover { + background: linear-gradient(200deg, #f0e2e2 0%, #e8d1d1 100%); + + .title { + color: #1a252f; + transform: scale(1.05); + } + + .desc { + color: #3a4c5d; + } + } +} + +.sectorBottom { + background: linear-gradient(180deg, #f0f8f0 0%, #e2f0e2 100%); + clip-path: polygon(0% 100%, 100% 100%, 50% 50%); + + &:hover { + background: linear-gradient(180deg, #e2f0e2 0%, #d1e8d1 100%); + + .title { + color: #1a252f; + transform: scale(1.05); + } + + .desc { + color: #3a4c5d; + } + } +} + +.content { + position: absolute; + z-index: 10; + text-align: center; + pointer-events: none; + transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1); +} + +.contentTopLeft { + left: 22%; + top: 28%; + transform: translate(-50%, -50%); +} + +.contentTopRight { + left: 78%; + top: 28%; + transform: translate(-50%, -50%); +} + +.contentBottom { + left: 50%; + top: 85%; + transform: translate(-50%, -50%); +} + +.title { + font-size: 2.2rem; + font-weight: 700; + color: #2c3e50; + margin-bottom: 16px; + letter-spacing: 4px; + text-transform: uppercase; + transition: all 0.3s ease; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.desc { + font-size: 1rem; + color: #5a6c7d; + line-height: 1.8; + margin-bottom: 28px; + max-width: 280px; + transition: all 0.3s ease; +} + +.btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 14px 32px; + background: rgba(255, 255, 255, 0.95); + border: 2px solid rgba(44, 62, 80, 0.15); + border-radius: 50px; + color: #2c3e50; + font-size: 0.95rem; + font-weight: 600; + letter-spacing: 2px; + transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06); + pointer-events: auto; + cursor: pointer; + + &:hover { + background: #2c3e50; + color: #fff; + border-color: #2c3e50; + transform: scale(1.06); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); + } +} + +.arrow { + transition: transform 0.25s ease; + + .btn:hover & { + transform: translateX(4px); + } +} + +.centerMask { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: calc(var(--logo-size) + 40px); + height: calc(var(--logo-size) + 40px); + background: #ffffff; + border-radius: 50%; + z-index: 50; + pointer-events: none; +} + +.logoRing { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: calc(var(--logo-size) + var(--logo-ring-gap)); + height: calc(var(--logo-size) + var(--logo-ring-gap)); + border: 2px solid rgba(183, 28, 28, 0.12); + border-radius: 50%; + z-index: 99; + pointer-events: none; + animation: spin 30s linear infinite; + + @keyframes spin { + from { + transform: translate(-50%, -50%) rotate(0deg); + } + to { + transform: translate(-50%, -50%) rotate(360deg); + } + } +} + +.logoBox { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: var(--logo-size); + height: var(--logo-size); + z-index: 100; + pointer-events: none; + + img { + width: 100%; + height: 100%; + object-fit: contain; + filter: drop-shadow(0 4px 16px rgba(0, 0, 0, 0.1)); + animation: breathe 4s ease-in-out infinite; + + @keyframes breathe { + 0%, + 100% { + transform: scale(1); + } + 50% { + transform: scale(1.03); + } + } + } +} + +@media (max-width: 768px) { + .title { + font-size: 1.5rem; + letter-spacing: 2px; + } + + .desc { + font-size: 0.85rem; + line-height: 1.6; + max-width: 200px; + } + + .btn { + padding: 10px 24px; + font-size: 0.85rem; + } + + .contentTopLeft { + left: 25%; + top: 22%; + } + + .contentTopRight { + left: 75%; + top: 22%; + } + + .contentBottom { + top: 78%; + } +} diff --git a/src/components/DataPreview/plaza/components/homePage/index.tsx b/src/components/DataPreview/plaza/components/homePage/index.tsx new file mode 100644 index 000000000..2d9333e83 --- /dev/null +++ b/src/components/DataPreview/plaza/components/homePage/index.tsx @@ -0,0 +1,92 @@ +import React, { useState } from 'react'; +import { IPlaza } from '@/ts/core'; +import EntityIcon from '@/components/Common/GlobalComps/entityIcon'; +import styles from './index.module.less'; + +interface Props { + plaza: IPlaza; + onSwitchResource: (id: string) => void; +} + +const HomePage: React.FC = ({ plaza, onSwitchResource }) => { + const [hovered, setHovered] = useState(null); + const resources = plaza.metadata.resources || []; + + const handleNavigate = (title: string) => { + const target = resources.find((r) => r.name === title); + if (target) { + onSwitchResource(target.id); + } + }; + + const sectors = [ + { + key: 'topLeft', + title: '交流厅', + desc: '中央财经大学公共资源配置促进中心、中关村公共资源优化配置促进会和新时代公共资源市场化配置促进中心、公共资源配置中关村论坛的联合网站', + btnText: '了解更多', + sectorCls: styles.sectorTopLeft, + contentCls: styles.contentTopLeft, + }, + { + key: 'topRight', + title: '共享厅', + desc: '资源要素共享平台', + btnText: '前往探索', + sectorCls: styles.sectorTopRight, + contentCls: styles.contentTopRight, + }, + { + key: 'bottom', + title: '会客厅', + desc: '组织和个人注册', + btnText: '前往注册', + sectorCls: styles.sectorBottom, + contentCls: styles.contentBottom, + }, + ]; + + return ( +
+ {sectors.map((s) => ( +
setHovered(s.key)} + onMouseLeave={() => setHovered(null)}> +
+

{s.title}

+

{s.desc}

+
handleNavigate(s.title)}> + {s.btnText} + + + +
+
+
+ ))} +
+
+
+ +
+
+ ); +}; + +export default HomePage; diff --git a/src/components/DataPreview/plaza/editForm.tsx b/src/components/DataPreview/plaza/editForm.tsx index c24142299..158821048 100644 --- a/src/components/DataPreview/plaza/editForm.tsx +++ b/src/components/DataPreview/plaza/editForm.tsx @@ -25,7 +25,7 @@ interface Iprops { const EditResourceForm = (props: Iprops) => { let title = ''; const [form, setForm] = useState(); - const types = ['群组', '数据', '视频', '公告', '市场']; + const types = ['首页', '群组', '数据', '视频', '公告', '市场']; const targetTypes = [ TargetType.Group, TargetType.Cohort, @@ -97,6 +97,8 @@ const EditResourceForm = (props: Iprops) => { ]; const allowTypeNames = []; switch (selectType) { + case '首页': + break; case '群组': allowTypeNames.push(...targetTypes); break; diff --git a/src/components/DataPreview/plaza/index.tsx b/src/components/DataPreview/plaza/index.tsx index ce4b5643f..3f0ffc75d 100644 --- a/src/components/DataPreview/plaza/index.tsx +++ b/src/components/DataPreview/plaza/index.tsx @@ -11,6 +11,7 @@ import EntityIcon from '@/components/Common/GlobalComps/entityIcon'; import OrgIcons from '@/components/Common/GlobalComps/orgIcons'; import EditResourceForm from './editForm'; import MarketPage from './components/market'; +import HomePage from './components/homePage'; const Plaza: React.FC<{ plaza: IPlaza }> = ({ plaza }) => { const [action, setAction] = useState(plaza.action); @@ -93,6 +94,16 @@ const Plaza: React.FC<{ plaza: IPlaza }> = ({ plaza }) => { return ; case '市场': return ; + case '首页': + return ( + { + plaza.switchInstance(id); + setAction(plaza.action); + }} + /> + ); } } return <>; diff --git a/src/components/DataPreview/session/info/accoutSetting.tsx b/src/components/DataPreview/session/info/accoutSetting.tsx index 786b05493..6bb0a2fa8 100644 --- a/src/components/DataPreview/session/info/accoutSetting.tsx +++ b/src/components/DataPreview/session/info/accoutSetting.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useMemo } from 'react'; import orgCtrl from '@/ts/controller'; import { Card, List, Modal, Space, Tag, message } from 'antd'; -import { RightOutlined, CheckCircleOutlined, PlusOutlined, UnlinkOutlined, EditOutlined } from '@ant-design/icons'; +import { RightOutlined, CheckCircleOutlined, PlusOutlined, DisconnectOutlined, EditOutlined } from '@ant-design/icons'; import QrCode from 'qrcode.react'; import { PlatformDisplayNames } from '@/ts/core/public/consts'; @@ -244,7 +244,7 @@ const AccoutSetting: React.FC<{}> = () => { key="unbind" onClick={() => handleBind(item)} style={{ color: '#ff4d4f' }}> - 解除 + 解除 ) : ( diff --git a/src/ts/core/thing/directory.ts b/src/ts/core/thing/directory.ts index c7de81d92..b2d03f72f 100644 --- a/src/ts/core/thing/directory.ts +++ b/src/ts/core/thing/directory.ts @@ -234,7 +234,8 @@ export class Directory extends Container implements IDirector await this.loadDirectoryResource(reload); } await this.loadFiles(reload); - if (this.plazas.length < 1 && this.parent === undefined) { + if ((this.plazas.length < 1 || reload) && this.parent === undefined) { + this.plazas = []; if (this.target.typeName === TargetType.Group) { this.plazas = await GenGroupPlazas(this.target as IGroup); } else { -- Gitee From 71b85d8c0f72a73e0d25f2de15034804b8e8a4b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=AE=97=E9=98=B3?= <1627320840@qq.com> Date: Fri, 8 May 2026 15:48:10 +0800 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=95=86?= =?UTF-8?q?=E5=9F=8E=E6=A8=A1=E6=9D=BFlogo=E5=9B=BE=E7=89=87=E5=B9=B6?= =?UTF-8?q?=E6=9B=BF=E6=8D=A2=E5=AE=9E=E4=BD=93=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/img/mallTemplate/logo.png | Bin 0 -> 9359 bytes .../plaza/components/homePage/index.tsx | 3 +-- 2 files changed, 1 insertion(+), 2 deletions(-) create mode 100644 public/img/mallTemplate/logo.png diff --git a/public/img/mallTemplate/logo.png b/public/img/mallTemplate/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c2dd1817b2a06fddf81d8cd6f9f01c61e009121b GIT binary patch literal 9359 zcmV;AByih_P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGf5&!@T5&_cPe*6Fc03mcmSad^jWnpw_ zZ*Cw|X>DZyGB7bWIxsmpFf$-BFgi0ZIx#aMNhQeu03-KFL_t(|UhQ28cpSx*uIio{ z`I4qbnvuvt3ED#P!HU=C*xQ~Pc5;nUDBsjn(Ko$rD$U!*caG79i zNr1x@c5@Iy*2LUL4lu_q8I7cwku2F7byw~GS9gzRMx)a zRc)!Y7VkGK%aUz%$&Y9$=L)H33n6!rLhJ;}lK5MXOQzvB#at}JCj16)t_SD3@OQls z`fozZm%+?)t-W1;R!Y6^OucV{U71Mi8WoYlrL_JThx_B#U``AN2^_@#3Os-?WB60! z7ptNhzbCa;j|~a;;U(GZU;XO$7VoKGS5I%)(TYVrFQqsZhadH84HSMa;`b*MUXVh+ z?g{y3q@dGQ6Pw7&usJCs%a8Sy6G{D4sUQDM2iZRzRB1@&8m_WF89 zsK_UTB@e~>jz-mv{u#VUh)0$7t~(~*^T@U~7u!s*6zD3aVUZ=QOK?bSBJ%j%YSkHlPCU0tJ+Q|hH9YCSc$en~!`Vg03+6`2=rI7moY=*)FJd8SfB=7teq8nv!r zAN)NDLOuaiasP~XK?!xq;#~LYZ6)AsBG}dOrUPwDUjqT`#bdnTRiX8@o%yaiA&dcY zQ3;@B^E?rii+j@Rc-)p~Iu?RhpuLUXw<1%1pj>r11|^yb(Vl3!P)q%EYftABJPy$j zPt6w6I;7C+E?=U$*??uZ*|MfJ#*-(4rt|Ua-Atv{&v;7y%c6YxH|BUNqSO{`F;>Ro zsgC-lyKGzh7llfJqTP;vHkX;xlI^@H6e?`wjP^w12_1>X1w0fQda@vgu!3jQ-|dOLGoJM=FK?!fHkT>5%2;qn@G z_4fLvuc8|-qupbjD+>>4VR0}suw;p?>+9=*p!=G$U5{wjyG2^^@Zb!TOeRHte-Fzm zr#H+rm4uf24CKYrEUv0=nvW|FY3}K~s5RI1R6f_e#hY8zWxY6D8Rn~LFhweWSa~hgE5>3ZPA~pmF0oT@UhleCj zP?=sH+gk67R-&*BPF8BuP_Fyl;IIs&l{Axf@<3k56L||RnG_~Hc(pmng_dQvqe$Dd z)*Ez}jN~Um2)98&Zj)Bzz@dER>d-H3b5bW;(%M~1_1{`q1FgCAFU<`TM2FQ_t83-F zp}x#Inwj=Q>f_MtPeNc<1(#F>g{OuhgNrs~-r+I*Y`Z415=Gknj8a|;HJ;}tib)A} zwbOK@Z3%Y$skr;jTrP7awZcC;9JFr{cnM&Xeq|Zx>>nq@P^sIqdPZsoD=KEg&h88n zn?Pj(e;IKBx`1wvK~1N%&~L!udN~wKDd<_ib1g_qzq?TtUSFEecLk@4fF1r<*!o61 ze@#nI=YO$oxn)gBI4PJm@j3i9dP*F-D3^YS=O%&)3zh=`eA9bS1f%+LwCF_?3cK{m z8TGRl_jbMHUs(>=*q;ec-1=-^=Bd#3hFFzId?=!0M+r+G0+QH`H==95guj17_k;(a zatM7*!y#0i0t@EA1~=iFU2x4D{Dn=_e-KJO4%_|U;%xSH^WagisxCPPrsTV=xy<6# z@#NJ*1@F2g{oQZz9C<;H{4QwuCJqbmc|Y6QlUc=czNM-a#&jKR) z4-|*sK`g>xhwI0OaSL{}liJs|tY4ux4a9ss6e^=6X;1wLZ0CvKz)d-+Q(Ak{H}hOI ztelbBJ{FT-$E%m(-49BwZf))Dc`7socUso;+S=5|puLRwca>7?poL7pmu2DwGLVBP zbD(7R|0}fg#lF7GpH5V&pXZIX)zvT5(mEY>7LBb}6bkyvB^xsT!}ILwj+43=O7I#Q z!T7w*JsB!X5i9EInwIwV8ecgO?v*HRgv~v8yf$}Sf;C-L#;9o+sPttJ>P7w;-$usj zQinj3mx9<1S6R>X^=96#o1U1psIU9E;4TIMhd_>n z2GOnJz=0kyAuzOE3#mVh2D+~`pLvCq)Z}S)&3Rz+Y;bw&P%d*h2RDYsADz~)orvl^ zY;83(=Q{7=^+aG}@6}@HVrcNQtb=fA7QTgI#?^&=$75>7bhp^eAynFz6o|VMEw+sH z)GYqx2+uBp&i@R&^x2DZ>A%#~HOyn2&+|lw)7T6b=0ym#Ub!3hM8kZ$tFNyQ5zndk zJ#E?6%Mj|NxO5?Y6BR1KdlvE;d={U{XY(C=7vITulLpd48*c`@A^9R=I>QG+{}tN2 zLTK;D46#?$HJA(>c_!~DrYH-_g!Q9gOpT?h1cFaRacwr2`2MzM27DEQ_53T% z0QD32rtgMY6U65?pi3_Flsb*}mD(~to;*WKc@dZ`hOyfM;Ou|E;CI*MyKbQ{19Ns8 zcz&ug-*qm-T+m}RpkZc;WK1~XZrJCCVV@sg*W2}%d3DLpwd6YQ;Ijfv{w<0N@Zj5< zv+2`VKN3b2EL|43?{u_t@Kfi7ekzHXA5X1>z-7NKoE8ocZBEliv?ux&^`+kpe8M&+ zI@0;B^T-5)1&v76$_>zOA2Pp#w$ief{Xf26d!zcrhA ztleok*F$8uB%gmHwEYAEl%1rAGEwB9_PEw;nl5Kmj3`(pB@jQ;RyBaKrD&nD$c4VF zl&WA!`7MN<=k{-~-V7t3_C(`e(9+cq>SfK@&Y$|Noy6!W&&6rTb##EDw{Xz|C^Eh; zhP@Sd6u-aV+M{vp=FoOz;8J}FXG6H(0{@FzvYpTPJt=ieI-gkuKhiGs z%Q=pOK@B55aIgZ!?t-^z_b{bdi4koqgnBLhpqJ=-L!qutH16G2m)wIRuh*M-7`HbR zhBhvhkqC^t5BP-pL#PkHPMt}iQgajWW;L|=w8YfcO0?VYZKO$WHEG1W&+wQPG0-d5+|H6aY|LSp~{!>~wiERZ(RNLooV zsxBoDK$Mj|BW|e`mp^R9j%qq3EOt7ot4V>ej3sPTuE}t>^%sGy;BXnp( zV?6aRTK5DBn)Qo&yPu~k(iU%QK{whDF4g($Lb002{Wv|L?q7yU2nifbO0WU8Bh93p zJdhW|6Tq1Un7>=eCp8;E$8!GK<4vF7d0$5QH5BItvMZuYuxP!n@Gn~88nlQZc1cgq z8!fr?4Y_>gNOX(s=EYNw!M{cLD#YDZ3f)QGKpG?< zKggD3nRchOCw+~l^q1U%`+L^Sh+9mntzkrEW(s^ib<3c{gatdPdxk-tOCEyk+nRtk z1FfQ;?9Fu@*^*EHma<&blm3%sDHHAnvdU3*z6^a71sjb;PQo7>zq`z62shKe& zoQpSDobP;O$}tlB#s@((4Z(-sJd~0(G`rgGK-Ihb?#K#p26cvF!cv#_9jLi zjQ#PJEeTVpad*SFTwomc34-_VUKH=fZ_?UJ9>@!MB5!_rQ;7b2&u>@NrOrc;@J-m* z-{86fVP83x7=$tlWk{F+!Jj%2e}?<;7P`VtRCi7V<2Wm|T#7rkhsAwFYi}*xEhed_ z%h+pZ**T)GFON8z4!oJlo*)zq`fXYaeXrEvF9jzGW6^2v28RZPp@9#SPFR-01Qa5A z0cd8Dq-+csC*CRKGP{V{+BhPsm-=;K7IPsS^;gw4>}GNy6;nYxx7taqhrnmLN_>PF zKJ(&;5TD^+oOqBzWf@L){ZOv^X=uZlcu}jc)Z4Z#KfrU-bnHZU3JM2?)P8*aNJ6Ct5{XVbA~-C9b)k1s2|kO@M zbG|%B*|9E^rB9rtjjN!QM*Q!Ct42bHm;hTBOOUcHp6Y_SI_Q!OW3Bo#^X)SB1Wm@u+T^T= zEyt4p36b}d_}oado~vsc=GsyFJ0R;w99GrHDuAn{AM%F0Wu-F~r3hw~Ilz<+#G}FW z^zKXgx?W=!7rJl|djiKDJ7(RWgt|kxgTG^F%c=Kf)!D~24=_R9Xu=#cJ85wxHH6Al z(=Ux@DW%l0Bcb*WQ2lL7a-HjVv0iyk!z_@`l7YoUsjaC~47U2FpNpoC&F?Of4m4xB%G9 z?~Dmc9IQP37>gJMjXfC$UM>{W0pVO!DcYUJqhm7mGCIzgIHbEbS=f9ApT%bebw%*q zqya^)FB=mfZ{YP&^33v3POY_Vy*JC8#%YT;eX`L>Uda3c>i}cZDN~GRW;PVy3O^x! zXjwH>Yg1Jn3pqs{%z2F`%6etM-FQ$qOB#ayVn?F!8fi(crfKqPw+fp0Og@|M2wLghB8s78@>dae(c88< z;DXEZ`Ho5@AuXf{#j%#nN6903CC}uYmCTVe_heSK=F$)HSu_`$O{g!!VClZt$v^Pk z;NRx{uD6@BnIHA`W`6a-y5yl4{muy2j{3`fgul|opZ6aPeHd1csPy^xEEst%^c`u+ zWozP4zKe1lmyS^J_*xz$1BmHkMP~Wu>ol#jVO-jik0yAhQE$XhP^Ih;c}Fz zYkhfZzLU1I2(~JsHF^Z|{F>?jm+gf?clVZT`k3Ba7nj;s8_EB;E}!{$b5Cb8Mti0? z$6D$>kM9mOD}+y@zC1V_hLA^}XKBU(sq{vr)k~h1FF@GuD9^hSUEfS7hBBl~X$m-^ z6~b9arQNk}=ew3$LS-#&EsTliqy9o(TrPA%3S}1k;-SOb8iEZ{$aQhrq6mh3F(=7M zK{E)9046g|Yw1b1hKtk8vLP*ouWQxn=S(!S$kBdXuItl??Z1J;vc+~4SV$U3OK@yJ zT1j(oScZ^C@=V@YX5fJ_=ymwTXW_mNTejQ|vNF9skRgB5C|C|qMyLctaK=n0c^{1b zJhaAggog{lO&6%E5z-noWue2Jf~DvLyC>oWWr>j0P7;Ea#RNqayo(z?(cF{1JviPB z$<>AxsHkIJw56x>N(h`T(y)ykn<0<$Y1s_YN}5qLerY)Z9?3I#2N)osX)f#6g;UVI ze~ELg1KuFl)P|reYbe+rGDRDpE@NtF2c;md7>4?qKT^jJo@O0aCU?=lXi2a z1R;;)nY^1yfH{qhMCwbSTtWmg9}yLsHYOH*821_D>fHHAY9@zOemAURc3Ek379xhN=!5vPYjVq@=Bh=&|F)EcfuUX z>e{9cMCFvF&-Z6d@+FiZeBReBv_PPFvKVHr%Vn0+cMX#ZUsh@`T;mTs#f=ofad8pZ zIB^pfq6K9Gv$!j z)XSJl&nJLJJ0flf%A4V-3;BQlU~GiEf!9OHGs|Y-bV=mKc`BknnhJhO~2_LR1G>R0qaR>d24qnyIM7=i3&Qg{cbAN69esq>mRdqtJ0QK68i?G9R(!xloKX zaD@FJ!k3*zSyHC-3Q#Hisg{ym){F98D>%qlHTL&hE!d4P$fV42MXlv>+wvW1Jme?* zJ390)n>Td+Z*X7`a~e2zHl8K!lLDT-H+x-hXb^L*P3|n6czwjPBdol|u#s@9_)6Hjw4X#q^VawXfp!H?siM)}= za7PgRd#=8r`^JdTReHlQxs0KoG7Ea&3xO;*Cr8kdW<9KJS!U%2G2{(ikLaSROT}%J z(1^3xvfM|77OT2BnSj$eEV09kQfof5m-1ZLCe6jZTl#%0us|C0<!xe|4fzuM` zE)ML?7q}Q#@>?(^jkOu-SWdtN2P1-eSf^Q2_Q5kbzw{ye{U?yf(Uxmf!?`8~7k!|* z8(M-`UccdK_2MSsJsoP-Xb7S+v!=BIN_*l{?3K9YMo2k_#df&7cUyt3 z(LAn-l=3|XrEhMd|X+_yFw+7pdu;h8_h{c9Md2G!+)ZBL{Qg`4sS zE_!WUu4{H>pim7A9N0WDt;gY=5qBCF$RE!0(=x$ch2uy$L48kMSRI2Ll{l1xT< z)nK->7SP+EeQTAcFRYxis|GgKd?)!b#5hZ9by!O_!--OZGQo0mYjaXG$a7&mK8rz% z`P4DdVLrAaJZ5_WlS1Eiw0#I^7^HS|w}%S7-mgnP>9jRK8yU5xd|FB~`_PQvNI7BR z@94t&unU6^uFbW<#~J>q(ZMYP@R;ZoJ=dDc%nxlYQ6^X-_=3j$sD1U>TxL&b?PwEp z27#U7ijg02_X)bkUKoC#fDIha&KVB^eqIZ`4)?sE6~95?t#>!*w~rIq-cn*qL2a26 zk2_H*XKF99BkcPu3^>fZvA57h?(!%CG@=g$1}d*{gHW2ik{SN zN~mKhzrZm}uz}Y85yb^i?{lHv<4TYOo05MAr}+%-o8soQE!gW7f{mI5g&W~0`VA~i zhMqQ(v!;_?>qVg|&pUz(sZB;q+P8Rv?g8Wduq4dpmti)UlUNIF{dlDU3==F-ZOdnH zYE+;rZG|nL?+M1-@;MqNwdIotF1#Bz)?1M&W<%Lnplx-v%zK`9ED5C+kZ@=LlPnWW zFK$clQ&#($?hhJCD`_s|yI!qat2PphO#G#dD9L~ za{p@UYfItFLQ)>wRzG5Leni1C*9fcOXWpf_G2Fmg3br^0J9PK+wUC|M#m5mj&{RZJ z@@#qxPy?GjB22`mz}<)u+O`7U$#;_m(n6X@8)+o1zNX3@EiEsMiRhK96G@X$;Q(nK z5TL!jekaO;Wy;3vQy2jw3YIWmkkEpwQyhLlEu1bcLdzygNG=Fl4;%Fg4SLL)!u?)? zT2aVnn0?pu4e5B(?!wcUhiktGW{1!6Ze^Hv;9Y#D*^v$!NDJx}X)~JZG#xS1Nv^Q%&G2cnu9s}zAN#eD5tTzMxVtdlqb zGP?s8;$jf~ze>wCX^ADC5Z761@U6;4ADl84mZYtISh*X&&B2gbGSf-%W4T|z@8i5~ zD}r9ljp+7wa~j1AHj$~Ey*M1Ic3zlU4#AJ=s14%HXgmV%K-7Y#m?wrF3BOiEPYI3k zb9gw+2|NdvY`?KwvQagb-~(J;Sd3zZ4{mdk-(fu$hk4p#k_KU-o+*{S!dBt{A#2Jq z_XHV%M(lxx=>dZ;a4tVtTU*PuTKMgwJjKjA*Hm$CX)+NR?|F*P;IsHlKAZ30yZBDN zn>17km9TAVO3bmpJ|GH@;TcUZZ40G}nT5s#c@Bii&qFO9kDoiH#)b%D=wTl;BbZQz zecG3snS-h#_+^gL$m;$2pjkBPqAN#sR>DeFizxkw(dPHLhB|XySBAnv!)&_`nuVzL z3BHaQjsK)#@^CkoSsrH7Lr~zh6w5?Gd?B1c;NV81&xOG6?1cX&u&_O_$BqZtIUm za*7=qd@$}w!))!3Anw{~3-Lnu(P#3#`)Fi{@qFf#i4MpPsUl1>NPb3Lt=SyC)OUt1e)1qkvTTjj&NV|oQ6fiKj$z@_ipX1TzQMb?Gi zrV3$91l$)b-4&L!rc?@*elMiI?Cu#e8YKp!S||C>h~l{6qAI9$@GGls^Jl~)t%%ty z?McgJqnZ-|_~~jcs|QK9YAIIOLR-qU?)O}OB9yd~2NWsP=g8Y+g-T3XumtDZ;leD0 z;ky+AOkf1R7Y6@cZiNpn5pK@CA7kB}D48z`hd}i61u3i+t;F?H>>BQlT!Jnd(@G7S zU?kJrv-(ml_a;Kmdn%{330p*W!9dCH5WW_TS|_7Nb(l|7RbX*n_p9imx;fYNdt)-> zCXY0ecJe@8$P;-BZkZG&Z3-&ERWoQO&c`P);AHW;WLWX(n|?J zdJtEu`E{vtAk0gQK!t!OqQ1DbrjNErV@pAT3f?A8eK0HL;K|o95ln3Z6T_0_oJN@YA9p5cOS!Xz6n3z+Ot&9~LsQ69%52lnRwm-1Iux0*#-F=RB}U z6f!X-Hp%pZf_N_T5x79SZ6ehS72l%Pcpvr&)lk0iWh^mQ1W7gr7f)7DgsnqQw4gatH z5uZ{R=Kk@|PHkJzPyEL}l(vmQhy5S_wBI%c&Bgz(e~eOy{{xPvq%63<0*3$q002ov JPDHLkV1g651yBG0 literal 0 HcmV?d00001 diff --git a/src/components/DataPreview/plaza/components/homePage/index.tsx b/src/components/DataPreview/plaza/components/homePage/index.tsx index 2d9333e83..6c535724f 100644 --- a/src/components/DataPreview/plaza/components/homePage/index.tsx +++ b/src/components/DataPreview/plaza/components/homePage/index.tsx @@ -1,6 +1,5 @@ import React, { useState } from 'react'; import { IPlaza } from '@/ts/core'; -import EntityIcon from '@/components/Common/GlobalComps/entityIcon'; import styles from './index.module.less'; interface Props { @@ -83,7 +82,7 @@ const HomePage: React.FC = ({ plaza, onSwitchResource }) => {
- + Logo
); -- Gitee From 23ff3a7d7159e5529f46ea97c322817e6b072be9 Mon Sep 17 00:00:00 2001 From: chenhao <2917815974@qq.com> Date: Mon, 11 May 2026 10:13:42 +0800 Subject: [PATCH 04/11] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E7=A0=81=E6=97=A0=E6=B3=95=E6=AD=A3=E5=B8=B8=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ts/core/provider/auth.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ts/core/provider/auth.ts b/src/ts/core/provider/auth.ts index 337f5ea86..8bdd821fd 100644 --- a/src/ts/core/provider/auth.ts +++ b/src/ts/core/provider/auth.ts @@ -42,7 +42,7 @@ export class AuthProvider { ): Promise> { let res = await kernel.auth('Login', params); if (res.success) { - await this.onAuthed(res.data.target); + await this._onAuthed(res.data.target); } return res; } @@ -55,7 +55,7 @@ export class AuthProvider { ): Promise> { let res = await kernel.auth('ThirdLogin', params); if (res.success) { - await this.onAuthed(res.data.target); + await this._onAuthed(res.data.target); } return res; } @@ -82,7 +82,7 @@ export class AuthProvider { ): Promise> { let res = await kernel.auth('Register', params); if (res.success) { - await this.onAuthed(res.data.target); + await this._onAuthed(res.data.target); } return res; } @@ -98,7 +98,7 @@ export class AuthProvider { ): Promise> { let res = await kernel.auth('ResetPwd', params); if (res.success) { - await this.onAuthed(res.data.target); + await this._onAuthed(res.data.target); } return res; } -- Gitee From 4d6cd5b784489186b761a84cae49691e05a196a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=AE=97=E9=98=B3?= <1627320840@qq.com> Date: Mon, 11 May 2026 16:31:28 +0800 Subject: [PATCH 05/11] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E8=B7=B3=E8=BD=AC=E5=92=8C=E6=96=87=E5=AD=97=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=E4=BD=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/homePage/index.module.less | 8 ++++---- .../plaza/components/homePage/index.tsx | 19 +++++++++++++++++-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/components/DataPreview/plaza/components/homePage/index.module.less b/src/components/DataPreview/plaza/components/homePage/index.module.less index 24681013c..59bc88469 100644 --- a/src/components/DataPreview/plaza/components/homePage/index.module.less +++ b/src/components/DataPreview/plaza/components/homePage/index.module.less @@ -96,14 +96,14 @@ .contentTopLeft { left: 22%; - top: 28%; - transform: translate(-50%, -50%); + top: 20%; + transform: translateX(-50%); } .contentTopRight { left: 78%; - top: 28%; - transform: translate(-50%, -50%); + top: 20%; + transform: translateX(-50%); } .contentBottom { diff --git a/src/components/DataPreview/plaza/components/homePage/index.tsx b/src/components/DataPreview/plaza/components/homePage/index.tsx index 6c535724f..f0b6b64a6 100644 --- a/src/components/DataPreview/plaza/components/homePage/index.tsx +++ b/src/components/DataPreview/plaza/components/homePage/index.tsx @@ -11,6 +11,8 @@ const HomePage: React.FC = ({ plaza, onSwitchResource }) => { const [hovered, setHovered] = useState(null); const resources = plaza.metadata.resources || []; + const REGISTER_URL = 'https://anxinwu.orginone.cn/#/auth'; + const handleNavigate = (title: string) => { const target = resources.find((r) => r.name === title); if (target) { @@ -55,14 +57,27 @@ const HomePage: React.FC = ({ plaza, onSwitchResource }) => { onMouseLeave={() => setHovered(null)}>

{s.title}

-

{s.desc}

+

+ {s.desc} +

handleNavigate(s.title)}> + onClick={() => { + if (s.key === 'bottom') { + window.open(REGISTER_URL, '_blank'); + } else { + handleNavigate(s.title); + } + }}> {s.btnText} Date: Tue, 12 May 2026 11:17:54 +0800 Subject: [PATCH 06/11] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=85=AC=E5=91=8A?= =?UTF-8?q?=E7=B1=BB=E8=B5=84=E6=BA=90=E6=A0=8F=E7=9B=AE=E4=BD=93=E9=AA=8C?= =?UTF-8?q?=EF=BC=8C=E6=B7=BB=E5=8A=A0=E8=A7=86=E9=A2=91=E7=B1=BB=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plaza/components/notice/notice.tsx | 40 ++- .../plaza/components/notice/noticeCard.tsx | 28 +- .../plaza/components/video/index.tsx | 289 ++++++++++++++++++ src/components/DataPreview/plaza/index.tsx | 3 + src/global.less | 11 +- src/ts/core/app/plaza/index.ts | 42 ++- src/ts/core/app/plaza/template/index.ts | 2 +- src/ts/core/app/plaza/template/video.ts | 34 ++- 8 files changed, 430 insertions(+), 19 deletions(-) create mode 100644 src/components/DataPreview/plaza/components/video/index.tsx diff --git a/src/components/DataPreview/plaza/components/notice/notice.tsx b/src/components/DataPreview/plaza/components/notice/notice.tsx index 756aff321..8489aba51 100644 --- a/src/components/DataPreview/plaza/components/notice/notice.tsx +++ b/src/components/DataPreview/plaza/components/notice/notice.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState, forwardRef, useImperativeHandle } from 'react'; import { Modal, Spin, Form, Input, Button, message, Upload, Space, Image } from 'antd'; import { Editor, Toolbar } from '@wangeditor/editor-for-react'; -import { IDomEditor } from '@wangeditor/editor'; +import { IDomEditor, IEditorConfig } from '@wangeditor/editor'; import SearchBar from '@/components/Directory/searchBar'; import OrgIcons from '@/components/Common/GlobalComps/orgIcons'; import { PlazaNotice } from '@/ts/core/app/plaza/template/notice'; @@ -35,6 +35,7 @@ const SimpleEditor = forwardRef( value?: string; onChange?: (value: string) => void; placeholder?: string; + current?: IBelong; }, ref, ) => { @@ -51,6 +52,36 @@ const SimpleEditor = forwardRef( props.onChange?.(html); }; + const editorConfig: IEditorConfig = { + placeholder: props.placeholder || '请输入公告内容', + maxLength: 10000, + MENU_CONF: { + uploadImage: { + customUpload: async (file: File, insertFn: (url: string) => void) => { + if (!props.current) { + message.error('请先选择操作空间'); + return; + } + try { + const result = await props.current.directory.createFile( + file.name, + file, + ); + if (result) { + const link = result.shareInfo().shareLink; + if (link) { + insertFn(shareOpenLink(link)); + message.success('图片上传成功'); + } + } + } catch (_err) { + message.error('图片上传失败'); + } + }, + }, + }, + }; + return (
@@ -305,7 +333,7 @@ const Notice: React.FC<{ resource: PlazaNotice; current: IBelong }> = ({ label="公告内容" name="content" rules={[{ required: true, message: '请输入公告内容' }]}> - + diff --git a/src/components/DataPreview/plaza/components/notice/noticeCard.tsx b/src/components/DataPreview/plaza/components/notice/noticeCard.tsx index 14986ee5f..b3383af04 100644 --- a/src/components/DataPreview/plaza/components/notice/noticeCard.tsx +++ b/src/components/DataPreview/plaza/components/notice/noticeCard.tsx @@ -3,7 +3,16 @@ import { Typography, Space, Image, Modal, List, Button } from 'antd'; import { DeleteOutlined } from '@ant-design/icons'; import { formatDate } from '@/utils'; -const { Title } = Typography; +const { Title, Paragraph } = Typography; + +const getContentPreview = (html: string, maxLength = 150) => { + const withoutImages = html.replace(/]*\/?>/gi, ''); + const text = withoutImages.replace(/<[^>]+>/g, '').replace(/ /g, ' ').trim(); + if (text.length > maxLength) { + return text.substring(0, maxLength) + '…'; + } + return text || '暂无内容'; +}; export interface NoticeItem { id: string; @@ -107,14 +116,17 @@ const NoticeCard: React.FC = ({ border: '1px solid #f0f0f0', borderRadius: 8, padding: 16, - minHeight: 150, + minHeight: 60, + maxHeight: 120, background: '#fafafa', overflow: 'hidden', - }} - dangerouslySetInnerHTML={{ - __html: item.content || '

暂无内容

', - }} - /> + }}> + + {getContentPreview(item.content)} + +
= ({ 公告内容
暂无内容

', diff --git a/src/components/DataPreview/plaza/components/video/index.tsx b/src/components/DataPreview/plaza/components/video/index.tsx new file mode 100644 index 000000000..eb33cbcf3 --- /dev/null +++ b/src/components/DataPreview/plaza/components/video/index.tsx @@ -0,0 +1,289 @@ +import React, { useEffect, useState } from 'react'; +import { Modal, Spin, Form, Input, Button, message, Upload, Space } from 'antd'; +import SearchBar from '@/components/Directory/searchBar'; +import OrgIcons from '@/components/Common/GlobalComps/orgIcons'; +import { PlazaVideo, PVideo } from '@/ts/core/app/plaza/template/video'; +import { IBelong } from '@/ts/core'; +import { shareOpenLink } from '@/utils/tools'; +import { JolPlayer } from 'jol-player'; + +const Video: React.FC<{ resource: PlazaVideo; current: IBelong }> = ({ + resource, + current, +}) => { + const [content, setContent] = useState([]); + const [searchValue, setSearchValue] = useState(''); + const [pageIndex, setPageIndex] = useState(1); + const [showDialog, setShowDialog] = useState(false); + const [loaded, setLoaded] = useState(true); + const [form] = Form.useForm(); + const [playingVideo, setPlayingVideo] = useState(null); + const [videoShareLink, setVideoShareLink] = useState(''); + + const loadMore = async (index: number, dt: PlazaVideo) => { + setLoaded(false); + try { + const data = await dt.load(index, 30, searchValue, []); + if (data.length > 0) { + setContent((pre) => [...pre, ...data]); + setPageIndex((pre) => pre + 1); + } + } catch (error) { + console.error('加载视频失败:', error); + message.error('加载视频失败'); + } finally { + setLoaded(true); + } + }; + + useEffect(() => { + setPageIndex(1); + setContent([]); + loadMore(1, resource); + }, [searchValue, resource, current]); + + const handlePublish = async (values: any) => { + try { + if (!resource.plaza.allowManager) { + message.warn('暂无权限发布!'); + return; + } + if (!videoShareLink) { + message.warn('请先上传视频文件'); + return; + } + const data = await resource.publish([{ ...values, shareLink: videoShareLink }]); + if (data && data.length > 0) { + setContent((pre) => [ + ...data, + ...pre.filter((i) => data.every((d: PVideo) => d.sourceId !== i.sourceId)), + ]); + message.success('视频发布成功!'); + setShowDialog(false); + setVideoShareLink(''); + form.resetFields(); + setPageIndex(1); + setContent([]); + loadMore(1, resource); + } else { + message.error('发布失败,请重试'); + } + } catch (error: any) { + message.error(`发布失败:${error.message || '未知错误'}`); + } + }; + + const handleDelete = (item: PVideo) => { + Modal.confirm({ + title: '确认删除', + content: `确定要删除视频"${item.name}"吗?`, + okText: '确认删除', + cancelText: '取消', + onOk: async () => { + try { + const success = await resource.delete(item.id); + if (success) { + setContent((pre) => pre.filter((i) => i.id !== item.id)); + message.success('删除成功'); + } else { + message.error('删除失败'); + } + } catch (error) { + message.error('删除失败,请重试'); + } + }, + }); + }; + + return ( +
+ setSearchValue(v)} + menus={{}} + rightBars={ +
+ setShowDialog(true)} + /> +
+ } + /> +
{ + const target = e.currentTarget; + if (target.scrollHeight - target.clientHeight <= target.scrollTop + 1) { + loadMore(pageIndex, resource); + } + }}> + + {content.length === 0 ? ( +
+
暂无视频数据
+
+ ) : ( +
+ {content.map((item) => ( +
setPlayingVideo(item)}> +
+ ▶ +
+
+
{item.name}
+ {item.remark && ( +
+ {item.remark} +
+ )} + {resource.plaza.allowManager && ( + + )} +
+
+ ))} +
+ )} +
+
+ + { + setShowDialog(false); + setVideoShareLink(''); + form.resetFields(); + }} + footer={null} + width={640} + destroyOnClose> +
+ + + + + + + + { + const isVideo = file.type.startsWith('video/'); + if (!isVideo) { + message.error('只能上传视频文件!'); + } + return isVideo || Upload.LIST_IGNORE; + }} + customRequest={async (options) => { + const file = options.file as File; + if (file) { + try { + const result = await current.directory.createFile( + file.name, + file, + ); + if (result) { + const link = result.shareInfo().shareLink; + if (link) { + setVideoShareLink(link); + message.success('上传成功'); + } + } + } catch (err) { + message.error('上传失败'); + } + } + options.onSuccess?.({}); + }}> + + + + + + + + + +
+
+ + setPlayingVideo(null)} + footer={null} + width={860} + destroyOnClose> + {playingVideo?.shareLink && typeof playingVideo.shareLink === 'string' && ( + + )} + {playingVideo?.remark && ( +
{playingVideo.remark}
+ )} +
+
+ ); +}; + +export default Video; diff --git a/src/components/DataPreview/plaza/index.tsx b/src/components/DataPreview/plaza/index.tsx index 8b58246de..c61cdae14 100644 --- a/src/components/DataPreview/plaza/index.tsx +++ b/src/components/DataPreview/plaza/index.tsx @@ -11,6 +11,7 @@ import EntityIcon from '@/components/Common/GlobalComps/entityIcon'; import OrgIcons from '@/components/Common/GlobalComps/orgIcons'; import EditResourceForm from './editForm'; import MarketPage from './components/market'; +import VideoPage from './components/video'; const Plaza: React.FC<{ plaza: IPlaza; mall?: boolean }> = ({ plaza, mall }) => { const [action, setAction] = useState(plaza.action); @@ -92,6 +93,8 @@ const Plaza: React.FC<{ plaza: IPlaza; mall?: boolean }> = ({ plaza, mall }) => return ; case '市场': return ; + case '视频': + return ; } } return <>; diff --git a/src/global.less b/src/global.less index 314cd2d55..6eaa7fe9b 100644 --- a/src/global.less +++ b/src/global.less @@ -2294,9 +2294,7 @@ h3 { height: 300px !important; } .w-e-text-container [data-slate-editor] { - height: 40px !important; - min-height: 40px !important; - overflow-y: scroll !important; + min-height: 300px !important; } .w-e-text-container p { margin: 8px 0 !important; @@ -2305,6 +2303,13 @@ h3 { top: 9px !important; } +.notice-detail-content { + img { + max-width: 100%; + height: auto; + } +} + .ogo-login-button { background: var(--ogo-login-button-background, #154ad8); } diff --git a/src/ts/core/app/plaza/index.ts b/src/ts/core/app/plaza/index.ts index 70a844b5b..2446ee37c 100644 --- a/src/ts/core/app/plaza/index.ts +++ b/src/ts/core/app/plaza/index.ts @@ -163,6 +163,32 @@ export class Plaza implements IPlaza { } const result = await this.coll.replace(newData); if (result && result.id === this.id) { + // 同步从该集群的其他广场文档中移除该资源 + if (this.group && this.group.hasDataAuth()) { + try { + const loadRes = await kernel.collectionLoad( + this.group.id, + this.group.belongId, + this.group.relations, + collName, + { options: { match: { shareId: this.group.id } } }, + ); + if (loadRes.success && Array.isArray(loadRes.data)) { + const otherColl = this.group.resource.genColl(collName); + for (const doc of loadRes.data) { + if (doc.id !== this.id && doc.resources) { + const filtered = doc.resources.filter((r: any) => r.id !== id); + if (filtered.length !== doc.resources.length) { + doc.resources = filtered; + await otherColl.replace(doc); + } + } + } + } + } catch (e) { + // 其他文档清理失败不影响主文档的删除结果 + } + } this.metadata = { ...result, ...this.metadata }; this.instances.delete(id); if (result.resources.length > 0) { @@ -225,7 +251,21 @@ export async function GenGroupPlaza(group: IGroup): Promise { }, ); if (res && res.success && Array.isArray(res.data) && res.data.length > 0) { - return new Plaza(res.data[0], group); + // 合并所有广场文档的资源,按资源id去重 + const seenIds = new Set(); + const allResources: schema.XPlazaResource[] = []; + for (const doc of res.data) { + if (doc.resources) { + for (const r of doc.resources) { + if (!seenIds.has(r.id)) { + seenIds.add(r.id); + allResources.push(r); + } + } + } + } + const merged = { ...res.data[0], resources: allResources }; + return new Plaza(merged, group); } return new Plaza( { diff --git a/src/ts/core/app/plaza/template/index.ts b/src/ts/core/app/plaza/template/index.ts index 7e4bafe4c..6121ce578 100644 --- a/src/ts/core/app/plaza/template/index.ts +++ b/src/ts/core/app/plaza/template/index.ts @@ -85,7 +85,7 @@ export abstract class PlazaResource implements IPlazaRes { options: { match: match, - sort: { _id: 1 }, + sort: { _id: -1 }, }, skip: (page - 1) * pageSize, take: pageSize, diff --git a/src/ts/core/app/plaza/template/video.ts b/src/ts/core/app/plaza/template/video.ts index 87f00dd92..f5a8b36b3 100644 --- a/src/ts/core/app/plaza/template/video.ts +++ b/src/ts/core/app/plaza/template/video.ts @@ -2,8 +2,23 @@ import { schema } from '@/ts/base'; import { IPlaza } from '@/ts/core'; import { PlazaResource } from '.'; +export type PVideo = { + /** 资源Id */ + resourceId: string; + /** 视频分享链接 */ + shareLink: string; + /** 联系人Id */ + contactId: string; + /** 发布时间 */ + pubTime: string; + /** 视频封面 */ + coverImage?: string; + /** 视频描述 */ + remark?: string; +} & schema.Xbase; + /** 视频实现 */ -export class PlazaVideo extends PlazaResource { +export class PlazaVideo extends PlazaResource { constructor(_metadata: schema.XPlazaResource, _plaza: IPlaza) { super(_metadata, _plaza, 'pr-plaza-video'); } @@ -13,4 +28,21 @@ export class PlazaVideo extends PlazaResource { } return {}; } + public async publish(data: any[]): Promise { + if (this.coll && data.length > 0) { + return await this.coll.insertMany( + data.map((item) => { + return { + ...item, + id: 'snowId()', + sourceId: item.id, + contactId: this.plaza.userId, + pubTime: 'sysdate()', + resourceId: this.id, + }; + }), + ); + } + return []; + } } -- Gitee From f77f4d9f3b68a0869e2a8dbcb4a7ed93f0a31c9c Mon Sep 17 00:00:00 2001 From: chenhao <2917815974@qq.com> Date: Mon, 18 May 2026 11:39:09 +0800 Subject: [PATCH 07/11] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 ++- src/ts/core/provider/auth.ts | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 9456e061f..7c362f419 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,5 @@ pnpm-lock.yaml public/domain.json -dist.tar \ No newline at end of file +dist.tar +任务分工 diff --git a/src/ts/core/provider/auth.ts b/src/ts/core/provider/auth.ts index d207037bd..a9471ad60 100644 --- a/src/ts/core/provider/auth.ts +++ b/src/ts/core/provider/auth.ts @@ -42,7 +42,7 @@ export class AuthProvider { ): Promise> { let res = await kernel.auth('Login', params); if (res.success) { - await this._onAuthed(res.data.target); + await this.onAuthed(res.data.target); } return res; } @@ -55,7 +55,7 @@ export class AuthProvider { ): Promise> { let res = await kernel.auth('ThirdLogin', params); if (res.success) { - await this._onAuthed(res.data.target); + await this.onAuthed(res.data.target); } return res; } @@ -82,7 +82,7 @@ export class AuthProvider { ): Promise> { let res = await kernel.auth('Register', params); if (res.success) { - await this._onAuthed(res.data.target); + await this.onAuthed(res.data.target); } return res; } @@ -98,7 +98,7 @@ export class AuthProvider { ): Promise> { let res = await kernel.auth('ResetPwd', params); if (res.success) { - await this._onAuthed(res.data.target); + await this.onAuthed(res.data.target); } return res; } -- Gitee From c5e5a147f59d10b646ddcfdebf05180af7be8db9 Mon Sep 17 00:00:00 2001 From: chenhao <2917815974@qq.com> Date: Tue, 19 May 2026 10:12:56 +0800 Subject: [PATCH 08/11] =?UTF-8?q?@=20feat:=20P1=E6=9E=B6=E6=9E=84=E4=B8=8E?= =?UTF-8?q?=E5=9F=BA=E7=A1=80=E8=AE=BE=E6=96=BD=E2=80=94=E2=80=94=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E5=AE=9A=E4=B9=89=E3=80=81Mock=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E5=B1=82=E3=80=81=E8=B7=AF=E7=94=B1=E9=85=8D=E7=BD=AE=E4=B8=8E?= =?UTF-8?q?=E5=85=AC=E5=85=B1=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增资格注册/对接大厅/产业链/融资链四模块TypeScript类型定义 - 搭建Mock API服务层(adapter接口+mock实现),共25个接口 - 新增11条路由及10个占位页面组件 - 封装三大公共组件: UploadPanel上传面板、ReviewTimeline审核时间线、StatusBadge状态徽章 - 新增公共样式变量与mixin文件 - 更新ts/core/index.ts导出新模块类型 任务分工参考: 任务分工/5-13任务分配-合并版.md Co-Authored-By: Claude Opus 4.7 @ --- src/components/Common/ReviewTimeline.tsx | 164 +++++++++++ src/components/Common/StatusBadge.tsx | 140 ++++++++++ src/components/Common/UploadPanel.tsx | 302 ++++++++++++++++++++ src/pages/Chain/ChainGraph.tsx | 17 ++ src/pages/Financing/FinancingList.tsx | 17 ++ src/pages/Financing/LoanApply.tsx | 21 ++ src/pages/Financing/MyApplications.tsx | 17 ++ src/pages/Financing/ProductDetail.tsx | 21 ++ src/pages/Match/MatchHall.tsx | 21 ++ src/pages/Register/QualifiedRegister.tsx | 21 ++ src/pages/Review/ReviewDetail.tsx | 21 ++ src/pages/Review/ReviewPending.tsx | 17 ++ src/routes.tsx | 60 ++++ src/services/adapters/chain.adapter.ts | 41 +++ src/services/adapters/financing.adapter.ts | 45 +++ src/services/adapters/matching.adapter.ts | 49 ++++ src/services/adapters/register.adapter.ts | 52 ++++ src/services/index.ts | 16 ++ src/services/mock/chain.mock.ts | 176 ++++++++++++ src/services/mock/financing.mock.ts | 200 ++++++++++++++ src/services/mock/matching.mock.ts | 168 ++++++++++++ src/services/mock/register.mock.ts | 272 ++++++++++++++++++ src/styles/common.mixins.less | 305 +++++++++++++++++++++ src/ts/core/chain/chain.ts | 153 +++++++++++ src/ts/core/chain/index.ts | 1 + src/ts/core/financing/financing.ts | 297 ++++++++++++++++++++ src/ts/core/financing/index.ts | 1 + src/ts/core/index.ts | 81 ++++++ src/ts/core/matching/index.ts | 1 + src/ts/core/matching/match.ts | 168 ++++++++++++ src/ts/core/register/index.ts | 1 + src/ts/core/register/qualified.ts | 215 +++++++++++++++ 32 files changed, 3081 insertions(+) create mode 100644 src/components/Common/ReviewTimeline.tsx create mode 100644 src/components/Common/StatusBadge.tsx create mode 100644 src/components/Common/UploadPanel.tsx create mode 100644 src/pages/Chain/ChainGraph.tsx create mode 100644 src/pages/Financing/FinancingList.tsx create mode 100644 src/pages/Financing/LoanApply.tsx create mode 100644 src/pages/Financing/MyApplications.tsx create mode 100644 src/pages/Financing/ProductDetail.tsx create mode 100644 src/pages/Match/MatchHall.tsx create mode 100644 src/pages/Register/QualifiedRegister.tsx create mode 100644 src/pages/Review/ReviewDetail.tsx create mode 100644 src/pages/Review/ReviewPending.tsx create mode 100644 src/services/adapters/chain.adapter.ts create mode 100644 src/services/adapters/financing.adapter.ts create mode 100644 src/services/adapters/matching.adapter.ts create mode 100644 src/services/adapters/register.adapter.ts create mode 100644 src/services/index.ts create mode 100644 src/services/mock/chain.mock.ts create mode 100644 src/services/mock/financing.mock.ts create mode 100644 src/services/mock/matching.mock.ts create mode 100644 src/services/mock/register.mock.ts create mode 100644 src/styles/common.mixins.less create mode 100644 src/ts/core/chain/chain.ts create mode 100644 src/ts/core/chain/index.ts create mode 100644 src/ts/core/financing/financing.ts create mode 100644 src/ts/core/financing/index.ts create mode 100644 src/ts/core/matching/index.ts create mode 100644 src/ts/core/matching/match.ts create mode 100644 src/ts/core/register/index.ts create mode 100644 src/ts/core/register/qualified.ts diff --git a/src/components/Common/ReviewTimeline.tsx b/src/components/Common/ReviewTimeline.tsx new file mode 100644 index 000000000..45b2bba38 --- /dev/null +++ b/src/components/Common/ReviewTimeline.tsx @@ -0,0 +1,164 @@ +import React from 'react'; +import { Steps, Tag, Space, Typography } from 'antd'; +import { + CheckCircleOutlined, + CloseCircleOutlined, + ClockCircleOutlined, + LoadingOutlined, +} from '@ant-design/icons'; + +const { Text } = Typography; + +/** + * 审核状态时间线组件 + * 用于展示审核流程、申请进度等步骤化的状态流转 + * 负责人: P1 架构 + * 使用者: P4 (审核历史), P7 (融资申请进度) + */ + +/** 步骤状态 */ +export type StepStatus = 'wait' | 'process' | 'finish' | 'error'; + +/** 单个步骤定义 */ +export interface TimelineStep { + /** 步骤标题 */ + title: string; + /** 步骤描述/备注 */ + description?: string; + /** 步骤状态 */ + status: StepStatus; + /** 步骤完成时间 */ + time?: string; + /** 操作人 */ + operator?: string; + /** 额外的状态标签文字 */ + tagLabel?: string; + /** 额外的状态标签颜色 */ + tagColor?: string; +} + +export interface ReviewTimelineProps { + /** 步骤列表 */ + steps: TimelineStep[]; + /** 当前激活的步骤索引(从 0 开始),用于非受控高亮 */ + current?: number; + /** 时间线方向,默认 horizontal */ + direction?: 'horizontal' | 'vertical'; + /** 是否显示当前步骤的 loading 动画 */ + showLoading?: boolean; + /** 自定义步骤图标(按索引) */ + stepIcons?: Record; + /** 当所有步骤完成时是否显示成功状态 */ + successOnFinish?: boolean; +} + +/** 根据状态获取默认图标 */ +function getStepIcon(status: StepStatus, showLoading?: boolean): React.ReactNode { + switch (status) { + case 'finish': + return ; + case 'error': + return ; + case 'process': + return showLoading ? : ; + default: + return ; + } +} + +/** 根据状态获取 tag 颜色 */ +function getDefaultTagColor(status: StepStatus): string { + switch (status) { + case 'finish': return 'success'; + case 'error': return 'error'; + case 'process': return 'processing'; + default: return 'default'; + } +} + +/** 根据状态获取默认 tag 文字 */ +function getDefaultTagLabel(status: StepStatus): string { + switch (status) { + case 'finish': return '已完成'; + case 'error': return '已驳回'; + case 'process': return '进行中'; + default: return '待处理'; + } +} + +const ReviewTimeline: React.FC = ({ + steps, + current = -1, + direction = 'horizontal', + showLoading = false, + successOnFinish = true, +}) => { + if (steps.length === 0) { + return ( +
+ 暂无进度信息 +
+ ); + } + + // 判断是否全部完成 + const allFinished = successOnFinish && steps.every((s) => s.status === 'finish'); + + const items = steps.map((step, index) => { + const isCurrent = index === current && step.status === 'process'; + + return { + title: ( + + + + {step.title} + + + {step.tagLabel || getDefaultTagLabel(step.status)} + + + {step.operator && ( + + {step.operator} + + )} + + ), + description: ( + + {step.description && ( + + {step.description} + + )} + {step.time && ( + + {step.time} + + )} + + ), + status: step.status, + icon: getStepIcon(step.status, isCurrent && showLoading), + }; + }); + + return ( +
+ = 0 ? current : undefined} + status={allFinished ? 'finish' : undefined} + direction={direction} + items={items} + size="small" + style={{ + maxWidth: direction === 'horizontal' ? '100%' : 400, + overflow: direction === 'horizontal' ? 'auto' : undefined, + }} + /> +
+ ); +}; + +export default ReviewTimeline; diff --git a/src/components/Common/StatusBadge.tsx b/src/components/Common/StatusBadge.tsx new file mode 100644 index 000000000..d84ce0f29 --- /dev/null +++ b/src/components/Common/StatusBadge.tsx @@ -0,0 +1,140 @@ +import React from 'react'; +import { Tag, Tooltip } from 'antd'; +import type { TagProps } from 'antd'; + +/** + * 状态徽章/角色标识组件 + * 用于展示用户角色、权限标识、审核状态等标签 + * 负责人: P1 架构 + * 使用者: P4 (审核状态标签), P5 (行为/资源标签), P6 (节点类型标签), P7 (还款状态标签) + */ + +export type BadgeVariant = 'filled' | 'outlined' | 'dot'; + +export interface StatusBadgeProps { + /** 显示文字 */ + label: string; + /** 颜色预设或自定义颜色值 */ + color?: string; + /** 变体样式 */ + variant?: BadgeVariant; + /** 图标 */ + icon?: React.ReactNode; + /** tooltip 提示文字 */ + tooltip?: string; + /** 尺寸 */ + size?: 'small' | 'default'; + /** 是否可关闭 */ + closable?: boolean; + /** 关闭回调 */ + onClose?: () => void; + /** 点击回调 */ + onClick?: () => void; + /** 自定义样式 */ + style?: React.CSSProperties; + /** 额外 className */ + className?: string; +} + +/** 预定义的角色徽章配置 */ +export const RoleBadgeConfig: Record = { + member: { color: '#1890ff', label: '会员' }, + researcher: { color: '#722ed1', label: '研究员' }, + partner: { color: '#fa8c16', label: '合伙人' }, + expert: { color: '#eb2f96', label: '专家' }, + admin: { color: '#f5222d', label: '管理员' }, +}; + +/** 预定义的审核状态徽章配置 */ +export const ReviewBadgeConfig: Record = { + pending: { color: '#faad14', label: '待审核' }, + approved: { color: '#52c41a', label: '已通过' }, + rejected: { color: '#ff4d4f', label: '已驳回' }, +}; + +/** 预定义的业务行为徽章配置 */ +export const BehaviorBadgeConfig: Record = { + trade: { color: '#1890ff', label: '交易' }, + match: { color: '#722ed1', label: '对接' }, + link: { color: '#13c2c2', label: '链接' }, + share: { color: '#52c41a', label: '共享' }, +}; + +/** 预定义的融资状态徽章配置 */ +export const LoanStatusBadgeConfig: Record = { + submitted: { color: '#1890ff', label: '已提交' }, + reviewing: { color: '#faad14', label: '审核中' }, + due_diligence: { color: '#722ed1', label: '尽职调查' }, + approving: { color: '#fa8c16', label: '审批中' }, + signing: { color: '#13c2c2', label: '签约中' }, + disbursed: { color: '#52c41a', label: '已放款' }, + rejected: { color: '#ff4d4f', label: '已拒绝' }, + settled: { color: '#8c8c8c', label: '已结清' }, +}; + +const StatusBadge: React.FC = ({ + label, + color, + variant = 'filled', + icon, + tooltip, + size = 'default', + closable = false, + onClose, + onClick, + style, + className, +}) => { + const tagProps: TagProps = { + color: variant === 'filled' ? (color || '#1890ff') : undefined, + closable, + onClose, + onClick, + style: { + cursor: onClick ? 'pointer' : undefined, + fontSize: size === 'small' ? 12 : 14, + padding: size === 'small' ? '0 7px' : undefined, + lineHeight: size === 'small' ? '20px' : undefined, + borderRadius: 4, + margin: '2px 4px 2px 0', + ...(variant === 'outlined' ? { + borderColor: color || '#1890ff', + color: color || '#1890ff', + backgroundColor: 'transparent', + borderWidth: 1, + borderStyle: 'solid' as React.CSSProperties['borderStyle'], + } : {}), + ...(variant === 'dot' ? { + border: 'none', + paddingLeft: 20, + background: 'transparent', + } : {}), + ...style, + }, + className, + icon: variant === 'dot' ? ( + + ) : icon, + }; + + const tagElement = {label}; + + if (tooltip) { + return {tagElement}; + } + + return tagElement; +}; + +export default StatusBadge; diff --git a/src/components/Common/UploadPanel.tsx b/src/components/Common/UploadPanel.tsx new file mode 100644 index 000000000..a1fbb9f57 --- /dev/null +++ b/src/components/Common/UploadPanel.tsx @@ -0,0 +1,302 @@ +import React, { useState, useCallback } from 'react'; +import { Upload, Modal, Progress, Button, Space, message } from 'antd'; +import { + InboxOutlined, + DeleteOutlined, + EyeOutlined, + ReloadOutlined, + FileOutlined, + FilePdfOutlined, + FileImageOutlined, +} from '@ant-design/icons'; +import type { UploadFile, RcFile } from 'antd/lib/upload/interface'; + +/** + * 通用上传面板组件 + * 支持拖拽/点击上传、进度条、缩略图预览、删除、重试 + * 负责人: P1 架构 + * 使用者: P2 (注册表单 Step3), P3 (材料上传), P7 (融资申请 Step3) + */ + +export interface UploadPanelProps { + /** 接受的文件类型,如 ['image/png', 'image/jpeg', 'application/pdf'] */ + accept?: string[]; + /** 单文件最大体积(MB),默认 10 */ + maxFileSize?: number; + /** 最大文件总数,默认 9 */ + maxCount?: number; + /** 是否显示上传列表,默认 true */ + showUploadList?: boolean; + /** 是否禁用,默认 false */ + disabled?: boolean; + /** 自定义上传方法,默认使用 mock */ + customRequest?: (file: File) => Promise<{ url: string; uid?: string }>; + /** 文件列表变化回调 */ + onChange?: (files: UploadFile[]) => void; + /** 已上传的文件列表(受控模式) */ + fileList?: UploadFile[]; + /** 提示文案 */ + hintText?: string; + /** 是否多选 */ + multiple?: boolean; + /** 拖拽区域高度 */ + height?: number; +} + +/** 获取文件图标 */ +function getFileIcon(fileName: string) { + const ext = fileName.split('.').pop()?.toLowerCase(); + switch (ext) { + case 'pdf': return ; + case 'png': + case 'jpg': + case 'jpeg': + case 'gif': + case 'svg': + case 'webp': return ; + default: return ; + } +} + +/** 判断文件是否为图片类型 */ +function isImageFile(file: UploadFile): boolean { + return file.type?.startsWith('image/') || /\.(png|jpe?g|gif|svg|webp)$/i.test(file.name); +} + +const UploadPanel: React.FC = ({ + accept = ['image/png', 'image/jpeg', 'image/jpg', 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'], + maxFileSize = 10, + maxCount = 9, + showUploadList = true, + disabled = false, + customRequest, + onChange, + fileList: controlledFileList, + hintText = '点击或拖拽文件至此处上传', + multiple = true, + height = 200, +}) => { + const [internalFileList, setInternalFileList] = useState([]); + const [previewVisible, setPreviewVisible] = useState(false); + const [previewFile, setPreviewFile] = useState(null); + + // 使用受控或非受控模式 + const fileList = controlledFileList !== undefined ? controlledFileList : internalFileList; + + const updateFileList = useCallback((newList: UploadFile[]) => { + setInternalFileList(newList); + onChange?.(newList); + }, [onChange]); + + /** 文件上传前的校验 */ + const beforeUpload = useCallback((file: RcFile): Promise => { + return new Promise((resolve, reject) => { + // 文件大小校验 + const isLtMaxSize = file.size / 1024 / 1024 < maxFileSize; + if (!isLtMaxSize) { + message.error(`文件 ${file.name} 超过 ${maxFileSize}MB 限制`); + return reject(new Error('file size exceeded')); + } + + // 文件类型校验 + const acceptedMimeTypes = accept.map((a) => a.replace('image/jpg', 'image/jpeg')); + const fileExt = '.' + file.name.split('.').pop()?.toLowerCase(); + const acceptedExts = accept.filter((a) => a.startsWith('.')); + const isAccepted = acceptedMimeTypes.includes(file.type) || acceptedExts.includes(fileExt); + if (accept.length > 0 && !isAccepted) { + message.error(`不支持 ${fileExt} 格式的文件`); + return reject(new Error('file type not supported')); + } + + resolve(file); + }); + }, [accept, maxFileSize]); + + /** 自定义上传逻辑 */ + const handleUpload = useCallback(async (options: { file: UploadFile; onSuccess: (body: object, xhr?: unknown) => void; onError: (err: Error) => void; onProgress: (event: { percent: number }) => void }) => { + const { file: uploadFile, onSuccess, onError, onProgress } = options; + + // 模拟进度 + let progress = 0; + const timer = setInterval(() => { + progress += Math.random() * 20 + 5; + if (progress >= 100) { + progress = 100; + clearInterval(timer); + } + onProgress({ percent: Math.min(progress, 100) }); + }, 200); + + try { + let result: { url: string; uid?: string }; + if (customRequest) { + result = await customRequest(uploadFile as unknown as File); + } else { + // 默认 mock 上传 + await new Promise((r) => setTimeout(r, 1000 + Math.random() * 1000)); + result = { + url: URL.createObjectURL(uploadFile as unknown as File), + uid: 'mock_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8), + }; + } + clearInterval(timer); + onProgress({ percent: 100 }); + onSuccess({ url: result.url, uid: result.uid || uploadFile.uid }, undefined); + } catch (err) { + clearInterval(timer); + onError(err as Error); + } + }, [customRequest]); + + /** 删除文件 */ + const handleRemove = useCallback((file: UploadFile) => { + const newList = fileList.filter((f) => f.uid !== file.uid); + updateFileList(newList); + return true; + }, [fileList, updateFileList]); + + /** 预览文件 */ + const handlePreview = useCallback((file: UploadFile) => { + setPreviewFile(file); + setPreviewVisible(true); + }, []); + + /** 重试上传 */ + const handleRetry = useCallback((file: UploadFile) => { + const newList = fileList.map((f) => { + if (f.uid === file.uid) { + return { ...f, status: 'uploading' as const, percent: 0, error: undefined }; + } + return f; + }); + updateFileList(newList); + }, [fileList, updateFileList]); + + /** 渲染上传列表项 */ + const renderUploadItem = useCallback((originNode: React.ReactNode, file: UploadFile) => { + if (file.status === 'error') { + return ( +
+ + {getFileIcon(file.name)} +
+
{file.name}
+
上传失败
+
+
+ +
+ ); + } + + if (file.status === 'uploading') { + return ( +
+ {getFileIcon(file.name)} +
+
{file.name}
+ +
+
+ ); + } + + // done 状态 + return ( +
+ + {isImageFile(file) && file.url ? ( + {file.name} + ) : ( + getFileIcon(file.name) + )} +
+
{file.name}
+
上传完成
+
+
+ + + + +
+ ); + }, [handleRetry, handlePreview, handleRemove]); + + return ( +
+ void} + onRemove={handleRemove} + onPreview={handlePreview} + onChange={(info) => { + const newList = info.fileList.map((f: UploadFile) => ({ + ...f, + uid: f.uid || 'mock_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8), + })); + updateFileList(newList); + }} + itemRender={renderUploadItem} + style={{ minHeight: height }}> +

+ +

+

{hintText}

+

+ 支持 {accept.map((a) => a.split('/').pop()).filter((v, i, arr) => arr.indexOf(v) === i).join('、')} 格式,单文件不超过 {maxFileSize}MB +

+
+ + {/* 文件预览弹窗 */} + setPreviewVisible(false)} + width={800}> + {previewFile && ( + isImageFile(previewFile) && previewFile.url ? ( + {previewFile.name} + ) : ( +
+ {getFileIcon(previewFile.name)} +

非图片文件,请下载后查看

+ {previewFile.url && ( + + )} +
+ ) + )} +
+
+ ); +}; + +export default UploadPanel; diff --git a/src/pages/Chain/ChainGraph.tsx b/src/pages/Chain/ChainGraph.tsx new file mode 100644 index 000000000..32b7ede61 --- /dev/null +++ b/src/pages/Chain/ChainGraph.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +/** + * 产业链图谱主页 + * Route: /chain + * 负责人: P6 白鑫 + */ +const ChainGraph: React.FC = () => { + return ( +
+

产业链可视化

+

此页面由 P6 负责实现 —— 关系图谱 + 链导航 + 搜索定位

+
+ ); +}; + +export default ChainGraph; diff --git a/src/pages/Financing/FinancingList.tsx b/src/pages/Financing/FinancingList.tsx new file mode 100644 index 000000000..325df37cb --- /dev/null +++ b/src/pages/Financing/FinancingList.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +/** + * 融资产品列表页 + * Route: /financing + * 负责人: P7 王宗阳 + */ +const FinancingList: React.FC = () => { + return ( +
+

融资产品

+

此页面由 P7 负责实现 —— 产品列表 + 筛选 + 产品卡片

+
+ ); +}; + +export default FinancingList; diff --git a/src/pages/Financing/LoanApply.tsx b/src/pages/Financing/LoanApply.tsx new file mode 100644 index 000000000..f30f1ea9f --- /dev/null +++ b/src/pages/Financing/LoanApply.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { useParams } from 'react-router-dom'; + +/** + * 融资申请页 + * Route: /financing/apply/:productId + * 负责人: P7 王宗阳 + */ +const LoanApply: React.FC = () => { + const { productId } = useParams<{ productId: string }>(); + + return ( +
+

融资申请

+

产品编号: {productId}

+

此页面由 P7 负责实现 —— 多步申请表单 + 材料上传 + 提交确认

+
+ ); +}; + +export default LoanApply; diff --git a/src/pages/Financing/MyApplications.tsx b/src/pages/Financing/MyApplications.tsx new file mode 100644 index 000000000..80443d908 --- /dev/null +++ b/src/pages/Financing/MyApplications.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +/** + * 我的融资申请页 + * Route: /financing/my-applications + * 负责人: P7 王宗阳 + */ +const MyApplications: React.FC = () => { + return ( +
+

我的融资申请

+

此页面由 P7 负责实现 —— 申请列表 + 进度追踪 + 还款计划

+
+ ); +}; + +export default MyApplications; diff --git a/src/pages/Financing/ProductDetail.tsx b/src/pages/Financing/ProductDetail.tsx new file mode 100644 index 000000000..7ac78c898 --- /dev/null +++ b/src/pages/Financing/ProductDetail.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { useParams } from 'react-router-dom'; + +/** + * 融资产品详情页 + * Route: /financing/product/:id + * 负责人: P7 王宗阳 + */ +const ProductDetail: React.FC = () => { + const { id } = useParams<{ id: string }>(); + + return ( +
+

融资产品详情

+

产品编号: {id}

+

此页面由 P7 负责实现 —— 产品完整信息 + 立即申请入口

+
+ ); +}; + +export default ProductDetail; diff --git a/src/pages/Match/MatchHall.tsx b/src/pages/Match/MatchHall.tsx new file mode 100644 index 000000000..cb8b085c3 --- /dev/null +++ b/src/pages/Match/MatchHall.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { useParams } from 'react-router-dom'; + +/** + * 对接大厅首页 / 按行为选择资源 + * Route: /match /match/:behavior + * 负责人: P5 王志炫 + */ +const MatchHall: React.FC = () => { + const { behavior } = useParams<{ behavior?: string }>(); + + return ( +
+

对接大厅

+ {behavior ?

当前行为: {behavior}

:

请选择业务行为(交易 / 对接 / 链接 / 共享)

} +

此页面由 P5 负责实现 —— 大厅首页 + 行为选择 + 资源路由

+
+ ); +}; + +export default MatchHall; diff --git a/src/pages/Register/QualifiedRegister.tsx b/src/pages/Register/QualifiedRegister.tsx new file mode 100644 index 000000000..8b74a64d3 --- /dev/null +++ b/src/pages/Register/QualifiedRegister.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { useParams } from 'react-router-dom'; + +/** + * 资格注册主页 / 按角色注册 + * Route: /register/qualified /register/qualified/:role + * 负责人: P2 陈晨杰 + */ +const QualifiedRegister: React.FC = () => { + const { role } = useParams<{ role?: string }>(); + + return ( +
+

资格注册

+ {role ?

当前角色: {role}

:

请选择注册角色(会员 / 研究员 / 合伙人)

} +

此页面由 P2 负责实现 —— 角色选择器 + 动态注册表单

+
+ ); +}; + +export default QualifiedRegister; diff --git a/src/pages/Review/ReviewDetail.tsx b/src/pages/Review/ReviewDetail.tsx new file mode 100644 index 000000000..1a8028913 --- /dev/null +++ b/src/pages/Review/ReviewDetail.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { useParams } from 'react-router-dom'; + +/** + * 审核详情页 + * Route: /review/detail/:id + * 负责人: P4 刘明剑 + */ +const ReviewDetail: React.FC = () => { + const { id } = useParams<{ id: string }>(); + + return ( +
+

审核详情

+

申请编号: {id}

+

此页面由 P4 负责实现 —— 申请人信息 + 材料查看 + 审核操作

+
+ ); +}; + +export default ReviewDetail; diff --git a/src/pages/Review/ReviewPending.tsx b/src/pages/Review/ReviewPending.tsx new file mode 100644 index 000000000..7e605d2b4 --- /dev/null +++ b/src/pages/Review/ReviewPending.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +/** + * 审核待办列表页 + * Route: /review/pending + * 负责人: P4 刘明剑 + */ +const ReviewPending: React.FC = () => { + return ( +
+

审核待办

+

此页面由 P4 负责实现 —— 审核列表 + 筛选 + 排序

+
+ ); +}; + +export default ReviewPending; diff --git a/src/routes.tsx b/src/routes.tsx index 433f60d1f..983c8daa4 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -77,6 +77,66 @@ const Routers: IRouteConfig[] = [ title: '视图', component: React.lazy(() => import('@/pages/View')), }, + // ===== 资格注册(Req 2)===== + { + path: '/register/qualified', + title: '资格注册', + component: React.lazy(() => import('@/pages/Register/QualifiedRegister')), + }, + { + path: '/register/qualified/:role', + title: '按角色注册', + component: React.lazy(() => import('@/pages/Register/QualifiedRegister')), + }, + // ===== 审核工作流(Req 2)===== + { + path: '/review/pending', + title: '审核待办', + component: React.lazy(() => import('@/pages/Review/ReviewPending')), + }, + { + path: '/review/detail/:id', + title: '审核详情', + component: React.lazy(() => import('@/pages/Review/ReviewDetail')), + }, + // ===== 对接大厅(Req 5)===== + { + path: '/match', + title: '对接大厅', + component: React.lazy(() => import('@/pages/Match/MatchHall')), + }, + { + path: '/match/:behavior', + title: '行为选择', + component: React.lazy(() => import('@/pages/Match/MatchHall')), + }, + // ===== 产业链(Req 4)===== + { + path: '/chain', + title: '产业链', + component: React.lazy(() => import('@/pages/Chain/ChainGraph')), + }, + // ===== 融资链(Req 6)===== + { + path: '/financing', + title: '融资产品', + component: React.lazy(() => import('@/pages/Financing/FinancingList')), + }, + { + path: '/financing/product/:id', + title: '产品详情', + component: React.lazy(() => import('@/pages/Financing/ProductDetail')), + }, + { + path: '/financing/apply/:productId', + title: '融资申请', + component: React.lazy(() => import('@/pages/Financing/LoanApply')), + }, + { + path: '/financing/my-applications', + title: '我的融资申请', + component: React.lazy(() => import('@/pages/Financing/MyApplications')), + }, { path: '*', title: '页面不存在', diff --git a/src/services/adapters/chain.adapter.ts b/src/services/adapters/chain.adapter.ts new file mode 100644 index 000000000..122ef538c --- /dev/null +++ b/src/services/adapters/chain.adapter.ts @@ -0,0 +1,41 @@ +/** + * 产业链可视化 Adapter 接口定义 + * 定义所有产业链相关 API 的签名,mock 和真实实现共用此接口 + */ + +import type { + ChainGraph, + ChainCategory, + ChainNode, + RelationFormData, + ChainSearchParams, +} from '@/ts/core/chain/chain'; + +export interface IChainAdapter { + /** 获取产业链分类导航列表 */ + getChainCategories(): Promise<{ code: number; data: ChainCategory[] }>; + + /** 获取指定产业链图谱完整数据 */ + getChainGraph(graphId: string): Promise<{ code: number; data: ChainGraph }>; + + /** 获取以某组织为中心的产业链图谱(一跳关联) */ + getCenteredGraph(targetId: string): Promise<{ code: number; data: ChainGraph }>; + + /** 获取当前用户所在组织的产业链图谱 */ + getMyChainGraph(): Promise<{ code: number; data: ChainGraph | null }>; + + /** 搜索组织节点 */ + searchNodes(params: ChainSearchParams): Promise<{ code: number; data: ChainNode[] }>; + + /** 添加关系 */ + addRelation(data: RelationFormData): Promise<{ code: number; data: { relationId: string } }>; + + /** 编辑关系 */ + updateRelation(relationId: string, data: Partial): Promise<{ code: number }>; + + /** 删除关系 */ + deleteRelation(relationId: string): Promise<{ code: number }>; + + /** 获取组织详情 */ + getNodeDetail(targetId: string): Promise<{ code: number; data: ChainNode }>; +} diff --git a/src/services/adapters/financing.adapter.ts b/src/services/adapters/financing.adapter.ts new file mode 100644 index 000000000..ade48a9a4 --- /dev/null +++ b/src/services/adapters/financing.adapter.ts @@ -0,0 +1,45 @@ +/** + * 融资链 Adapter 接口定义 + * 定义所有融资相关 API 的签名,mock 和真实实现共用此接口 + */ + +import type { + FinancingProduct, + LoanApplicationForm, + LoanApplication, + RepaymentInstallment, + RepaymentRecord, +} from '@/ts/core/financing/financing'; + +export interface IFinancingAdapter { + /** 获取融资产品列表 */ + getProductList(params: { + type?: string; + amountMin?: number; + amountMax?: number; + sortBy?: string; + page?: number; + pageSize?: number; + }): Promise<{ code: number; data: { total: number; list: FinancingProduct[] } }>; + + /** 获取融资产品详情 */ + getProductDetail(productId: string): Promise<{ code: number; data: FinancingProduct }>; + + /** 提交融资申请 */ + submitApplication(form: LoanApplicationForm): Promise<{ code: number; data: { applicationId: string; serialNo: string } }>; + + /** 获取我的融资申请列表 */ + getMyApplications(params: { page?: number; pageSize?: number }): Promise<{ code: number; data: { total: number; list: LoanApplication[] } }>; + + /** 获取融资申请详情(含进度) */ + getApplicationDetail(applicationId: string): Promise<{ code: number; data: LoanApplication }>; + + /** 获取还款计划表 */ + getRepaymentPlan(applicationId: string): Promise<{ code: number; data: RepaymentInstallment[] }>; + + /** 获取还款记录 */ + getRepaymentRecords(applicationId: string): Promise<{ code: number; data: RepaymentRecord[] }>; + + /** 执行还款 */ + performRepayment(applicationId: string, period: number, method: 'bank_transfer' | 'auto_debit'): Promise<{ code: number; msg?: string }>; +} diff --git a/src/services/adapters/matching.adapter.ts b/src/services/adapters/matching.adapter.ts new file mode 100644 index 000000000..186f6f745 --- /dev/null +++ b/src/services/adapters/matching.adapter.ts @@ -0,0 +1,49 @@ +/** + * 对接大厅 Adapter 接口定义 + * 定义所有对接相关 API 的签名,mock 和真实实现共用此接口 + */ + +import type { + MatchDemand, + MatchInvitation, + MatchRecord, + HallStatistics, +} from '@/ts/core/matching/match'; + +export interface IMatchingAdapter { + /** 获取大厅首页统计数据 */ + getHallStatistics(): Promise<{ code: number; data: HallStatistics }>; + + /** 获取行为入口卡片数据 */ + getBehaviorCards(): Promise<{ code: number; data: import('@/ts/core/matching/match').BehaviorCardData[] }>; + + /** 发布对接需求 */ + publishDemand(demand: Omit): Promise<{ code: number; data: { demandId: string } }>; + + /** 获取对接需求列表 */ + getDemandList(params: { + behavior?: string; + resourceType?: string; + status?: string; + page?: number; + pageSize?: number; + }): Promise<{ code: number; data: { total: number; list: MatchDemand[] } }>; + + /** 获取对接需求详情 */ + getDemandDetail(demandId: string): Promise<{ code: number; data: MatchDemand }>; + + /** 发起对接邀请 */ + sendInvitation(invitation: Omit): Promise<{ code: number; data: { invitationId: string } }>; + + /** 处理对接邀请(接受/拒绝) */ + handleInvitation(invitationId: string, action: 'accepted' | 'rejected', message?: string): Promise<{ code: number }>; + + /** 获取我的对接列表(我发起的) */ + getMyDemands(params: { page?: number; pageSize?: number }): Promise<{ code: number; data: { total: number; list: MatchDemand[] } }>; + + /** 获取收到对接邀请列表 */ + getReceivedInvitations(params: { page?: number; pageSize?: number }): Promise<{ code: number; data: { total: number; list: MatchInvitation[] } }>; + + /** 获取对接记录 */ + getMatchRecords(params: { page?: number; pageSize?: number }): Promise<{ code: number; data: { total: number; list: MatchRecord[] } }>; +} diff --git a/src/services/adapters/register.adapter.ts b/src/services/adapters/register.adapter.ts new file mode 100644 index 000000000..087b3a684 --- /dev/null +++ b/src/services/adapters/register.adapter.ts @@ -0,0 +1,52 @@ +/** + * 资格注册 Adapter 接口定义 + * 定义所有注册相关 API 的签名,mock 和真实实现共用此接口 + */ + +import type { + RegistrationFormData, + RegistrationRecord, + ReviewRecord, + ReviewAction, + MaterialItem, + MaterialCategoryConfig, +} from '@/ts/core/register/qualified'; + +export interface IRegisterAdapter { + /** 提交注册申请 */ + submitApplication(form: RegistrationFormData): Promise<{ code: number; data: { applicationId: string; serialNo: string }; msg?: string }>; + + /** 获取注册申请状态 */ + getApplicationStatus(applicationId: string): Promise<{ code: number; data: RegistrationRecord }>; + + /** 获取审核列表 */ + getReviewList(params: { + roleType?: string; + status?: string; + startTime?: string; + endTime?: string; + page?: number; + pageSize?: number; + }): Promise<{ code: number; data: { total: number; list: ReviewRecord[] } }>; + + /** 获取审核详情 */ + getReviewDetail(applicationId: string): Promise<{ code: number; data: ReviewRecord }>; + + /** 执行审核操作(通过/驳回) */ + performReview(action: ReviewAction): Promise<{ code: number; msg?: string }>; + + /** 上传材料文件 */ + uploadMaterial(file: File, categoryKey: string): Promise<{ code: number; data: MaterialItem }>; + + /** 删除已上传材料 */ + deleteMaterial(uid: string): Promise<{ code: number }>; + + /** 获取我的申请列表 */ + getMyApplications(params: { page?: number; pageSize?: number }): Promise<{ code: number; data: { total: number; list: RegistrationRecord[] } }>; + + /** 获取角色字段配置 */ + getRoleFieldConfig(roleType: string): Promise<{ code: number; data: import('@/ts/core/register/qualified').RoleFieldConfig[] }>; + + /** 获取角色材料配置 */ + getMaterialConfig(roleType: string): Promise<{ code: number; data: MaterialCategoryConfig }>; +} diff --git a/src/services/index.ts b/src/services/index.ts new file mode 100644 index 000000000..b887297bd --- /dev/null +++ b/src/services/index.ts @@ -0,0 +1,16 @@ +/** + * Services 统一导出 + * 开发阶段使用 mock 实现,联调阶段切换为 api 实现 + */ + +// Adapter 接口 +export type { IRegisterAdapter } from './adapters/register.adapter'; +export type { IMatchingAdapter } from './adapters/matching.adapter'; +export type { IChainAdapter } from './adapters/chain.adapter'; +export type { IFinancingAdapter } from './adapters/financing.adapter'; + +// Mock 实现(开发阶段使用) +export { mockRegisterAdapter } from './mock/register.mock'; +export { mockMatchingAdapter } from './mock/matching.mock'; +export { mockChainAdapter } from './mock/chain.mock'; +export { mockFinancingAdapter } from './mock/financing.mock'; diff --git a/src/services/mock/chain.mock.ts b/src/services/mock/chain.mock.ts new file mode 100644 index 000000000..a2925b08b --- /dev/null +++ b/src/services/mock/chain.mock.ts @@ -0,0 +1,176 @@ +/** + * 产业链可视化 Mock 实现 + * 模拟产业链图谱、关系管理、搜索等接口 + */ + +import { IChainAdapter } from '../adapters/chain.adapter'; +import { ChainNodeType, ChainRelationType } from '@/ts/core/chain/chain'; + +import type { + ChainGraph, ChainCategory, ChainNode, ChainEdge, + RelationFormData, ChainSearchParams, +} from '@/ts/core/chain/chain'; + +function delay(ms = 400): Promise { + return new Promise((resolve) => setTimeout(resolve, ms + Math.random() * 200)); +} + +function uid(): string { + return 'mock_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8); +} + +/** Mock 组织节点 */ +const mockNodes: ChainNode[] = [ + { id: 'node_1', targetId: 't_1', name: '某新能源科技公司', nodeType: ChainNodeType.Customer, industry: '新能源', scale: '200-500人', description: '专注于新能源汽车电池研发与生产', x: 400, y: 100 }, + { id: 'node_2', targetId: 't_2', name: '某锂业集团', nodeType: ChainNodeType.Supplier, industry: '矿产', scale: '1000人以上', description: '锂矿开采与碳酸锂加工', x: 100, y: 250 }, + { id: 'node_3', targetId: 't_3', name: '某电池材料公司', nodeType: ChainNodeType.Supplier, industry: '新材料', scale: '200-500人', description: '正极材料研发与制造', x: 250, y: 350 }, + { id: 'node_4', targetId: 't_4', name: '某汽车制造集团', nodeType: ChainNodeType.Customer, industry: '汽车制造', scale: '1000人以上', description: '新能源汽车整车制造', x: 550, y: 250 }, + { id: 'node_5', targetId: 't_5', name: '某充电桩运营商', nodeType: ChainNodeType.Collaborator, industry: '基础设施', scale: '100-200人', description: '充电桩建设与运营服务', x: 650, y: 400 }, + { id: 'node_6', targetId: 't_6', name: '某电解液供应商', nodeType: ChainNodeType.Supplier, industry: '化工', scale: '100-200人', description: '锂电池电解液生产', x: 100, y: 450 }, + { id: 'node_7', targetId: 't_7', name: '某电池回收公司', nodeType: ChainNodeType.Collaborator, industry: '环保', scale: '50-100人', description: '动力电池梯次利用与回收', x: 500, y: 480 }, + { id: 'node_8', targetId: 't_8', name: '某研究院', nodeType: ChainNodeType.Collaborator, industry: '科研', scale: '200-500人', description: '新能源材料基础研究', x: 300, y: 150 }, +]; + +/** Mock 关系连边 */ +const mockEdges: ChainEdge[] = [ + { id: 'edge_1', source: 'node_2', target: 'node_3', relationType: ChainRelationType.Downstream, description: '供应碳酸锂原料', createTime: '2025-06-01T00:00:00Z' }, + { id: 'edge_2', source: 'node_3', target: 'node_1', relationType: ChainRelationType.Downstream, description: '供应正极材料', createTime: '2025-08-15T00:00:00Z' }, + { id: 'edge_3', source: 'node_1', target: 'node_4', relationType: ChainRelationType.Downstream, description: '供应动力电池', createTime: '2025-10-01T00:00:00Z' }, + { id: 'edge_4', source: 'node_1', target: 'node_5', relationType: ChainRelationType.Horizontal, description: '充电设施合作', createTime: '2026-01-15T00:00:00Z' }, + { id: 'edge_5', source: 'node_6', target: 'node_1', relationType: ChainRelationType.Upstream, description: '供应电解液', createTime: '2025-07-20T00:00:00Z' }, + { id: 'edge_6', source: 'node_1', target: 'node_7', relationType: ChainRelationType.Resource, description: '电池回收合作', createTime: '2026-02-10T00:00:00Z' }, + { id: 'edge_7', source: 'node_8', target: 'node_1', relationType: ChainRelationType.Horizontal, description: '联合技术研发', createTime: '2025-05-01T00:00:00Z' }, +]; + +/** Mock 产业链图谱 */ +const mockGraph: ChainGraph = { + id: 'graph_1', + name: '新能源汽车动力电池产业链', + category: '新能源', + nodes: mockNodes, + edges: mockEdges, + centerNodeId: 'node_1', + nodeCount: mockNodes.length, + edgeCount: mockEdges.length, + updateTime: '2026-05-15T10:00:00Z', +}; + +const mockChains: ChainGraph[] = [ + mockGraph, + { + id: 'graph_2', name: '智能制造装备产业链', category: '智能制造', + nodes: [ + { id: 'n_21', targetId: 't_21', name: '某数控机床公司', nodeType: ChainNodeType.Supplier, industry: '高端装备', scale: '500-1000人' }, + { id: 'n_22', targetId: 't_22', name: '某自动化集成商', nodeType: ChainNodeType.Collaborator, industry: '自动化', scale: '200-500人' }, + { id: 'n_23', targetId: 't_23', name: '某精密部件厂', nodeType: ChainNodeType.Supplier, industry: '精密制造', scale: '100-200人' }, + ], + edges: [ + { id: 'e_21', source: 'n_21', target: 'n_22', relationType: ChainRelationType.Downstream, description: '设备供应' }, + { id: 'e_22', source: 'n_23', target: 'n_21', relationType: ChainRelationType.Upstream, description: '零部件供应' }, + ], + nodeCount: 3, edgeCount: 2, updateTime: '2026-05-10T08:00:00Z', + }, + { + id: 'graph_3', name: '数字经济产业协作链', category: '数字经济', + nodes: [ + { id: 'n_31', targetId: 't_31', name: '某云计算公司', nodeType: ChainNodeType.Collaborator, industry: '云计算', scale: '1000人以上' }, + { id: 'n_32', targetId: 't_32', name: '某大数据平台', nodeType: ChainNodeType.Collaborator, industry: '大数据', scale: '200-500人' }, + { id: 'n_33', targetId: 't_33', name: '某AI解决方案商', nodeType: ChainNodeType.Customer, industry: '人工智能', scale: '200-500人' }, + ], + edges: [ + { id: 'e_31', source: 'n_31', target: 'n_33', relationType: ChainRelationType.Horizontal, description: '技术合作' }, + { id: 'e_32', source: 'n_32', target: 'n_31', relationType: ChainRelationType.Resource, description: '数据共享' }, + ], + nodeCount: 3, edgeCount: 2, updateTime: '2026-05-12T14:00:00Z', + }, +]; + +const mockCategories: ChainCategory[] = [ + { id: 'cat_1', name: '新能源', chains: [mockChains[0]] }, + { id: 'cat_2', name: '智能制造', chains: [mockChains[1]] }, + { id: 'cat_3', name: '数字经济', chains: [mockChains[2]] }, +]; + +export class MockChainAdapter implements IChainAdapter { + async getChainCategories() { + await delay(); + return { code: 200, data: mockCategories }; + } + + async getChainGraph(graphId: string) { + await delay(); + const graph = mockChains.find((g) => g.id === graphId); + return graph ? { code: 200, data: graph } : { code: 404, data: null as unknown as ChainGraph }; + } + + async getCenteredGraph(targetId: string) { + await delay(); + // 返回以指定组织为中心的一跳关联图谱 + const node = mockNodes.find((n) => n.targetId === targetId); + if (!node) return { code: 404, data: null as unknown as ChainGraph }; + const relatedEdges = mockEdges.filter((e) => e.source === node.id || e.target === node.id); + const relatedNodeIds = new Set(relatedEdges.flatMap((e) => [e.source, e.target])); + const relatedNodes = mockNodes.filter((n) => relatedNodeIds.has(n.id)); + return { + code: 200, + data: { + id: uid(), name: `${node.name} 产业链`, category: node.industry || '', + nodes: relatedNodes, edges: relatedEdges, + centerNodeId: node.id, nodeCount: relatedNodes.length, + edgeCount: relatedEdges.length, updateTime: new Date().toISOString(), + }, + }; + } + + async getMyChainGraph() { + await delay(); + // 模拟返回当前用户所在组织的图谱 + return { code: 200, data: mockGraph }; + } + + async searchNodes(params: ChainSearchParams) { + await delay(200); + const kw = params.keyword.toLowerCase(); + const results = mockNodes.filter((n) => n.name.toLowerCase().includes(kw)); + return { code: 200, data: results }; + } + + async addRelation(data: RelationFormData) { + await delay(); + const newEdge: ChainEdge = { + id: uid(), + source: `node_${data.sourceTargetId}`, + target: `node_${data.targetTargetId}`, + relationType: data.relationType, + description: data.description, + createTime: new Date().toISOString(), + }; + mockEdges.push(newEdge); + return { code: 200, data: { relationId: newEdge.id } }; + } + + async updateRelation(relationId: string, data: Partial) { + await delay(); + const edge = mockEdges.find((e) => e.id === relationId); + if (edge) { + if (data.relationType) edge.relationType = data.relationType; + if (data.description !== undefined) edge.description = data.description; + } + return { code: 200 }; + } + + async deleteRelation(relationId: string) { + await delay(); + const idx = mockEdges.findIndex((e) => e.id === relationId); + if (idx >= 0) mockEdges.splice(idx, 1); + return { code: 200 }; + } + + async getNodeDetail(targetId: string) { + await delay(); + const node = mockNodes.find((n) => n.targetId === targetId); + return node ? { code: 200, data: node } : { code: 404, data: null as unknown as ChainNode }; + } +} + +export const mockChainAdapter = new MockChainAdapter(); diff --git a/src/services/mock/financing.mock.ts b/src/services/mock/financing.mock.ts new file mode 100644 index 000000000..9e41ccd65 --- /dev/null +++ b/src/services/mock/financing.mock.ts @@ -0,0 +1,200 @@ +/** + * 融资链 Mock 实现 + * 模拟融资产品、申请、进度、还款等接口 + */ + +import { IFinancingAdapter } from '../adapters/financing.adapter'; +import { FinancingType, LoanStatus, RepayStatus, RepaymentMethod } from '@/ts/core/financing/financing'; + +import type { + FinancingProduct, LoanApplicationForm, LoanApplication, + RepaymentInstallment, RepaymentRecord, +} from '@/ts/core/financing/financing'; + +function delay(ms = 500): Promise { + return new Promise((resolve) => setTimeout(resolve, ms + Math.random() * 200)); +} + +function uid(): string { + return 'mock_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8); +} + +/** Mock 融资产品 */ +const mockProducts: FinancingProduct[] = [ + { + id: 'prod_1', name: '小微企业信用贷', institutionName: '某商业银行', type: FinancingType.Credit, + rateMin: 3.5, rateMax: 5.8, amountMin: 1, amountMax: 100, termMin: 3, termMax: 36, + repaymentMethod: RepaymentMethod.EqualInstallment, + rateDescription: '年化利率根据企业信用评级确定,优质客户可享最低 3.5%', + conditions: ['企业成立满2年', '年营业收入不低于100万元', '企业及法定代表人信用良好', '无重大违法违规记录'], + requiredMaterials: ['营业执照', '最近一年财务报表', '法定代表人身份证', '企业征信报告'], + feeDescription: '无手续费,提前还款免违约金', + tags: ['审批快', '纯信用', '随借随还'], + }, + { + id: 'prod_2', name: '房产抵押经营贷', institutionName: '某国有银行', type: FinancingType.Mortgage, + rateMin: 2.8, rateMax: 4.5, amountMin: 10, amountMax: 500, termMin: 12, termMax: 60, + repaymentMethod: RepaymentMethod.EqualInstallment, + rateDescription: 'LPR 基准利率下浮,优质客户可享 2.8% 起', + conditions: ['企业成立满1年', '抵押物权属清晰', '企业经营状况良好', '法定代表人无不良征信'], + requiredMaterials: ['营业执照', '最近两年财务报表', '法定代表人身份证', '房产证/不动产权证', '企业征信报告'], + feeDescription: '评估费 0.1%,抵押登记费实报实销', + tags: ['低利率', '额度高', '期限长'], + }, + { + id: 'prod_3', name: '供应链金融贷', institutionName: '某股份制银行', type: FinancingType.SupplyChain, + rateMin: 3.0, rateMax: 5.0, amountMin: 5, amountMax: 300, termMin: 3, termMax: 24, + repaymentMethod: RepaymentMethod.InterestFirst, + rateDescription: '基于核心企业信用,供应商/经销商可享优惠利率', + conditions: ['与核心企业有稳定合作关系', '合作年限不低于1年', '应收账款/订单真实有效'], + requiredMaterials: ['营业执照', '最近一年财务报表', '与核心企业的购销合同', '应收账款明细', '法定代表人身份证'], + feeDescription: '无手续费', + tags: ['供应链', '核心企业背书', '线上申请'], + }, + { + id: 'prod_4', name: '科技型企业信用贷', institutionName: '某城商行', type: FinancingType.Credit, + rateMin: 3.2, rateMax: 4.8, amountMin: 10, amountMax: 200, termMin: 6, termMax: 36, + repaymentMethod: RepaymentMethod.EqualInstallment, + rateDescription: '高新技术企业专享优惠利率', + conditions: ['高新技术企业认定', '拥有自主知识产权', '年营业收入不低于300万元'], + requiredMaterials: ['营业执照', '最近两年财务报表', '高新技术企业证书', '知识产权清单', '法定代表人身份证'], + feeDescription: '无手续费', + tags: ['科技金融', '高新企业', '知识产权'], + }, + { + id: 'prod_5', name: '设备融资租赁贷', institutionName: '某金融租赁公司', type: FinancingType.Mortgage, + rateMin: 4.0, rateMax: 6.5, amountMin: 20, amountMax: 500, termMin: 12, termMax: 60, + repaymentMethod: RepaymentMethod.EqualInstallment, + rateDescription: '以设备作为租赁物,融资比例最高 80%', + conditions: ['设备价值不低于25万元', '设备权属清晰', '企业经营年限不低于2年'], + requiredMaterials: ['营业执照', '最近一年财务报表', '设备清单及价值评估', '法定代表人身份证'], + feeDescription: '租赁服务费 1%,设备评估费实报实销', + tags: ['设备融资', '融资租赁', '长期限'], + }, + { + id: 'prod_6', name: '订单融资贷', institutionName: '某商业银行', type: FinancingType.SupplyChain, + rateMin: 3.5, rateMax: 5.5, amountMin: 5, amountMax: 100, termMin: 1, termMax: 12, + repaymentMethod: RepaymentMethod.LumpSum, + rateDescription: '基于已签订单的短期融资,订单金额最高 80%', + conditions: ['订单真实有效', '采购方资信良好', '订单金额不低于10万元'], + requiredMaterials: ['营业执照', '订单合同', '采购方基本信息', '法定代表人身份证'], + feeDescription: '无手续费', + tags: ['订单融资', '短期', '快速放款'], + }, +]; + +/** Mock 申请记录 */ +const mockApplications: LoanApplication[] = [ + { + id: 'app_1', serialNo: 'LOAN20260510001', productId: 'prod_1', productName: '小微企业信用贷', + amount: 50, term: 24, status: LoanStatus.Reviewing, + applyTime: '2026-05-10T09:00:00Z', estimatedFinishTime: '2026-05-17T18:00:00Z', + }, + { + id: 'app_2', serialNo: 'LOAN20260508002', productId: 'prod_3', productName: '供应链金融贷', + amount: 200, term: 12, status: LoanStatus.Disbursed, + applyTime: '2026-04-20T10:00:00Z', + }, +]; + +/** Mock 还款计划(app_2 已放款的) */ +const mockRepaymentPlan: RepaymentInstallment[] = [ + { period: 1, dueDate: '2026-06-20', principal: 0, interest: 8333.33, total: 8333.33, status: RepayStatus.Pending }, + { period: 2, dueDate: '2026-07-20', principal: 0, interest: 8333.33, total: 8333.33, status: RepayStatus.Pending }, + { period: 3, dueDate: '2026-08-20', principal: 0, interest: 8333.33, total: 8333.33, status: RepayStatus.Pending }, + { period: 4, dueDate: '2026-09-20', principal: 0, interest: 8333.33, total: 8333.33, status: RepayStatus.Paid, paidDate: '2026-09-19' }, + { period: 5, dueDate: '2026-10-20', principal: 0, interest: 8333.33, total: 8333.33, status: RepayStatus.Pending }, + { period: 6, dueDate: '2026-11-20', principal: 0, interest: 8333.33, total: 8333.33, status: RepayStatus.Pending }, + { period: 7, dueDate: '2026-12-20', principal: 0, interest: 8333.33, total: 8333.33, status: RepayStatus.Pending }, + { period: 8, dueDate: '2027-01-20', principal: 0, interest: 8333.33, total: 8333.33, status: RepayStatus.Pending }, + { period: 9, dueDate: '2027-02-20', principal: 0, interest: 8333.33, total: 8333.33, status: RepayStatus.Pending }, + { period: 10, dueDate: '2027-03-20', principal: 0, interest: 8333.33, total: 8333.33, status: RepayStatus.Pending }, + { period: 11, dueDate: '2027-04-20', principal: 0, interest: 8333.33, total: 8333.33, status: RepayStatus.Pending }, + { period: 12, dueDate: '2027-05-20', principal: 2000000, interest: 8333.33, total: 2008333.33, status: RepayStatus.Pending }, +]; + +/** Mock 还款记录 */ +const mockRepaymentRecords: RepaymentRecord[] = [ + { id: 'rec_1', applicationId: 'app_2', period: 4, amount: 8333.33, method: 'bank_transfer', payDate: '2026-09-19T10:30:00Z', status: 'success' }, +]; + +export class MockFinancingAdapter implements IFinancingAdapter { + async getProductList(params) { + await delay(); + let list = [...mockProducts]; + if (params.type) list = list.filter((p) => p.type === params.type); + if (params.amountMin) list = list.filter((p) => p.amountMax >= params.amountMin!); + if (params.amountMax) list = list.filter((p) => p.amountMin <= params.amountMax!); + if (params.sortBy === 'rate') list.sort((a, b) => a.rateMin - b.rateMin); + const page = params.page || 1; + const pageSize = params.pageSize || 10; + const start = (page - 1) * pageSize; + return { code: 200, data: { total: list.length, list: list.slice(start, start + pageSize) } }; + } + + async getProductDetail(productId: string) { + await delay(); + const p = mockProducts.find((p) => p.id === productId); + return p ? { code: 200, data: p } : { code: 404, data: null as unknown as FinancingProduct }; + } + + async submitApplication(form: LoanApplicationForm) { + await delay(800); + const newApp: LoanApplication = { + id: uid(), + serialNo: 'LOAN' + new Date().toISOString().replace(/\D/g, '').slice(0, 14), + productId: form.productId, + productName: mockProducts.find((p) => p.id === form.productId)?.name || '', + amount: form.amount, + term: form.term, + status: LoanStatus.Submitted, + applyTime: new Date().toISOString(), + estimatedFinishTime: new Date(Date.now() + 7 * 86400000).toISOString(), + }; + mockApplications.unshift(newApp); + return { code: 200, data: { applicationId: newApp.id, serialNo: newApp.serialNo } }; + } + + async getMyApplications(params) { + await delay(); + const page = params.page || 1; + const pageSize = params.pageSize || 10; + const start = (page - 1) * pageSize; + return { code: 200, data: { total: mockApplications.length, list: mockApplications.slice(start, start + pageSize) } }; + } + + async getApplicationDetail(applicationId: string) { + await delay(); + const app = mockApplications.find((a) => a.id === applicationId); + return app ? { code: 200, data: app } : { code: 404, data: null as unknown as LoanApplication }; + } + + async getRepaymentPlan(applicationId: string) { + await delay(); + if (applicationId === 'app_2') return { code: 200, data: mockRepaymentPlan }; + // 返回空计划 + return { code: 200, data: [] }; + } + + async getRepaymentRecords(applicationId: string) { + await delay(); + const records = mockRepaymentRecords.filter((r) => r.applicationId === applicationId); + return { code: 200, data: records }; + } + + async performRepayment(applicationId: string, period: number, method: 'bank_transfer' | 'auto_debit') { + await delay(800); + const record: RepaymentRecord = { + id: uid(), applicationId, period, + amount: mockRepaymentPlan.find((r) => r.period === period)?.total || 0, + method, payDate: new Date().toISOString(), status: 'success', + }; + mockRepaymentRecords.push(record); + // 更新还款计划状态 + const plan = mockRepaymentPlan.find((r) => r.period === period); + if (plan) { plan.status = RepayStatus.Paid; plan.paidDate = new Date().toISOString(); } + return { code: 200, msg: '还款成功' }; + } +} + +export const mockFinancingAdapter = new MockFinancingAdapter(); diff --git a/src/services/mock/matching.mock.ts b/src/services/mock/matching.mock.ts new file mode 100644 index 000000000..3a0c0e0bc --- /dev/null +++ b/src/services/mock/matching.mock.ts @@ -0,0 +1,168 @@ +/** + * 对接大厅 Mock 实现 + * 模拟对接需求、邀请、统计数据等接口 + */ + +import { IMatchingAdapter } from '../adapters/matching.adapter'; +import { BizBehavior, ResourceType, MatchStatus } from '@/ts/core/matching/match'; + +import type { MatchDemand, MatchInvitation, MatchRecord, HallStatistics, BehaviorCardData } from '@/ts/core/matching/match'; + +function delay(ms = 500): Promise { + return new Promise((resolve) => setTimeout(resolve, ms + Math.random() * 200)); +} + +function uid(): string { + return 'mock_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8); +} + +/** mock 对接需求 */ +const mockDemands: MatchDemand[] = [ + { + id: 'demand_1', title: '寻找新能源电池供应商', behavior: BizBehavior.Match, + resourceType: ResourceType.Goods, description: '需要寻找具备量产能力的锂电池供应商,年需求 50 万组', + budgetMin: 1000000, budgetMax: 5000000, publisherId: 'user_1', publisherName: '某新能源科技公司', + publishTime: '2026-05-10T08:00:00Z', status: MatchStatus.Published, responseCount: 3, + }, + { + id: 'demand_2', title: '共享智能制造专利技术', behavior: BizBehavior.Share, + resourceType: ResourceType.Knowledge, description: '共享 5 项智能制造相关专利,寻求产业化合作伙伴', + publisherId: 'user_2', publisherName: '某研究院', publishTime: '2026-05-09T10:00:00Z', + status: MatchStatus.InProgress, responseCount: 7, + }, + { + id: 'demand_3', title: '链接环保项目资源', behavior: BizBehavior.Link, + resourceType: ResourceType.Project, description: '链接污水处理项目上下游资源', + publisherId: 'user_3', publisherName: '某环保工程公司', publishTime: '2026-05-08T14:00:00Z', + status: MatchStatus.Published, responseCount: 1, + }, + { + id: 'demand_4', title: '大宗商品交易——钢材', behavior: BizBehavior.Trade, + resourceType: ResourceType.Goods, description: '求购 Q235 钢材 500 吨', + budgetMin: 2000000, budgetMax: 2500000, publisherId: 'user_4', publisherName: '某建筑集团', + publishTime: '2026-05-07T09:00:00Z', status: MatchStatus.Completed, responseCount: 12, + }, + { + id: 'demand_5', title: '寻找供应链金融服务', behavior: BizBehavior.Match, + resourceType: ResourceType.Capital, description: '制造业企业寻求 500 万元供应链融资', + budgetMin: 5000000, budgetMax: 5000000, publisherId: 'user_5', publisherName: '某制造有限公司', + publishTime: '2026-05-06T16:00:00Z', status: MatchStatus.InProgress, responseCount: 5, + }, + { + id: 'demand_6', title: 'IT 运维服务外包', behavior: BizBehavior.Trade, + resourceType: ResourceType.Service, description: '需要专业的 IT 基础设施运维服务团队', + budgetMin: 200000, budgetMax: 500000, publisherId: 'user_6', publisherName: '某互联网公司', + publishTime: '2026-05-05T11:00:00Z', status: MatchStatus.Published, responseCount: 8, + }, +]; + +/** mock 邀请 */ +const mockInvitations: MatchInvitation[] = [ + { + id: 'inv_1', demandId: 'demand_1', demandTitle: '寻找新能源电池供应商', + fromId: 'user_7', fromName: '某电池科技公司', toId: 'user_1', toName: '某新能源科技公司', + status: 'pending', message: '我司具备量产能力,年产能 80 万组,期待合作', + createTime: '2026-05-11T09:00:00Z', + }, + { + id: 'inv_2', demandId: 'demand_1', demandTitle: '寻找新能源电池供应商', + fromId: 'user_8', fromName: '某能源集团', toId: 'user_1', toName: '某新能源科技公司', + status: 'accepted', message: '可提供磷酸铁锂电池定制方案', + createTime: '2026-05-11T10:00:00Z', handleTime: '2026-05-12T08:00:00Z', + }, + { + id: 'inv_3', demandId: 'demand_5', demandTitle: '寻找供应链金融服务', + fromId: 'user_9', fromName: '某银行支行', toId: 'user_5', toName: '某制造有限公司', + status: 'pending', message: '我行可提供供应链金融专项贷款,利率优惠', + createTime: '2026-05-10T14:00:00Z', + }, +]; + +const mockStats: HallStatistics = { + tradeCount: 23, + matchCount: 5, + shareCount: 12, +}; + +const behaviorCards: BehaviorCardData[] = [ + { behavior: BizBehavior.Trade, icon: 'icon-trade', title: '交易', description: '买卖商品与服务', publishedCount: 156, actionLabel: '我要交易', secondaryActionLabel: '我的交易' }, + { behavior: BizBehavior.Match, icon: 'icon-match', title: '对接', description: '寻找合作伙伴,匹配供需', publishedCount: 48, actionLabel: '我要对接', secondaryActionLabel: '查看对接' }, + { behavior: BizBehavior.Link, icon: 'icon-link', title: '链接', description: '资源关联与关系链接', publishedCount: 32, actionLabel: '资源链接' }, + { behavior: BizBehavior.Share, icon: 'icon-share', title: '共享', description: '知识与资源共享', publishedCount: 89, actionLabel: '我要共享', secondaryActionLabel: '浏览共享' }, +]; + +export class MockMatchingAdapter implements IMatchingAdapter { + async getHallStatistics() { await delay(); return { code: 200, data: mockStats }; } + async getBehaviorCards() { await delay(); return { code: 200, data: behaviorCards }; } + + async publishDemand(demand) { + await delay(); + const newDemand: MatchDemand = { + ...demand, id: uid(), publishTime: new Date().toISOString(), + status: MatchStatus.Published, responseCount: 0, + }; + mockDemands.unshift(newDemand); + return { code: 200, data: { demandId: newDemand.id } }; + } + + async getDemandList(params) { + await delay(); + let list = [...mockDemands]; + if (params.behavior) list = list.filter((d) => d.behavior === params.behavior); + if (params.resourceType) list = list.filter((d) => d.resourceType === params.resourceType); + if (params.status) list = list.filter((d) => d.status === params.status); + const page = params.page || 1; + const pageSize = params.pageSize || 10; + const start = (page - 1) * pageSize; + return { code: 200, data: { total: list.length, list: list.slice(start, start + pageSize) } }; + } + + async getDemandDetail(demandId: string) { + await delay(); + const d = mockDemands.find((m) => m.id === demandId); + return d ? { code: 200, data: d } : { code: 404, data: null as unknown as MatchDemand }; + } + + async sendInvitation(invitation) { + await delay(); + const newInv: MatchInvitation = { ...invitation, id: uid(), status: 'pending', createTime: new Date().toISOString() }; + mockInvitations.push(newInv); + return { code: 200, data: { invitationId: newInv.id } }; + } + + async handleInvitation(invitationId: string, action: 'accepted' | 'rejected') { + await delay(); + const inv = mockInvitations.find((i) => i.id === invitationId); + if (inv) { inv.status = action; inv.handleTime = new Date().toISOString(); } + return { code: 200 }; + } + + async getMyDemands(params) { + await delay(); + const page = params.page || 1; + const pageSize = params.pageSize || 10; + const start = (page - 1) * pageSize; + return { code: 200, data: { total: mockDemands.length, list: mockDemands.slice(start, start + pageSize) } }; + } + + async getReceivedInvitations(params) { + await delay(); + const page = params.page || 1; + const pageSize = params.pageSize || 10; + const start = (page - 1) * pageSize; + return { code: 200, data: { total: mockInvitations.length, list: mockInvitations.slice(start, start + pageSize) } }; + } + + async getMatchRecords(params) { + await delay(); + const records: MatchRecord[] = [ + { id: 'rec_1', demandId: 'demand_4', partyA: 'user_4', partyB: 'user_10', startTime: '2026-05-08T00:00:00Z', status: MatchStatus.Completed, remark: '交易完成' }, + ]; + const page = params.page || 1; + const pageSize = params.pageSize || 10; + const start = (page - 1) * pageSize; + return { code: 200, data: { total: records.length, list: records.slice(start, start + pageSize) } }; + } +} + +export const mockMatchingAdapter = new MockMatchingAdapter(); diff --git a/src/services/mock/register.mock.ts b/src/services/mock/register.mock.ts new file mode 100644 index 000000000..a73818724 --- /dev/null +++ b/src/services/mock/register.mock.ts @@ -0,0 +1,272 @@ +/** + * 资格注册 Mock 实现 + * 模拟注册申请、审核列表、审核操作、材料上传等接口 + */ + +import { IRegisterAdapter } from '../adapters/register.adapter'; +import { RoleType, ReviewStatus } from '@/ts/core/register/qualified'; + +import type { + RegistrationFormData, + RegistrationRecord, + ReviewRecord, + MaterialItem, + MaterialCategoryConfig, + RoleFieldConfig, +} from '@/ts/core/register/qualified'; + +/** 模拟网络延迟 */ +function delay(ms = 600): Promise { + return new Promise((resolve) => setTimeout(resolve, ms + Math.random() * 300)); +} + +/** 生成唯一 ID */ +function uid(): string { + return 'mock_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8); +} + +/** 内存 mock 数据存储 */ +const store = { + applications: new Map(), + reviews: new Map(), + materials: new Map(), +}; + +/** 预设驳回理由 */ +export const presetRejectReasons = [ + '材料不齐全,请补充后重新提交', + '申请信息填写有误,请核对后修改', + '不符合当前角色的注册条件', + '证件照片不清晰,请重新上传', + '企业资质已过期,请更新后提交', +]; + +/** 会员字段配置 */ +const memberFields: RoleFieldConfig[] = [ + { name: 'name', label: '姓名', type: 'text', required: true, placeholder: '请输入真实姓名', roleGroup: 'common' }, + { name: 'phone', label: '手机号', type: 'phone', required: true, placeholder: '请输入手机号', roleGroup: 'common' }, + { name: 'verifyCode', label: '验证码', type: 'text', required: true, placeholder: '请输入短信验证码', roleGroup: 'common' }, + { name: 'password', label: '设置密码', type: 'password', required: true, placeholder: '8-20位,含字母+数字', roleGroup: 'common' }, + { name: 'confirmPassword', label: '确认密码', type: 'password', required: true, placeholder: '请再次输入密码', roleGroup: 'common' }, + { name: 'organization', label: '所属单位', type: 'text', required: true, placeholder: '请输入单位全称', roleGroup: 'member' }, + { name: 'position', label: '职务', type: 'text', required: true, placeholder: '请输入职务', roleGroup: 'member' }, + { name: 'referrer', label: '推荐人', type: 'text', required: false, placeholder: '选填,推荐人姓名或手机号', roleGroup: 'member' }, + { name: 'agreeTerms', label: '同意协议', type: 'text', required: true, roleGroup: 'common' }, +]; + +/** 研究员字段配置 */ +const researcherFields: RoleFieldConfig[] = [ + { name: 'name', label: '姓名', type: 'text', required: true, placeholder: '请输入真实姓名', roleGroup: 'common' }, + { name: 'phone', label: '手机号', type: 'phone', required: true, placeholder: '请输入手机号', roleGroup: 'common' }, + { name: 'verifyCode', label: '验证码', type: 'text', required: true, placeholder: '请输入短信验证码', roleGroup: 'common' }, + { name: 'password', label: '设置密码', type: 'password', required: true, placeholder: '8-20位,含字母+数字', roleGroup: 'common' }, + { name: 'confirmPassword', label: '确认密码', type: 'password', required: true, placeholder: '请再次输入密码', roleGroup: 'common' }, + { name: 'education', label: '学历', type: 'select', required: true, placeholder: '请选择最高学历', roleGroup: 'researcher', options: [ + { label: '本科', value: '本科' }, { label: '硕士', value: '硕士' }, { label: '博士', value: '博士' }, { label: '博士后', value: '博士后' }, + ]}, + { name: 'researchDirection', label: '研究方向', type: 'text', required: true, placeholder: '请输入研究方向', roleGroup: 'researcher' }, + { name: 'title', label: '职称', type: 'select', required: true, placeholder: '请选择职称', roleGroup: 'researcher', options: [ + { label: '教授', value: '教授' }, { label: '副教授', value: '副教授' }, { label: '研究员', value: '研究员' }, + { label: '高级工程师', value: '高级工程师' }, { label: '其他', value: '其他' }, + ]}, + { name: 'representativeWork', label: '代表作链接', type: 'url', required: false, placeholder: '选填,代表作访问链接', roleGroup: 'researcher' }, + { name: 'institution', label: '所属机构', type: 'text', required: true, placeholder: '请输入所属机构全称', roleGroup: 'researcher' }, + { name: 'agreeTerms', label: '同意协议', type: 'text', required: true, roleGroup: 'common' }, +]; + +/** 合伙人字段配置 */ +const partnerFields: RoleFieldConfig[] = [ + { name: 'name', label: '姓名', type: 'text', required: true, placeholder: '请输入真实姓名', roleGroup: 'common' }, + { name: 'phone', label: '手机号', type: 'phone', required: true, placeholder: '请输入手机号', roleGroup: 'common' }, + { name: 'verifyCode', label: '验证码', type: 'text', required: true, placeholder: '请输入短信验证码', roleGroup: 'common' }, + { name: 'password', label: '设置密码', type: 'password', required: true, placeholder: '8-20位,含字母+数字', roleGroup: 'common' }, + { name: 'confirmPassword', label: '确认密码', type: 'password', required: true, placeholder: '请再次输入密码', roleGroup: 'common' }, + { name: 'companyName', label: '企业名称', type: 'text', required: true, placeholder: '请输入企业营业执照全称', roleGroup: 'partner' }, + { name: 'creditCode', label: '统一社会信用代码', type: 'text', required: true, placeholder: '18位统一社会信用代码', roleGroup: 'partner' }, + { name: 'mainBusiness', label: '主营业务', type: 'textarea', required: true, placeholder: '请描述企业主营业务范围', roleGroup: 'partner' }, + { name: 'providedResources', label: '可提供资源', type: 'tags', required: false, placeholder: '输入后回车添加', roleGroup: 'partner' }, + { name: 'requiredResources', label: '所需资源', type: 'tags', required: false, placeholder: '输入后回车添加', roleGroup: 'partner' }, + { name: 'agreeTerms', label: '同意协议', type: 'text', required: true, roleGroup: 'common' }, +]; + +/** 材料配置 */ +const materialConfigs: Record = { + [RoleType.Member]: { + roleType: RoleType.Member, + categories: [ + { key: 'id_card', name: '身份证(正反面)', required: true, description: '请上传身份证正反面清晰照片', maxFiles: 2 }, + { key: 'resume', name: '个人简介/简历', required: false, description: '选传,Word 或 PDF 格式', maxFiles: 1 }, + ], + acceptFileTypes: ['image/jpeg', 'image/png', 'image/jpg', 'application/pdf'], + maxFileSize: 10, + maxTotalFiles: 3, + }, + [RoleType.Researcher]: { + roleType: RoleType.Researcher, + categories: [ + { key: 'id_card', name: '身份证(正反面)', required: true, description: '请上传身份证正反面清晰照片', maxFiles: 2 }, + { key: 'education', name: '学历/学位证明', required: true, description: '请上传学历学位证书扫描件', maxFiles: 2 }, + { key: 'title_cert', name: '职称证书', required: false, description: '选传,职称证书扫描件', maxFiles: 2 }, + { key: 'resume', name: '个人简介/简历', required: true, description: '请上传个人简历', maxFiles: 1 }, + ], + acceptFileTypes: ['image/jpeg', 'image/png', 'image/jpg', 'application/pdf', 'application/msword'], + maxFileSize: 10, + maxTotalFiles: 7, + }, + [RoleType.Partner]: { + roleType: RoleType.Partner, + categories: [ + { key: 'id_card', name: '身份证(正反面)', required: true, description: '请上传法定代表人身份证', maxFiles: 2 }, + { key: 'business_license', name: '营业执照', required: true, description: '请上传企业营业执照扫描件', maxFiles: 1 }, + { key: 'legal_id', name: '法定代表人身份证', required: true, description: '请上传法定代表人身份证正反面', maxFiles: 2 }, + { key: 'qualifications', name: '资质/许可证', required: false, description: '选传,行业相关资质证书', maxFiles: 3 }, + { key: 'company_intro', name: '企业介绍', required: false, description: '选传,企业介绍材料', maxFiles: 1 }, + ], + acceptFileTypes: ['image/jpeg', 'image/png', 'image/jpg', 'application/pdf'], + maxFileSize: 10, + maxTotalFiles: 9, + }, +}; + +export class MockRegisterAdapter implements IRegisterAdapter { + async submitApplication(form: RegistrationFormData) { + await delay(); + const id = uid(); + const record: RegistrationRecord = { + id, + serialNo: 'REG' + Date.now(), + applyTime: new Date().toISOString(), + roleType: form.roleType, + formData: form, + materials: form.materials || [], + reviewStatus: ReviewStatus.Pending, + }; + store.applications.set(id, record); + + const review: ReviewRecord = { + id: uid(), + applicationId: id, + applicantName: form.name, + roleType: form.roleType, + status: ReviewStatus.Pending, + }; + store.reviews.set(id, review); + + return { code: 200, data: { applicationId: id, serialNo: record.serialNo } }; + } + + async getApplicationStatus(applicationId: string) { + await delay(300); + const app = store.applications.get(applicationId); + if (!app) return { code: 404, data: null as unknown as RegistrationRecord }; + return { code: 200, data: app }; + } + + async getReviewList(params) { + await delay(); + let list = Array.from(store.reviews.values()); + if (params.roleType) list = list.filter((r) => r.roleType === params.roleType); + if (params.status) list = list.filter((r) => r.status === params.status); + + // 补充 mock 数据 + if (list.length === 0) { + list = generateMockReviews(); + } + + const page = params.page || 1; + const pageSize = params.pageSize || 10; + const start = (page - 1) * pageSize; + return { code: 200, data: { total: list.length, list: list.slice(start, start + pageSize) } }; + } + + async getReviewDetail(applicationId: string) { + await delay(); + const review = store.reviews.get(applicationId); + if (!review) { + const fallback = generateMockReviews().find((r) => r.applicationId === applicationId); + if (!fallback) return { code: 404, data: null as unknown as ReviewRecord }; + return { code: 200, data: fallback }; + } + return { code: 200, data: review }; + } + + async performReview(action) { + await delay(); + const review = store.reviews.get(action.applicationId); + if (review) { + review.status = action.action === 'approve' ? ReviewStatus.Approved : ReviewStatus.Rejected; + review.reviewer = '当前管理员'; + review.reviewTime = new Date().toISOString(); + review.remark = action.remark; + review.rejectReason = action.rejectReason; + } + return { code: 200, msg: action.action === 'approve' ? '审核通过' : '已驳回' }; + } + + async uploadMaterial(file: File, categoryKey: string) { + await delay(800); + const item: MaterialItem = { + uid: uid(), + name: file.name, + size: file.size, + type: file.type, + status: 'done', + percent: 100, + url: URL.createObjectURL(file), + categoryKey, + uploadTime: new Date().toISOString(), + }; + store.materials.set(item.uid, item); + return { code: 200, data: item }; + } + + async deleteMaterial(uid: string) { + await delay(300); + store.materials.delete(uid); + return { code: 200 }; + } + + async getMyApplications(params) { + await delay(); + const list = Array.from(store.applications.values()); + const page = params.page || 1; + const pageSize = params.pageSize || 10; + const start = (page - 1) * pageSize; + return { code: 200, data: { total: list.length, list: list.slice(start, start + pageSize) } }; + } + + async getRoleFieldConfig(roleType: string) { + await delay(200); + const configs: Record = { + [RoleType.Member]: memberFields, + [RoleType.Researcher]: researcherFields, + [RoleType.Partner]: partnerFields, + }; + return { code: 200, data: configs[roleType] || memberFields }; + } + + async getMaterialConfig(roleType: string) { + await delay(200); + return { code: 200, data: materialConfigs[roleType] || materialConfigs[RoleType.Member] }; + } +} + +/** 生成 mock 审核记录 */ +function generateMockReviews(): ReviewRecord[] { + const names = ['张三', '李四', '王五', '赵六', '钱七', '孙八', '周九', '吴十']; + const roles = [RoleType.Member, RoleType.Researcher, RoleType.Partner]; + const statuses = [ReviewStatus.Pending, ReviewStatus.Pending, ReviewStatus.Approved, ReviewStatus.Rejected]; + return names.map((name, i) => ({ + id: `mock_review_${i}`, + applicationId: `mock_app_${i}`, + applicantName: name, + roleType: roles[i % 3], + status: statuses[i % 4], + reviewer: statuses[i % 4] !== ReviewStatus.Pending ? '管理员' : undefined, + reviewTime: statuses[i % 4] !== ReviewStatus.Pending ? new Date(Date.now() - (i * 86400000)).toISOString() : undefined, + remark: statuses[i % 4] === ReviewStatus.Rejected ? presetRejectReasons[i % presetRejectReasons.length] : undefined, + })); +} + +/** 导出单例 */ +export const mockRegisterAdapter = new MockRegisterAdapter(); diff --git a/src/styles/common.mixins.less b/src/styles/common.mixins.less new file mode 100644 index 000000000..250c58bf9 --- /dev/null +++ b/src/styles/common.mixins.less @@ -0,0 +1,305 @@ +/** + * 公共样式变量 与 mixin + * 负责人: P1 架构 + * 使用者: P2-P7 全员 + * + * 此文件提供新模块通用的 Less 变量、mixin 和工具类 + * 与 global.less 互补 —— global.less 管理全局基础样式,此处侧重新业务模块的卡片、表单、标签页等样式 + */ + +// ===== 颜色变量 ===== +// 角色颜色 +@role-member-color: #1890ff; +@role-researcher-color: #722ed1; +@role-partner-color: #fa8c16; + +// 状态颜色 +@status-pending-color: #faad14; +@status-approved-color: #52c41a; +@status-rejected-color: #ff4d4f; +@status-processing-color: #1890ff; + +// 关系类型颜色(产业链) +@chain-upstream-color: #1890ff; +@chain-downstream-color: #52c41a; +@chain-horizontal-color: #fa8c16; +@chain-resource-color: #8c8c8c; + +// 融资类型颜色 +@financing-credit-color: #1890ff; +@financing-mortgage-color: #fa8c16; +@financing-supply-chain-color: #52c41a; + +// 通用功能色 +@text-secondary: rgba(0, 0, 0, 0.45); +@text-primary: rgba(0, 0, 0, 0.85); +@border-light: #f0f0f0; +@bg-light: #fafafa; +@bg-card-hover: #f5f5f5; + +// ===== 卡片 mixin ===== +.card-hover() { + cursor: pointer; + transition: all 0.3s ease; + border: 1px solid @border-light; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + + &:hover { + transform: translateY(-4px); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); + border-color: #d9d9d9; + } +} + +// 模块卡片容器 +.module-card() { + background: @component-background; + border-radius: @border-radius-base; + padding: 16px; + margin-bottom: 16px; +} + +// 卡片网格布局 +.card-grid(@columns: 3, @gap: 16px) { + display: grid; + grid-template-columns: repeat(@columns, 1fr); + gap: @gap; + + @media (max-width: 1200px) { grid-template-columns: repeat(2, 1fr); } + @media (max-width: 768px) { grid-template-columns: 1fr; } +} + +// ===== 表单 mixin ===== +.form-section() { + margin-bottom: 24px; + + .section-title { + font-size: 16px; + font-weight: 600; + color: @text-primary; + margin-bottom: 16px; + padding-bottom: 8px; + border-bottom: 1px solid @border-light; + } +} + +// 多步表单向导 +.steps-form-container() { + width: 100%; + max-width: 800px; + margin: 0 auto; + padding: 24px 0; + + .steps-content { + min-height: 300px; + margin: 32px 0; + padding: 24px; + background: @bg-light; + border-radius: @border-radius-base; + } + + .steps-action { + display: flex; + justify-content: center; + gap: 12px; + margin-top: 24px; + } +} + +// ===== 页头 mixin ===== +.page-header() { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24px; + + .page-title { + font-size: 24px; + font-weight: 600; + color: @text-primary; + margin: 0; + } + + .page-extra { + display: flex; + gap: 8px; + } +} + +// ===== 筛选栏 mixin ===== +.filter-bar() { + display: flex; + flex-wrap: wrap; + gap: 12px; + padding: 16px; + margin-bottom: 16px; + background: @bg-light; + border-radius: @border-radius-base; + align-items: center; +} + +// ===== 空状态 mixin ===== +.empty-state() { + text-align: center; + padding: 48px 24px; + color: @text-secondary; + + .empty-icon { + font-size: 64px; + margin-bottom: 16px; + color: #d9d9d9; + } + + .empty-text { + font-size: 14px; + margin-bottom: 16px; + } +} + +// ===== 统计数值面板 ===== +.stats-panel() { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; + margin-bottom: 24px; + + @media (max-width: 768px) { grid-template-columns: 1fr; } + + .stat-card { + text-align: center; + padding: 24px; + background: @component-background; + border-radius: @border-radius-base; + border: 1px solid @border-light; + cursor: pointer; + transition: all 0.3s; + + &:hover { + border-color: @primary-color; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + } + + .stat-value { + font-size: 36px; + font-weight: 700; + color: @primary-color; + margin-bottom: 8px; + } + + .stat-label { + font-size: 14px; + color: @text-secondary; + } + } +} + +// ===== 详情页两栏布局 ===== +.detail-two-column() { + display: flex; + gap: 24px; + + @media (max-width: 1024px) { flex-direction: column; } + + .detail-main { + flex: 1; + min-width: 0; + } + + .detail-sidebar { + width: 380px; + position: sticky; + top: 16px; + align-self: flex-start; + + @media (max-width: 1024px) { + width: 100%; + position: static; + } + } +} + +// ===== 标签组 mixin ===== +.tag-group() { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; +} + +// ===== 操作栏(固定在底部) ===== +.fixed-bottom-bar() { + position: sticky; + bottom: 0; + z-index: 10; + background: @component-background; + padding: 12px 24px; + border-top: 1px solid @border-light; + display: flex; + justify-content: center; + gap: 12px; + margin-top: 24px; + box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06); +} + +// ===== 响应式断点 ===== +@screen-xs: 480px; +@screen-sm: 576px; +@screen-md: 768px; +@screen-lg: 992px; +@screen-xl: 1200px; +@screen-xxl: 1600px; + +// ===== 常用间距 ===== +@spacing-xs: 4px; +@spacing-sm: 8px; +@spacing-md: 16px; +@spacing-lg: 24px; +@spacing-xl: 32px; + +// ===== 公共模块样式类 ===== +// 资格注册相关 +.qualified-register { + .role-selector { + .card-grid(3, 16px); + + .role-card { + .card-hover(); + padding: 24px; + text-align: center; + border-radius: @border-radius-base; + + &.selected { + border-color: @primary-color; + border-width: 2px; + box-shadow: 0 4px 16px rgba(24, 144, 255, 0.15); + } + } + } +} + +// 对接大厅 +.match-hall { + .behavior-cards { + .card-grid(4, 16px); + } +} + +// 产业链图谱 +.chain-graph { + .graph-container { + width: 100%; + height: calc(100vh - 200px); + min-height: 500px; + border: 1px solid @border-light; + border-radius: @border-radius-base; + position: relative; + background: @bg-light; + } +} + +// 融资产品 +.financing-products { + .product-cards { + .card-grid(3, 16px); + } +} diff --git a/src/ts/core/chain/chain.ts b/src/ts/core/chain/chain.ts new file mode 100644 index 000000000..4f087e1e9 --- /dev/null +++ b/src/ts/core/chain/chain.ts @@ -0,0 +1,153 @@ +/** + * 产业链可视化相关类型定义 + * 覆盖:关系类型、图谱节点/连边、产业链导航 + */ + +/** 产业链关系类型 */ +export enum ChainRelationType { + /** 上游(供应商) */ + Upstream = 'upstream', + /** 下游(客户/分销商) */ + Downstream = 'downstream', + /** 横向协作 */ + Horizontal = 'horizontal', + /** 资源关联 */ + Resource = 'resource', +} + +/** 关系类型中文映射 */ +export const ChainRelationLabel: Record = { + [ChainRelationType.Upstream]: '上游', + [ChainRelationType.Downstream]: '下游', + [ChainRelationType.Horizontal]: '横向协作', + [ChainRelationType.Resource]: '资源关联', +}; + +/** 关系类型视觉配置 */ +export interface RelationStyleConfig { + /** 线条颜色 */ + color: string; + /** 线条类型 solid / dashed / dotted */ + lineType: 'solid' | 'dashed' | 'dotted'; + /** 箭头是否有 */ + hasArrow: boolean; +} + +/** 四种关系类型的线型颜色配置 */ +export const ChainRelationStyle: Record = { + [ChainRelationType.Upstream]: { color: '#1890ff', lineType: 'solid', hasArrow: true }, + [ChainRelationType.Downstream]: { color: '#52c41a', lineType: 'solid', hasArrow: true }, + [ChainRelationType.Horizontal]: { color: '#fa8c16', lineType: 'dashed', hasArrow: false }, + [ChainRelationType.Resource]: { color: '#8c8c8c', lineType: 'dotted', hasArrow: false }, +}; + +/** 组织节点类型标签 */ +export enum ChainNodeType { + /** 供应商 */ + Supplier = 'supplier', + /** 客户 */ + Customer = 'customer', + /** 协作方 */ + Collaborator = 'collaborator', +} + +/** 节点类型视觉配置 */ +export const ChainNodeStyle: Record = { + [ChainNodeType.Supplier]: { color: '#1890ff', label: '供应商' }, + [ChainNodeType.Customer]: { color: '#52c41a', label: '客户' }, + [ChainNodeType.Collaborator]: { color: '#fa8c16', label: '协作方' }, +}; + +/** 产业链图谱节点 */ +export interface ChainNode { + /** 节点唯一 ID */ + id: string; + /** 关联的组织 ID */ + targetId: string; + /** 组织名称 */ + name: string; + /** 组织 Logo URL */ + logo?: string; + /** 节点类型 */ + nodeType: ChainNodeType; + /** 行业 */ + industry?: string; + /** 规模 */ + scale?: string; + /** 简介 */ + description?: string; + /** 图谱中的 x 坐标 */ + x?: number; + /** 图谱中的 y 坐标 */ + y?: number; +} + +/** 产业链图谱连边 */ +export interface ChainEdge { + /** 连边唯一 ID */ + id: string; + /** 源节点 ID */ + source: string; + /** 目标节点 ID */ + target: string; + /** 关系类型 */ + relationType: ChainRelationType; + /** 关系描述 */ + description?: string; + /** 关系建立时间 */ + createTime?: string; +} + +/** 产业链图谱完整数据 */ +export interface ChainGraph { + /** 图谱 ID */ + id: string; + /** 图谱名称 */ + name: string; + /** 所属行业/领域分类 */ + category: string; + /** 节点列表 */ + nodes: ChainNode[]; + /** 连边列表 */ + edges: ChainEdge[]; + /** 中心节点 ID */ + centerNodeId?: string; + /** 包含组织数 */ + nodeCount: number; + /** 关系数 */ + edgeCount: number; + /** 最近更新时间 */ + updateTime: string; +} + +/** 产业链导航分类 */ +export interface ChainCategory { + /** 分类 ID */ + id: string; + /** 分类名称 */ + name: string; + /** 图标 */ + icon?: string; + /** 包含的产业链列表 */ + chains: ChainGraph[]; +} + +/** 添加/编辑关系参数 */ +export interface RelationFormData { + /** 源组织 ID */ + sourceTargetId: string; + /** 目标组织 ID */ + targetTargetId: string; + /** 关系类型 */ + relationType: ChainRelationType; + /** 关系描述 */ + description?: string; +} + +/** 图谱搜索参数 */ +export interface ChainSearchParams { + /** 搜索关键词 */ + keyword: string; + /** 搜索的图谱 ID(可选,不传则全局搜索) */ + graphId?: string; +} diff --git a/src/ts/core/chain/index.ts b/src/ts/core/chain/index.ts new file mode 100644 index 000000000..481a3404a --- /dev/null +++ b/src/ts/core/chain/index.ts @@ -0,0 +1 @@ +export * from './chain'; diff --git a/src/ts/core/financing/financing.ts b/src/ts/core/financing/financing.ts new file mode 100644 index 000000000..be7b46ce6 --- /dev/null +++ b/src/ts/core/financing/financing.ts @@ -0,0 +1,297 @@ +/** + * 融资链相关类型定义 + * 覆盖:融资产品、申请表单、进度追踪、还款管理 + */ + +/** 融资产品类型 */ +export enum FinancingType { + /** 信用贷 */ + Credit = 'credit', + /** 抵押贷 */ + Mortgage = 'mortgage', + /** 供应链金融 */ + SupplyChain = 'supply_chain', +} + +/** 融资产品类型中文映射 */ +export const FinancingTypeLabel: Record = { + [FinancingType.Credit]: '信用贷', + [FinancingType.Mortgage]: '抵押贷', + [FinancingType.SupplyChain]: '供应链金融', +}; + +/** 融资产品类型标签颜色 */ +export const FinancingTypeColor: Record = { + [FinancingType.Credit]: '#1890ff', + [FinancingType.Mortgage]: '#fa8c16', + [FinancingType.SupplyChain]: '#52c41a', +}; + +/** 借款用途 */ +export enum LoanPurpose { + /** 经营周转 */ + BusinessTurnover = 'business_turnover', + /** 设备采购 */ + EquipmentPurchase = 'equipment_purchase', + /** 研发投入 */ + RdInvestment = 'rd_investment', + /** 补充流动资金 */ + WorkingCapital = 'working_capital', + /** 其他 */ + Other = 'other', +} + +/** 借款用途中文映射 */ +export const LoanPurposeLabel: Record = { + [LoanPurpose.BusinessTurnover]: '经营周转', + [LoanPurpose.EquipmentPurchase]: '设备采购', + [LoanPurpose.RdInvestment]: '研发投入', + [LoanPurpose.WorkingCapital]: '补充流动资金', + [LoanPurpose.Other]: '其他', +}; + +/** 还款方式 */ +export enum RepaymentMethod { + /** 等额本息 */ + EqualInstallment = 'equal_installment', + /** 先息后本 */ + InterestFirst = 'interest_first', + /** 到期还本付息 */ + LumpSum = 'lump_sum', +} + +/** 还款方式中文映射 */ +export const RepaymentMethodLabel: Record = { + [RepaymentMethod.EqualInstallment]: '等额本息', + [RepaymentMethod.InterestFirst]: '先息后本', + [RepaymentMethod.LumpSum]: '到期还本付息', +}; + +/** 贷款申请状态 */ +export enum LoanStatus { + /** 已提交 */ + Submitted = 'submitted', + /** 审核中 */ + Reviewing = 'reviewing', + /** 尽职调查 */ + DueDiligence = 'due_diligence', + /** 审批中 */ + Approving = 'approving', + /** 签约中 */ + Signing = 'signing', + /** 已放款 */ + Disbursed = 'disbursed', + /** 已拒绝 */ + Rejected = 'rejected', + /** 已结清 */ + Settled = 'settled', +} + +/** 贷款申请状态中文映射 */ +export const LoanStatusLabel: Record = { + [LoanStatus.Submitted]: '已提交', + [LoanStatus.Reviewing]: '审核中', + [LoanStatus.DueDiligence]: '尽职调查', + [LoanStatus.Approving]: '审批中', + [LoanStatus.Signing]: '签约中', + [LoanStatus.Disbursed]: '已放款', + [LoanStatus.Rejected]: '已拒绝', + [LoanStatus.Settled]: '已结清', +}; + +/** 贷款申请状态流转顺序 */ +export const LoanStatusFlow: LoanStatus[] = [ + LoanStatus.Submitted, + LoanStatus.Reviewing, + LoanStatus.DueDiligence, + LoanStatus.Approving, + LoanStatus.Signing, + LoanStatus.Disbursed, +]; + +/** 还款状态 */ +export enum RepayStatus { + /** 待还 */ + Pending = 'pending', + /** 已还 */ + Paid = 'paid', + /** 逾期 */ + Overdue = 'overdue', +} + +/** 还款状态中文映射 */ +export const RepayStatusLabel: Record = { + [RepayStatus.Pending]: '待还', + [RepayStatus.Paid]: '已还', + [RepayStatus.Overdue]: '逾期', +}; + +/** 融资产品 */ +export interface FinancingProduct { + /** 产品 ID */ + id: string; + /** 产品名称 */ + name: string; + /** 金融机构名称 */ + institutionName: string; + /** 金融机构 Logo */ + institutionLogo?: string; + /** 产品类型 */ + type: FinancingType; + /** 年化利率范围-最低(%) */ + rateMin: number; + /** 年化利率范围-最高(%) */ + rateMax: number; + /** 额度范围-最低(万元) */ + amountMin: number; + /** 额度范围-最高(万元) */ + amountMax: number; + /** 期限范围-最短(月) */ + termMin: number; + /** 期限范围-最长(月) */ + termMax: number; + /** 还款方式 */ + repaymentMethod: RepaymentMethod; + /** 利率说明 */ + rateDescription?: string; + /** 申请条件列表 */ + conditions: string[]; + /** 所需材料清单 */ + requiredMaterials: string[]; + /** 费用说明 */ + feeDescription?: string; + /** 产品标签 */ + tags?: string[]; +} + +/** 融资申请表单数据 */ +export interface LoanApplicationForm { + /** 产品 ID */ + productId: string; + /** 申请金额(万元) */ + amount: number; + /** 贷款期限(月) */ + term: number; + /** 借款用途 */ + purposes: LoanPurpose[]; + /** 企业名称 */ + companyName: string; + /** 统一社会信用代码 */ + creditCode: string; + /** 注册地址 */ + registeredAddress: string; + /** 成立时间 */ + establishedDate: string; + /** 所属行业 */ + industry: string; + /** 已上传材料 */ + materials: { uid: string; name: string; url: string }[]; +} + +/** 融资申请记录 */ +export interface LoanApplication { + /** 申请 ID */ + id: string; + /** 申请编号 */ + serialNo: string; + /** 产品 ID */ + productId: string; + /** 产品名称 */ + productName: string; + /** 申请金额 */ + amount: number; + /** 贷款期限 */ + term: number; + /** 当前状态 */ + status: LoanStatus; + /** 申请时间 */ + applyTime: string; + /** 预计审核完成时间 */ + estimatedFinishTime?: string; + /** 拒绝原因 */ + rejectReason?: string; +} + +/** 还款计划期次 */ +export interface RepaymentInstallment { + /** 期次序号 */ + period: number; + /** 还款日期 */ + dueDate: string; + /** 应还本金 */ + principal: number; + /** 应还利息 */ + interest: number; + /** 月供总额 */ + total: number; + /** 还款状态 */ + status: RepayStatus; + /** 实际还款日期 */ + paidDate?: string; +} + +/** 还款记录 */ +export interface RepaymentRecord { + /** 记录 ID */ + id: string; + /** 申请 ID */ + applicationId: string; + /** 还款期次 */ + period: number; + /** 还款金额 */ + amount: number; + /** 还款方式 */ + method: 'bank_transfer' | 'auto_debit'; + /** 还款日期 */ + payDate: string; + /** 状态 */ + status: 'success' | 'failed' | 'processing'; +} + +/** 融资申请金额/期限联动校验规则 */ +export interface LoanValidationRule { + /** 产品类型 */ + type: FinancingType; + /** 最大金额(万元) */ + maxAmount: number; + /** 最小金额(万元) */ + minAmount: number; + /** 最大期限(月) */ + maxTerm: number; + /** 最小期限(月) */ + minTerm: number; +} + +/** 默认校验规则 */ +export const DefaultLoanRules: Record = { + [FinancingType.Credit]: { + type: FinancingType.Credit, + maxAmount: 100, + minAmount: 1, + maxTerm: 36, + minTerm: 3, + }, + [FinancingType.Mortgage]: { + type: FinancingType.Mortgage, + maxAmount: 500, + minAmount: 10, + maxTerm: 60, + minTerm: 6, + }, + [FinancingType.SupplyChain]: { + type: FinancingType.SupplyChain, + maxAmount: 300, + minAmount: 5, + maxTerm: 24, + minTerm: 3, + }, +}; + +/** 还款方式枚举 */ +export type RepayActionMethod = 'bank_transfer' | 'auto_debit'; + +/** 还款方式中文映射 */ +export const RepayActionMethodLabel: Record = { + bank_transfer: '银行转账', + auto_debit: '自动扣款', +}; diff --git a/src/ts/core/financing/index.ts b/src/ts/core/financing/index.ts new file mode 100644 index 000000000..cab0b6939 --- /dev/null +++ b/src/ts/core/financing/index.ts @@ -0,0 +1 @@ +export * from './financing'; diff --git a/src/ts/core/index.ts b/src/ts/core/index.ts index 8f19173b7..0ff6abfbe 100644 --- a/src/ts/core/index.ts +++ b/src/ts/core/index.ts @@ -53,3 +53,84 @@ export type { IWorkApply } from './work/apply'; export type { IFinancial } from './work/financial'; export type { IPeriod } from './work/financial/period'; export type { IWorkTask, TaskTypeName } from './work/task'; + +// 资格注册 +export { + RoleType, + ReviewStatus, +} from './register/qualified'; +export type { + MaterialCategory, + MaterialItem, + CommonRegisterFields, + MemberFields, + ResearcherFields, + PartnerFields, + RegistrationFormData, + RegistrationRecord, + ReviewRecord, + ReviewAction, + RoleFieldConfig, + MaterialCategoryConfig, +} from './register/qualified'; + +// 对接大厅 +export { + BizBehavior, + BizBehaviorLabel, + ResourceType, + ResourceTypeLabel, + BehaviorResourceMap, + MatchStatus, +} from './matching/match'; +export type { + MatchDemand, + MatchInvitation, + MatchRecord, + BehaviorCardData, + HallStatistics, +} from './matching/match'; + +// 产业链 +export { + ChainRelationType, + ChainRelationLabel, + ChainRelationStyle, + ChainNodeType, + ChainNodeStyle, +} from './chain/chain'; +export type { + RelationStyleConfig, + ChainNode, + ChainEdge, + ChainGraph, + ChainCategory, + RelationFormData, + ChainSearchParams, +} from './chain/chain'; + +// 融资链 +export { + FinancingType, + FinancingTypeLabel, + FinancingTypeColor, + LoanPurpose, + LoanPurposeLabel, + RepaymentMethod, + RepaymentMethodLabel, + LoanStatus, + LoanStatusLabel, + LoanStatusFlow, + RepayStatus, + RepayStatusLabel, + DefaultLoanRules, +} from './financing/financing'; +export type { + FinancingProduct, + LoanApplicationForm, + LoanApplication, + RepaymentInstallment, + RepaymentRecord, + LoanValidationRule, + RepayActionMethod, +} from './financing/financing'; diff --git a/src/ts/core/matching/index.ts b/src/ts/core/matching/index.ts new file mode 100644 index 000000000..b76a5e8a4 --- /dev/null +++ b/src/ts/core/matching/index.ts @@ -0,0 +1 @@ +export * from './match'; diff --git a/src/ts/core/matching/match.ts b/src/ts/core/matching/match.ts new file mode 100644 index 000000000..abfb82b94 --- /dev/null +++ b/src/ts/core/matching/match.ts @@ -0,0 +1,168 @@ +/** + * 对接大厅相关类型定义 + * 覆盖:行为选择、资源类型、对接需求发布、对接邀请 + */ + +/** 业务行为类型 */ +export enum BizBehavior { + /** 交易(买卖) */ + Trade = 'trade', + /** 对接(匹配合作伙伴) */ + Match = 'match', + /** 链接(资源关联) */ + Link = 'link', + /** 共享(资源共享) */ + Share = 'share', +} + +/** 业务行为中文映射 */ +export const BizBehaviorLabel: Record = { + [BizBehavior.Trade]: '交易', + [BizBehavior.Match]: '对接', + [BizBehavior.Link]: '链接', + [BizBehavior.Share]: '共享', +}; + +/** 资源类型 */ +export enum ResourceType { + /** 商品 */ + Goods = 'goods', + /** 服务 */ + Service = 'service', + /** 项目 */ + Project = 'project', + /** 知识 */ + Knowledge = 'knowledge', + /** 资金 */ + Capital = 'capital', +} + +/** 资源类型中文映射 */ +export const ResourceTypeLabel: Record = { + [ResourceType.Goods]: '商品', + [ResourceType.Service]: '服务', + [ResourceType.Project]: '项目', + [ResourceType.Knowledge]: '知识', + [ResourceType.Capital]: '资金', +}; + +/** 行为与可选的资源类型映射 */ +export const BehaviorResourceMap: Record = { + [BizBehavior.Trade]: [ResourceType.Goods, ResourceType.Service], + [BizBehavior.Match]: [ResourceType.Project, ResourceType.Capital, ResourceType.Service], + [BizBehavior.Link]: [ResourceType.Knowledge, ResourceType.Project, ResourceType.Service], + [BizBehavior.Share]: [ResourceType.Knowledge, ResourceType.Service, ResourceType.Goods], +}; + +/** 对接需求状态 */ +export enum MatchStatus { + /** 已发布 */ + Published = 'published', + /** 对接中 */ + InProgress = 'in_progress', + /** 已完成 */ + Completed = 'completed', + /** 已关闭 */ + Closed = 'closed', +} + +/** 对接需求 */ +export interface MatchDemand { + /** 需求 ID */ + id: string; + /** 需求标题 */ + title: string; + /** 行为类型 */ + behavior: BizBehavior; + /** 资源类型 */ + resourceType: ResourceType; + /** 需求描述 */ + description: string; + /** 预算范围(最小值) */ + budgetMin?: number; + /** 预算范围(最大值) */ + budgetMax?: number; + /** 期望完成时间 */ + expectDate?: string; + /** 发布人 ID */ + publisherId: string; + /** 发布人名称 */ + publisherName: string; + /** 发布时间 */ + publishTime: string; + /** 状态 */ + status: MatchStatus; + /** 已响应数 */ + responseCount: number; +} + +/** 对接邀请/响应 */ +export interface MatchInvitation { + /** 邀请 ID */ + id: string; + /** 关联的对接需求 ID */ + demandId: string; + /** 需求标题 */ + demandTitle: string; + /** 发起方 ID */ + fromId: string; + /** 发起方名称 */ + fromName: string; + /** 接收方 ID */ + toId: string; + /** 接收方名称 */ + toName: string; + /** 邀请状态 */ + status: 'pending' | 'accepted' | 'rejected'; + /** 邀请信息 */ + message?: string; + /** 创建时间 */ + createTime: string; + /** 处理时间 */ + handleTime?: string; +} + +/** 对接记录 */ +export interface MatchRecord { + /** 记录 ID */ + id: string; + /** 对接需求 ID */ + demandId: string; + /** 对接双方 ID */ + partyA: string; + partyB: string; + /** 开始时间 */ + startTime: string; + /** 状态 */ + status: MatchStatus; + /** 备注 */ + remark?: string; +} + +/** 行为入口卡片数据 */ +export interface BehaviorCardData { + /** 行为类型 */ + behavior: BizBehavior; + /** 图标 */ + icon: string; + /** 主标题 */ + title: string; + /** 描述文案 */ + description: string; + /** 已发布数量 */ + publishedCount: number; + /** 主操作按钮文案 */ + actionLabel: string; + /** 次操作按钮文案 */ + secondaryActionLabel?: string; +} + +/** 大厅首页统计数据 */ +export interface HallStatistics { + /** 我的交易数 */ + tradeCount: number; + /** 对接中数量 */ + matchCount: number; + /** 共享资源数 */ + shareCount: number; +} diff --git a/src/ts/core/register/index.ts b/src/ts/core/register/index.ts new file mode 100644 index 000000000..4fd6aa934 --- /dev/null +++ b/src/ts/core/register/index.ts @@ -0,0 +1 @@ +export * from './qualified'; diff --git a/src/ts/core/register/qualified.ts b/src/ts/core/register/qualified.ts new file mode 100644 index 000000000..cbe805dbb --- /dev/null +++ b/src/ts/core/register/qualified.ts @@ -0,0 +1,215 @@ +/** + * 资格注册相关类型定义 + * 覆盖:会员/研究员/合伙人 三种角色的分类注册、材料上传、审核流程 + */ + +import { XTarget } from '@/ts/base/schema'; + +/** 注册角色类型 */ +export enum RoleType { + /** 会员 */ + Member = '会员', + /** 研究员(专家) */ + Researcher = '研究员', + /** 合伙会员 */ + Partner = '合伙人', +} + +/** 审核状态 */ +export enum ReviewStatus { + /** 待审核 */ + Pending = 'pending', + /** 审核通过 */ + Approved = 'approved', + /** 审核驳回 */ + Rejected = 'rejected', +} + +/** 材料分类 */ +export interface MaterialCategory { + /** 分类唯一标识 */ + key: string; + /** 分类名称 */ + name: string; + /** 是否必传 */ + required: boolean; + /** 分类描述 */ + description?: string; + /** 该分类最多上传文件数 */ + maxFiles: number; +} + +/** 已上传材料项 */ +export interface MaterialItem { + /** 文件唯一标识 */ + uid: string; + /** 文件名 */ + name: string; + /** 文件大小(字节) */ + size: number; + /** 文件类型/MIME */ + type: string; + /** 上传状态 */ + status: 'uploading' | 'done' | 'error' | 'removed'; + /** 上传进度 0-100 */ + percent?: number; + /** 远端文件 URL */ + url?: string; + /** 缩略图 URL */ + thumbUrl?: string; + /** 所属材料分类 key */ + categoryKey: string; + /** 上传时间 */ + uploadTime?: string; + /** 错误信息 */ + errorMsg?: string; +} + +/** 公共注册字段(三种角色共有) */ +export interface CommonRegisterFields { + /** 姓名 */ + name: string; + /** 手机号 */ + phone: string; + /** 验证码 */ + verifyCode: string; + /** 密码 */ + password: string; + /** 确认密码 */ + confirmPassword: string; + /** 是否同意协议 */ + agreeTerms: boolean; +} + +/** 会员角色特有字段 */ +export interface MemberFields { + /** 所属单位 */ + organization: string; + /** 职务 */ + position: string; + /** 推荐人 */ + referrer?: string; +} + +/** 研究员角色特有字段 */ +export interface ResearcherFields { + /** 学历 */ + education: string; + /** 研究方向 */ + researchDirection: string; + /** 职称 */ + title: string; + /** 代表作链接 */ + representativeWork?: string; + /** 所属机构 */ + institution: string; +} + +/** 合伙人角色特有字段 */ +export interface PartnerFields { + /** 企业名称 */ + companyName: string; + /** 统一社会信用代码 */ + creditCode: string; + /** 主营业务 */ + mainBusiness: string; + /** 可提供资源类型 */ + providedResources?: string[]; + /** 所需资源类型 */ + requiredResources?: string[]; +} + +/** 按角色联合的注册表单数据 */ +export type RegistrationFormData = CommonRegisterFields & + (MemberFields | ResearcherFields | PartnerFields) & { + /** 注册角色 */ + roleType: RoleType; + /** 已上传材料列表 */ + materials: MaterialItem[]; + }; + +/** 注册申请记录 */ +export interface RegistrationRecord { + /** 申请编号 */ + id: string; + /** 申请流水号 */ + serialNo: string; + /** 申请时间 */ + applyTime: string; + /** 角色类型 */ + roleType: RoleType; + /** 表单数据 */ + formData: RegistrationFormData; + /** 材料列表 */ + materials: MaterialItem[]; + /** 审核状态 */ + reviewStatus: ReviewStatus; +} + +/** 审核记录 */ +export interface ReviewRecord { + /** 审核编号 */ + id: string; + /** 关联的注册申请 ID */ + applicationId: string; + /** 申请人姓名 */ + applicantName: string; + /** 角色类型 */ + roleType: RoleType; + /** 审核状态 */ + status: ReviewStatus; + /** 审核人 */ + reviewer?: string; + /** 审核时间 */ + reviewTime?: string; + /** 审核意见/备注 */ + remark?: string; + /** 驳回理由 */ + rejectReason?: string; +} + +/** 审核操作参数 */ +export interface ReviewAction { + /** 申请 ID */ + applicationId: string; + /** 操作类型 */ + action: 'approve' | 'reject'; + /** 审核意见 */ + remark?: string; + /** 驳回理由(驳回时必填) */ + rejectReason?: string; +} + +/** 角色字段配置项 */ +export interface RoleFieldConfig { + /** 字段名 */ + name: string; + /** 字段标签 */ + label: string; + /** 字段类型 */ + type: 'text' | 'phone' | 'password' | 'select' | 'textarea' | 'tags' | 'url'; + /** 是否必填 */ + required: boolean; + /** 占位提示 */ + placeholder?: string; + /** 校验规则 */ + rules?: Record; + /** 选项(type=select 时) */ + options?: { label: string; value: string }[]; + /** 所属角色组 */ + roleGroup?: 'common' | 'member' | 'researcher' | 'partner'; +} + +/** 材料分类配置 */ +export interface MaterialCategoryConfig { + /** 角色类型 */ + roleType: RoleType; + /** 该角色所需材料分类列表 */ + categories: MaterialCategory[]; + /** 接受的文件类型 */ + acceptFileTypes: string[]; + /** 单文件最大体积(MB) */ + maxFileSize: number; + /** 最大文件总数 */ + maxTotalFiles: number; +} -- Gitee From 388e2e704c3a4fb5bb3c31aa53ddc6915c6314ad Mon Sep 17 00:00:00 2001 From: bai_xin123 <1813467895@qq.com> Date: Tue, 19 May 2026 16:35:14 +0800 Subject: [PATCH 09/11] =?UTF-8?q?p7=E4=BB=BB=E5=8A=A1=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ChainGraph/ChainGraph.tsx | 260 +++++++++++++ src/components/ChainGraph/ChainLegend.tsx | 54 +++ src/components/ChainGraph/ChainNav.tsx | 151 ++++++++ src/components/ChainGraph/ChainNode.tsx | 44 +++ src/components/ChainGraph/RelationManager.tsx | 355 ++++++++++++++++++ src/components/ChainGraph/index.module.less | 350 +++++++++++++++++ src/pages/Chain/ChainGraph.tsx | 155 +++++++- 7 files changed, 1357 insertions(+), 12 deletions(-) create mode 100644 src/components/ChainGraph/ChainGraph.tsx create mode 100644 src/components/ChainGraph/ChainLegend.tsx create mode 100644 src/components/ChainGraph/ChainNav.tsx create mode 100644 src/components/ChainGraph/ChainNode.tsx create mode 100644 src/components/ChainGraph/RelationManager.tsx create mode 100644 src/components/ChainGraph/index.module.less diff --git a/src/components/ChainGraph/ChainGraph.tsx b/src/components/ChainGraph/ChainGraph.tsx new file mode 100644 index 000000000..31507c430 --- /dev/null +++ b/src/components/ChainGraph/ChainGraph.tsx @@ -0,0 +1,260 @@ +import type { ChainGraph as ChainGraphData, ChainNode as ChainNodeData } from '@/ts/core/chain/chain'; +import { ChainRelationStyle, ChainRelationType } from '@/ts/core/chain/chain'; +import { mockChainAdapter } from '@/services'; +import { Graph } from '@antv/x6'; +import { Dnd } from '@antv/x6-plugin-dnd'; +import { Selection } from '@antv/x6-plugin-selection'; +import { register } from '@antv/x6-react-shape'; +import { message } from 'antd'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { ChainNodeView } from './ChainNode'; +import ChainLegend from './ChainLegend'; +import styles from './index.module.less'; + +interface ChainGraphProps { + /** 图谱 ID */ + graphId?: string; + /** 中心节点 ID(以某节点为中心展示一跳关系) */ + centerNodeId?: string; + /** 数据加载后回调 */ + onDataLoaded?: (data: ChainGraphData) => void; + /** 单击选中节点 */ + onNodeClick?: (node: ChainNodeData) => void; + /** 双击节点 */ + onNodeDoubleClick?: (node: ChainNodeData) => void; +} + +let nodeRegistered = false; + +const ChainGraph: React.FC = ({ + graphId, + centerNodeId, + onDataLoaded, + onNodeClick, + onNodeDoubleClick, +}) => { + const containerRef = useRef(null); + const graphRef = useRef(null); + const [ready, setReady] = useState(false); + + // ---------- 初始化画布 ---------- + const initGraph = useCallback(() => { + if (!containerRef.current) return; + + const graph = new Graph({ + container: containerRef.current, + grid: { + size: 10, + visible: true, + args: { color: '#f0f0f0', thickness: 1 }, + }, + autoResize: true, + panning: { enabled: true, modifiers: [] }, + mousewheel: { + enabled: true, + modifiers: ['ctrl'], + factor: 1.1, + }, + background: { color: '#fafafa' }, + interacting: { + nodeMovable: true, + edgeLabelMovable: false, + }, + }); + + graph.use(new Dnd({ target: graph })); + graph.use( + new Selection({ + enabled: true, + multiple: false, + rubberband: false, + movable: false, + pointerEvents: 'none', + }), + ); + + // 单击节点 → 选中 + graph.on('node:click', ({ node }) => { + const data = node.getData(); + if (data && onNodeClick) { + onNodeClick(data); + } + }); + + // 双击节点 → 展开详情 + graph.on('node:dblclick', ({ node }) => { + const data = node.getData(); + if (data && onNodeDoubleClick) { + onNodeDoubleClick(data); + } + }); + + // 窗口自适应居中 + graph.on('resize', () => { + graph.centerContent(); + }); + + // 注册 React 节点(全局只注册一次) + if (!nodeRegistered) { + register({ + shape: 'chain-node', + width: 200, + height: 72, + component: ChainNodeView, + effect: ['data'], + }); + nodeRegistered = true; + } + + graphRef.current = graph; + setReady(true); + }, [onNodeClick, onNodeDoubleClick]); + + // ---------- 加载图谱数据 ---------- + const loadGraphData = useCallback(async () => { + const graph = graphRef.current; + if (!graph) return; + + try { + let res: { code: number; data: ChainGraphData | null }; + + if (centerNodeId) { + // 以节点为中心加载一跳关系 + res = await mockChainAdapter.getCenteredGraph(centerNodeId); + } else if (graphId) { + res = await mockChainAdapter.getChainGraph(graphId); + } else { + const myRes = await mockChainAdapter.getMyChainGraph(); + res = { code: myRes.code, data: myRes.data }; + } + + if (!res || res.code !== 200 || !res.data) { + message.error('加载图谱数据失败'); + return; + } + + const { nodes, edges } = res.data; + const highlightId = centerNodeId || res.data.centerNodeId; + + // 构建节点 cells + const nodeCells = nodes.map((n, idx) => { + const hasPos = typeof n.x === 'number' && typeof n.y === 'number'; + let x = hasPos ? n.x! : 0; + let y = hasPos ? n.y! : 0; + + if (!hasPos) { + const radius = Math.max(250, nodes.length * 30); + const angle = (2 * Math.PI * idx) / nodes.length - Math.PI / 2; + x = 500 + radius * Math.cos(angle); + y = 350 + radius * Math.sin(angle); + } + + return { + id: n.id, + shape: 'chain-node', + x, + y, + // isCenter 标记让 React 节点渲染高亮 + data: { ...n, isCenter: n.id === highlightId || n.targetId === highlightId }, + }; + }); + + // 构建边 cells + const edgeCells = edges.map((e) => { + const rStyle = + ChainRelationStyle[e.relationType] || + ChainRelationStyle[ChainRelationType.Horizontal]; + return { + id: e.id, + shape: 'edge', + source: e.source, + target: e.target, + data: e, + attrs: { + line: { + stroke: rStyle.color, + strokeWidth: 2, + strokeDasharray: + rStyle.lineType === 'dashed' + ? '8 4' + : rStyle.lineType === 'dotted' + ? '2 4' + : undefined, + targetMarker: rStyle.hasArrow + ? { name: 'block', width: 8, height: 6, fill: rStyle.color } + : undefined, + }, + }, + labels: e.description + ? [ + { + position: { distance: 0.5 }, + attrs: { + text: { text: e.description, fontSize: 11, fill: '#666' }, + rect: { + fill: '#fff', + stroke: '#e8e8e8', + rx: 3, + strokeWidth: 1, + refWidth: '120%', + refHeight: '120%', + refX: -4, + refY: -3, + }, + }, + }, + ] + : undefined, + zIndex: -1, + }; + }); + + graph.clearCells(); + nodeCells.forEach((cell) => graph.addNode(cell)); + edgeCells.forEach((cell) => graph.addEdge(cell)); + + // 中心节点居中 + if (highlightId) { + const centerCell = graph.getCellById(highlightId); + if (centerCell) { + graph.centerCell(centerCell); + } + } else { + graph.centerContent(); + } + + // 通知父组件数据已加载 + if (onDataLoaded) { + onDataLoaded(res.data); + } + } catch { + message.error('加载图谱数据失败'); + } + }, [graphId, centerNodeId, onDataLoaded]); + + // ---------- 生命周期 ---------- + useEffect(() => { + initGraph(); + return () => { + graphRef.current?.dispose(); + graphRef.current = null; + }; + }, []); + + useEffect(() => { + if (ready && graphRef.current) { + loadGraphData(); + } + }, [ready, loadGraphData]); + + return ( +
+
+ +
+ ); +}; + +ChainGraph.displayName = 'ChainGraph'; + +export default ChainGraph; diff --git a/src/components/ChainGraph/ChainLegend.tsx b/src/components/ChainGraph/ChainLegend.tsx new file mode 100644 index 000000000..332a988d0 --- /dev/null +++ b/src/components/ChainGraph/ChainLegend.tsx @@ -0,0 +1,54 @@ +import { + ChainRelationLabel, + ChainRelationStyle, + ChainRelationType, +} from '@/ts/core/chain/chain'; +import React from 'react'; +import styles from './index.module.less'; + +const relationTypes = [ + ChainRelationType.Upstream, + ChainRelationType.Downstream, + ChainRelationType.Horizontal, + ChainRelationType.Resource, +]; + +const ChainLegend: React.FC = () => { + return ( +
+
图例
+ {relationTypes.map((type) => { + const style = ChainRelationStyle[type]; + const label = ChainRelationLabel[type]; + return ( +
+ + + {style.hasArrow && ( + + )} + + {label} +
+ ); + })} +
+ ); +}; + +ChainLegend.displayName = 'ChainLegend'; + +export default ChainLegend; diff --git a/src/components/ChainGraph/ChainNav.tsx b/src/components/ChainGraph/ChainNav.tsx new file mode 100644 index 000000000..c4d45afd7 --- /dev/null +++ b/src/components/ChainGraph/ChainNav.tsx @@ -0,0 +1,151 @@ +import { mockChainAdapter } from '@/services'; +import type { ChainCategory, ChainGraph } from '@/ts/core/chain/chain'; +import { AppstoreOutlined, ReloadOutlined, RightOutlined, NodeIndexOutlined } from '@ant-design/icons'; +import { Button, Spin, Tooltip, message } from 'antd'; +import React, { useCallback, useEffect, useState } from 'react'; +import styles from './index.module.less'; + +interface ChainNavProps { + /** 当前加载的图谱 ID */ + activeGraphId?: string; + /** 选择产业链回调 */ + onSelectChain: (graph: ChainGraph) => void; + /** 加载"我的产业链"回调 */ + onMyChain: () => void; +} + +const ChainNav: React.FC = ({ activeGraphId, onSelectChain, onMyChain }) => { + const [collapsed, setCollapsed] = useState(false); + const [categories, setCategories] = useState([]); + const [loading, setLoading] = useState(false); + const [expandedCats, setExpandedCats] = useState>({}); + + const loadCategories = useCallback(async () => { + setLoading(true); + try { + const res = await mockChainAdapter.getChainCategories(); + if (res.code === 200 && res.data) { + setCategories(res.data); + // 默认展开所有分类 + const expanded: Record = {}; + res.data.forEach((c) => { expanded[c.id] = true; }); + setExpandedCats(expanded); + } + } catch { + message.error('加载产业链分类失败'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + loadCategories(); + }, [loadCategories]); + + const toggleCategory = (catId: string) => { + setExpandedCats((prev) => ({ ...prev, [catId]: !prev[catId] })); + }; + + const toggleCollapse = () => setCollapsed(!collapsed); + + const formatDate = (dateStr: string) => { + if (!dateStr) return ''; + return dateStr.slice(0, 10); + }; + + if (collapsed) { + return ( +
+ +
+ ); + } + + return ( +
+
+ + + 产业链导航 + +
+ +
+
+ + {/* 我的产业链快捷入口 */} +
+ + 我的产业链 +
+ + {/* 分类列表 */} +
+ + {categories.map((cat) => ( +
+
toggleCategory(cat.id)}> + + {cat.name} + {cat.chains.length} +
+ {expandedCats[cat.id] && + cat.chains.map((chain) => ( +
onSelectChain(chain)} + > +
{chain.name}
+
+ {chain.nodeCount} 组织 + {chain.edgeCount} 关系 + {formatDate(chain.updateTime)} +
+
+ ))} +
+ ))} + {!loading && categories.length === 0 && ( +
暂无产业链数据
+ )} +
+
+
+ ); +}; + +ChainNav.displayName = 'ChainNav'; + +export default ChainNav; diff --git a/src/components/ChainGraph/ChainNode.tsx b/src/components/ChainGraph/ChainNode.tsx new file mode 100644 index 000000000..0166837b6 --- /dev/null +++ b/src/components/ChainGraph/ChainNode.tsx @@ -0,0 +1,44 @@ +import { ChainNode as ChainNodeData, ChainNodeStyle } from '@/ts/core/chain/chain'; +import { Node } from '@antv/x6'; +import { Graph } from '@antv/x6'; +import React from 'react'; +import styles from './index.module.less'; + +interface ChainNodeProps { + node: Node; + graph: Graph; +} + +const ChainNodeView: React.FC = ({ node }) => { + const rawData = node.getData(); + const data = rawData; + const style = ChainNodeStyle[data.nodeType] || { color: '#666', label: data.nodeType }; + const isCenter = data.isCenter; + + return ( +
+
+ {style.label} +
+
+
+ {data.logo ? ( + {data.name} + ) : ( + {data.name.charAt(0)} + )} +
+
+
{data.name}
+
+ {data.industry || ''}{data.scale ? ` | ${data.scale}` : ''} +
+
+
+
+ ); +}; + +ChainNodeView.displayName = 'ChainNode'; + +export { ChainNodeView }; diff --git a/src/components/ChainGraph/RelationManager.tsx b/src/components/ChainGraph/RelationManager.tsx new file mode 100644 index 000000000..33bbee0d0 --- /dev/null +++ b/src/components/ChainGraph/RelationManager.tsx @@ -0,0 +1,355 @@ +import { + ChainRelationLabel, + ChainRelationStyle, + ChainRelationType, +} from '@/ts/core/chain/chain'; +import type { ChainEdge, ChainNode, RelationFormData } from '@/ts/core/chain/chain'; +import { mockChainAdapter } from '@/services'; +import { + DeleteOutlined, + EditOutlined, + LinkOutlined, + PlusOutlined, +} from '@ant-design/icons'; +import { + Button, + Drawer, + Form, + Input, + message, + Popconfirm, + Select, + Space, + Tag, + Typography, +} from 'antd'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import styles from './index.module.less'; + +const { TextArea } = Input; +const { Text } = Typography; + +interface RelationManagerProps { + visible: boolean; + selectedNode: ChainNode | null; + currentEdges: ChainEdge[]; + onClose: () => void; + onRelationsChanged: () => void; +} + +const RelationManager: React.FC = ({ + visible, + selectedNode, + currentEdges, + onClose, + onRelationsChanged, +}) => { + const [form] = Form.useForm(); + const [adding, setAdding] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [editingId, setEditingId] = useState(null); + const [searchOptions, setSearchOptions] = useState([]); + const [searching, setSearching] = useState(false); + const searchTimer = useRef>(); + + // 与当前选中节点相关的连边 + const relatedEdges = selectedNode + ? currentEdges.filter( + (e) => e.source === selectedNode.id || e.target === selectedNode.id, + ) + : []; + + // 获取连边的另一侧节点名称 + const getPeerName = (edge: ChainEdge) => { + if (!selectedNode) return ''; + const peerId = edge.source === selectedNode.id ? edge.target : edge.source; + return peerId; + }; + + // 防抖搜索 + const handleSearch = useCallback((keyword: string) => { + if (!keyword || keyword.length < 1) { + setSearchOptions([]); + return; + } + if (searchTimer.current) clearTimeout(searchTimer.current); + searchTimer.current = setTimeout(async () => { + setSearching(true); + try { + const res = await mockChainAdapter.searchNodes({ keyword }); + if (res.code === 200 && res.data) { + // 过滤掉当前节点自身 + setSearchOptions(res.data.filter((n) => n.id !== selectedNode?.id)); + } + } catch { + message.error('搜索组织失败'); + } finally { + setSearching(false); + } + }, 350); + }, [selectedNode]); + + // 清理定时器 + useEffect(() => { + return () => { + if (searchTimer.current) clearTimeout(searchTimer.current); + }; + }, []); + + // 关闭时重置 + useEffect(() => { + if (!visible) { + setAdding(false); + setEditingId(null); + form.resetFields(); + } + }, [visible, form]); + + // 提交添加关系 + const handleAdd = async () => { + if (!selectedNode) return; + try { + const values = await form.validateFields(); + setSubmitting(true); + const data: RelationFormData = { + sourceTargetId: selectedNode.targetId, + targetTargetId: values.targetNodeId, + relationType: values.relationType, + description: values.description, + }; + const res = await mockChainAdapter.addRelation(data); + if (res.code === 200) { + message.success('关系添加成功'); + setAdding(false); + form.resetFields(); + onRelationsChanged(); + } + } catch { + // 表单校验失败 + } finally { + setSubmitting(false); + } + }; + + // 提交编辑 + const handleEdit = async () => { + if (!editingId) return; + try { + const values = await form.validateFields(); + setSubmitting(true); + const res = await mockChainAdapter.updateRelation(editingId, { + relationType: values.relationType, + description: values.description, + }); + if (res.code === 200) { + message.success('关系修改成功'); + setEditingId(null); + form.resetFields(); + onRelationsChanged(); + } + } catch { + // 表单校验失败 + } finally { + setSubmitting(false); + } + }; + + // 删除关系 + const handleDelete = async (edgeId: string) => { + const res = await mockChainAdapter.deleteRelation(edgeId); + if (res.code === 200) { + message.success('关系已删除'); + onRelationsChanged(); + } + }; + + // 进入编辑模式 + const startEdit = (edge: ChainEdge) => { + setEditingId(edge.id); + setAdding(false); + form.setFieldsValue({ + relationType: edge.relationType, + description: edge.description || '', + }); + }; + + // 取消编辑/添加 + const cancelEdit = () => { + setEditingId(null); + setAdding(false); + form.resetFields(); + }; + + if (!selectedNode) return null; + + return ( + + + 关系管理 —— {selectedNode.name} + + } + placement="right" + width={420} + open={visible} + onClose={onClose} + destroyOnClose + > + {/* 已有关系列表 */} +
+
+ 当前关系 ({relatedEdges.length}) + {!adding && !editingId && ( + + )} +
+ + {relatedEdges.length === 0 && !adding && ( +
暂无关系数据,点击"添加关系"新建
+ )} + + {relatedEdges.map((edge) => ( +
+ {editingId === edge.id ? ( + /* ---- 编辑模式 ---- */ +
+ + + + +