@jiangood/open-admin 3.1.7 → 3.1.8

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 (31) hide show
  1. package/package.json +1 -1
  2. package/src/framework/biz/PermActions.tsx +1 -1
  3. package/src/framework/components/DownloadModal/index.tsx +3 -3
  4. package/src/framework/components/FormModal/index.tsx +4 -4
  5. package/src/framework/components/NamedIcon/index.tsx +1 -1
  6. package/src/framework/components/Page/index.tsx +18 -4
  7. package/src/framework/components/PageLoading/index.tsx +2 -2
  8. package/src/framework/components/ProTable/index.less +44 -0
  9. package/src/framework/components/ProTable/index.tsx +54 -13
  10. package/src/framework/fields/BaseRemoteSelect/index.tsx +4 -4
  11. package/src/framework/fields/FieldDateRange/index.tsx +1 -1
  12. package/src/framework/fields/FieldDictSelect/index.tsx +1 -1
  13. package/src/framework/fields/FieldNumberRange/index.tsx +1 -1
  14. package/src/framework/fields/FieldUploadImage/index.tsx +4 -4
  15. package/src/framework/utils/UrlUtils.ts +1 -1
  16. package/src/layouts/admin/index.jsx +1 -1
  17. package/src/layouts/admin/index.less +25 -1
  18. package/src/pages/article/$code.jsx +2 -2
  19. package/src/pages/index.jsx +1 -1
  20. package/src/pages/system/article/index.jsx +13 -15
  21. package/src/pages/system/codegen/index.jsx +1 -3
  22. package/src/pages/system/dict/index.jsx +2 -2
  23. package/src/pages/system/file/index.jsx +3 -3
  24. package/src/pages/system/job/index.jsx +13 -13
  25. package/src/pages/system/log/index.jsx +2 -2
  26. package/src/pages/system/org/index.jsx +5 -5
  27. package/src/pages/system/role/index.jsx +12 -13
  28. package/src/pages/system/role/rolePerm.jsx +1 -1
  29. package/src/pages/system/user/index.jsx +7 -7
  30. package/src/pages/system/user/userPerm.jsx +2 -2
  31. package/src/pages/userCenter/permView.jsx +2 -2
package/package.json CHANGED
@@ -55,7 +55,7 @@
55
55
  "lint": "eslint src --max-warnings=0",
56
56
  "lint:fix": "eslint src --fix"
57
57
  },
58
- "version": "3.1.7",
58
+ "version": "3.1.8",
59
59
  "overrides": {
60
60
  "zod-validation-error": "^4.0.2"
61
61
  }
