@yuku123/z-frontend-common 1.1.2 → 1.1.4

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.
package/dist/index.js CHANGED
@@ -1,43 +1,43 @@
1
- import { C as be, a as we, D as ke, E as Ie, b as Ce, F as Re, G as ve, L as ze, P as Me, c as Be, R as Le, S as Oe, d as je, e as Ee, T as Te, f as $e, g as Ke, h as De, l as Pe, n as qe, p as Fe, r as He, s as We, i as Ne } from "./ComponentDoc-C4XFElke.js";
1
+ import { C as be, a as we, D as Ie, b as ke, E as Ce, c as Re, F as ve, G as Le, L as ze, d as Me, P as Be, e as Oe, R as Te, S as je, f as Ee, g as $e, T as De, h as Ke, i as Fe, j as Pe, k as qe, l as He, n as Ne, p as We, r as Ge, s as Je, m as _e, u as Ae } from "./ComponentDoc-22ujWxLE.js";
2
2
  import { jsxs as h, jsx as r } from "react/jsx-runtime";
3
- import { Layout as k, Spin as E, Menu as T, Dropdown as $, Avatar as K, Button as I, Tooltip as D, Card as P, Alert as q, Form as x, Input as z, message as S, Tag as F, Result as H } from "antd";
4
- import { MenuUnfoldOutlined as W, MenuFoldOutlined as N, DownOutlined as G, LoginOutlined as J, UserOutlined as Q, LockOutlined as V } from "@ant-design/icons";
5
- import { useNavigate as X, useLocation as Y, Outlet as Z } from "react-router-dom";
6
- import { useState as B, Component as _ } from "react";
7
- import U from "axios";
8
- const { Header: A, Sider: ee, Content: te } = k;
3
+ import { Layout as I, Spin as j, Menu as E, Dropdown as $, Avatar as D, Button as k, Tooltip as K, Card as F, Alert as P, Form as x, Input as L, message as S, Tag as q, Result as H } from "antd";
4
+ import { MenuUnfoldOutlined as N, MenuFoldOutlined as W, DownOutlined as G, LoginOutlined as J, UserOutlined as _, LockOutlined as A } from "@ant-design/icons";
5
+ import { useNavigate as Q, useLocation as U, Outlet as V } from "react-router-dom";
6
+ import { useState as M, Component as X } from "react";
7
+ import Y from "axios";
8
+ const { Header: Z, Sider: ee, Content: te } = I;
9
9
  function re(e, t = 0.85) {
10
10
  const s = /^#?([0-9a-f]{6})$/i.exec(e || "");
11
11
  if (!s) return e;
12
- const d = parseInt(s[1], 16), f = Math.max(0, Math.min(255, Math.round((d >> 16 & 255) * t))), n = Math.max(0, Math.min(255, Math.round((d >> 8 & 255) * t))), i = Math.max(0, Math.min(255, Math.round((d & 255) * t)));
13
- return "#" + (f << 16 | n << 8 | i).toString(16).padStart(6, "0");
12
+ const d = parseInt(s[1], 16), u = Math.max(0, Math.min(255, Math.round((d >> 16 & 255) * t))), n = Math.max(0, Math.min(255, Math.round((d >> 8 & 255) * t))), i = Math.max(0, Math.min(255, Math.round((d & 255) * t)));
13
+ return "#" + (u << 16 | n << 8 | i).toString(16).padStart(6, "0");
14
14
  }
