@yuku123/z-frontend-common 0.2.9 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +25 -0
  2. package/README.md +55 -42
  3. package/dist/index.js +1 -0
  4. package/package.json +49 -44
  5. package/src/components/ErrorBoundary/index.jsx +24 -77
  6. package/src/components/Layout/index.jsx +141 -38
  7. package/src/components/LoginPage/index.jsx +47 -21
  8. package/src/components/StatusTag/index.jsx +14 -12
  9. package/src/index.js +32 -38
  10. package/src/services/request.js +74 -4
  11. package/dist/z-frontend-common.css +0 -1
  12. package/dist/z-frontend-common.es.js +0 -51275
  13. package/dist/z-frontend-common.umd.js +0 -459
  14. package/src/components/Ctc/Layout.jsx +0 -330
  15. package/src/components/Ctc/Layout.module.css +0 -145
  16. package/src/components/Ctc/agentTeam/index.tsx +0 -308
  17. package/src/components/Ctc/login/AuthPage.module.css +0 -26
  18. package/src/components/Ctc/login/AuthPage.tsx +0 -235
  19. package/src/components/Ctc/login/index.less +0 -49
  20. package/src/components/Ctc/login/index.tsx +0 -142
  21. package/src/components/Ctc/uc/UCLogin.tsx +0 -31
  22. package/src/components/Ctc/uc/UCPhoneLogin.tsx +0 -62
  23. package/src/components/Ctc/uc/UCRegister.tsx +0 -37
  24. package/src/components/Ctc/uc/UCResetPassword.tsx +0 -69
  25. package/src/components/Ctc/uc/index.tsx +0 -15
  26. package/src/components/Ctc/userPanel/index.tsx +0 -999
  27. package/src/components/Ctc/webide/index.tsx +0 -272
  28. package/src/components/LowCode/LowCodeModel.jsx +0 -962
  29. package/src/components/LowCode/LowCodePage.jsx +0 -31
  30. package/src/components/LowCode/LowCodeRuntime.jsx +0 -335
  31. package/src/components/LowCode/MaterializePage.jsx +0 -235
  32. package/src/components/LowCode/index.js +0 -1
  33. package/src/components/MockPlatform/CurlImportModal.jsx +0 -362
  34. package/src/components/MockPlatform/EndpointsTab.jsx +0 -509
  35. package/src/components/MockPlatform/EnvironmentsTab.jsx +0 -212
  36. package/src/components/MockPlatform/MockTemplateHelper.jsx +0 -200
  37. package/src/components/MockPlatform/OpenApiImportModal.jsx +0 -305
  38. package/src/components/MockPlatform/RecordingsTab.jsx +0 -397
  39. package/src/components/MockPlatform/RequestLogsTab.jsx +0 -239
  40. package/src/components/MockPlatform/ScenariosTab.jsx +0 -236
  41. package/src/components/MockPlatform/TestCasesTab.jsx +0 -462
  42. package/src/components/MockPlatform/index.jsx +0 -127
  43. package/src/components/Overview.jsx +0 -74
  44. package/src/dev.jsx +0 -28
  45. package/src/services/agentTeam.js +0 -7
  46. package/src/services/api.js +0 -84
  47. package/src/services/ctcAc.js +0 -7
  48. package/src/services/ctcAcDomain.js +0 -7
  49. package/src/services/ctcAuthorization.js +0 -7
  50. package/src/services/ctcSurl.js +0 -7
  51. package/src/services/ctcUser.js +0 -7
  52. package/src/services/job.js +0 -7
  53. package/src/services/metaApp.js +0 -7
  54. package/src/services/privateConfig.js +0 -7
  55. package/src/services/webide.js +0 -6
  56. package/src/services/workspace.js +0 -21
  57. package/src/utils/request.ts +0 -102
package/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ MIT License
2
+
3
+ <<<<<<< HEAD
4
+ Copyright (c) 2026 z-opc-foundation‌
5
+ =======
6
+ Copyright (c) 2026 z-opc-foundation
7
+ >>>>>>> 037d896 (feat: z-frontend-common 独立仓首版 1.0.0(lead 008 §1-§16 唯一承载仓))
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
package/README.md CHANGED
@@ -1,59 +1,72 @@
1
- # @yuku123/z-frontend-common
1
+ # z-frontend-common
2
2
 
3
- > z-opc 共享前端组件库 · React 19 + Ant Design 6
3
+ > z-opc-foundation 全仓共享前端基座 —— lead 008《前端开发规范》§1-§16 的唯一承载仓。
4
4
 
5
- ## 安装
5
+ 40+ 个 `z-*` 产品仓的 suit 通过同一份 AppLayout / LoginPage 获得一致的
6
+ 左菜单架子、登录流程与右上角用户组件;21 个 `<repo>-component` 的 services
7
+ 层消费同一份 axios 拦截器工厂。**改一处,全仓同步**。
6
8
 
7
- ```bash
8
- npm install @yuku123/z-frontend-common
9
- # 国内用户加速
10
- # .npmrc: @yuku1231:registry=https://registry.npmmirror.com/
11
- ```
12
-
13
- ## 导出能力
9
+ ---
14
10
 