@@ -101,7 +101,7 @@ export class PermActions extends React.Component<PermActionsProps> {
101
101
 
102
102
  if (!more || visible.length <= 1) {
103
103
  const buttons = visible.map((action, index) => (
104
- <React.Fragment key={index}>{this.renderButton(action, size)}</React.Fragment> // NOSONAR: 按钮无稳定 id,静态过滤后顺序不变
104
+ <React.Fragment key={index}>{this.renderButton(action, size)}</React.Fragment>
105
105
  ));
106
106
  return <Space>{buttons}</Space>;
107
107
  }
@@ -40,8 +40,8 @@ export interface DownloadModalProps {
40
40
  onFinish?: () => void;
41
41
  }
42
42
 
43
- export class DownloadModal extends React.Component<DownloadModalProps, ModalState> { // NOSONAR: state/定时器字段构造器赋值,无法全部 readonly
44
- private abortController: AbortController | null = null; // NOSONAR: 构造器中赋值
43
+ export class DownloadModal extends React.Component<DownloadModalProps, ModalState> {
44
+ private abortController: AbortController | null = null;
45
45
  private lastOptions: DownloadOptions | null = null;
46
46
  private lastTime: number = 0;
47
47
  private lastLoaded: number = 0;
@@ -61,7 +61,7 @@ export class DownloadModal extends React.Component<DownloadModalProps, ModalStat
61
61
  };
62
62
  }
63
63
 
64
- download = (options: DownloadOptions) => { // NOSONAR: ref 暴露给父组件/业务项目调用的公共 API
64
+ download = (options: DownloadOptions) => {
65
65
  this.startDownload(options);
66
66
  };
67
67
 
@@ -12,9 +12,9 @@ export interface FormModalProps {
12
12
  children?: React.ReactNode
13
13
  }
14
14
 
15
- export default class FormModal extends React.Component<FormModalProps> { // NOSONAR: state 构造器中赋值
15
+ export default class FormModal extends React.Component<FormModalProps> {
16
16
 
17
- readonly state = { // NOSONAR: React state 惯例
17
+ readonly state = {
18
18
  visible: false,
19
19
  confirmLoading: false,
20
20
  }
@@ -22,11 +22,11 @@ export default class FormModal extends React.Component<FormModalProps> { // NOSO
22
22
  private pendingValues: Record<string, unknown> | null = null
23
23
  private formRef = React.createRef<FormInstance>()
24
24
 
25
- get formInstance() { // NOSONAR: ref 暴露给父组件/业务项目调用的公共 API
25
+ get formInstance() {
26
26
  return this.formRef.current
27
27
  }
28
28
 
29
- open = (values?: Record<string, unknown>) => { // NOSONAR: ref 暴露给父组件/业务项目调用的公共 API
29
+ open = (values?: Record<string, unknown>) => {
30
30
  this.pendingValues = values || {}
31
31
  this.setState({visible: true})
32
32
  }
@@ -12,7 +12,7 @@ const iconLoaders: Record<string, () => Promise<{ default: IconComponent }>> = {
12
12
  for (const [path, loader] of Object.entries(
13
13
  import.meta.glob('/node_modules/@ant-design/icons/es/icons/*.js')
14
14
  )) {
15
- const name = /([^/]+)\.js$/.exec(path)?.[1]; // NOSONAR: 提取文件名,已最简
15
+ const name = /([^/]+)\.js$/.exec(path)?.[1];
16
16
  if (name) {
17
17
  iconLoaders[name] = loader as () => Promise<{ default: IconComponent }>;
18
18
  }
@@ -1,8 +1,19 @@
1
1
  import React from "react";
2
- import { Typography } from "antd";
2
+ import { Space, Typography } from "antd";
3
3
  import './index.less'
4
4
  import {getToken} from "../../config";
5
- import { PermActions } from "../../biz/PermActions";
5
+ import { PermActions, type PermAction } from "../../biz/PermActions";
6
+ import { PermUtils } from "../../utils";
7
+
8
+ /** 兼容旧写法:按子节点的 perm 属性过滤(新代码请用 PermAction[]) */
9
+ function renderActionNodes(nodes: React.ReactNode) {
10
+ const visible = React.Children.toArray(nodes).filter((node) => {
11
+ const perm = (node as React.ReactElement<{ perm?: string }>)?.props?.perm;
12
+ return !perm || PermUtils.hasPermission(perm);
13
+ });
14
+ if (visible.length === 0) return null;
15
+ return <Space>{visible}</Space>;
16
+ }
6
17
 
7
18
  interface PageProps {
8
19
  padding?: boolean;
@@ -10,7 +21,8 @@ interface PageProps {
10
21
  debug?: boolean;
11
22
  title?: React.ReactNode;
12
23
  description?: React.ReactNode;
13
- actions?: React.ReactNode;
24
+ /** 右侧操作区:推荐传 PermAction[](数据驱动),兼容带 perm 属性的节点 */
25
+ actions?: PermAction[] | React.ReactNode;
14
26
  children?: React.ReactNode;
15
27
  }
16
28
 
@@ -44,7 +56,9 @@ export class Page extends React.Component<PageProps> {
44
56
  {description && <div><Typography.Text type="secondary">{description}</Typography.Text></div>}
45
57
  </div>
46
58
  </div>
47
- {actions && <PermActions>{actions}</PermActions>}
59
+ {actions != null && (Array.isArray(actions)
60
+ ? <PermActions actions={actions}/>
61
+ : renderActionNodes(actions))}
48
62
  </div>
49
63
  </div>
50
64
  )}
@@ -9,7 +9,7 @@ export interface PageLoadingProps {
9
9
  messages?: string[];
10
10
  }
11
11
 
12
- export function PageLoading(props: PageLoadingProps) { // NOSONAR: 仅读取 props,函数组件无需变更
12
+ export function PageLoading(props: PageLoadingProps) {
13
13
  const titles = props.messages || (props.message ? [props.message] : ['页面加载中...']);
14
14
  const primaryColor = getToken().colorPrimary ?? "";
15
15
 
@@ -22,7 +22,7 @@ export function PageLoading(props: PageLoadingProps) { // NOSONAR: 仅读取 pro
22
22
  <div className="oa-page-loading-messages">
23
23
  {titles.map((msg, i) => (
24
24
  <Alert
25
- key={i} // NOSONAR: 提示文案列表静态无稳定 key
25
+ key={i}
26
26
  title={<span style={{color: primaryColor}}>{msg}</span>}
27
27
  type="info"
28
28
  showIcon
@@ -69,6 +69,11 @@
69
69
  gap: 8px;
70
70
  flex: 1;
71
71
  min-width: 0;
72
+
73
+ /* 仅内联搜索时允许换行,存量工具栏布局与之前完全一致 */
74
+ &.has-inline-search {
75
+ flex-wrap: wrap;
76
+ }
72
77
  }
73
78
 
74
79
  .pro-table-toolbar-right {
@@ -81,6 +86,45 @@
81
86
  }
82
87
  }
83
88
 
89
+ /* searchInline:搜索控件内联在工具栏左侧,与操作按钮同一行 */
90
+ .pro-table-search-inline {
91
+ display: flex;
92
+ align-items: center;
93
+ gap: 8px;
94
+ flex-wrap: wrap;
95
+ margin: 0;
96
+
97
+ /* 定宽由 --oa-search-inline-width 控制,业务侧可覆盖;noStyle 的输入控件同样收敛 */
98
+ > .ant-form-item,
99
+ > .ant-input,
100
+ > .ant-input-affix-wrapper,
101
+ > .ant-picker,
102
+ > .ant-select {
103
+ width: var(--oa-search-inline-width, 260px);
104
+ max-width: 100%;
105
+ }
106
+
107
+ .ant-form-item {
108
+ margin: 0;
109
+ }
110
+
111
+ /* 视觉隐藏的提交按钮:保留回车提交能力,且不占位(不用 display:none,避免浏览器忽略默认提交按钮) */
112
+ .pro-table-search-submit {
113
+ position: absolute;
114
+ width: 1px;
115
+ height: 1px;
116
+ min-width: 0;
117
+ padding: 0;
118
+ margin: -1px;
119
+ border: 0;
120
+ overflow: hidden;
121
+ white-space: nowrap;
122
+ clip-path: inset(50%);
123
+ opacity: 0;
124
+ pointer-events: none;
125
+ }
126
+ }
127
+
84
128
  /* 桌面窄窗口下收敛搜索栏列数,避免标签+输入挤压/溢出 */
85
129
  @media (max-width: 1400px) {
86
130
  .filter-bar {
@@ -57,8 +57,14 @@ export interface ProTableProps<T = unknown> {
57
57
  onChange?: (selectedRowKeys: React.Key[], selectedRows: T[]) => void;
58
58
  };
59
59
  defaultPageSize?: number;
60
- /** 搜索栏每行列数,默认 4 */
60
+ /** 搜索栏每行列数,默认 4(searchInline 时忽略) */
61
61
  searchFormCols?: number;
62
+ /**
63
+ * 搜索表单内联渲染到工具栏左侧,与操作按钮同一行(单条件页面省一行空间)。
64
+ * 内联模式不渲染「查询/重置」按钮:回车提交,条件清空后自动回到无筛选状态。
65
+ * 默认 false = 现有独立筛选卡
66
+ */
67
+ searchInline?: boolean;
62
68
  /** 搜索表单渲染函数,返回 Form.Item 列表 */
63
69
  searchFormRender?: () => React.ReactNode;
64
70
  scrollY?: number | string;
@@ -201,13 +207,15 @@ this.setState({loading: true})
201
207
  // 有筛选条件时区分「无匹配结果」与「暂无数据」,避免业务人员误以为数据丢失
202
208
  const hasFilter = Object.keys(this.state.params || {}).length > 0
203
209
 
210
+ const inlineSearch = this.renderInlineSearch()
204
211
 
205
212
  return <div className={'oa-pro-table '} id={this.id}>
206
213
  {this.renderForm()}
207
214
  <div className="pro-table-wrapper">
208
- {(toolBarRender || toolBarRightRender) && <div className="pro-table-toolbar">
209
- {toolBarRender && <div className="pro-table-toolbar-left">
210
- {this.getToolBarRenderNode(toolBarRender)}
215
+ {(inlineSearch || toolBarRender || toolBarRightRender) && <div className="pro-table-toolbar">
216
+ {(inlineSearch || toolBarRender) && <div className={'pro-table-toolbar-left' + (inlineSearch ? ' has-inline-search' : '')}>
217
+ {inlineSearch}
218
+ {toolBarRender && this.getToolBarRenderNode(toolBarRender)}
211
219
  </div>}
212
220
  {toolBarRightRender && <div className="pro-table-toolbar-right">
213
221
  {this.getToolBarRenderNode(toolBarRightRender)}
@@ -258,10 +266,9 @@ this.setState({loading: true})
258
266
 
259
267
 
260
268
  renderForm = () => {
261
- if (this.props.children) {
262
- throw new Error('[ProTable] children 已废弃,请使用 searchFormRender prop 替代')
263
- }
264
- const searchContent = this.props.searchFormRender?.()
269
+ // 内联模式:搜索表单改由工具栏渲染(见 renderInlineSearch)
270
+ if (this.props.searchInline) return
271
+ const searchContent = this.getSearchContent()
265
272
  if (!searchContent) return
266
273
 
267
274
  return (
@@ -269,10 +276,7 @@ this.setState({loading: true})
269
276
  className="filter-bar"
270
277
  style={{'--oa-filter-cols': this.props.searchFormCols ?? 4} as React.CSSProperties}
271
278
  onFinish={(values) => this.onSearch(values)}
272
- ref={(instance) => {
273
- this.formRef.current = instance;
274
- if (this.props.formRef) this.props.formRef.current = instance;
275
- }}
279
+ ref={this.bindFormRef}
276
280
  >
277
281
  {searchContent}
278
282
  <div className="filter-actions">
@@ -286,6 +290,43 @@ this.setState({loading: true})
286
290
  )
287
291
  };
288
292
 
293
+ /** 内联搜索:单条件页面把搜索控件放到工具栏左侧,回车即查询,复用 onSearch 的取数逻辑 */
294
+ renderInlineSearch = () => {
295
+ if (!this.props.searchInline) return
296
+ const searchContent = this.getSearchContent()
297
+ if (!searchContent) return
298
+
299
+ return (
300
+ <Form
301
+ className="pro-table-search-inline"
302
+ onFinish={(values) => this.onSearch(values)}
303
+ onValuesChange={(changedValues) => {
304
+ // 条件被清空(allowClear 等)后自动回到无筛选状态,否则用户还要再按一次回车
305
+ const cleared = Object.values(changedValues).some(v => v == null || v === '' || (Array.isArray(v) && v.length === 0))
306
+ if (cleared) this.formRef.current?.submit()
307
+ }}
308
+ ref={this.bindFormRef}
309
+ >
310
+ {searchContent}
311
+ {/* 视觉隐藏的提交按钮:多字段内联时也能回车提交 */}
312
+ <Button className="pro-table-search-submit" htmlType="submit" tabIndex={-1} aria-hidden/>
313
+ </Form>
314
+ )
315
+ };
316
+
317
+ /** 校验已废弃的 children 用法,返回搜索表单内容(无搜索表单时返回空) */
318
+ getSearchContent = () => {
319
+ if (this.props.children) {
320
+ throw new Error('[ProTable] children 已废弃,请使用 searchFormRender prop 替代')
321
+ }
322
+ return this.props.searchFormRender?.()
323
+ }
324
+
325
+ bindFormRef = (instance: FormInstance | null) => {
326
+ this.formRef.current = instance;
327
+ if (this.props.formRef) this.props.formRef.current = instance;
328
+ }
329
+
289
330
  getToolBarRenderNode(toolBarRender: ProTableToolBarRender<T>) {
290
331
  if (!toolBarRender) {
291
332
  return
@@ -323,7 +364,7 @@ this.setState({loading: true})
323
364
  this.setState({params: values, current: 1, sorter: {}}, this.loadData)
324
365
  }
325
366
 
326
- changeFormValues = (values: Record<string, unknown>) => { // NOSONAR: ref 暴露给父组件/业务项目调用的公共 API
367
+ changeFormValues = (values: Record<string, unknown>) => {
327
368
  if (this.formRef.current) {
328
369
  this.formRef.current.resetFields()
329
370
  this.formRef.current.setFieldsValue(values)
@@ -21,8 +21,8 @@ export interface BaseRemoteSelectProps {
21
21
  * 处理通用的数据加载、防抖搜索、竞态处理、错误提示逻辑。
22
22
  * 子类只需覆写 getLoadParams() 和 render()。
23
23
  */
24
- export class BaseRemoteSelect<P extends BaseRemoteSelectProps = BaseRemoteSelectProps> extends React.Component<P, BaseRemoteSelectState> { // NOSONAR: state/防抖字段构造器赋值
25
- private fetchIdRef: number = 0; // NOSONAR: 构造器中赋值
24
+ export class BaseRemoteSelect<P extends BaseRemoteSelectProps = BaseRemoteSelectProps> extends React.Component<P, BaseRemoteSelectState> {
25
+ private fetchIdRef: number = 0;
26
26
  private loadDataDebounce: ReturnType<typeof debounce>;
27
27
 
28
28
  static readonly defaultProps = {
@@ -117,14 +117,14 @@ export class BaseRemoteSelect<P extends BaseRemoteSelectProps = BaseRemoteSelect
117
117
 
118
118
  // ========== Helper 方法 ==========
119
119
 
120
- getShowSearch(): { filterOption: false; onSearch: (value: string) => void } { // NOSONAR: 由子类通过 this.getShowSearch() 调用
120
+ getShowSearch(): { filterOption: false; onSearch: (value: string) => void } {
121
121
  return {
122
122
  filterOption: false,
123
123
  onSearch: this.handleSearch,
124
124
  };
125
125
  }
126
126
 
127
- getNotFoundContent(): React.ReactNode { // NOSONAR: 由子类通过 this.getNotFoundContent() 调用
127
+ getNotFoundContent(): React.ReactNode {
128
128
  return this.state.loading ? <Spin size="small" /> : '数据为空';
129
129
  }
130
130
  }
@@ -11,7 +11,7 @@ import type {FieldProps} from '../types';
11
11
  const SP = StringUtils.ISO_SPLITTER;
12
12
 
13
13
  /** 日期范围值,形如 "2024-01-01/2024-01-31"(起止以 / 分隔) */
14
- export type FieldDateRangeValue = string; // NOSONAR: npm 包导出类型,业务项目引用
14
+ export type FieldDateRangeValue = string;
15
15
 
16
16
  export interface FieldDateRangeProps extends FieldProps<FieldDateRangeValue> {
17
17
  /** 日期类型,如 YYYY-MM-DD、YYYY-MM、HH:mm:ss 等,默认 YYYY-MM-DD */
@@ -12,7 +12,7 @@ export class FieldDictSelect extends React.Component<FieldDictSelectProps> {
12
12
  render() {
13
13
  const {value, typeCode} = this.props;
14
14
  const options = DictUtils.dictOptions(typeCode);
15
- const strValue = value == null ? null : String(value); // NOSONAR: value 经 String() 显式转换,无隐式字符串化
15
+ const strValue = value == null ? null : String(value);
16
16
 
17
17
  return <Select value={strValue}
18
18
  onChange={this.props.onChange}
@@ -6,7 +6,7 @@ import type {FieldProps} from '../types';
6
6
  const SP = StringUtils.ISO_SPLITTER;
7
7
 
8
8
  /** 数字范围值,形如 "1/100"(起止以 / 分隔) */
9
- export type FieldNumberRangeValue = string; // NOSONAR: npm 包导出类型,业务项目引用
9
+ export type FieldNumberRangeValue = string;
10
10
 
11
11
  export type RangePartValue = number | string | null;
12
12
 
@@ -91,7 +91,7 @@ function readDims(url: string): Promise<Dims> {
91
91
 
92
92
  function compressToFile(source: Blob, options: {maxWidth?: number; maxHeight?: number; width?: number; height?: number; quality?: number}): Promise<File> {
93
93
  return new Promise((resolve, reject) => {
94
- new Compressor(source, { // NOSONAR (typescript:S1848) — compressorjs 通过构造函数副作用异步压缩,回调内 resolve/reject,实例无需保留
94
+ new Compressor(source, {
95
95
  maxWidth: options.maxWidth,
96
96
  maxHeight: options.maxHeight,
97
97
  width: options.width,
@@ -139,9 +139,9 @@ async function compressToTarget(source: Blob, maxWidth: number | undefined, targ
139
139
  return best;
140
140
  }
141
141
 
142
- export class FieldUploadImage extends React.Component<FieldUploadImageProps, FieldUploadImageState> { // NOSONAR: state 构造器中按 props 初始化,无法全部 readonly
142
+ export class FieldUploadImage extends React.Component<FieldUploadImageProps, FieldUploadImageState> {
143
143
 
144
- state: FieldUploadImageState = { // NOSONAR: 构造器中按 props 初始化,无法 readonly
144
+ state: FieldUploadImageState = {
145
145
  // 传入的参数
146
146
  maxCount: 1,
147
147
  thumbWidth: 300,
@@ -170,7 +170,7 @@ export class FieldUploadImage extends React.Component<FieldUploadImageProps, Fie
170
170
  if (props.value) this.state.objectNames = props.value.split(',');
171
171
  }
172
172
 
173
- componentDidUpdate(prevProps: FieldUploadImageProps, prevState: FieldUploadImageState) { // NOSONAR: 顺序同步多个 props 到 state,拆分反而更难读
173
+ componentDidUpdate(prevProps: FieldUploadImageProps, prevState: FieldUploadImageState) {
174
174
  const next: Partial<FieldUploadImageState> = {};
175
175
  if (this.props.maxCount !== prevProps.maxCount) next.maxCount = this.props.maxCount;
176
176
  if (this.props.thumbWidth !== prevProps.thumbWidth) next.thumbWidth = this.props.thumbWidth;
@@ -67,6 +67,6 @@ export class UrlUtils {
67
67
  static contextPath(path: string): string {
68
68
  const base = import.meta.env.VITE_SERVER_SERVLET_CONTEXT_PATH;
69
69
  if (base === '/' || base === '') return path;
70
- return base.replace(/\/+$/, '') + path; // NOSONAR: 需去除尾部斜杠,正则已最简
70
+ return base.replace(/\/+$/, '') + path;
71
71
  }
72
72
  }
@@ -112,7 +112,7 @@ export default class AdminLayout extends React.Component {
112
112
  this.setState({menuLoading: false})
113
113
  })
114
114
  }
115
- actionRef = React.createRef() // NOSONAR: React ref 由父组件通过 ref 访问
115
+ actionRef = React.createRef()
116
116
 
117
117
  classifyMenus = (menuTree) => {
118
118
  const topMenus = [];
@@ -95,7 +95,11 @@
95
95
  display: flex;
96
96
  align-items: center;
97
97
  gap: 8px;
98
- padding: 12px 16px;
98
+ /* 与顶栏同高,保证 logo 基线与一级菜单对齐 */
99
+ height: 48px;
100
+ padding: 0 16px;
101
+ /* 底边线:与右侧顶栏/标签栏分隔线连成一线;色值由侧栏文字变量派生,避免硬编码 */
102
+ border-bottom: 1px solid color-mix(in srgb, var(--oa-sider-text, rgba(255, 255, 255, 0.85)) 14%, transparent);
99
103
 
100
104
  .logo-img {
101
105
  height: 36px;
@@ -115,10 +119,25 @@
115
119
  margin: 0;
116
120
  font-size: 14px;
117
121
  line-height: 1;
122
+ /* 允许在 flex 中收缩,长站点名省略号截断而不溢出侧栏 */
123
+ min-width: 0;
124
+ overflow: hidden;
118
125
  white-space: nowrap;
126
+
127
+ a {
128
+ display: block;
129
+ overflow: hidden;
130
+ text-overflow: ellipsis;
131
+ }
119
132
  }
120
133
  }
121
134
 
135
+ /* 折叠态:logo 居中,去掉左侧内边距 */
136
+ &.ant-layout-sider-collapsed .sider-header {
137
+ justify-content: center;
138
+ padding-inline: 0;
139
+ }
140
+
122
141
  &.ant-layout-sider-collapsed .sider-header h3 {
123
142
  display: none;
124
143
  }
@@ -129,6 +148,11 @@
129
148
  scrollbar-width: thin;
130
149
  scrollbar-color: unset;
131
150
 
151
+ /* 菜单分隔线:加大上下间距,让不同菜单分组更清晰 */
152
+ .ant-menu-item-divider {
153
+ margin-block: 8px;
154
+ }
155
+
132
156
  .ant-menu-submenu-selected {
133
157
  &>div {
134
158
  color: var(--oa-sider-text, rgba(255, 255, 255, 0.85));
@@ -26,13 +26,13 @@ export default class ArticlePage extends React.Component {
26
26
  const {article, loading} = this.state
27
27
 
28
28
  if (loading) {
29
- return <Page title="加载中...">
29
+ return <Page>
30
30
  <div>加载中...</div>
31
31
  </Page>
32
32
  }
33
33
 
34
34
  if (!article) {
35
- return <Page title="文章不存在">
35
+ return <Page>
36
36
  <div>文章不存在或已禁用</div>
37
37
  </Page>
38
38
  }
@@ -4,7 +4,7 @@ import {Page} from "../framework";
4
4
 
5
5
  export default class HomePage extends React.Component {
6
6
  render() {
7
- return <Page title="首页" description="欢迎使用本系统">
7
+ return <Page>
8
8
  <Card>欢迎使用本系统</Card>
9
9
  </Page>
10
10
  }
@@ -1,5 +1,5 @@
1
- import {PlusOutlined} from '@ant-design/icons'
2
- import {Button, Form, Input, InputNumber} from 'antd'
1
+ import {PlusOutlined, SearchOutlined} from '@ant-design/icons'
2
+ import {Form, Input, InputNumber} from 'antd'
3
3
  import React from 'react'
4
4
  import {
5
5
  DictUtils,
@@ -106,26 +106,24 @@ export default class ArticleListPage extends React.Component {
106
106
  ]
107
107
 
108
108
  render() {
109
- return <Page
110
- title="文章管理"
111
- description="管理系统文章,如关于、帮助等页面"
112
- >
109
+ return <Page>
113
110
  <ProTable
114
111
  actionRef={this.tableRef}
115
- toolBarRender={() => ( // NOSONAR: AntD 渲染函数惯例
116
- <PermActions>
117
- <Button perm='article:create' type='primary' icon={<PlusOutlined/>} onClick={this.handleAdd}>新增</Button>
118
- </PermActions>
112
+ searchInline
113
+ toolBarRightRender={() => (
114
+ <PermActions actions={[
115
+ {label: '新增', type: 'primary', icon: <PlusOutlined/>, perm: 'article:create', onClick: this.handleAdd},
116
+ ]}/>
119
117
  )}
120
118
  request={(params) => HttpClient.get('admin/article/page', params)}
121
119
  columns={this.columns}
122
- searchFormRender={() => ( // NOSONAR: AntD 渲染函数惯例
120
+ searchFormRender={() => (
123
121
  <>
124
- <Form.Item label='编码' name='code'>
125
- <Input/>
122
+ <Form.Item name='code' noStyle>
123
+ <Input prefix={<SearchOutlined/>} placeholder='文章编码' allowClear/>
126
124
  </Form.Item>
127
- <Form.Item label='标题' name='title'>
128
- <Input/>
125
+ <Form.Item name='title' noStyle>
126
+ <Input placeholder='文章标题' allowClear/>
129
127
  </Form.Item>
130
128
  </>
131
129
  )}
@@ -96,9 +96,7 @@ export default class CodegenPage extends React.Component {
96
96
 
97
97
  render() {
98
98
  const selected = this.selectedOption()
99
- return <Page title='代码生成'
100
- description='扫描实体,生成 Repository / Service / Controller / 前端页面 / 菜单 代码并写入项目源码目录'
101
- actions={
99
+ return <Page actions={
102
100
  <Space>
103
101
  <Perm code='sys-codegen:generate'>
104
102
  <Button type='primary' loading={this.state.loading} onClick={this.handlePreview}>预览</Button>
@@ -164,7 +164,7 @@ export default class DictPage extends React.Component {
164
164
  const {selectedType, selectedTypeCode} = this.state
165
165
  const hasTypeSelected = selectedType != null && selectedTypeCode != null
166
166
 
167
- return <Page title="数据字典" description="管理数据字典类型和字典项">
167
+ return <Page>
168
168
  <Splitter>
169
169
  <Splitter.Panel defaultSize={300} style={{paddingRight: 8}}>
170
170
  <Card loading={this.state.treeLoading}
@@ -269,7 +269,7 @@ export default class DictPage extends React.Component {
269
269
  allowClear
270
270
  maxLength={20}
271
271
  options={COLOR_OPTIONS}
272
- filterOption={(input, option) => // NOSONAR: AntD AutoComplete 过滤回调惯例
272
+ filterOption={(input, option) =>
273
273
  (option.value + option.label).toLowerCase().includes(input.toLowerCase())
274
274
  }
275
275
  placeholder='如 SUCCESS、#ff0000'
@@ -32,7 +32,7 @@ export default class FilePage extends React.Component {
32
32
  title: '缩略图',
33
33
  dataIndex: 'objectName',
34
34
  width: 80,
35
- render: (objectName, record) => { // NOSONAR: 返回元素或占位文本,类型不同属预期
35
+ render: (objectName, record) => {
36
36
  if (record.type !== 'image') {
37
37
  return '-'
38
38
  }
@@ -151,7 +151,7 @@ export default class FilePage extends React.Component {
151
151
  <ProTable
152
152
  actionRef={this.tableRef}
153
153
  rowSelection={{}}
154
- toolBarRender={(params, selection) => ( // NOSONAR: AntD 渲染函数惯例
154
+ toolBarRightRender={(params, selection) => (
155
155
  <Space>
156
156
  <Button type='primary' icon={<CloudUploadOutlined/>} onClick={() => this.setState({formOpen: true})}>上传文件</Button>
157
157
  <PermActions
@@ -171,7 +171,7 @@ export default class FilePage extends React.Component {
171
171
  }}
172
172
 
173
173
  columns={this.columns}
174
- searchFormRender={() => ( // NOSONAR: AntD 渲染函数惯例
174
+ searchFormRender={() => (
175
175
  <>
176
176
  <Form.Item label='文件名' name='originName'>
177
177
  <Input/>
@@ -2,7 +2,6 @@ import React from 'react'
2
2
  import {
3
3
  Alert,
4
4
  AutoComplete,
5
- Button,
6
5
  Divider,
7
6
  Form,
8
7
  Input,
@@ -12,7 +11,7 @@ import {
12
11
  Switch,
13
12
  Tag
14
13
  } from 'antd'
15
- import {PlusOutlined} from "@ant-design/icons";
14
+ import {PlusOutlined, SearchOutlined} from "@ant-design/icons";
16
15
  import {FormModal, getMessageApi, HttpClient, Page, PermActions, ProTable, UrlUtils, ValueType} from "../../../framework";
17
16
 
18
17
 
@@ -200,21 +199,22 @@ export default class JobPage extends React.Component {
200
199
  return <Page>
201
200
  <ProTable
202
201
  actionRef={this.tableRef}
203
- toolBarRender={() => ( // NOSONAR: AntD 渲染函数惯例
204
- <PermActions>
205
- <Button type='primary' perm='job:create' icon={<PlusOutlined/>} onClick={() => this.handleAdd()}>新增</Button>
206
- <Button perm='job:read' onClick={this.showStatus}>查看状态</Button>
207
- </PermActions>
202
+ searchInline
203
+ toolBarRightRender={() => (
204
+ <PermActions actions={[
205
+ {label: '新增', type: 'primary', icon: <PlusOutlined/>, perm: 'job:create', onClick: () => this.handleAdd()},
206
+ {label: '查看状态', perm: 'job:read', onClick: this.showStatus},
207
+ ]}/>
208
208
  )}
209
209
  request={(params) => HttpClient.get('admin/job/page', params)}
210
210
  columns={this.columns}
211
- searchFormRender={() => ( // NOSONAR: AntD 渲染函数惯例
211
+ searchFormRender={() => (
212
212
  <>
213
- <Form.Item label='名称' name='name'>
214
- <Input/>
213
+ <Form.Item name='name' noStyle>
214
+ <Input prefix={<SearchOutlined/>} placeholder='任务名称' allowClear/>
215
215
  </Form.Item>
216
- <Form.Item label='执行类' name='jobClass'>
217
- <Input/>
216
+ <Form.Item name='jobClass' noStyle>
217
+ <Input placeholder='执行类' allowClear/>
218
218
  </Form.Item>
219
219
  </>
220
220
  )}
@@ -348,7 +348,7 @@ export default class JobPage extends React.Component {
348
348
  const option = this.state.jobClassOptions.find(o => o.value === changed.jobClass)
349
349
  if (option) {
350
350
  const {label} = option;
351
- const match = label.match(/[((](.*?)[))]/); // NOSONAR: 匹配中英文括号内内容,已最简
351
+ const match = label.match(/[((](.*?)[))]/);
352
352
  if (match) { // 取括号内中文描述设置为name
353
353
  this.modalRef.current.formInstance.setFieldValue("name", match[1])
354
354
  }
@@ -53,11 +53,11 @@ export default class LogPage extends React.Component {
53
53
 
54
54
 
55
55
  render() {
56
- return <Page title="操作日志" description="查看系统操作日志">
56
+ return <Page>
57
57
  <ProTable
58
58
  request={(params) => HttpClient.get('admin/sysLog/page', params)}
59
59
  columns={this.columns}
60
- searchFormRender={() => ( // NOSONAR: AntD 渲染函数惯例
60
+ searchFormRender={() => (
61
61
  <>
62
62
  <Form.Item label='操作' name='operation'>
63
63
  <Input/>
@@ -94,13 +94,13 @@ export default class OrgPage extends React.Component {
94
94
  const params = this.state.params
95
95
  const canSort = PermUtils.hasPermission('sys-org:update')
96
96
 
97
- return <Page title="组织机构" description="管理组织机构树" actions={
98
- <Button type='primary' perm='sys-org:create' icon={<PlusOutlined/>} onClick={this.handleAdd}>新增</Button>
99
- }>
97
+ return <Page>
100
98
  <Splitter>
101
99
  <Splitter.Panel defaultSize={400} style={{paddingRight: 8}}>
102
100
 
103
- <Card size='small' >
101
+
102
+ <Card title="机构树" extra={<Button type='primary' perm='sys-org:create'
103
+ icon={<PlusOutlined/>} onClick={this.handleAdd}>新增</Button>}>
104
104
  <div style={{display: 'flex', alignItems: 'center', gap: 4}}>
105
105
  <Input.Search placeholder='搜索' value={params.searchText} onChange={e => {
106
106
  params.searchText = e.target.value
@@ -140,7 +140,7 @@ export default class OrgPage extends React.Component {
140
140
  onSelect={this.onSelect}
141
141
  showIcon
142
142
  blockNode
143
- icon={item => <NamedIcon name={item.data.iconName}/>} // NOSONAR: AntD 渲染函数惯例
143
+ icon={item => <NamedIcon name={item.data.iconName}/>}
144
144
  draggable={this.state.draggable}
145
145
  onDrop={this.onDrop}
146
146
  showLine
@@ -1,5 +1,5 @@
1
- import {PlusOutlined} from '@ant-design/icons'
2
- import {Button, Form, Input, InputNumber} from 'antd'
1
+ import {PlusOutlined, SearchOutlined} from '@ant-design/icons'
2
+ import {Form, Input, InputNumber} from 'antd'
3
3
  import React from 'react'
4
4
  import {FieldBoolean, FormModal, HttpClient, Page, PageUtils, PermActions, ProTable, UserPicker, ViewText} from "../../../framework";
5
5
 
@@ -133,25 +133,24 @@ export default class RolePage extends React.Component {
133
133
 
134
134
  render() {
135
135
  return <Page
136
- title="角色管理"
137
- description="管理系统角色,包括角色权限分配、用户设置等"
138
136
  >
139
137
  <ProTable
140
138
  actionRef={this.tableRef}
141
- toolBarRender={() => ( // NOSONAR: AntD 渲染函数惯例
142
- <PermActions>
143
- <Button perm='sys-role:create' type='primary' icon={<PlusOutlined/>} onClick={this.handleAdd}>新增</Button>
144
- </PermActions>
139
+ toolBarRightRender={() => (
140
+ <PermActions actions={[
141
+ {label: '新增', type: 'primary', icon: <PlusOutlined/>, perm: 'sys-role:create', onClick: this.handleAdd},
142
+ ]}/>
145
143
  )}
146
144
  request={(params) => HttpClient.get('admin/sysRole/page', params)}
147
145
  columns={this.columns}
148
- searchFormRender={() => ( // NOSONAR: AntD 渲染函数惯例
146
+ searchInline
147
+ searchFormRender={() => (
149
148
  <>
150
- <Form.Item label='角色名称' name='name'>
151
- <Input/>
149
+ <Form.Item name='name' noStyle>
150
+ <Input prefix={<SearchOutlined/>} placeholder='角色名称' allowClear/>
152
151
  </Form.Item>
153
- <Form.Item label='角色编码' name='code'>
154
- <Input/>
152
+ <Form.Item name='code' noStyle>
153
+ <Input placeholder='角色编码' allowClear/>
155
154
  </Form.Item>
156
155
  </>
157
156
  )}
@@ -93,7 +93,7 @@ export default class RolePermPage extends React.Component {
93
93
 
94
94
 
95
95
  render() {
96
- return <Page title="角色权限设置" description="设置角色的菜单和权限">
96
+ return <Page>
97
97
  <Card title='角色权限设置' loading={this.state.loading}
98
98
  variant={"borderless"}
99
99
  extra={<Button type='primary' icon={<SaveOutlined/>} onClick={this.savePerms}>保存权限</Button>}>
@@ -152,9 +152,9 @@ export default class UserPage extends React.Component {
152
152
 
153
153
  render() {
154
154
 
155
- return <Page title="用户管理" description="管理系统用户">
155
+ return <Page >
156
156
  <Splitter>
157
- <Splitter.Panel defaultSize={240} style={{paddingRight: 8}}>
157
+ <Splitter.Panel defaultSize={340} style={{paddingRight: 8}}>
158
158
  <Card size='small'>
159
159
  <OrgTree onChange={this.onSelectOrg}/>
160
160
  </Card>
@@ -163,17 +163,17 @@ export default class UserPage extends React.Component {
163
163
  <ProTable
164
164
  searchFormCols={3}
165
165
  actionRef={this.tableRef}
166
- toolBarRender={() => ( // NOSONAR: AntD 渲染函数惯例
167
- <PermActions>
168
- <Button perm='sys-user:create' type='primary' icon={<PlusOutlined/>} onClick={this.handleAdd}>新增</Button>
169
- </PermActions>
166
+ toolBarRightRender={() => (
167
+ <PermActions actions={[
168
+ {label: '新增', type: 'primary', icon: <PlusOutlined/>, perm: 'sys-user:create', onClick: this.handleAdd},
169
+ ]}/>
170
170
  )}
171
171
  request={(params) => {
172
172
  params.orgId = this.state.currentOrgId
173
173
  return HttpClient.get('admin/sysUser/page', params)
174
174
  }}
175
175
  columns={this.columns}
176
- searchFormRender={() => ( // NOSONAR: AntD 渲染函数惯例
176
+ searchFormRender={() => (
177
177
  <>
178
178
  <Form.Item label='姓名' name='name'>
179
179
  <Input/>
@@ -15,9 +15,9 @@ export default class UserPerm extends React.Component {
15
15
  dataPermType: null
16
16
  },
17
17
  }
18
- formRef = React.createRef() // NOSONAR: React ref,内部通过 this.formRef 使用
18
+ formRef = React.createRef()
19
19
 
20
- show(item) { // NOSONAR: ref 暴露给父组件调用的公共 API
20
+ show(item) {
21
21
  this.setState({visible: true})
22
22
 
23
23
  HttpClient.get('admin/sysUser/get-perm-info', {id: item.id}).then(rs => {
@@ -26,7 +26,7 @@ export default class PermView extends React.Component {
26
26
  },
27
27
  {
28
28
  title: '状态', dataIndex: 'status', key: 'status', width: 140,
29
- render: (status) => { // NOSONAR: 返回元素或占位文本,类型不同属预期
29
+ render: (status) => {
30
30
  if (status === 'mine') {
31
31
  return <Tag color='blue'>我的机构</Tag>;
32
32
  }
@@ -48,7 +48,7 @@ export default class PermView extends React.Component {
48
48
  },
49
49
  {
50
50
  title: '状态', dataIndex: 'status', key: 'status', width: 140,
51
- render: (status) => { // NOSONAR: 返回元素或占位文本,类型不同属预期
51
+ render: (status) => {
52
52
  if (status === 'all') {
53
53
  return <Tag color='green'>已授权</Tag>;
54
54
  }