15
15
  function ne({
16
16
  menuItems: e = [],
17
17
  appTitle: t = "One Company",
18
18
  appShort: s = "OC",
19
19
  appVersion: d,
20
- appIcon: f,
20
+ appIcon: u,
21
21
  appUser: n,
22
22
  loginPath: i,
23
23
  headerExtra: p,
24
24
  loading: m = !1
25
25
  }) {
26
- const [o, g] = B(!1), y = X(), c = Y(), O = ({ key: u }) => {
27
- y(u);
26
+ const [o, g] = M(!1), y = Q(), c = U(), O = ({ key: f }) => {
27
+ y(f);
28
28
  }, R = () => {
29
29
  if (i) return i;
30
- const u = e && e[0], l = u && u.key;
30
+ const f = e && e[0], l = f && f.key;
31
31
  return typeof l == "string" && l.endsWith("/home") ? l.slice(0, -4) + "login" : "/login";
32
- }, v = (u, l = []) => {
33
- for (const a of u || [])
32
+ }, v = (f, l = []) => {
33
+ for (const a of f || [])
34
34
  a != null && a.key && l.push(a.key), a != null && a.children && v(a.children, l);
35
35
  return l;
36
- }, j = (() => {
37
- const u = v(e), l = c.pathname;
38
- return u.filter((a) => l === a || l.startsWith(a + "/")).sort((a, w) => w.length - a.length)[0] || l;
36
+ }, T = (() => {
37
+ const f = v(e), l = c.pathname;
38
+ return f.filter((a) => l === a || l.startsWith(a + "/")).sort((a, w) => w.length - a.length)[0] || l;
39
39
  })();
40
- return /* @__PURE__ */ h(k, { style: { minHeight: "100vh" }, children: [
40
+ return /* @__PURE__ */ h(I, { style: { minHeight: "100vh" }, children: [
41
41
  /* @__PURE__ */ h(ee, { trigger: null, collapsible: !0, collapsed: o, children: [
42
42
  /* @__PURE__ */ r("div", { style: {
43
43
  height: 64,
@@ -46,8 +46,8 @@ function ne({
46
46
  justifyContent: "center"
47
47
  }, children: (() => {
48
48
  if (o) {
49
- if (f) {
50
- const { icon: u, color: l = "#7c3aed", label: a = t } = f, w = /* @__PURE__ */ r("div", { style: {
49
+ if (u) {
50
+ const { icon: f, color: l = "#7c3aed", label: a = t } = u, w = /* @__PURE__ */ r("div", { style: {
51
51
  width: 36,
52
52
  height: 36,
53
53
  borderRadius: 9,
@@ -59,8 +59,8 @@ function ne({
59
59
  fontSize: 20,
60
60
  boxShadow: "0 2px 6px rgba(0,0,0,0.25)",
61
61
  overflow: "hidden"
62
- }, children: u });
63
- return /* @__PURE__ */ r(D, { title: a, placement: "right", children: w });
62
+ }, children: f });
63
+ return /* @__PURE__ */ r(K, { title: a, placement: "right", children: w });
64
64
  }
65
65
  return s;
66
66
  }
@@ -77,19 +77,19 @@ function ne({
77
77
  ] })
78
78
  ] });
79
79
  })() }),
80
- m ? /* @__PURE__ */ r("div", { style: { padding: 16, textAlign: "center" }, children: /* @__PURE__ */ r(E, { size: "small" }) }) : /* @__PURE__ */ r(
81
- T,
80
+ m ? /* @__PURE__ */ r("div", { style: { padding: 16, textAlign: "center" }, children: /* @__PURE__ */ r(j, { size: "small" }) }) : /* @__PURE__ */ r(
81
+ E,
82
82
  {
83
83
  theme: "dark",
84
84
  mode: "inline",
85
- selectedKeys: [j],
85
+ selectedKeys: [T],
86
86
  items: e,
87
87
  onClick: O
88
88
  }
89
89
  )
90
90
  ] }),
91
- /* @__PURE__ */ h(k, { children: [
92
- /* @__PURE__ */ h(A, { style: {
91
+ /* @__PURE__ */ h(I, { children: [
92
+ /* @__PURE__ */ h(Z, { style: {
93
93
  height: 48,
94
94
  lineHeight: "48px",
95
95
  padding: "0 16px",
@@ -99,7 +99,7 @@ function ne({
99
99
  justifyContent: "space-between",
100
100
  borderBottom: "1px solid #f1f5f9"
101
101
  }, children: [
102
- /* @__PURE__ */ r("span", { onClick: () => g(!o), style: { fontSize: 18, cursor: "pointer" }, children: o ? /* @__PURE__ */ r(W, {}) : /* @__PURE__ */ r(N, {}) }),
102
+ /* @__PURE__ */ r("span", { onClick: () => g(!o), style: { fontSize: 18, cursor: "pointer" }, children: o ? /* @__PURE__ */ r(N, {}) : /* @__PURE__ */ r(W, {}) }),
103
103
  /* @__PURE__ */ h("div", { style: { display: "flex", alignItems: "center", gap: 12 }, children: [
104
104
  n && /* @__PURE__ */ r(
105
105
  $,
@@ -119,14 +119,14 @@ function ne({
119
119
  },
120
120
  placement: "bottomRight",
121
121
  children: /* @__PURE__ */ h("span", { style: { cursor: "pointer", display: "inline-flex", alignItems: "center", gap: 6 }, children: [
122
- /* @__PURE__ */ r(K, { size: 28, style: { background: "#7c3aed" }, children: (n.name || "?").slice(0, 1).toUpperCase() }),
122
+ /* @__PURE__ */ r(D, { size: 28, style: { background: "#7c3aed" }, children: (n.name || "?").slice(0, 1).toUpperCase() }),
123
123
  /* @__PURE__ */ r("span", { style: { fontSize: 13, color: "#0f172a" }, children: n.name }),
124
124
  /* @__PURE__ */ r(G, { style: { fontSize: 10, color: "#94a3b8" } })
125
125
  ] })
126
126
  }
127
127
  ),
128
128
  !n && /* @__PURE__ */ r(
129
- I,
129
+ k,
130
130
  {
131
131
  type: "primary",
132
132
  size: "small",
@@ -140,7 +140,7 @@ function ne({
140
140
  p
141
141
  ] })
142
142
  ] }),
143
- /* @__PURE__ */ r(te, { style: { margin: 16, padding: 24, background: "#fff", minHeight: 280 }, children: /* @__PURE__ */ r(Z, {}) })
143
+ /* @__PURE__ */ r(te, { style: { margin: 16, padding: 24, background: "#fff", minHeight: 280 }, children: /* @__PURE__ */ r(V, {}) })
144
144
  ] })
145
145
  ] });
146
146
  }
@@ -151,7 +151,7 @@ function se(e, t = {}) {
151
151
  const {
152
152
  tokenKey: s = "token",
153
153
  userInfoKey: d = "userInfo",
154
- unauthorizedRedirect: f = "/login"
154
+ unauthorizedRedirect: u = "/login"
155
155
  } = t;
156
156
  return e.interceptors.request.use((n) => {
157
157
  const i = localStorage.getItem(s);
@@ -163,14 +163,14 @@ function se(e, t = {}) {
163
163
  const i = (p = n.response) == null ? void 0 : p.status;
164
164
  if (i === 401) {
165
165
  const g = (m = n.response) == null ? void 0 : m.data;
166
- !!(g && typeof g == "object" && g.errorCode) ? console.warn(`[401] 业务鉴权失败(不是会话过期,不跳登录): ${(o = n.config) == null ? void 0 : o.url} → ${g.errorCode}`) : (localStorage.removeItem(s), localStorage.removeItem(d), window.location.href = f);
166
+ !!(g && typeof g == "object" && g.errorCode) ? console.warn(`[401] 业务鉴权失败(不是会话过期,不跳登录): ${(o = n.config) == null ? void 0 : o.url} → ${g.errorCode}`) : (localStorage.removeItem(s), localStorage.removeItem(d), window.location.href = u);
167
167
  } else i === 403 ? console.error("没有权限访问该资源") : i === 500 && console.error("服务器内部错误");
168
168
  return Promise.reject(n);
169
169
  }
170
170
  ), e;
171
171
  }
172
172
  function C(e = {}) {
173
- const t = U.create({
173
+ const t = Y.create({
174
174
  baseURL: e.baseURL ?? "/api",
175
175
  timeout: e.timeout ?? 1e4
176
176
  });
@@ -186,10 +186,10 @@ function ie({
186
186
  mockAuth: t,
187
187
  loginApi: s = "/auth/login",
188
188
  redirectUrl: d = "/",
189
- onSuccess: f
189
+ onSuccess: u
190
190
  }) {
191
- const [n, i] = B(!1), p = (o) => {
192
- localStorage.setItem("token", t ? "mock-" + t.username : (o == null ? void 0 : o.token) || "token"), localStorage.setItem("userInfo", JSON.stringify(o || {})), S.success("登录成功" + (t ? "(mock 模式,待 CTC/SSO 接入)" : "")), f ? f(o) : window.location.href = d;
191
+ const [n, i] = M(!1), p = (o) => {
192
+ localStorage.setItem("token", t ? "mock-" + t.username : (o == null ? void 0 : o.token) || "token"), localStorage.setItem("userInfo", JSON.stringify(o || {})), S.success("登录成功" + (t ? "(mock 模式,待 CTC/SSO 接入)" : "")), u ? u(o) : window.location.href = d;
193
193
  }, m = async (o) => {
194
194
  var g, y;
195
195
  i(!0);
@@ -199,7 +199,7 @@ function ie({
199
199
  return;
200
200
  }
201
201
  const c = await b.post(s, o);
202
- c != null && c.token ? (localStorage.setItem("token", c.token), localStorage.setItem("userInfo", JSON.stringify(c.user || c)), S.success("登录成功"), f ? f(c) : window.location.href = d) : S.error("登录失败:未获取到 token");
202
+ c != null && c.token ? (localStorage.setItem("token", c.token), localStorage.setItem("userInfo", JSON.stringify(c.user || c)), S.success("登录成功"), u ? u(c) : window.location.href = d) : S.error("登录失败:未获取到 token");
203
203
  } catch (c) {
204
204
  S.error(((y = (g = c.response) == null ? void 0 : g.data) == null ? void 0 : y.message) || c.message || "登录失败");
205
205
  } finally {
@@ -212,9 +212,9 @@ function ie({
212
212
  justifyContent: "center",
213
213
  alignItems: "center",
214
214
  background: "#f0f2f5"
215
- }, children: /* @__PURE__ */ h(P, { title: e, style: { width: 400 }, children: [
215
+ }, children: /* @__PURE__ */ h(F, { title: e, style: { width: 400 }, children: [
216
216
  t && /* @__PURE__ */ r(
217
- q,
217
+ P,
218
218
  {
219
219
  type: "info",
220
220
  showIcon: !0,
@@ -223,13 +223,13 @@ function ie({
223
223
  }
224
224
  ),
225
225
  /* @__PURE__ */ h(x, { onFinish: m, size: "large", children: [
226
- /* @__PURE__ */ r(x.Item, { name: "username", rules: [{ required: !0, message: "请输入用户名" }], children: /* @__PURE__ */ r(z, { prefix: /* @__PURE__ */ r(Q, {}), placeholder: "用户名" }) }),
227
- /* @__PURE__ */ r(x.Item, { name: "password", rules: [{ required: !0, message: "请输入密码" }], children: /* @__PURE__ */ r(z.Password, { prefix: /* @__PURE__ */ r(V, {}), placeholder: "密码" }) }),
228
- /* @__PURE__ */ r(x.Item, { children: /* @__PURE__ */ r(I, { type: "primary", htmlType: "submit", loading: n, block: !0, children: "登 录" }) })
226
+ /* @__PURE__ */ r(x.Item, { name: "username", rules: [{ required: !0, message: "请输入用户名" }], children: /* @__PURE__ */ r(L, { prefix: /* @__PURE__ */ r(_, {}), placeholder: "用户名" }) }),
227
+ /* @__PURE__ */ r(x.Item, { name: "password", rules: [{ required: !0, message: "请输入密码" }], children: /* @__PURE__ */ r(L.Password, { prefix: /* @__PURE__ */ r(A, {}), placeholder: "密码" }) }),
228
+ /* @__PURE__ */ r(x.Item, { children: /* @__PURE__ */ r(k, { type: "primary", htmlType: "submit", loading: n, block: !0, children: "登 录" }) })
229
229
  ] })
230
230
  ] }) });
231
231
  }
232
- const L = {
232
+ const B = {
233
233
  1: { color: "green", text: "正常" },
234
234
  0: { color: "red", text: "禁用" },
235
235
  pending: { color: "orange", text: "待处理" },
@@ -237,10 +237,10 @@ const L = {
237
237
  rejected: { color: "red", text: "已拒绝" }
238
238
  };
239
239
  function le({ status: e, map: t }) {
240
- const s = (t == null ? void 0 : t[e]) || L[e] || { color: "default", text: String(e) };
241
- return /* @__PURE__ */ r(F, { color: s.color, children: s.text });
240
+ const s = (t == null ? void 0 : t[e]) || B[e] || { color: "default", text: String(e) };
241
+ return /* @__PURE__ */ r(q, { color: s.color, children: s.text });
242
242
  }
243
- class ce extends _ {
243
+ class ce extends X {
244
244
  constructor(t) {
245
245
  super(t), this.state = { hasError: !1, error: null, errorInfo: null }, this.handleReset = () => {
246
246
  this.setState({ hasError: !1, error: null, errorInfo: null }), typeof this.props.onReset == "function" && this.props.onReset();
@@ -260,15 +260,15 @@ class ce extends _ {
260
260
  status: "error",
261
261
  title: "页面出错了",
262
262
  subTitle: String(((t = this.state.error) == null ? void 0 : t.message) || this.state.error),
263
- extra: /* @__PURE__ */ r(I, { type: "primary", onClick: this.handleReset, children: "重试" })
263
+ extra: /* @__PURE__ */ r(k, { type: "primary", onClick: this.handleReset, children: "重试" })
264
264
  }
265
265
  ) : this.props.children;
266
266
  }
267
267
  }
268
- const de = ["get", "post", "put", "patch", "delete", "head", "options"], M = {
268
+ const de = ["get", "post", "put", "patch", "delete", "head", "options"], z = {
269
269
  AppLayout: ne,
270
270
  StatusTag: le,
271
- defaultStatusMap: L,
271
+ defaultStatusMap: B,
272
272
  LoginPage: ie,
273
273
  ErrorBoundary: ce,
274
274
  request: b,
@@ -276,41 +276,45 @@ const de = ["get", "post", "put", "patch", "delete", "head", "options"], M = {
276
276
  createRequest: C
277
277
  };
278
278
  for (const e of de)
279
- typeof b[e] == "function" && !(e in M) && (M[e] = (...t) => b[e](...t));
279
+ typeof b[e] == "function" && !(e in z) && (z[e] = (...t) => b[e](...t));
280
280
  export {
281
281
  ne as AppLayout,
282
282
  be as CategoryDot,
283
283
  we as ComponentDoc,
284
+ Ie as DEFAULT_PAGINATION,
284
285
  ke as DarkBanner,
285
- Ie as EmptyState,
286
+ Ce as EmptyState,
286
287
  ce as ErrorBoundary,
287
- Ce as ErrorState,
288
- Re as FeatureCard,
289
- ve as GradientStatCard,
290
- ze as LoadingState,
288
+ Re as ErrorState,
289
+ ve as FeatureCard,
290
+ Le as GradientStatCard,
291
+ ze as ListTable,
292
+ Me as LoadingState,
291
293
  ie as LoginPage,
292
- Me as PageHeader,
293
- Be as PagedTable,
294
- Le as RefreshButton,
295
- Oe as SearchInput,
296
- je as SectionHeader,
297
- Ee as StatusBadge,
294
+ Be as PageHeader,
295
+ Oe as PagedTable,
296
+ Te as RefreshButton,
297
+ je as SearchInput,
298
+ Ee as SectionHeader,
299
+ $e as StatusBadge,
298
300
  le as StatusTag,
299
- Te as TableToolbar,
300
- $e as antdTheme,
301
+ De as TableToolbar,
302
+ Ke as antdTheme,
301
303
  ae as authRequest,
302
- Ke as brand,
304
+ Fe as brand,
303
305
  C as createRequest,
304
- M as default,
305
- L as defaultStatusMap,
306
- De as fontSize,
307
- Pe as lift,
308
- qe as neutral,
309
- Fe as palette,
310
- He as radius,
306
+ z as default,
307
+ B as defaultStatusMap,
308
+ Pe as filterRows,
309
+ qe as fontSize,
310
+ He as lift,
311
+ Ne as neutral,
312
+ We as palette,
313
+ Ge as radius,
311
314
  b as request,
312
315
  se as setupInterceptors,
313
- We as shadow,
314
- Ne as space,
315
- oe as unwrapResponse
316
+ Je as shadow,
317
+ _e as space,
318
+ oe as unwrapResponse,
319
+ Ae as useListFilter
316
320
  };
package/dist/ui.js CHANGED
@@ -1,27 +1,31 @@
1
- import { C as s, a as t, D as r, E as o, b as n, F as d, G as S, L as i, P as l, c as p, R as g, S as u, d as b, e as c, T as f, f as h, g as C, h as T, l as m, n as D, p as B, r as E, s as P, i as y } from "./ComponentDoc-C4XFElke.js";
1
+ import { C as e, a as t, D as r, b as o, E as n, c as d, F as i, G as l, L as S, d as u, P as T, e as p, R as b, S as f, f as g, g as c, T as h, h as m, i as C, j as D, k as L, l as E, n as F, p as P, r as A, s as B, m as G, u as I } from "./ComponentDoc-22ujWxLE.js";
2
2
  export {
3
- s as CategoryDot,
3
+ e as CategoryDot,
4
4
  t as ComponentDoc,
5
- r as DarkBanner,
6
- o as EmptyState,
7
- n as ErrorState,
8
- d as FeatureCard,
9
- S as GradientStatCard,
10
- i as LoadingState,
11
- l as PageHeader,
5
+ r as DEFAULT_PAGINATION,
6
+ o as DarkBanner,
7
+ n as EmptyState,
8
+ d as ErrorState,
9
+ i as FeatureCard,
10
+ l as GradientStatCard,
11
+ S as ListTable,
12
+ u as LoadingState,
13
+ T as PageHeader,
12
14
  p as PagedTable,
13
- g as RefreshButton,
14
- u as SearchInput,
15
- b as SectionHeader,
15
+ b as RefreshButton,
16
+ f as SearchInput,
17
+ g as SectionHeader,
16
18
  c as StatusBadge,
17
- f as TableToolbar,
18
- h as antdTheme,
19
+ h as TableToolbar,
20
+ m as antdTheme,
19
21
  C as brand,
20
- T as fontSize,
21
- m as lift,
22
- D as neutral,
23
- B as palette,
24
- E as radius,
25
- P as shadow,
26
- y as space
22
+ D as filterRows,
23
+ L as fontSize,
24
+ E as lift,
25
+ F as neutral,
26
+ P as palette,
27
+ A as radius,
28
+ B as shadow,
29
+ G as space,
30
+ I as useListFilter
27
31
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuku123/z-frontend-common",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "z-opc 全仓共享前端基座 — AppLayout(§1/§11/§12/§15/§16) + LoginPage(mockAuth 双模式) + EmptyState/StatusTag/ErrorBoundary + createRequest 工厂。lead 008 前端开发规范唯一承载仓",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,107 @@
1
+ import {Card, Table} from 'antd'
2
+ import EmptyState from './EmptyState'
3
+ import SearchInput from './SearchInput'
4
+ import useListFilter from './useListFilter'
5
+
6
+ /**
7
+ * ListTable —— 列表页的标准形态(lead 008 §2 强制规则 2/3/4 的成品件)。
8
+ *
9
+ * 为什么要有这个组件:
10
+ * §2 要求每个 `<Table>` 渲染的列表页都必须凑齐三件套 ——
11
+ * 规则 2 分页(`pageSize` + `showSizeChanger` + `pageSizeOptions` + `showTotal`)
12
+ * 规则 3 查询(Card 的 extra 里一个过滤框,端侧过滤)
13
+ * 规则 4 空态用 `<EmptyState>`,不是 antd 默认空图
14
+ * 逐页手写这三样,分母是全库几百个 Table 元素 ⇒ 漏一处就是一处不一致,
15
+ * 而且每处都要自己决定「按哪个字段过滤」,很容易各写各的。
16
+ * 收进基座后,各仓只需要声明「按哪几个字段过滤」和「空态说什么」。
17
+ *
18
+ * 为什么搜索用基座自己的 SearchInput 而不是 antd 的 `Input.Search`:
19
+ * 两者都是 300ms 防抖 + allowClear,但 SearchInput 是基座实现,Esc 立即清空、
20
+ * Enter 立即出结果,全仓口径一致;各仓不用再各自决定用哪个。
21
+ *
22
+ * 透传约定:
23
+ * - `cardProps` → 传给外层 `<Card>`(如 `style` / `size` / `title`)
24
+ * - `cardExtra` → 传给外层 `<Card>` 的 `extra`(动作按钮)。若同时给了 `searchKeys`,
25
+ * 过滤框与 extra **并排**渲染,不会二选一吞掉另一个。
26
+ * - `tableProps` → 传给内层 `<Table>`(如 `scroll` / `columns` 之外的表级属性)
27
+ * - 其余 props → 直接给 `<Table>`(与 antd 一致)
28
+ * - `pagination={false}` 仍然允许显式传(用于「数据已知 ≤10 行且不变」的豁免场景,
29
+ * 但按 §2 必须在该处注释豁免原因)。
30
+ */
31
+ export const DEFAULT_PAGINATION = {
32
+ pageSize: 10,
33
+ showSizeChanger: true,
34
+ pageSizeOptions: ['10', '20', '50', '100'],
35
+ showTotal: (total) => `共 ${total} 条`,
36
+ }
37
+
38
+ export default function ListTable({
39
+ rows,
40
+ columns,
41
+ rowKey,
42
+ loading,
43
+ searchKeys,
44
+ searchPlaceholder = '过滤',
45
+ searchWidth = 220,
46
+ cardTitle,
47
+ cardExtra,
48
+ emptyTitle = '暂无数据',
49
+ emptyDescription,
50
+ emptyActionText,
51
+ emptyOnAction,
52
+ pagination = DEFAULT_PAGINATION,
53
+ cardProps = {},
54
+ tableProps = {},
55
+ ...rest
56
+ }) {
57
+ const {keyword, setKeyword, filtered} = useListFilter(rows, searchKeys)
58
+
59
+ // searchKeys 为空 ⇒ 不渲染搜索框(过滤也是空操作)
60
+ const search = searchKeys
61
+ ? (
62
+ <SearchInput
63
+ allowClear
64
+ width={searchWidth}
65
+ placeholder={searchPlaceholder}
66
+ value={keyword}
67
+ onChange={setKeyword}
68
+ />
69
+ )
70
+ : null
71
+
72
+ // Card 的 extra 可以同时挂「过滤框」和「动作按钮」(例如「关系」表既有搜索又有「新增」)。
73
+ // 二选一会把其中一个静默吞掉,所以这里两个都有就并排渲染。
74
+ const extra = (search && cardExtra)
75
+ ? (
76
+ <span style={{display: 'inline-flex', alignItems: 'center', gap: 8}}>
77
+ {search}
78
+ {cardExtra}
79
+ </span>
80
+ )
81
+ : (search || cardExtra)
82
+
83
+ return (
84
+ <Card title={cardTitle} extra={extra} {...cardProps}>
85
+ <Table
86
+ rowKey={rowKey}
87
+ columns={columns}
88
+ dataSource={filtered}
89
+ loading={loading}
90
+ size="small"
91
+ pagination={pagination}
92
+ locale={{
93
+ emptyText: (
94
+ <EmptyState
95
+ title={emptyTitle}
96
+ description={emptyDescription}
97
+ actionText={emptyActionText}
98
+ onAction={emptyOnAction}
99
+ />
100
+ ),
101
+ }}
102
+ {...tableProps}
103
+ {...rest}
104
+ />
105
+ </Card>
106
+ )
107
+ }
@@ -26,6 +26,10 @@ export {default as EmptyState} from './EmptyState'
26
26
  export {default as ErrorState} from './ErrorState'
27
27
  export {default as LoadingState} from './LoadingState'
28
28
  export {default as SearchInput} from './SearchInput'
29
+ // useListFilter —— §2 强制规则 3 的客户端过滤(各仓只声明按哪几个字段过滤,口径统一收在基座)
30
+ export {default as useListFilter, filterRows} from './useListFilter'
31
+ // ListTable —— §2 强制规则 2/3/4 的成品件(分页 + 过滤 + EmptyState 三件套一次到位)
32
+ export {default as ListTable, DEFAULT_PAGINATION} from './ListTable'
29
33
  // EChartsChart 走 ./echarts 子路径: 它是本目录唯一引 echarts 的组件,
30
34
  export {default as PageHeader} from './PageHeader'
31
35
  export {default as TableToolbar, RefreshButton} from './TableToolbar'
@@ -0,0 +1,74 @@
1
+ /**
2
+ * useListFilter —— 列表页的客户端过滤(lead 008 §2 强制规则 3)。
3
+ *
4
+ * 为什么要有这个 hook:
5
+ * §2 要求「任何列表页必须有查询」,而且明确「不要求走后端搜索(端侧过滤足够,
6
+ * 除非集合 >1k 行)」。这意味着每个列表页都要写一遍「关键字 → 过滤行」的逻辑。
7
+ * 分散写就会分裂出 N 种口径:有的大小写敏感、有的只匹配第一个字段、有的把
8
+ * null 当成空串匹配到全部。统一收进基座,各仓只声明「按哪几个字段过滤」。
9
+ *
10
+ * 口径(与 §2 一致):
11
+ * - 大小写不敏感(用户按任意大小写输入都能命中)。
12
+ * - 多字段「或」匹配:任一字段命中即保留该行。
13
+ * - 字段值为 null / undefined 时跳过,不参与匹配(否则 `String(null)` = "null"
14
+ * 会把含 "null" 的关键字误命中到每一行)。
15
+ * - 关键字为空(去掉首尾空白后)时**原样返回入参数组**,不过滤、不复制语义。
16
+ *
17
+ * 为什么过滤键用「字符串或数组都收」:
18
+ * 各仓调用时通常写 `useListFilter(rows, 'id,name')` 这种字面量。若把数组直接
19
+ * 放进 useMemo 依赖,每次渲染都是新数组 → 依赖永远不等 → memo 形同虚设。
20
+ * 这里把 keys 归一化成字符串再进依赖,保证引用稳定。
21
+ */
22
+ import {useCallback, useMemo, useState} from 'react'
23
+
24
+ /**
25
+ * 纯函数:按给定字段做大小写不敏感的「或」匹配过滤。
26
+ * @param {Array<object>} rows 待过滤的行
27
+ * @param {string|string[]} keys 参与匹配的字段名
28
+ * @param {string} keyword 关键字
29
+ * @returns {Array<object>} 过滤后的行(keyword 为空时返回入参本身)
30
+ */
31
+ export function filterRows(rows, keys, keyword) {
32
+ const list = Array.isArray(rows) ? rows : []
33
+ const kw = String(keyword == null ? '' : keyword).trim().toLowerCase()
34
+ if (!kw) return list
35
+
36
+ const fields = (Array.isArray(keys) ? keys : String(keys || '').split(','))
37
+ .map((k) => String(k || '').trim())
38
+ .filter(Boolean)
39
+ if (!fields.length) return list
40
+
41
+ return list.filter((row) => {
42
+ if (!row || typeof row !== 'object') return false
43
+ return fields.some((field) => {
44
+ const v = row[field]
45
+ if (v === null || v === undefined) return false
46
+ return String(v).toLowerCase().includes(kw)
47
+ })
48
+ })
49
+ }
50
+
51
+ /**
52
+ * Hook 版:返回 {keyword, setKeyword, filtered, reset}。
53
+ *
54
+ * @param {Array<object>} rows 数据源
55
+ * @param {string|string[]} keys 参与匹配的字段名(逗号分隔字符串或数组)
56
+ * @returns {{keyword: string, setKeyword: Function, filtered: Array<object>, reset: Function}}
57
+ */
58
+ export function useListFilter(rows, keys) {
59
+ const [keyword, setKeyword] = useState('')
60
+
61
+ // keys 归一成字符串再进依赖:调用方传字面量数组时也不会让 memo 失效
62
+ const keySig = (Array.isArray(keys) ? keys.join(',') : String(keys || '')).trim()
63
+
64
+ const filtered = useMemo(
65
+ () => filterRows(rows, keySig, keyword),
66
+ [rows, keySig, keyword],
67
+ )
68
+
69
+ const reset = useCallback(() => setKeyword(''), [])
70
+
71
+ return {keyword, setKeyword, filtered, reset}
72
+ }
73
+
74
+ export default useListFilter