@yuku123/z-frontend-common 1.1.2 → 1.1.3
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/README.md +6 -5
- package/dist/{ComponentDoc-C4XFElke.js → ComponentDoc-CJL2TB6b.js} +408 -328
- package/dist/index.js +80 -76
- package/dist/ui.js +25 -21
- package/package.json +1 -1
- package/src/components/ui/ListTable.jsx +94 -0
- package/src/components/ui/index.js +4 -0
- package/src/components/ui/useListFilter.js +74 -0
package/dist/index.js
CHANGED
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
import { C as be, a as we, D as ke, E as
|
|
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-CJL2TB6b.js";
|
|
2
2
|
import { jsxs as h, jsx as r } from "react/jsx-runtime";
|
|
3
|
-
import { Layout as
|
|
4
|
-
import { MenuUnfoldOutlined as
|
|
5
|
-
import { useNavigate as
|
|
6
|
-
import { useState as
|
|
7
|
-
import
|
|
8
|
-
const { Header:
|
|
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),
|
|
13
|
-
return "#" + (
|
|
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:
|
|
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] =
|
|
27
|
-
y(
|
|
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
|
|
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 = (
|
|
33
|
-
for (const a of
|
|
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
|
-
},
|
|
37
|
-
const
|
|
38
|
-
return
|
|
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(
|
|
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 (
|
|
50
|
-
const { icon:
|
|
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:
|
|
63
|
-
return /* @__PURE__ */ r(
|
|
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(
|
|
81
|
-
|
|
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: [
|
|
85
|
+
selectedKeys: [T],
|
|
86
86
|
items: e,
|
|
87
87
|
onClick: O
|
|
88
88
|
}
|
|
89
89
|
)
|
|
90
90
|
] }),
|
|
91
|
-
/* @__PURE__ */ h(
|
|
92
|
-
/* @__PURE__ */ h(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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 =
|
|
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 =
|
|
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:
|
|
189
|
+
onSuccess: u
|
|
190
190
|
}) {
|
|
191
|
-
const [n, i] =
|
|
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 接入)" : "")),
|
|
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("登录成功"),
|
|
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(
|
|
215
|
+
}, children: /* @__PURE__ */ h(F, { title: e, style: { width: 400 }, children: [
|
|
216
216
|
t && /* @__PURE__ */ r(
|
|
217
|
-
|
|
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(
|
|
227
|
-
/* @__PURE__ */ r(x.Item, { name: "password", rules: [{ required: !0, message: "请输入密码" }], children: /* @__PURE__ */ r(
|
|
228
|
-
/* @__PURE__ */ r(x.Item, { children: /* @__PURE__ */ r(
|
|
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
|
|
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]) ||
|
|
241
|
-
return /* @__PURE__ */ r(
|
|
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(
|
|
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"],
|
|
268
|
+
const de = ["get", "post", "put", "patch", "delete", "head", "options"], z = {
|
|
269
269
|
AppLayout: ne,
|
|
270
270
|
StatusTag: le,
|
|
271
|
-
defaultStatusMap:
|
|
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
|
|
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
|
-
|
|
286
|
+
Ce as EmptyState,
|
|
286
287
|
ce as ErrorBoundary,
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
ze as
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
+
De as TableToolbar,
|
|
302
|
+
Ke as antdTheme,
|
|
301
303
|
ae as authRequest,
|
|
302
|
-
|
|
304
|
+
Fe as brand,
|
|
303
305
|
C as createRequest,
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
314
|
-
|
|
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
|
|
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-CJL2TB6b.js";
|
|
2
2
|
export {
|
|
3
|
-
|
|
3
|
+
e as CategoryDot,
|
|
4
4
|
t as ComponentDoc,
|
|
5
|
-
r as
|
|
6
|
-
o as
|
|
7
|
-
n as
|
|
8
|
-
d as
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
b as RefreshButton,
|
|
16
|
+
f as SearchInput,
|
|
17
|
+
g as SectionHeader,
|
|
16
18
|
c as StatusBadge,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
h as TableToolbar,
|
|
20
|
+
m as antdTheme,
|
|
19
21
|
C as brand,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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.
|
|
3
|
+
"version": "1.1.3",
|
|
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,94 @@
|
|
|
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
|
+
* - `tableProps` → 传给内层 `<Table>`(如 `scroll` / `columns` 之外的表级属性)
|
|
25
|
+
* - 其余 props → 直接给 `<Table>`(与 antd 一致)
|
|
26
|
+
* - `pagination={false}` 仍然允许显式传(用于「数据已知 ≤10 行且不变」的豁免场景,
|
|
27
|
+
* 但按 §2 必须在该处注释豁免原因)。
|
|
28
|
+
*/
|
|
29
|
+
export const DEFAULT_PAGINATION = {
|
|
30
|
+
pageSize: 10,
|
|
31
|
+
showSizeChanger: true,
|
|
32
|
+
pageSizeOptions: ['10', '20', '50', '100'],
|
|
33
|
+
showTotal: (total) => `共 ${total} 条`,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default function ListTable({
|
|
37
|
+
rows,
|
|
38
|
+
columns,
|
|
39
|
+
rowKey,
|
|
40
|
+
loading,
|
|
41
|
+
searchKeys,
|
|
42
|
+
searchPlaceholder = '过滤',
|
|
43
|
+
searchWidth = 220,
|
|
44
|
+
cardTitle,
|
|
45
|
+
cardExtra,
|
|
46
|
+
emptyTitle = '暂无数据',
|
|
47
|
+
emptyDescription,
|
|
48
|
+
emptyActionText,
|
|
49
|
+
emptyOnAction,
|
|
50
|
+
pagination = DEFAULT_PAGINATION,
|
|
51
|
+
cardProps = {},
|
|
52
|
+
tableProps = {},
|
|
53
|
+
...rest
|
|
54
|
+
}) {
|
|
55
|
+
const {keyword, setKeyword, filtered} = useListFilter(rows, searchKeys)
|
|
56
|
+
|
|
57
|
+
// searchKeys 为空 ⇒ 不渲染搜索框(过滤也是空操作)
|
|
58
|
+
const search = searchKeys
|
|
59
|
+
? (
|
|
60
|
+
<SearchInput
|
|
61
|
+
allowClear
|
|
62
|
+
width={searchWidth}
|
|
63
|
+
placeholder={searchPlaceholder}
|
|
64
|
+
value={keyword}
|
|
65
|
+
onChange={setKeyword}
|
|
66
|
+
/>
|
|
67
|
+
)
|
|
68
|
+
: null
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<Card title={cardTitle} extra={search || cardExtra} {...cardProps}>
|
|
72
|
+
<Table
|
|
73
|
+
rowKey={rowKey}
|
|
74
|
+
columns={columns}
|
|
75
|
+
dataSource={filtered}
|
|
76
|
+
loading={loading}
|
|
77
|
+
size="small"
|
|
78
|
+
pagination={pagination}
|
|
79
|
+
locale={{
|
|
80
|
+
emptyText: (
|
|
81
|
+
<EmptyState
|
|
82
|
+
title={emptyTitle}
|
|
83
|
+
description={emptyDescription}
|
|
84
|
+
actionText={emptyActionText}
|
|
85
|
+
onAction={emptyOnAction}
|
|
86
|
+
/>
|
|
87
|
+
),
|
|
88
|
+
}}
|
|
89
|
+
{...tableProps}
|
|
90
|
+
{...rest}
|
|
91
|
+
/>
|
|
92
|
+
</Card>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
@@ -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
|