15
- ### 组件
16
- - `AppLayout` — 统一布局(Sider + Header + Content)
17
- - `StatusTag` — 状态标签
18
- - `LoginPage` — 统一登录页
19
- - `ErrorBoundary` — React 错误边界
11
+ ## 基本信息
20
12
 
21
- ### 请求工具
22
- - `request` — 默认实例(`baseURL=/api`,识别 `{code, data, message}` 后端响应)
23
- - `authRequest` — 同上(语义区分)
24
- - `createRequest({baseURL, tokenKey, userInfoKey, unauthorizedRedirect, timeout})` — 工厂
25
- - `setupInterceptors(instance, options)` — 自定义拦截器
13
+ | 字段 | 值 |
14
+ |------|-----|
15
+ | 仓 | `z-opc-foundation/z-frontend-common`(本仓) |
16
+ | npm 包 | `@yuku123/z-frontend-common`(scope 沿用 yuku123,与 0.1.x/0.2.x 旧线连续) |
17
+ | 当前版本 | `1.0.0` —— 1.0 语义:§1-§16 规范化起点。0.1.x 是 pre-§16 形态;0.2.x 内嵌 react-dom 已废弃 |
18
+ | 技术栈 | React 19 + antd 6 + react-router-dom 7 + Vite 6(lead 005 §3.0 版本线单一尺子) |
19
+ | 产物 | ESM 单文件 `dist/index.js`;react / antd / @ant-design/icons / react-router-dom 全部 external |
20
+ | axios | `dependencies` 内联(`_shared/` vendoring 时期的决策保留:避免消费方从无 node_modules 的目录解析 axios) |
26
21
 
27
- ## 使用示例
28
-
29
- ```jsx
30
- import { AppLayout, LoginPage, request } from '@yuku123/z-frontend-common'
22
+ ## 对外 API(与 0.1.1 完全兼容 + 三处升级)
31
23
 
32
- // 业务模块推荐:在 src/utils/request.js 写 2-10 行适配层
33
- import { createRequest } from '@yuku123/z-frontend-common'
34
- const request = createRequest({ baseURL: '/meta', tokenKey: 'token' })
35
- export default request
36
- ```
24
+ | 导出 | 说明 |
25
+ |------|------|
26
+ | `AppLayout` | **升级**:§1 sider 双模式品牌区(暗主题显式白字 / 折叠 36×36 圆角图标 + Tooltip)、§11 selectedKey 最长菜单前缀、§12 sider 顶 `产品名@版本`、§15 Header 48px + 1px 下边线、§16 `appUser` 右上角下拉(profile + 退出登录,`loginPath` 按 `menuItems[0].key` 自动推导可覆盖) |
27
+ | `LoginPage` | **升级**:`mockAuth` 双模式 —— 传 `{username, password, user}` 走纯前端校验(CTC/SSO 接入前短期形态);缺省走 `loginApi` 真实端点 |
28
+ | `EmptyState`(占位) | 目录已建,组件下一波随 §2 列表页规范收口时进 |
29
+ | `StatusTag` / `defaultStatusMap` | 0.1.1 契约原样保留 |
30
+ | `ErrorBoundary` | 0.1.1 契约原样保留(antd Result 形态重绘) |
31
+ | `createRequest(options)` | axios 实例工厂:`{baseURL='/api', timeout=10000, tokenKey, userInfoKey, unauthorizedRedirect}`;21 个 component 仓 services 层消费 |
32
+ | `request` / `authRequest` | 预建实例(`createRequest()` 双份,0.1.1 契约) |
33
+ | `setupInterceptors` | 拦截器装配(Bearer token 注入 + 401 清凭跳登录 + 响应解包) |
34
+ | `unwrapResponse` | **新增显式导出**:`{code, data}` 包装解包 / 裸数据透传(z-opc Spring MVC 统一返回体兼容) |
35
+ | `default`(桶) | `{AppLayout, StatusTag, defaultStatusMap, LoginPage, ErrorBoundary, request, authRequest, createRequest}` —— 0.1.1 形状原样 |
37
36
 
38
- ## 统一后端响应格式
37
+ ## AppLayout props
39
38
 
40
- ```json
41
- { "code": 200, "data": T, "message": "ok" }
39
+ ```jsx
40
+ <AppLayout
41
+ menuItems={menuConfig} // antd Menu items,每项必须带 icon(§10)
42
+ appTitle="z-vector" // sider 展开态文字
43
+ appShort="ZV" // 无 appIcon 时折叠态缩写
44
+ appVersion="1.0.5" // §12:展开态渲染 z-vector@1.0.5(不要 "v" 前缀)
45
+ appIcon={{icon, color, label}} // §1:折叠态 36×36 圆角色板 + Tooltip
46
+ appUser={{name, role}} // §16:右上角 Avatar+name+DownOutlined 下拉
47
+ loginPath="/z-vector/login" // 可选,缺省按 menuItems[0].key 自动推导 /<产品>/login
48
+ headerExtra={<.../>} // Header 右侧追加内容(版本号别放这,§12)
49
+ loading={false} // 菜单区 loading 占位
50
+ />
42
51
  ```
43
52
 
44
- - `code=200` → 自动返回 `data`
45
- - 其他 code → reject
46
- - 响应无 `code` 字段 → pass-through 透传
53
+ ## suit 接入模板(lead 008 §16)
47
54
 
48
- ## 长期方案
55
+ ```jsx
56
+ import { AppLayout, LoginPage, ErrorBoundary } from '@yuku123/z-frontend-common'
57
+ import { menuConfig, routes, appMeta } from '@yuku123/<repo>-component/pages'
49
58
 
