@jiangood/open-admin 3.1.6 → 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.
- package/package.json +2 -5
- package/src/framework/biz/PermActions.tsx +1 -1
- package/src/framework/biz/UserPicker.less +17 -0
- package/src/framework/biz/UserPicker.tsx +296 -0
- package/src/framework/biz/index.tsx +2 -0
- package/src/framework/biz/pickerUser.ts +19 -0
- package/src/framework/components/ContextMenu/index.tsx +11 -8
- package/src/framework/components/DownloadModal/index.tsx +7 -7
- package/src/framework/components/ErrorBoundary/index.tsx +1 -1
- package/src/framework/components/FormModal/index.tsx +4 -4
- package/src/framework/components/NamedIcon/index.tsx +1 -1
- package/src/framework/components/OrgSwitcher/index.tsx +1 -1
- package/src/framework/components/Page/index.tsx +18 -4
- package/src/framework/components/PageLoading/index.tsx +2 -2
- package/src/framework/components/ProTable/index.less +70 -8
- package/src/framework/components/ProTable/index.tsx +63 -16
- package/src/framework/components/ThemeSettings/index.tsx +173 -0
- package/src/framework/components/index.ts +1 -0
- package/src/framework/config/index.ts +44 -2
- package/src/framework/config/layoutConfig.ts +52 -0
- package/src/framework/config/themeConfig.ts +449 -14
- package/src/framework/fields/BaseRemoteSelect/index.tsx +4 -4
- package/src/framework/fields/FieldDateRange/index.tsx +1 -1
- package/src/framework/fields/FieldDictSelect/index.tsx +1 -1
- package/src/framework/fields/FieldEditor/index.tsx +2 -2
- package/src/framework/fields/FieldNumberRange/index.tsx +1 -1
- package/src/framework/fields/FieldUploadFile/index.tsx +3 -3
- package/src/framework/fields/FieldUploadImage/index.tsx +19 -19
- package/src/framework/fields/FieldUserPicker/index.tsx +124 -0
- package/src/framework/fields/index.ts +1 -0
- package/src/framework/utils/HttpClient.ts +15 -4
- package/src/framework/utils/MessageApi.ts +16 -3
- package/src/framework/utils/UrlUtils.ts +1 -1
- package/src/framework/views/ViewText/index.tsx +1 -1
- package/src/layouts/admin/AdminTabs.less +11 -11
- package/src/layouts/admin/HeaderRight.jsx +23 -12
- package/src/layouts/admin/SinglePageLayout.jsx +36 -0
- package/src/layouts/admin/TabLayout.jsx +21 -1
- package/src/layouts/admin/index.jsx +46 -26
- package/src/layouts/admin/index.less +68 -24
- package/src/layouts/index.tsx +29 -4
- package/src/pages/article/$code.jsx +2 -2
- package/src/pages/article/article.less +13 -13
- package/src/pages/index.jsx +1 -1
- package/src/pages/public/login.jsx +29 -15
- package/src/pages/public/login.less +243 -22
- package/src/pages/standalone/forceUpdatePwd.jsx +8 -7
- package/src/pages/system/article/index.jsx +13 -15
- package/src/pages/system/codegen/index.jsx +54 -51
- package/src/pages/system/dict/index.jsx +2 -2
- package/src/pages/system/file/index.jsx +3 -3
- package/src/pages/system/job/index.jsx +57 -20
- package/src/pages/system/log/index.jsx +2 -2
- package/src/pages/system/org/index.jsx +5 -5
- package/src/pages/system/role/index.jsx +28 -55
- package/src/pages/system/role/rolePerm.jsx +1 -1
- package/src/pages/system/user/index.jsx +7 -11
- package/src/pages/system/user/userPerm.jsx +5 -5
- package/src/pages/userCenter/permView.jsx +2 -2
- package/src/style/global.less +6 -6
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
.oa-pro-table {
|
|
2
2
|
.filter-bar {
|
|
3
3
|
display: grid;
|
|
4
|
-
|
|
4
|
+
/* 列数由 searchFormCols 通过 --oa-filter-cols 注入,窄窗口下按断点收敛 */
|
|
5
|
+
grid-template-columns: repeat(var(--oa-filter-cols, 4), 1fr);
|
|
5
6
|
gap: 12px 20px;
|
|
6
7
|
align-items: center;
|
|
7
8
|
padding: 16px 20px;
|
|
8
|
-
background: #fff;
|
|
9
|
-
border: 1px solid #e8e8e8;
|
|
10
|
-
border-radius: 6px;
|
|
9
|
+
background: var(--oa-color-bg-container, #fff);
|
|
10
|
+
border: 1px solid var(--oa-color-border, #e8e8e8);
|
|
11
|
+
border-radius: var(--oa-border-radius, 6px);
|
|
11
12
|
margin-bottom: 16px;
|
|
12
13
|
|
|
13
14
|
.ant-form-item {
|
|
@@ -28,7 +29,7 @@
|
|
|
28
29
|
|
|
29
30
|
label {
|
|
30
31
|
font-size: 13px;
|
|
31
|
-
color: #666;
|
|
32
|
+
color: var(--oa-color-text-secondary, #666);
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -50,9 +51,9 @@
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
.pro-table-wrapper {
|
|
53
|
-
background: #fff;
|
|
54
|
-
border: 1px solid #e8e8e8;
|
|
55
|
-
border-radius: 6px;
|
|
54
|
+
background: var(--oa-color-bg-container, #fff);
|
|
55
|
+
border: 1px solid var(--oa-color-border, #e8e8e8);
|
|
56
|
+
border-radius: var(--oa-border-radius, 6px);
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
.pro-table-toolbar {
|
|
@@ -68,6 +69,11 @@
|
|
|
68
69
|
gap: 8px;
|
|
69
70
|
flex: 1;
|
|
70
71
|
min-width: 0;
|
|
72
|
+
|
|
73
|
+
/* 仅内联搜索时允许换行,存量工具栏布局与之前完全一致 */
|
|
74
|
+
&.has-inline-search {
|
|
75
|
+
flex-wrap: wrap;
|
|
76
|
+
}
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
.pro-table-toolbar-right {
|
|
@@ -79,5 +85,61 @@
|
|
|
79
85
|
margin-left: auto;
|
|
80
86
|
}
|
|
81
87
|
}
|
|
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
|
+
|
|
128
|
+
/* 桌面窄窗口下收敛搜索栏列数,避免标签+输入挤压/溢出 */
|
|
129
|
+
@media (max-width: 1400px) {
|
|
130
|
+
.filter-bar {
|
|
131
|
+
--oa-filter-cols: 3 !important;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@media (max-width: 1100px) {
|
|
136
|
+
.filter-bar {
|
|
137
|
+
--oa-filter-cols: 2 !important;
|
|
138
|
+
|
|
139
|
+
.filter-actions {
|
|
140
|
+
grid-column: 1 / -1;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
82
144
|
}
|
|
83
145
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {Button, Form, Table} from 'antd';
|
|
2
|
+
import {Button, Empty, Form, Table} from 'antd';
|
|
3
3
|
import type {FormInstance, TableProps} from 'antd';
|
|
4
4
|
|
|
5
5
|
import {StringUtils, type AjaxBody} from "../../utils";
|
|
@@ -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;
|
|
@@ -198,14 +204,18 @@ this.setState({loading: true})
|
|
|
198
204
|
rowSelection,
|
|
199
205
|
rowKey = "id",
|
|
200
206
|
} = this.props
|
|
207
|
+
// 有筛选条件时区分「无匹配结果」与「暂无数据」,避免业务人员误以为数据丢失
|
|
208
|
+
const hasFilter = Object.keys(this.state.params || {}).length > 0
|
|
201
209
|
|
|
210
|
+
const inlineSearch = this.renderInlineSearch()
|
|
202
211
|
|
|
203
212
|
return <div className={'oa-pro-table '} id={this.id}>
|
|
204
213
|
{this.renderForm()}
|
|
205
214
|
<div className="pro-table-wrapper">
|
|
206
|
-
{(toolBarRender || toolBarRightRender) && <div className="pro-table-toolbar">
|
|
207
|
-
{toolBarRender && <div className=
|
|
208
|
-
{
|
|
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)}
|
|
209
219
|
</div>}
|
|
210
220
|
{toolBarRightRender && <div className="pro-table-toolbar-right">
|
|
211
221
|
{this.getToolBarRenderNode(toolBarRightRender)}
|
|
@@ -226,7 +236,7 @@ this.setState({loading: true})
|
|
|
226
236
|
total: this.state.total,
|
|
227
237
|
pageSize: this.state.pageSize,
|
|
228
238
|
current: this.state.current,
|
|
229
|
-
pageSizeOptions: [10, 20, 50, 100
|
|
239
|
+
pageSizeOptions: [10, 20, 50, 100],
|
|
230
240
|
showTotal: (total) => `共 ${total} 条`
|
|
231
241
|
}}
|
|
232
242
|
|
|
@@ -244,6 +254,10 @@ this.setState({loading: true})
|
|
|
244
254
|
|
|
245
255
|
footer={this.state.extData.summary ? () => this.state.extData.summary : null}
|
|
246
256
|
bordered={this.props.bordered ?? false}
|
|
257
|
+
locale={{
|
|
258
|
+
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE}
|
|
259
|
+
description={hasFilter ? '没有符合条件的数据' : '暂无数据'}/>,
|
|
260
|
+
}}
|
|
247
261
|
/>
|
|
248
262
|
</div>
|
|
249
263
|
</div>
|
|
@@ -252,21 +266,17 @@ this.setState({loading: true})
|
|
|
252
266
|
|
|
253
267
|
|
|
254
268
|
renderForm = () => {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const searchContent = this.props.searchFormRender?.()
|
|
269
|
+
// 内联模式:搜索表单改由工具栏渲染(见 renderInlineSearch)
|
|
270
|
+
if (this.props.searchInline) return
|
|
271
|
+
const searchContent = this.getSearchContent()
|
|
259
272
|
if (!searchContent) return
|
|
260
273
|
|
|
261
274
|
return (
|
|
262
275
|
<Form
|
|
263
276
|
className="filter-bar"
|
|
264
|
-
style={{
|
|
277
|
+
style={{'--oa-filter-cols': this.props.searchFormCols ?? 4} as React.CSSProperties}
|
|
265
278
|
onFinish={(values) => this.onSearch(values)}
|
|
266
|
-
ref={
|
|
267
|
-
this.formRef.current = instance;
|
|
268
|
-
if (this.props.formRef) this.props.formRef.current = instance;
|
|
269
|
-
}}
|
|
279
|
+
ref={this.bindFormRef}
|
|
270
280
|
>
|
|
271
281
|
{searchContent}
|
|
272
282
|
<div className="filter-actions">
|
|
@@ -280,6 +290,43 @@ this.setState({loading: true})
|
|
|
280
290
|
)
|
|
281
291
|
};
|
|
282
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
|
+
|
|
283
330
|
getToolBarRenderNode(toolBarRender: ProTableToolBarRender<T>) {
|
|
284
331
|
if (!toolBarRender) {
|
|
285
332
|
return
|
|
@@ -317,7 +364,7 @@ this.setState({loading: true})
|
|
|
317
364
|
this.setState({params: values, current: 1, sorter: {}}, this.loadData)
|
|
318
365
|
}
|
|
319
366
|
|
|
320
|
-
changeFormValues = (values: Record<string, unknown>) => {
|
|
367
|
+
changeFormValues = (values: Record<string, unknown>) => {
|
|
321
368
|
if (this.formRef.current) {
|
|
322
369
|
this.formRef.current.resetFields()
|
|
323
370
|
this.formRef.current.setFieldsValue(values)
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Button, Drawer, Segmented } from 'antd';
|
|
3
|
+
import { CheckOutlined } from '@ant-design/icons';
|
|
4
|
+
import {
|
|
5
|
+
THEME_PRESETS,
|
|
6
|
+
DEFAULT_COLORS,
|
|
7
|
+
DEFAULT_SIDER_BG,
|
|
8
|
+
applyThemePreset,
|
|
9
|
+
getActivePresetKey,
|
|
10
|
+
getThemeMode,
|
|
11
|
+
resetAllSettings,
|
|
12
|
+
setThemeMode,
|
|
13
|
+
DENSITY_PRESETS,
|
|
14
|
+
RADIUS_PRESETS,
|
|
15
|
+
applyDensityPreset,
|
|
16
|
+
getActiveDensityKey,
|
|
17
|
+
applyRadiusPreset,
|
|
18
|
+
getActiveRadiusKey,
|
|
19
|
+
getLayoutMode,
|
|
20
|
+
setLayoutMode,
|
|
21
|
+
} from '../../config';
|
|
22
|
+
import type { ThemeMode, ThemeDensity, ThemeRadius, LayoutMode } from '../../config';
|
|
23
|
+
import { EventBus } from '../../utils/EventBus';
|
|
24
|
+
|
|
25
|
+
export interface ThemeSettingsProps {
|
|
26
|
+
open: boolean;
|
|
27
|
+
onClose: () => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function SectionTitle({ children }: { children: React.ReactNode }) {
|
|
31
|
+
return <div style={{ margin: '20px 0 12px', fontWeight: 600 }}>{children}</div>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 界面设置面板:外观模式、主题色、界面密度、圆角、界面布局。
|
|
36
|
+
* 所有变更即时生效并持久化到 localStorage。
|
|
37
|
+
*/
|
|
38
|
+
export function ThemeSettings({ open, onClose }: ThemeSettingsProps) {
|
|
39
|
+
const [activeKey, setActiveKey] = React.useState(getActivePresetKey());
|
|
40
|
+
const [mode, setMode] = React.useState<ThemeMode>(getThemeMode());
|
|
41
|
+
const [density, setDensity] = React.useState<ThemeDensity>(getActiveDensityKey());
|
|
42
|
+
const [radius, setRadius] = React.useState<ThemeRadius>(getActiveRadiusKey());
|
|
43
|
+
const [layout, setLayout] = React.useState<LayoutMode>(getLayoutMode());
|
|
44
|
+
|
|
45
|
+
React.useEffect(() => {
|
|
46
|
+
const offTheme = EventBus.on('themeChange', () => {
|
|
47
|
+
setActiveKey(getActivePresetKey());
|
|
48
|
+
setMode(getThemeMode());
|
|
49
|
+
setDensity(getActiveDensityKey());
|
|
50
|
+
setRadius(getActiveRadiusKey());
|
|
51
|
+
});
|
|
52
|
+
const offLayout = EventBus.on('layoutChange', () => {
|
|
53
|
+
setLayout(getLayoutMode());
|
|
54
|
+
});
|
|
55
|
+
return () => {
|
|
56
|
+
offTheme();
|
|
57
|
+
offLayout();
|
|
58
|
+
};
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
const radiusValue = (key: ThemeRadius) =>
|
|
62
|
+
(RADIUS_PRESETS.find(p => p.key === key)?.token.borderRadius as number) ?? 6;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<Drawer title="界面设置" open={open} onClose={onClose} size={340}>
|
|
66
|
+
<div style={{ fontWeight: 600 }}>外观模式</div>
|
|
67
|
+
<Segmented
|
|
68
|
+
block
|
|
69
|
+
style={{ marginTop: 12 }}
|
|
70
|
+
value={mode}
|
|
71
|
+
onChange={value => setThemeMode(value as ThemeMode)}
|
|
72
|
+
options={[
|
|
73
|
+
{ label: '浅色', value: 'light' },
|
|
74
|
+
{ label: '深色', value: 'dark' },
|
|
75
|
+
]}
|
|
76
|
+
/>
|
|
77
|
+
|
|
78
|
+
<SectionTitle>主题色</SectionTitle>
|
|
79
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 12 }}>
|
|
80
|
+
{THEME_PRESETS.map(preset => {
|
|
81
|
+
const primary = preset.colors.colorPrimary || DEFAULT_COLORS.colorPrimary || '#1677ff';
|
|
82
|
+
const sider = preset.colors.siderBg || DEFAULT_SIDER_BG;
|
|
83
|
+
const active = preset.key === activeKey;
|
|
84
|
+
return (
|
|
85
|
+
<button
|
|
86
|
+
key={preset.key}
|
|
87
|
+
type="button"
|
|
88
|
+
aria-pressed={active}
|
|
89
|
+
onClick={() => applyThemePreset(preset.key)}
|
|
90
|
+
style={{
|
|
91
|
+
cursor: 'pointer',
|
|
92
|
+
padding: 0,
|
|
93
|
+
overflow: 'hidden',
|
|
94
|
+
borderRadius: 8,
|
|
95
|
+
textAlign: 'left',
|
|
96
|
+
background: 'transparent',
|
|
97
|
+
border: active
|
|
98
|
+
? `2px solid ${primary}`
|
|
99
|
+
: '1px solid var(--oa-color-border, #e8e8e8)',
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
<div style={{ display: 'flex', height: 44 }}>
|
|
103
|
+
<div
|
|
104
|
+
style={{
|
|
105
|
+
width: '50%',
|
|
106
|
+
background: primary,
|
|
107
|
+
display: 'flex',
|
|
108
|
+
alignItems: 'center',
|
|
109
|
+
justifyContent: 'center',
|
|
110
|
+
color: '#fff',
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
{active && <CheckOutlined />}
|
|
114
|
+
</div>
|
|
115
|
+
<div style={{ width: '50%', background: sider }} />
|
|
116
|
+
</div>
|
|
117
|
+
<div style={{ padding: '6px 8px', fontSize: 12 }}>{preset.name}</div>
|
|
118
|
+
</button>
|
|
119
|
+
);
|
|
120
|
+
})}
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<SectionTitle>界面密度</SectionTitle>
|
|
124
|
+
<Segmented
|
|
125
|
+
block
|
|
126
|
+
value={density}
|
|
127
|
+
onChange={value => applyDensityPreset(value as ThemeDensity)}
|
|
128
|
+
options={DENSITY_PRESETS.map(p => ({ label: p.name, value: p.key }))}
|
|
129
|
+
/>
|
|
130
|
+
|
|
131
|
+
<SectionTitle>圆角</SectionTitle>
|
|
132
|
+
<Segmented
|
|
133
|
+
block
|
|
134
|
+
value={radius}
|
|
135
|
+
onChange={value => applyRadiusPreset(value as ThemeRadius)}
|
|
136
|
+
options={RADIUS_PRESETS.map(p => ({
|
|
137
|
+
value: p.key,
|
|
138
|
+
label: (
|
|
139
|
+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
|
|
140
|
+
<span
|
|
141
|
+
style={{
|
|
142
|
+
display: 'inline-block',
|
|
143
|
+
width: 14,
|
|
144
|
+
height: 14,
|
|
145
|
+
border: '1.5px solid currentColor',
|
|
146
|
+
borderRadius: radiusValue(p.key),
|
|
147
|
+
}}
|
|
148
|
+
/>
|
|
149
|
+
{p.name}
|
|
150
|
+
</span>
|
|
151
|
+
),
|
|
152
|
+
}))}
|
|
153
|
+
/>
|
|
154
|
+
|
|
155
|
+
<SectionTitle>界面布局</SectionTitle>
|
|
156
|
+
<Segmented
|
|
157
|
+
block
|
|
158
|
+
value={layout}
|
|
159
|
+
onChange={value => setLayoutMode(value as LayoutMode)}
|
|
160
|
+
options={[
|
|
161
|
+
{ label: '多标签页', value: 'tabs' },
|
|
162
|
+
{ label: '单页', value: 'single' },
|
|
163
|
+
]}
|
|
164
|
+
/>
|
|
165
|
+
|
|
166
|
+
<Button block style={{ marginTop: 20 }} onClick={() => resetAllSettings()}>
|
|
167
|
+
恢复默认
|
|
168
|
+
</Button>
|
|
169
|
+
</Drawer>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export default ThemeSettings;
|
|
@@ -8,6 +8,7 @@ export * from './ValueType'
|
|
|
8
8
|
export * from './ContextMenu'
|
|
9
9
|
export * from './DownloadModal'
|
|
10
10
|
export * from './OrgSwitcher'
|
|
11
|
+
export * from './ThemeSettings'
|
|
11
12
|
export { default as ErrorBoundary } from './ErrorBoundary'
|
|
12
13
|
export { default as FormModal } from './FormModal'
|
|
13
14
|
export { default as FormDrawer } from './DrawerForm'
|
|
@@ -1,2 +1,44 @@
|
|
|
1
|
-
export {
|
|
2
|
-
|
|
1
|
+
export {
|
|
2
|
+
getThemeConfig,
|
|
3
|
+
getSiderThemeConfig,
|
|
4
|
+
getToken,
|
|
5
|
+
setThemeColors,
|
|
6
|
+
getThemeMode,
|
|
7
|
+
setThemeMode,
|
|
8
|
+
toggleThemeMode,
|
|
9
|
+
applyThemePreset,
|
|
10
|
+
getActivePresetKey,
|
|
11
|
+
applyDensityPreset,
|
|
12
|
+
getActiveDensityKey,
|
|
13
|
+
applyRadiusPreset,
|
|
14
|
+
getActiveRadiusKey,
|
|
15
|
+
resetUserTheme,
|
|
16
|
+
DEFAULT_COLORS,
|
|
17
|
+
DEFAULT_PRESET_KEY,
|
|
18
|
+
DEFAULT_SIDER_BG,
|
|
19
|
+
DEFAULT_SIDER_TRIGGER_BG,
|
|
20
|
+
THEME_PRESETS,
|
|
21
|
+
DEFAULT_DENSITY_KEY,
|
|
22
|
+
DENSITY_PRESETS,
|
|
23
|
+
DEFAULT_RADIUS_KEY,
|
|
24
|
+
RADIUS_PRESETS,
|
|
25
|
+
} from './themeConfig'
|
|
26
|
+
export type { ThemeColors, ThemeMode, ThemePreset, ThemeDensity, ThemeRadius, ThemeDensityPreset, ThemeRadiusPreset } from './themeConfig'
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
getLayoutMode,
|
|
30
|
+
setLayoutMode,
|
|
31
|
+
toggleLayoutMode,
|
|
32
|
+
resetLayoutMode,
|
|
33
|
+
DEFAULT_LAYOUT_MODE,
|
|
34
|
+
} from './layoutConfig'
|
|
35
|
+
export type { LayoutMode } from './layoutConfig'
|
|
36
|
+
|
|
37
|
+
import { resetUserTheme } from './themeConfig'
|
|
38
|
+
import { resetLayoutMode } from './layoutConfig'
|
|
39
|
+
|
|
40
|
+
/** 一键恢复全部界面设置:主题(颜色/明暗/密度/圆角)+ 布局 */
|
|
41
|
+
export function resetAllSettings() {
|
|
42
|
+
resetUserTheme()
|
|
43
|
+
resetLayoutMode()
|
|
44
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { EventBus } from '../utils/EventBus';
|
|
2
|
+
|
|
3
|
+
export type LayoutMode = 'tabs' | 'single';
|
|
4
|
+
|
|
5
|
+
const LAYOUT_MODE_KEY = 'oa-layout-tabs';
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_LAYOUT_MODE: LayoutMode = 'tabs';
|
|
8
|
+
|
|
9
|
+
const LAYOUT_MODES: LayoutMode[] = ['tabs', 'single'];
|
|
10
|
+
|
|
11
|
+
function readStoredLayoutMode(): LayoutMode {
|
|
12
|
+
if (typeof window === 'undefined') return DEFAULT_LAYOUT_MODE;
|
|
13
|
+
try {
|
|
14
|
+
const mode = localStorage.getItem(LAYOUT_MODE_KEY);
|
|
15
|
+
if (mode && LAYOUT_MODES.includes(mode as LayoutMode)) return mode as LayoutMode;
|
|
16
|
+
} catch {
|
|
17
|
+
// 隐私模式下 localStorage 不可用,忽略
|
|
18
|
+
}
|
|
19
|
+
return DEFAULT_LAYOUT_MODE;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let currentLayoutMode: LayoutMode = readStoredLayoutMode();
|
|
23
|
+
|
|
24
|
+
/** 当前布局模式:'tabs' 多标签页 / 'single' 单页 */
|
|
25
|
+
export function getLayoutMode(): LayoutMode {
|
|
26
|
+
return currentLayoutMode;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 设置布局模式,持久化并广播 layoutChange 供管理布局重渲染 */
|
|
30
|
+
export function setLayoutMode(mode: LayoutMode) {
|
|
31
|
+
if (!LAYOUT_MODES.includes(mode) || currentLayoutMode === mode) return;
|
|
32
|
+
currentLayoutMode = mode;
|
|
33
|
+
if (typeof window !== 'undefined') {
|
|
34
|
+
try {
|
|
35
|
+
localStorage.setItem(LAYOUT_MODE_KEY, mode);
|
|
36
|
+
} catch {
|
|
37
|
+
// 忽略
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
EventBus.emit('layoutChange', mode);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function toggleLayoutMode(): LayoutMode {
|
|
44
|
+
const next: LayoutMode = currentLayoutMode === 'tabs' ? 'single' : 'tabs';
|
|
45
|
+
setLayoutMode(next);
|
|
46
|
+
return next;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 恢复默认布局(多标签页) */
|
|
50
|
+
export function resetLayoutMode() {
|
|
51
|
+
setLayoutMode(DEFAULT_LAYOUT_MODE);
|
|
52
|
+
}
|