50
- - 模板从 z-meta 拉取(数据库驱动)
51
- - 业务侧 `res.data.xxx` 写法统一
59
+ const MOCK_AUTH = { username: 'admin', password: '123456', user: { name: 'admin', role: '管理员' } }
52
60
 
53
- ## 仓库
61
+ // <Route path="/<repo>/login" element={<LoginPage title="<repo> 管理台" mockAuth={MOCK_AUTH} redirectUrl="/<repo>/home" />} />
62
+ // <Route element={<ProtectedShell/>}> 内挂 <AppLayout menuItems={menuConfig} appTitle={appMeta.title} appVersion="…" appUser={user} />
63
+ ```
54
64
 
55
- https://github.com/yuku123/z-opc/tree/main/z-frontend-common
65
+ 完整模板见 lead 008 §16;出厂自检清单见 §9。
56
66
 
57
- ## License
67
+ ## 历史
58
68
 
59
- MIT
69
+ - `0.1.0 – 0.1.6`:早期形态(pre-§16),源码曾位于 `yuku123/z-opc` 仓的 `z-frontend-common/` 子目录
70
+ - `0.2.x`:内嵌 react-dom 的失败尝试线,已废弃
71
+ - 应急过渡:`z-opc-foundation/_shared/z-frontend-common-local/` vendoring(不入 git,1.0.0 发版后退役)
72
+ - `1.0.0`:独立仓首版 —— 源码自 vendoring 迁入,API 契约兼容 0.1.1,AppLayout/LoginPage 升级 §1-§16 规范版
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{Avatar as H,Button as j,Dropdown as W,Layout as R,Menu as N,Spin as J,Tooltip as A}from"antd";import{DownOutlined as G,LoginOutlined as Q,MenuFoldOutlined as X,MenuUnfoldOutlined as Y}from"@ant-design/icons";import{Outlet as Z,useLocation as _,useNavigate as U}from"react-router-dom";import{useState as V}from"react";import{jsx as o,jsxs as m}from"react/jsx-runtime";var{Header:ee,Sider:te,Content:re}=R;function oe(e,t=.85){let n=/^#?([0-9a-f]{6})$/i.exec(e||"");if(!n)return e;let l=parseInt(n[1],16),f=Math.max(0,Math.min(255,Math.round((l>>16&255)*t))),r=Math.max(0,Math.min(255,Math.round((l>>8&255)*t))),s=Math.max(0,Math.min(255,Math.round((l&255)*t)));return"#"+(f<<16|r<<8|s).toString(16).padStart(6,"0")}function L({menuItems:e=[],appTitle:t="One Company",appShort:n="OC",appVersion:l,appIcon:f,appUser:r,loginPath:s,headerExtra:v,loading:C=!1}){let[i,c]=V(!1),F=U(),$=_(),P=({key:d})=>{F(d)},M=()=>{if(s)return s;let d=e&&e[0],a=d&&d.key;return typeof a=="string"&&a.endsWith("/home")?a.slice(0,-4)+"login":"/login"},z=(d,a=[])=>{for(let u of d||[])u?.key&&a.push(u.key),u?.children&&z(u.children,a);return a},D=(()=>{let d=z(e),a=$.pathname;return d.filter(u=>a===u||a.startsWith(u+"/")).sort((u,b)=>b.length-u.length)[0]||a})();return m(R,{style:{minHeight:"100vh"},children:[m(te,{trigger:null,collapsible:!0,collapsed:i,children:[o("div",{style:{height:64,display:"flex",alignItems:"center",justifyContent:"center"},children:(()=>{if(i){if(f){let{icon:d,color:a="#7c3aed",label:u=t}=f,b=o("div",{style:{width:36,height:36,borderRadius:9,background:`linear-gradient(135deg, ${a}, ${oe(a,.7)})`,display:"flex",alignItems:"center",justifyContent:"center",color:"#fff",fontSize:20,boxShadow:"0 2px 6px rgba(0,0,0,0.25)",overflow:"hidden"},children:d});return o(A,{title:u,placement:"right",children:b})}return n}return m("span",{style:{color:"#fff",fontWeight:600,fontSize:16,whiteSpace:"nowrap"},children:[t,l&&m("span",{style:{color:"rgba(255,255,255,0.55)",fontWeight:400,fontSize:12,marginLeft:4},children:["@",l]})]})})()}),C?o("div",{style:{padding:16,textAlign:"center"},children:o(J,{size:"small"})}):o(N,{theme:"dark",mode:"inline",selectedKeys:[D],items:e,onClick:P})]}),m(R,{children:[m(ee,{style:{height:48,lineHeight:"48px",padding:"0 16px",background:"#fff",display:"flex",alignItems:"center",justifyContent:"space-between",borderBottom:"1px solid #f1f5f9"},children:[o("span",{onClick:()=>c(!i),style:{fontSize:18,cursor:"pointer"},children:i?o(Y,{}):o(X,{})}),m("div",{style:{display:"flex",alignItems:"center",gap:12},children:[r&&o(W,{menu:{items:[{key:"profile",label:`${r.name}\uFF08${r.role||"\u7528\u6237"}\uFF09`,disabled:!0},{type:"divider"},{key:"logout",label:"\u9000\u51FA\u767B\u5F55",onClick:()=>{localStorage.removeItem("token"),localStorage.removeItem("userInfo"),window.location.href=M()}}]},placement:"bottomRight",children:m("span",{style:{cursor:"pointer",display:"inline-flex",alignItems:"center",gap:6},children:[o(H,{size:28,style:{background:"#7c3aed"},children:(r.name||"?").slice(0,1).toUpperCase()}),o("span",{style:{fontSize:13,color:"#0f172a"},children:r.name}),o(G,{style:{fontSize:10,color:"#94a3b8"}})]})}),!r&&o(j,{type:"primary",size:"small",icon:o(Q,{}),onClick:()=>{window.location.href=M()},children:"\u767B\u5F55"}),v]})]}),o(re,{style:{margin:16,padding:24,background:"#fff",minHeight:280},children:o(Z,{})})]})]})}import{useState as se}from"react";import{Alert as ae,Button as ie,Card as le,Form as S,Input as K,message as h}from"antd";import{LockOutlined as fe,UserOutlined as ue}from"@ant-design/icons";import ne from"axios";import"antd";function B(e){return e&&typeof e=="object"&&"code"in e&&"data"in e?e.code!==200?Promise.reject(new Error(e.message||"\u8BF7\u6C42\u5931\u8D25")):e.data:e}function O(e,t={}){let{tokenKey:n="token",userInfoKey:l="userInfo",unauthorizedRedirect:f="/login"}=t;return e.interceptors.request.use(r=>{let s=localStorage.getItem(n);return s&&(r.headers.Authorization=`Bearer ${s}`),r}),e.interceptors.response.use(r=>B(r.data),r=>{let s=r.response?.status;return s===401?(localStorage.removeItem(n),localStorage.removeItem(l),window.location.href=f):s===403?console.error("\u6CA1\u6709\u6743\u9650\u8BBF\u95EE\u8BE5\u8D44\u6E90"):s===500&&console.error("\u670D\u52A1\u5668\u5185\u90E8\u9519\u8BEF"),Promise.reject(r)}),e}function y(e={}){let t=ne.create({baseURL:e.baseURL??"/api",timeout:e.timeout??1e4});return O(t,{tokenKey:e.tokenKey,userInfoKey:e.userInfoKey,unauthorizedRedirect:e.unauthorizedRedirect})}var x=y(),q=y();import{jsx as p,jsxs as T}from"react/jsx-runtime";function I({title:e="\u7EDF\u4E00\u7BA1\u7406\u5E73\u53F0",mockAuth:t,loginApi:n="/auth/login",redirectUrl:l="/",onSuccess:f}){let[r,s]=se(!1),v=i=>{localStorage.setItem("token",t?"mock-"+t.username:i?.token||"token"),localStorage.setItem("userInfo",JSON.stringify(i||{})),h.success("\u767B\u5F55\u6210\u529F"+(t?"\uFF08mock \u6A21\u5F0F\uFF0C\u5F85 CTC/SSO \u63A5\u5165\uFF09":"")),f?f(i):window.location.href=l},C=async i=>{s(!0);try{if(t){i.username===t.username&&i.password===t.password?v(t.user):h.error("\u7528\u6237\u540D\u6216\u5BC6\u7801\u9519\u8BEF");return}let c=await x.post(n,i);c?.token?(localStorage.setItem("token",c.token),localStorage.setItem("userInfo",JSON.stringify(c.user||c)),h.success("\u767B\u5F55\u6210\u529F"),f?f(c):window.location.href=l):h.error("\u767B\u5F55\u5931\u8D25\uFF1A\u672A\u83B7\u53D6\u5230 token")}catch(c){h.error(c.response?.data?.message||c.message||"\u767B\u5F55\u5931\u8D25")}finally{s(!1)}};return p("div",{style:{height:"100vh",display:"flex",justifyContent:"center",alignItems:"center",background:"#f0f2f5"},children:T(le,{title:e,style:{width:400},children:[t&&p(ae,{type:"info",showIcon:!0,style:{marginBottom:16},message:`Mock \u6A21\u5F0F\uFF1A\u7528\u6237\u540D ${t.username} / \u5BC6\u7801 ${t.password}`}),T(S,{onFinish:C,size:"large",children:[p(S.Item,{name:"username",rules:[{required:!0,message:"\u8BF7\u8F93\u5165\u7528\u6237\u540D"}],children:p(K,{prefix:p(ue,{}),placeholder:"\u7528\u6237\u540D"})}),p(S.Item,{name:"password",rules:[{required:!0,message:"\u8BF7\u8F93\u5165\u5BC6\u7801"}],children:p(K.Password,{prefix:p(fe,{}),placeholder:"\u5BC6\u7801"})}),p(S.Item,{children:p(ie,{type:"primary",htmlType:"submit",loading:r,block:!0,children:"\u767B \u5F55"})})]})]})})}import{Tag as ce}from"antd";import{jsx as de}from"react/jsx-runtime";var w={1:{color:"green",text:"\u6B63\u5E38"},0:{color:"red",text:"\u7981\u7528"},pending:{color:"orange",text:"\u5F85\u5904\u7406"},approved:{color:"blue",text:"\u5DF2\u901A\u8FC7"},rejected:{color:"red",text:"\u5DF2\u62D2\u7EDD"}};function k({status:e,map:t}){let n=t?.[e]||w[e]||{color:"default",text:String(e)};return de(ce,{color:n.color,children:n.text})}import{Component as pe}from"react";import{Button as me,Result as ge}from"antd";import{jsx as E}from"react/jsx-runtime";var g=class extends pe{constructor(t){super(t),this.state={hasError:!1,error:null,errorInfo:null},this.handleReset=()=>{this.setState({hasError:!1,error:null,errorInfo:null}),typeof this.props.onReset=="function"&&this.props.onReset()}}static getDerivedStateFromError(t){return{hasError:!0,error:t}}componentDidCatch(t,n){console.error("[ErrorBoundary] caught error:",t,n),this.setState({errorInfo:n})}render(){return this.state.hasError?E(ge,{status:"error",title:"\u9875\u9762\u51FA\u9519\u4E86",subTitle:String(this.state.error?.message||this.state.error),extra:E(me,{type:"primary",onClick:this.handleReset,children:"\u91CD\u8BD5"})}):this.props.children}};var Ae={AppLayout:L,StatusTag:k,defaultStatusMap:w,LoginPage:I,ErrorBoundary:g,request:x,authRequest:q,createRequest:y};export{L as AppLayout,g as ErrorBoundary,I as LoginPage,k as StatusTag,q as authRequest,y as createRequest,Ae as default,w as defaultStatusMap,x as request,O as setupInterceptors,B as unwrapResponse};
package/package.json CHANGED
@@ -1,64 +1,69 @@
1
1
  {
2
2
  "name": "@yuku123/z-frontend-common",
3
- "version": "0.2.9",
4
- "private": false,
5
- "description": "z-opc 共享前端组件库 - AppLayout / StatusTag / LoginPage / ErrorBoundary + 统一 createRequest 工厂",
6
- "keywords": [
7
- "react",
8
- "antd",
9
- "vite",
10
- "frontend",
11
- "z-opc"
12
- ],
13
- "license": "MIT",
14
- "author": {
15
- "name": "zifang",
16
- "email": "1340947819@qq.com"
17
- },
18
- "homepage": "https://github.com/yuku123/z-opc-frontend/tree/main/packages/z-frontend-common#readme",
19
- "repository": {
20
- "type": "git",
21
- "url": "https://github.com/yuku123/z-opc-frontend.git",
22
- "directory": "packages/z-frontend-common"
23
- },
24
- "bugs": {
25
- "url": "https://github.com/yuku123/z-opc-frontend/issues"
26
- },
27
- "main": "./dist/z-frontend-common.umd.js",
28
- "module": "./dist/z-frontend-common.es.js",
3
+ "version": "1.0.0",
4
+ "description": "z-opc 全仓共享前端基座 — AppLayout(§1/§11/§12/§15/§16) + LoginPage(mockAuth 双模式) + EmptyState/StatusTag/ErrorBoundary + createRequest 工厂。lead 008 前端开发规范唯一承载仓",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
29
8
  "types": "./dist/index.d.ts",
30
9
  "exports": {
31
10
  ".": {
32
- "import": "./dist/z-frontend-common.es.js",
33
- "require": "./dist/z-frontend-common.umd.js"
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
34
13
  }
35
14
  },
36
15
  "files": [
37
16
  "dist",
38
- "src",
39
- "README.md"
17
+ "src"
40
18
  ],
41
- "publishConfig": {
42
- "access": "public",
43
- "registry": "https://registry.npmjs.org/"
44
- },
45
19
  "scripts": {
46
- "dev": "vite --port 3099",
47
20
  "build": "vite build",
48
- "preview": "vite preview"
49
- },
50
- "peerDependencies": {
51
- "react": "^19.2.0",
52
- "react-dom": "^19.2.0",
53
- "antd": "^6.3.3",
54
- "react-router-dom": "^7.13.1"
21
+ "dev": "vite build --watch",
22
+ "prepublishOnly": "vite build"
55
23
  },
56
24
  "dependencies": {
57
25
  "axios": "^1.16.1"
58
26
  },
27
+ "peerDependencies": {
28
+ "@ant-design/icons": "^6.0.0",
29
+ "antd": "^6.0.0",
30
+ "react": "^19.0.0",
31
+ "react-dom": "^19.0.0",
32
+ "react-router-dom": "^7.0.0"
33
+ },
59
34
  "devDependencies": {
35
+ "@ant-design/icons": "^6.1.0",
36
+ "@types/react": "^19.2.0",
37
+ "@types/react-dom": "^19.2.0",
60
38
  "@vitejs/plugin-react": "^4.2.1",
61
- "vite": "^6.2.0"
39
+ "antd": "^6.3.3",
40
+ "axios": "^1.16.1",
41
+ "react": "^19.2.0",
42
+ "react-dom": "^19.2.0",
43
+ "react-router-dom": "^7.13.1",
44
+ "vite": "^6.2.6"
45
+ },
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/z-opc-foundation/z-frontend-common.git"
62
49
  },
63
- "style": "./dist/z-frontend-common.css"
50
+ "bugs": {
51
+ "url": "https://github.com/z-opc-foundation/z-frontend-common/issues"
52
+ },
53
+ "homepage": "https://github.com/z-opc-foundation/z-frontend-common#readme",
54
+ "keywords": [
55
+ "react",
56
+ "antd",
57
+ "vite",
58
+ "z-opc",
59
+ "frontend-common",
60
+ "applayout",
61
+ "admin-shell"
62
+ ],
63
+ "author": "yuku123",
64
+ "license": "MIT",
65
+ "publishConfig": {
66
+ "registry": "https://registry.npmjs.org/",
67
+ "access": "public"
68
+ }
64
69
  }
@@ -1,93 +1,40 @@
1
- import React from 'react'
2
- import {Alert, Button, Space} from 'antd'
3
- import {BugOutlined, ReloadOutlined} from '@ant-design/icons'
1
+ import { Component } from 'react'
2
+ import { Button, Result } from 'antd'
4
3
 
5
4
  /**
6
- * React Error Boundary — 捕获子组件的 render/lifecycle/effect 异常,
7
- * 用 fallback UI 替代冒泡到整棵 React 树导致整体卸载。
8
- *
9
- * 用法:
10
- * <ErrorBoundary fallbackTitle="流程编辑器加载失败">
11
- * <WorkflowEditor />
12
- * </ErrorBoundary>
5
+ * 兜底错误边界(0.1.1 契约保留):render 崩了显示错误 + 重置按钮。
6
+ * props: children + 可选 onReset(点「重试」时回调,缺省只清内部态)。
13
7
  */
14
- export default class ErrorBoundary extends React.Component {
8
+ export default class ErrorBoundary extends Component {
15
9
  constructor(props) {
16
10
  super(props)
17
- this.state = {hasError: false, error: null, errorInfo: null}
11
+ this.state = { hasError: false, error: null, errorInfo: null }
12
+ this.handleReset = () => {
13
+ this.setState({ hasError: false, error: null, errorInfo: null })
14
+ typeof this.props.onReset === 'function' && this.props.onReset()
15
+ }
18
16
  }
19
17
 
20
18
  static getDerivedStateFromError(error) {
21
- return {hasError: true, error}
22
- }
23
-
24
- componentDidCatch(error, errorInfo) {
25
- // eslint-disable-next-line no-console
26
- console.error('[ErrorBoundary] caught error:', error, errorInfo)
27
- this.setState({errorInfo})
19
+ return { hasError: true, error }
28
20
  }
29
21
 
30
- handleReset = () => {
31
- this.setState({hasError: false, error: null, errorInfo: null})
32
- if (typeof this.props.onReset === 'function') {
33
- this.props.onReset()
34
- }
22
+ componentDidCatch(error, info) {
23
+ console.error('[ErrorBoundary] caught error:', error, info)
24
+ this.setState({ errorInfo: info })
35
25
  }
36
26
 
37
27
  render() {
38
- if (!this.state.hasError) {
39
- return this.props.children
40
- }
41
-
42
- const {fallbackTitle = '组件加载失败', fallbackDescription, showReload = true} = this.props
43
- const errMsg = (this.state.error && (this.state.error.message || String(this.state.error))) || '未知错误'
44
-
45
- return (
46
- <div
47
- style={{
48
- padding: 24,
49
- background: '#fff2f0',
50
- border: '1px solid #ffccc7',
51
- borderRadius: 8,
52
- minHeight: 200,
53
- }}
54
- >
55
- <Alert
56
- type="error"
57
- showIcon
58
- icon={<BugOutlined/>}
59
- message={fallbackTitle}
60
- description={
61
- <div>
62
- <div style={{marginBottom: 8}}>
63
- {fallbackDescription || '当前组件发生了异常,已被错误边界捕获。其他功能仍可正常使用。'}
64
- </div>
65
- <div
66
- style={{
67
- fontFamily: 'Menlo, Monaco, Consolas, monospace',
68
- fontSize: 12,
69
- color: '#cf1322',
70
- background: '#fff',
71
- padding: 8,
72
- borderRadius: 4,
73
- border: '1px solid #ffccc7',
74
- wordBreak: 'break-word',
75
- whiteSpace: 'pre-wrap',
76
- }}
77
- >
78
- {errMsg}
79
- </div>
80
- </div>
81
- }
28
+ if (this.state.hasError) {
29
+ return (
30
+ <Result
31
+ status="error"
32
+ title="页面出错了"
33
+ subTitle={String(this.state.error?.message || this.state.error)}
34
+ extra={<Button type="primary" onClick={this.handleReset}>重试</Button>}
82
35
  />
83
- {showReload && (
84
- <Space style={{marginTop: 16}}>
85
- <Button icon={<ReloadOutlined/>} onClick={this.handleReset}>
86
- 重试
87
- </Button>
88
- </Space>
89
- )}
90
- </div>
91
- )
36
+ )
37
+ }
38
+ return this.props.children
92
39
  }
93
40
  }
@@ -1,69 +1,172 @@
1
- import {Layout, Menu, Spin} from 'antd'
2
- import {MenuFoldOutlined, MenuUnfoldOutlined} from '@ant-design/icons'
3
- import {Outlet, useLocation, useNavigate} from 'react-router-dom'
4
- import {useState} from 'react'
1
+ import { Avatar, Button, Dropdown, Layout, Menu, Spin, Tooltip } from 'antd'
2
+ import { DownOutlined, LoginOutlined, MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'
3
+ import { Outlet, useLocation, useNavigate } from 'react-router-dom'
4
+ import { useState } from 'react'
5
5
 
6
- const {Header, Sider, Content} = Layout
6
+ const { Header, Sider, Content } = Layout
7
+
8
+ function shade(hex, amount = 0.85) {
9
+ const m = /^#?([0-9a-f]{6})$/i.exec(hex || '')
10
+ if (!m) return hex
11
+ const n = parseInt(m[1], 16)
12
+ const r = Math.max(0, Math.min(255, Math.round(((n >> 16) & 0xff) * amount)))
13
+ const g = Math.max(0, Math.min(255, Math.round(((n >> 8) & 0xff) * amount)))
14
+ const b = Math.max(0, Math.min(255, Math.round((n & 0xff) * amount)))
15
+ return '#' + ((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')
16
+ }
7
17
 
8
18
  /**
9
- * 统一布局组件
10
- * @param {Object} props
11
- * @param {Array} props.menuItems - antd Menu items 格式
12
- * @param {string} props.appTitle - 应用标题
13
- * @param {string} props.appShort - 折叠时标题缩写
14
- * @param {ReactNode} props.headerExtra - 头部右侧额外内容
15
- * @param {boolean} props.loading - 是否加载中
19
+ * 通用左菜单架子 AppLayout — lead 008 §1/§11/§12/§15/§16 全 46 仓 suit 强制落地。
20
+ *
21
+ * props:
22
+ * - menuItems / appTitle / appShort / appIcon / headerExtra / loading: 同 z-vector-component 原版
23
+ * - appVersion?: string — sider 展开态展示「z-vector@1.0.5」格式
24
+ * - appUser?: { name, role } — Header 右上角 avatar+name+下拉(profile/退出登录)
25
+ * - loginPath?: string — 退出登录跳转目标,缺省按 menuItems[0].key 自动推导(/home → /login)
16
26
  */
17
- export default function AppLayout({
18
- menuItems = [],
19
- appTitle = 'One Company',
20
- appShort = 'OC',
21
- headerExtra,
22
- loading = false,
23
- }) {
27
+ export function AppLayout({
28
+ menuItems = [],
29
+ appTitle = 'One Company',
30
+ appShort = 'OC',
31
+ appVersion,
32
+ appIcon,
33
+ appUser,
34
+ loginPath,
35
+ headerExtra,
36
+ loading = false,
37
+ }) {
24
38
  const [collapsed, setCollapsed] = useState(false)
25
39
  const navigate = useNavigate()
26
40
  const location = useLocation()
27
41
 
28
- const handleMenuClick = ({key}) => {
29
- navigate(key)
42
+ const handleMenuClick = ({ key }) => { navigate(key) }
43
+
44
+ // 退出登录跳转:loginPath 显式传优先;否则按 menuItems[0].key 自动推导 /<产品>/home → /<产品>/login
45
+ const computeLoginPath = () => {
46
+ if (loginPath) return loginPath
47
+ const first = menuItems && menuItems[0]
48
+ const k = first && first.key
49
+ if (typeof k === 'string' && k.endsWith('/home')) {
50
+ return k.slice(0, -4) + 'login'
51
+ }
52
+ return '/login'
53
+ }
54
+
55
+ // URL 即状态(lead 008 §11):用 location.pathname 的最长菜单前缀作 selectedKey
56
+ const collectKeys = (items, acc = []) => {
57
+ for (const it of items || []) {
58
+ if (it?.key) acc.push(it.key)
59
+ if (it?.children) collectKeys(it.children, acc)
60
+ }
61
+ return acc
62
+ }
63
+ const selectedKey = (() => {
64
+ const keys = collectKeys(menuItems)
65
+ const p = location.pathname
66
+ return keys.filter((k) => p === k || p.startsWith(k + '/'))
67
+ .sort((a, b) => b.length - a.length)[0] || p
68
+ })()
69
+
70
+ const renderBrand = () => {
71
+ if (collapsed) {
72
+ if (appIcon) {
73
+ const { icon, color = '#7c3aed', label = appTitle } = appIcon
74
+ const node = (
75
+ <div style={{
76
+ width: 36, height: 36, borderRadius: 9,
77
+ background: `linear-gradient(135deg, ${color}, ${shade(color, 0.7)})`,
78
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
79
+ color: '#fff', fontSize: 20, boxShadow: '0 2px 6px rgba(0,0,0,0.25)',
80
+ overflow: 'hidden',
81
+ }}>{icon}</div>
82
+ )
83
+ return <Tooltip title={label} placement="right">{node}</Tooltip>
84
+ }
85
+ return appShort
86
+ }
87
+ return (
88
+ <span style={{ color: '#fff', fontWeight: 600, fontSize: 16, whiteSpace: 'nowrap' }}>
89
+ {appTitle}
90
+ {appVersion && (
91
+ <span style={{
92
+ color: 'rgba(255,255,255,0.55)', fontWeight: 400, fontSize: 12, marginLeft: 4,
93
+ }}>@{appVersion}</span>
94
+ )}
95
+ </span>
96
+ )
30
97
  }
31
98
 
32
99
  return (
33
- <Layout style={{minHeight: '100vh'}}>
100
+ <Layout style={{ minHeight: '100vh' }}>
34
101
  <Sider trigger={null} collapsible collapsed={collapsed}>
35
102
  <div style={{
36
103
  height: 64, display: 'flex', alignItems: 'center', justifyContent: 'center',
37
- color: 'white', fontSize: collapsed ? 14 : 18, fontWeight: 'bold',
38
104
  }}>
39
- {collapsed ? appShort : appTitle}
105
+ {renderBrand()}
40
106
  </div>
41
107
  {loading ? (
42
- <div style={{padding: 16, textAlign: 'center'}}>
43
- <Spin size="small"/>
44
- </div>
108
+ <div style={{ padding: 16, textAlign: 'center' }}><Spin size="small" /></div>
45
109
  ) : (
46
- <Menu theme="dark" mode="inline"
47
- selectedKeys={[location.pathname]}
48
- items={menuItems}
49
- onClick={handleMenuClick}
110
+ <Menu
111
+ theme="dark"
112
+ mode="inline"
113
+ selectedKeys={[selectedKey]}
114
+ items={menuItems}
115
+ onClick={handleMenuClick}
50
116
  />
51
117
  )}
52
118
  </Sider>
53
119
  <Layout>
54
120
  <Header style={{
55
- padding: '0 16px', background: '#fff',
121
+ height: 48, lineHeight: '48px', padding: '0 16px', background: '#fff',
56
122
  display: 'flex', alignItems: 'center', justifyContent: 'space-between',
123
+ borderBottom: '1px solid #f1f5f9',
57
124
  }}>
58
- <span onClick={() => setCollapsed(!collapsed)} style={{fontSize: 18, cursor: 'pointer'}}>
59
- {collapsed ? <MenuUnfoldOutlined/> : <MenuFoldOutlined/>}
60
- </span>
61
- {headerExtra}
125
+ <span onClick={() => setCollapsed(!collapsed)} style={{ fontSize: 18, cursor: 'pointer' }}>
126
+ {collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
127
+ </span>
128
+ <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
129
+ {appUser && (
130
+ <Dropdown
131
+ menu={{
132
+ items: [
133
+ { key: 'profile', label: `${appUser.name}(${appUser.role || '用户'})`, disabled: true },
134
+ { type: 'divider' },
135
+ {
136
+ key: 'logout', label: '退出登录', onClick: () => {
137
+ localStorage.removeItem('token')
138
+ localStorage.removeItem('userInfo')
139
+ window.location.href = computeLoginPath()
140
+ },
141
+ },
142
+ ],
143
+ }}
144
+ placement="bottomRight"
145
+ >
146
+ <span style={{ cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 6 }}>
147
+ <Avatar size={28} style={{ background: '#7c3aed' }}>
148
+ {(appUser.name || '?').slice(0, 1).toUpperCase()}
149
+ </Avatar>
150
+ <span style={{ fontSize: 13, color: '#0f172a' }}>{appUser.name}</span>
151
+ <DownOutlined style={{ fontSize: 10, color: '#94a3b8' }} />
152
+ </span>
153
+ </Dropdown>
154
+ )}
155
+ {!appUser && (
156
+ <Button type="primary" size="small" icon={<LoginOutlined />}
157
+ onClick={() => { window.location.href = computeLoginPath() }}>
158
+ 登录
159
+ </Button>
160
+ )}
161
+ {headerExtra}
162
+ </div>
62
163
  </Header>
63
- <Content style={{margin: 16, padding: 24, background: '#fff', minHeight: 280}}>
64
- <Outlet/>
164
+ <Content style={{ margin: 16, padding: 24, background: '#fff', minHeight: 280 }}>
165
+ <Outlet />
65
166
  </Content>
66
167
  </Layout>
67
168
  </Layout>
68
169
  )
69
170
  }
171
+
172
+ export default AppLayout