Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
P
problem_rectification
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
David Yang
problem_rectification
Commits
f53d7e42
Commit
f53d7e42
authored
Aug 19, 2026
by
Andy-bubu
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: finalize Xinyuan HeroUI design standards
parent
837b6947
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
23 changed files
with
682 additions
and
208 deletions
+682
-208
ui-architecture.md
docs/ui-architecture.md
+0
-0
ui-migration-checklist.md
docs/ui-migration-checklist.md
+0
-0
xinyuan-design-system.md
docs/xinyuan-design-system.md
+0
-0
xinyuan-ui-audit.md
docs/xinyuan-ui-audit.md
+5
-3
AppIcons.jsx
src/components/AppIcons.jsx
+6
-0
DashboardSidebar.jsx
src/components/DashboardSidebar.jsx
+2
-5
FormDatePicker.jsx
src/components/FormDatePicker.jsx
+33
-6
ReferenceSearchSelect.jsx
src/components/ReferenceSearchSelect.jsx
+0
-2
TableActionButton.jsx
src/components/TableActionButton.jsx
+17
-7
XinyuanAutocomplete.jsx
src/components/XinyuanAutocomplete.jsx
+13
-5
index.jsx
src/components/record-workbench/index.jsx
+40
-75
chartPalette.js
src/design-system/chartPalette.js
+110
-0
shell.css
src/design-system/shell.css
+205
-8
theme.css
src/design-system/theme.css
+50
-48
tokens.css
src/design-system/tokens.css
+79
-1
SubjectViewContent.jsx
...ages/RecordDetailDrawer/components/SubjectViewContent.jsx
+16
-4
TailwindEditableField.jsx
...s/RecordDetailDrawer/components/TailwindEditableField.jsx
+37
-7
ApplicationSwitcherDialog.jsx
...pages/problem_rectification/ApplicationSwitcherDialog.jsx
+18
-5
index.jsx
src/pages/problem_rectification/archive/index.jsx
+3
-3
index.jsx
src/pages/problem_rectification/dashboard/index.jsx
+0
-0
index.jsx
src/pages/problem_rectification/flow-table/index.jsx
+3
-3
index.jsx
src/pages/problem_rectification/form/index.jsx
+36
-15
index.jsx
src/pages/problem_rectification/list/index.jsx
+9
-11
No files found.
docs/ui-architecture.md
View file @
f53d7e42
This diff is collapsed.
Click to expand it.
docs/ui-migration-checklist.md
View file @
f53d7e42
This diff is collapsed.
Click to expand it.
docs/xinyuan-design-system.md
View file @
f53d7e42
This diff is collapsed.
Click to expand it.
docs/xinyuan-ui-audit.md
View file @
f53d7e42
# 问题整改系统 UI/UX 全局审计
# 问题整改系统 UI/UX 全局审计
> 2026-08-19 复核:Modal 根面统一保持 HeroUI `overlay`;应用切换项、附件编辑器、流程详情摘要等需要独立边界的弹窗内对象统一使用 HeroUI `secondary`。普通详情查看态以一个 `Surface secondary` 承载整组无框标签/值,编辑态则由 `secondary` 控件直接位于 Overlay。已清理上述范围内的 L1-on-L1 默认 Card,不改变弹窗数据、回调或状态流转;深浅模式实际层级仍待浏览器验收。
审计日期:2026-08-17
审计日期:2026-08-17
目标分支:
`codex/ui-ux-xinyuan-migration`
目标分支:
`codex/ui-ux-xinyuan-migration`
...
@@ -31,7 +33,7 @@
...
@@ -31,7 +33,7 @@
-
迁移前
`package.json`
声明
`1.0.0-beta.8`
,锁文件和实际镜像为
`1.0.0-beta.7`
,
`npm ls`
报 invalid。
-
迁移前
`package.json`
声明
`1.0.0-beta.8`
,锁文件和实际镜像为
`1.0.0-beta.7`
,
`npm ls`
报 invalid。
-
Pro 包通过跨项目符号链接加载;相邻工程与当前工程各有一套物理 HeroUI/React Aria,存在 Context 双实例风险。
-
Pro 包通过跨项目符号链接加载;相邻工程与当前工程各有一套物理 HeroUI/React Aria,存在 Context 双实例风险。
-
本地镜像包含入口、CSS、Sidebar、Navbar、DataGrid、DropZone、ActionBar 和 EmptyState。
-
本地镜像包含入口、CSS、Sidebar、Navbar、DataGrid、DropZone、ActionBar 和 EmptyState。
-
本地
`file:`
安装仅用于迁移启动,团队交付仍需授权私有 npm 或统一
`.tgz`
。
-
迁移启动阶段曾使用本地
`file:`
镜像;该方式已退出当前交付链。当前团队统一通过 HeroUI 官方
`hpsetup`
授权流程取得精确固定的
`1.0.0-beta.8`
完整制品,仓库与开发机不再依赖相邻项目路径
。
### 主题
### 主题
...
@@ -71,8 +73,8 @@
...
@@ -71,8 +73,8 @@
## 迁移后复核
## 迁移后复核
-
`src/design-system/theme.css`
已
与 2026-08-18 skill 权威文件逐字节一致,SHA-256 为
`8f98b720884ded1163cfb355a235cf050f203fec85e7ded2faec87a8cf29673a
`
。
-
`src/design-system/theme.css`
已
同步 2026-08-19 从 HeroUI 官方主题工具下载并批准的基础值,并在同一权威主题文件内固化三项鑫元语义映射:
`field-background = surface`
、
`default = surface-secondary`
、
`overlay = surface`
。该映射属于共享主题,不是页面私有覆盖。
`/Users/liuchenlong/Desktop/theme.css`
、项目文件与 skill 基线逐字节一致,SHA-256 为
`3ba654ba04b040316eb3f6fcf8bde18bf5d5698281477b95e860bc5e2eeb6ddf
`
。
-
`tokens.css`
已收敛为 Shell/Login 别名
层,不再覆盖 HeroUI 的 Card、Tabs、DataGrid、Modal、Checkbox、Pagination、控件高度、圆角或阴影;内容组件外观由 HeroUI/HeroUI Pro CSS 与权威
`theme.css`
决定
。
-
`tokens.css`
已收敛为 Shell/Login 别名
、页面几何与图表数据色扩展层,不再覆盖 HeroUI 的 Card、Tabs、DataGrid、Modal、Checkbox、Pagination、控件高度、圆角或阴影;内容组件外观由 HeroUI/HeroUI Pro CSS 与权威
`theme.css`
决定。图表系列色在深浅模式中固定,HeroUI Pro 负责坐标轴、网格、文字、Tooltip 和表面的主题适配
。
-
Tabs 复核确认当前
`@heroui/styles@3.2.2`
提供
`Tabs.ListContainer`
内置
`ScrollShadow`
,但默认 Tab 的
`w-full`
会压缩或均分中文标签。活动 Tabs 已仅在官方 compound slot 的
`className`
设置
`w-fit/w-max/w-auto`
、
`min-w-max`
、
`flex-none`
和
`whitespace-nowrap`
,使每项宽度跟随自身文字;未改选中逻辑,未新增外层滚动,也未覆盖官方外观。
-
Tabs 复核确认当前
`@heroui/styles@3.2.2`
提供
`Tabs.ListContainer`
内置
`ScrollShadow`
,但默认 Tab 的
`w-full`
会压缩或均分中文标签。活动 Tabs 已仅在官方 compound slot 的
`className`
设置
`w-fit/w-max/w-auto`
、
`min-w-max`
、
`flex-none`
和
`whitespace-nowrap`
,使每项宽度跟随自身文字;未改选中逻辑,未新增外层滚动,也未覆盖官方外观。
-
行操作复核发现共享按钮的普通状态使用
`ghost`
,导致静止态没有正圆背景;操作组同时存在右对齐、6px 间距和 128px 固定列宽,三个操作会被挤向右侧。现已改为 HeroUI 官方
`secondary`
/
`danger-soft`
,Ant Design 包装层与 SVG 固定 16px 网格居中,组间距 8px并与表头左对齐,操作列按实际入口数量动态计算。两个推送弹窗内的重复删除按钮也已接入同一共享实现,业务回调与禁用条件未变。
-
行操作复核发现共享按钮的普通状态使用
`ghost`
,导致静止态没有正圆背景;操作组同时存在右对齐、6px 间距和 128px 固定列宽,三个操作会被挤向右侧。现已改为 HeroUI 官方
`secondary`
/
`danger-soft`
,Ant Design 包装层与 SVG 固定 16px 网格居中,组间距 8px并与表头左对齐,操作列按实际入口数量动态计算。两个推送弹窗内的重复删除按钮也已接入同一共享实现,业务回调与禁用条件未变。
-
短列表复核发现共享
`DataTable`
将所有业务列转换为固定像素宽度,HeroUI DataGrid 外层虽为全宽,实际单元格宽度总和不足时仍会在右侧留下大块次表面空白。共享层现已保留 HeroUI Pro 的
`%`
/
`fr`
`ColumnSize`
,非宽表第一业务主列使用
`1fr`
,内容层使用
`min-w-full`
;宽表路径、数据、排序、选择和行操作均未改变。
-
短列表复核发现共享
`DataTable`
将所有业务列转换为固定像素宽度,HeroUI DataGrid 外层虽为全宽,实际单元格宽度总和不足时仍会在右侧留下大块次表面空白。共享层现已保留 HeroUI Pro 的
`%`
/
`fr`
`ColumnSize`
,非宽表第一业务主列使用
`1fr`
,内容层使用
`min-w-full`
;宽表路径、数据、排序、选择和行操作均未改变。
...
...
src/components/AppIcons.jsx
View file @
f53d7e42
...
@@ -17,6 +17,7 @@ import EyeOutlined from '@ant-design/icons/EyeOutlined';
...
@@ -17,6 +17,7 @@ import EyeOutlined from '@ant-design/icons/EyeOutlined';
import
ExportOutlined
from
'@ant-design/icons/ExportOutlined'
;
import
ExportOutlined
from
'@ant-design/icons/ExportOutlined'
;
import
FileTextOutlined
from
'@ant-design/icons/FileTextOutlined'
;
import
FileTextOutlined
from
'@ant-design/icons/FileTextOutlined'
;
import
FolderOpenOutlined
from
'@ant-design/icons/FolderOpenOutlined'
;
import
FolderOpenOutlined
from
'@ant-design/icons/FolderOpenOutlined'
;
import
GroupOutlined
from
'@ant-design/icons/GroupOutlined'
;
import
PaperClipOutlined
from
'@ant-design/icons/PaperClipOutlined'
;
import
PaperClipOutlined
from
'@ant-design/icons/PaperClipOutlined'
;
import
LeftOutlined
from
'@ant-design/icons/LeftOutlined'
;
import
LeftOutlined
from
'@ant-design/icons/LeftOutlined'
;
import
LogoutOutlined
from
'@ant-design/icons/LogoutOutlined'
;
import
LogoutOutlined
from
'@ant-design/icons/LogoutOutlined'
;
...
@@ -45,6 +46,10 @@ function iconStyle(size, style) {
...
@@ -45,6 +46,10 @@ function iconStyle(size, style) {
return
size
===
undefined
?
style
:
{
fontSize
:
size
,
...
style
};
return
size
===
undefined
?
style
:
{
fontSize
:
size
,
...
style
};
}
}
export
const
FIELD_SUFFIX_ICON_STYLE
=
Object
.
freeze
({
color
:
'var(--field-placeholder, var(--muted))'
,
});
function
withSize
(
Icon
)
{
function
withSize
(
Icon
)
{
return
function
AppIcon
({
size
,
style
,
...
props
})
{
return
function
AppIcon
({
size
,
style
,
...
props
})
{
return
(
return
(
...
@@ -73,6 +78,7 @@ export const OpenExternal = withSize(ExportOutlined);
...
@@ -73,6 +78,7 @@ export const OpenExternal = withSize(ExportOutlined);
export
const
FileText
=
withSize
(
FileTextOutlined
);
export
const
FileText
=
withSize
(
FileTextOutlined
);
export
const
Info
=
withSize
(
InfoCircleOutlined
);
export
const
Info
=
withSize
(
InfoCircleOutlined
);
export
const
Folder
=
withSize
(
FolderOpenOutlined
);
export
const
Folder
=
withSize
(
FolderOpenOutlined
);
export
const
Group
=
withSize
(
GroupOutlined
);
export
const
LogOut
=
withSize
(
LogoutOutlined
);
export
const
LogOut
=
withSize
(
LogoutOutlined
);
export
const
Lock
=
withSize
(
LockOutlined
);
export
const
Lock
=
withSize
(
LockOutlined
);
export
function
SidebarPanel
({
size
=
20
,
style
,
...
props
})
{
export
function
SidebarPanel
({
size
=
20
,
style
,
...
props
})
{
...
...
src/components/DashboardSidebar.jsx
View file @
f53d7e42
...
@@ -21,12 +21,9 @@ function SidebarUserMenu({ userInfo, onLogout }) {
...
@@ -21,12 +21,9 @@ function SidebarUserMenu({ userInfo, onLogout }) {
return
(
return
(
<
Dropdown
>
<
Dropdown
>
<
Button
<
Dropdown
.
Trigger
fullWidth
aria
-
label=
"打开用户菜单"
aria
-
label=
"打开用户菜单"
className=
"app-sidebar-user-trigger rc-sidebar-user-trigger"
className=
"app-sidebar-user-trigger rc-sidebar-user-trigger"
size=
"sm"
variant=
"ghost"
>
>
<
Avatar
className=
"app-sidebar-user-avatar rc-sidebar-avatar"
>
<
Avatar
className=
"app-sidebar-user-avatar rc-sidebar-avatar"
>
{
userInfo
?.
avatar
?
(
{
userInfo
?.
avatar
?
(
...
@@ -41,7 +38,7 @@ function SidebarUserMenu({ userInfo, onLogout }) {
...
@@ -41,7 +38,7 @@ function SidebarUserMenu({ userInfo, onLogout }) {
<
span
className=
"app-sidebar-user-name"
>
{
user
.
displayName
}
</
span
>
<
span
className=
"app-sidebar-user-name"
>
{
user
.
displayName
}
</
span
>
<
span
className=
"app-sidebar-user-description"
>
{
user
.
username
}
</
span
>
<
span
className=
"app-sidebar-user-description"
>
{
user
.
username
}
</
span
>
</
span
>
</
span
>
</
Button
>
</
Dropdown
.
Trigger
>
<
Dropdown
.
Popover
placement=
"top start"
>
<
Dropdown
.
Popover
placement=
"top start"
>
<
Dropdown
.
Menu
<
Dropdown
.
Menu
aria
-
label=
"用户菜单"
aria
-
label=
"用户菜单"
...
...
src/components/FormDatePicker.jsx
View file @
f53d7e42
import
{
import
{
Calendar
as
HeroCalendar
,
Calendar
as
HeroCalendar
,
CloseButton
,
DateField
,
DateField
,
DatePicker
,
DatePicker
,
I18nProvider
,
I18nProvider
,
...
@@ -7,7 +8,11 @@ import {
...
@@ -7,7 +8,11 @@ import {
}
from
'@heroui/react'
;
}
from
'@heroui/react'
;
import
{
parseDate
,
parseDateTime
,
parseTime
}
from
'@internationalized/date'
;
import
{
parseDate
,
parseDateTime
,
parseTime
}
from
'@internationalized/date'
;
import
{
useMemo
}
from
'react'
;
import
{
useMemo
}
from
'react'
;
import
{
Calendar
as
CalendarIcon
}
from
'./AppIcons'
;
import
{
Calendar
as
CalendarIcon
,
Close
,
FIELD_SUFFIX_ICON_STYLE
,
}
from
'./AppIcons'
;
function
parseValue
(
value
,
mode
)
{
function
parseValue
(
value
,
mode
)
{
if
(
!
value
)
{
if
(
!
value
)
{
...
@@ -91,6 +96,7 @@ export default function FormDatePicker({
...
@@ -91,6 +96,7 @@ export default function FormDatePicker({
className
,
className
,
disabled
=
false
,
disabled
=
false
,
granularity
,
granularity
,
isClearable
=
true
,
max
,
max
,
min
,
min
,
mode
,
mode
,
...
@@ -123,6 +129,8 @@ export default function FormDatePicker({
...
@@ -123,6 +129,8 @@ export default function FormDatePicker({
const
minValue
=
useMemo
(()
=>
parseValue
(
min
,
resolvedMode
),
[
min
,
resolvedMode
]);
const
minValue
=
useMemo
(()
=>
parseValue
(
min
,
resolvedMode
),
[
min
,
resolvedMode
]);
const
maxValue
=
useMemo
(()
=>
parseValue
(
max
,
resolvedMode
),
[
max
,
resolvedMode
]);
const
maxValue
=
useMemo
(()
=>
parseValue
(
max
,
resolvedMode
),
[
max
,
resolvedMode
]);
const
timeValue
=
useMemo
(()
=>
parseTimeValue
(
value
),
[
value
]);
const
timeValue
=
useMemo
(()
=>
parseTimeValue
(
value
),
[
value
]);
const
canClear
=
isClearable
&&
Boolean
(
value
)
&&
!
disabled
&&
!
readOnly
&&
!
required
;
return
(
return
(
<
I18nProvider
locale=
"zh-CN"
>
<
I18nProvider
locale=
"zh-CN"
>
...
@@ -141,16 +149,35 @@ export default function FormDatePicker({
...
@@ -141,16 +149,35 @@ export default function FormDatePicker({
emitDateChange
(
onChange
,
name
,
formatValue
(
nextValue
,
resolvedMode
))
emitDateChange
(
onChange
,
name
,
formatValue
(
nextValue
,
resolvedMode
))
}
}
>
>
<
DateField
.
Group
fullWidth
variant=
{
variant
}
>
<
DateField
.
Group
className=
"relative"
fullWidth
variant=
{
variant
}
>
<
DateField
.
Input
>
<
DateField
.
Input
className=
{
canClear
?
'pe-14'
:
'pe-8'
}
>
{
(
segment
)
=>
(
{
(
segment
)
=>
(
<
DateField
.
Segment
segment=
{
segment
}
/>
<
DateField
.
Segment
segment=
{
segment
}
/>
)
}
)
}
</
DateField
.
Input
>
</
DateField
.
Input
>
<
DateField
.
Suffix
>
<
DateField
.
Suffix
className=
"absolute inset-y-0 end-1 mr-0 gap-0"
>
<
DatePicker
.
Trigger
aria
-
label=
"打开日期选择"
>
{
canClear
?
(
<
CloseButton
aria
-
label=
{
`清空${ariaLabel || '日期'}`
}
onPress=
{
()
=>
emitDateChange
(
onChange
,
name
,
''
)
}
>
<
Close
className=
"grid size-4 place-items-center leading-none [&>svg]:block [&>svg]:size-4"
size=
{
16
}
style=
{
FIELD_SUFFIX_ICON_STYLE
}
/>
</
CloseButton
>
)
:
null
}
<
DatePicker
.
Trigger
aria
-
label=
"打开日期选择"
className=
"size-6 w-6 shrink-0 justify-center p-1 text-field-placeholder"
>
<
DatePicker
.
TriggerIndicator
>
<
DatePicker
.
TriggerIndicator
>
<
CalendarIcon
/>
<
CalendarIcon
className=
"grid size-4 place-items-center leading-none [&>svg]:block [&>svg]:size-4"
size=
{
16
}
style=
{
FIELD_SUFFIX_ICON_STYLE
}
/>
</
DatePicker
.
TriggerIndicator
>
</
DatePicker
.
TriggerIndicator
>
</
DatePicker
.
Trigger
>
</
DatePicker
.
Trigger
>
</
DateField
.
Suffix
>
</
DateField
.
Suffix
>
...
...
src/components/ReferenceSearchSelect.jsx
View file @
f53d7e42
...
@@ -306,7 +306,6 @@ export default function ReferenceSearchSelect({
...
@@ -306,7 +306,6 @@ export default function ReferenceSearchSelect({
pageSize
=
20
,
pageSize
=
20
,
queryFilter
,
queryFilter
,
searchFields
=
DEFAULT_SEARCH_FIELDS
,
searchFields
=
DEFAULT_SEARCH_FIELDS
,
optionsPanelClassName
=
''
,
optionsAnchor
,
optionsAnchor
,
showClearAction
=
true
,
showClearAction
=
true
,
selectedSummaryMode
=
'labels'
,
selectedSummaryMode
=
'labels'
,
...
@@ -602,7 +601,6 @@ export default function ReferenceSearchSelect({
...
@@ -602,7 +601,6 @@ export default function ReferenceSearchSelect({
options=
{
menuOptions
}
options=
{
menuOptions
}
placement=
{
optionsAnchor
||
'bottom start'
}
placement=
{
optionsAnchor
||
'bottom start'
}
placeholder=
{
placeholder
}
placeholder=
{
placeholder
}
popoverClassName=
{
optionsPanelClassName
}
popoverHeader=
{
allowClear
?
(
popoverHeader=
{
allowClear
?
(
<
div
className=
"flex justify-end px-3 pb-1"
>
<
div
className=
"flex justify-end px-3 pb-1"
>
<
Button
size=
"sm"
variant=
"tertiary"
onPress=
{
clearSelection
}
>
<
Button
size=
"sm"
variant=
"tertiary"
onPress=
{
clearSelection
}
>
...
...
src/components/TableActionButton.jsx
View file @
f53d7e42
...
@@ -22,6 +22,13 @@ import { Children, Fragment, isValidElement } from 'react';
...
@@ -22,6 +22,13 @@ import { Children, Fragment, isValidElement } from 'react';
const
ACTION_ICON_CLASS
=
const
ACTION_ICON_CLASS
=
'grid size-4 shrink-0 place-items-center [line-height:0] [&>svg]:!m-0 [&>svg]:block [&>svg]:size-4'
;
'grid size-4 shrink-0 place-items-center [line-height:0] [&>svg]:!m-0 [&>svg]:block [&>svg]:size-4'
;
const
ACTION_BUTTON_LAYOUT_CLASS
=
'grid place-items-center rounded-full p-0'
;
const
ACTION_BUTTON_LAYOUT_CLASS
=
'grid place-items-center rounded-full p-0'
;
const
IMPORTANT_ACTION_TONES
=
new
Set
([
'approve'
,
'submit'
]);
function
getActionVariant
(
tone
)
{
if
([
'delete'
,
'reject'
].
includes
(
tone
))
return
'danger-soft'
;
if
(
IMPORTANT_ACTION_TONES
.
has
(
tone
))
return
'secondary'
;
return
'tertiary'
;
}
function
resolveActionTone
({
icon
,
tone
})
{
function
resolveActionTone
({
icon
,
tone
})
{
if
(
tone
)
{
if
(
tone
)
{
...
@@ -108,9 +115,10 @@ export function TableActionGroup({ className, children, ...props }) {
...
@@ -108,9 +115,10 @@ export function TableActionGroup({ className, children, ...props }) {
buttonVariants
({
buttonVariants
({
isIconOnly
:
true
,
isIconOnly
:
true
,
size
:
'sm'
,
size
:
'sm'
,
variant
:
'
second
ary'
,
variant
:
'
terti
ary'
,
}),
}),
ACTION_BUTTON_LAYOUT_CLASS
,
ACTION_BUTTON_LAYOUT_CLASS
,
'pm-table-action'
,
)
}
)
}
title=
"更多操作"
title=
"更多操作"
>
>
...
@@ -184,13 +192,15 @@ export function TableActionButton({
...
@@ -184,13 +192,15 @@ export function TableActionButton({
title=
{
label
}
title=
{
label
}
isDisabled=
{
isDisabled
}
isDisabled=
{
isDisabled
}
size=
"sm"
size=
"sm"
variant=
{
variant=
{
getActionVariant
(
resolvedTone
)
}
[
'delete'
,
'reject'
].
includes
(
resolvedTone
)
?
'danger-soft'
:
'secondary'
}
onPress=
{
onPress
}
onPress=
{
onPress
}
className=
{
clsx
(
ACTION_BUTTON_LAYOUT_CLASS
,
className
)
}
className=
{
clsx
(
ACTION_BUTTON_LAYOUT_CLASS
,
'pm-table-action'
,
IMPORTANT_ACTION_TONES
.
has
(
resolvedTone
)
&&
'pm-table-action--important'
,
[
'delete'
,
'reject'
].
includes
(
resolvedTone
)
&&
'pm-table-action--danger'
,
className
,
)
}
>
>
<
Icon
<
Icon
className=
{
ACTION_ICON_CLASS
}
className=
{
ACTION_ICON_CLASS
}
...
...
src/components/XinyuanAutocomplete.jsx
View file @
f53d7e42
import
{
ChevronDown
,
Close
,
Search
}
from
'@/components/AppIcons'
;
import
{
ChevronDown
,
Close
,
FIELD_SUFFIX_ICON_STYLE
,
Search
,
}
from
'@/components/AppIcons'
;
import
{
import
{
Autocomplete
,
Autocomplete
,
Label
,
Label
,
...
@@ -39,7 +44,6 @@ export default function XinyuanAutocomplete({
...
@@ -39,7 +44,6 @@ export default function XinyuanAutocomplete({
options
=
[],
options
=
[],
placeholder
=
'请选择'
,
placeholder
=
'请选择'
,
placement
=
'bottom start'
,
placement
=
'bottom start'
,
popoverClassName
,
popoverHeader
,
popoverHeader
,
renderOption
,
renderOption
,
searchPlaceholder
=
'输入关键词搜索'
,
searchPlaceholder
=
'输入关键词搜索'
,
...
@@ -94,17 +98,21 @@ export default function XinyuanAutocomplete({
...
@@ -94,17 +98,21 @@ export default function XinyuanAutocomplete({
</
span
>
</
span
>
</
Autocomplete
.
Value
>
</
Autocomplete
.
Value
>
<
Autocomplete
.
Indicator
>
<
Autocomplete
.
Indicator
>
<
ChevronDown
size=
{
1
4
}
/>
<
ChevronDown
size=
{
1
6
}
style=
{
FIELD_SUFFIX_ICON_STYLE
}
/>
</
Autocomplete
.
Indicator
>
</
Autocomplete
.
Indicator
>
</
Autocomplete
.
Trigger
>
</
Autocomplete
.
Trigger
>
<
Autocomplete
.
Popover
className=
{
popoverClassName
}
placement=
{
placement
}
>
<
Autocomplete
.
Popover
placement=
{
placement
}
>
<
Autocomplete
.
Filter
<
Autocomplete
.
Filter
filter=
{
shouldFilter
?
filter
||
defaultFilter
:
()
=>
true
}
filter=
{
shouldFilter
?
filter
||
defaultFilter
:
()
=>
true
}
inputValue=
{
inputValue
}
inputValue=
{
inputValue
}
onInputChange=
{
onInputChange
}
onInputChange=
{
onInputChange
}
>
>
{
popoverHeader
}
{
popoverHeader
}
<
SearchField
aria
-
label=
{
searchPlaceholder
}
fullWidth
>
<
SearchField
aria
-
label=
{
searchPlaceholder
}
fullWidth
variant=
{
variant
}
>
<
SearchField
.
Group
>
<
SearchField
.
Group
>
<
SearchField
.
SearchIcon
>
<
SearchField
.
SearchIcon
>
<
Search
size=
{
16
}
/>
<
Search
size=
{
16
}
/>
...
...
src/components/record-workbench/index.jsx
View file @
f53d7e42
...
@@ -13,8 +13,10 @@ import {
...
@@ -13,8 +13,10 @@ import {
Delete
,
Delete
,
Eye
,
Eye
,
ArrowPathIcon
,
ArrowPathIcon
,
ChartBarSquareIcon
,
ChevronDownIcon
,
ChevronDownIcon
,
ChevronRightIcon
,
ChevronRightIcon
,
Group
,
MagnifyingGlassIcon
,
MagnifyingGlassIcon
,
}
from
'@/components/AppIcons'
;
}
from
'@/components/AppIcons'
;
import
FilterSelect
from
'@/components/FilterSelect'
;
import
FilterSelect
from
'@/components/FilterSelect'
;
...
@@ -23,12 +25,12 @@ import { formatFieldValue, getFieldValue } from '@/utils/subjectView';
...
@@ -23,12 +25,12 @@ import { formatFieldValue, getFieldValue } from '@/utils/subjectView';
import
{
import
{
Alert
,
Alert
,
Button
,
Button
,
Card
,
Disclosure
,
Disclosure
,
Input
,
Input
,
SearchField
,
SearchField
,
S
witch
,
S
urface
,
Tabs
,
Tabs
,
ToggleButton
,
Tooltip
,
Tooltip
,
}
from
'@heroui/react'
;
}
from
'@heroui/react'
;
import
clsx
from
'clsx'
;
import
clsx
from
'clsx'
;
...
@@ -65,35 +67,6 @@ function FilterField({ label, children }) {
...
@@ -65,35 +67,6 @@ function FilterField({ label, children }) {
);
);
}
}
function
isRootDarkTheme
()
{
if
(
typeof
document
===
'undefined'
)
{
return
false
;
}
const
root
=
document
.
documentElement
;
return
root
.
classList
.
contains
(
'dark'
)
||
root
.
dataset
.
theme
===
'dark'
;
}
function
useRootDarkTheme
()
{
const
[
isDark
,
setIsDark
]
=
useState
(
isRootDarkTheme
);
useEffect
(()
=>
{
const
root
=
document
.
documentElement
;
const
syncTheme
=
()
=>
setIsDark
(
isRootDarkTheme
());
const
observer
=
new
MutationObserver
(
syncTheme
);
syncTheme
();
observer
.
observe
(
root
,
{
attributes
:
true
,
attributeFilter
:
[
'class'
,
'data-theme'
],
});
return
()
=>
observer
.
disconnect
();
},
[]);
return
isDark
;
}
function
getPinnedSide
(
column
)
{
function
getPinnedSide
(
column
)
{
const
pinnedValue
=
column
?.
lockPosition
||
column
?.
pinned
;
const
pinnedValue
=
column
?.
lockPosition
||
column
?.
pinned
;
if
(
pinnedValue
===
'left'
||
pinnedValue
===
'right'
)
{
if
(
pinnedValue
===
'left'
||
pinnedValue
===
'right'
)
{
...
@@ -247,22 +220,23 @@ function orderColumnsByGroups(columns = [], fieldToGroup = {}, groupById = {}) {
...
@@ -247,22 +220,23 @@ function orderColumnsByGroups(columns = [], fieldToGroup = {}, groupById = {}) {
return
orderedColumns
;
return
orderedColumns
;
}
}
function
SummaryCard
({
label
,
value
,
tone
=
'neutral'
,
icon
:
Icon
})
{
function
SummaryCard
({
label
,
value
,
tone
,
icon
:
Icon
})
{
const
iconStatus
=
[
'danger'
,
'success'
,
'warning'
].
includes
(
tone
)
?
tone
:
undefined
;
const
numericValue
=
Number
(
value
);
const
numericValue
=
Number
(
value
);
return
(
return
(
<
KPI
>
<
KPI
className=
"rc-card-surface rc-dashboard-card"
>
<
KPI
.
Header
>
<
KPI
.
Header
>
<
KPI
.
Title
>
{
label
}
</
KPI
.
Title
>
<
KPI
.
Title
>
{
label
}
</
KPI
.
Title
>
</
KPI
.
Header
>
</
KPI
.
Header
>
<
KPI
.
Content
>
<
KPI
.
Content
>
<
KPI
.
Value
value=
{
Number
.
isFinite
(
numericValue
)
?
numericValue
:
0
}
/>
<
KPI
.
Value
value=
{
Number
.
isFinite
(
numericValue
)
?
numericValue
:
0
}
/>
{
Icon
?
(
{
Icon
?
(
<
KPI
.
Icon
status=
{
iconStatus
}
aria
-
label=
{
label
}
>
<
KPI
.
Icon
<
Icon
/>
aria
-
label=
{
label
}
className=
"rc-metric-icon-surface"
data
-
tone=
{
tone
||
undefined
}
>
<
Icon
size=
{
20
}
/>
</
KPI
.
Icon
>
</
KPI
.
Icon
>
)
:
null
}
)
:
null
}
</
KPI
.
Content
>
</
KPI
.
Content
>
...
@@ -289,7 +263,7 @@ function getActionButtonVariant(action) {
...
@@ -289,7 +263,7 @@ function getActionButtonVariant(action) {
return
action
?.
variant
===
'danger'
?
'danger'
:
'danger-soft'
;
return
action
?.
variant
===
'danger'
?
'danger'
:
'danger-soft'
;
}
}
return
action
?.
variant
||
(
action
?.
tone
===
'primary'
?
'primary'
:
'
outline
'
);
return
action
?.
variant
||
(
action
?.
tone
===
'primary'
?
'primary'
:
'
secondary
'
);
}
}
function
defaultGetRecordId
(
record
)
{
function
defaultGetRecordId
(
record
)
{
...
@@ -357,26 +331,23 @@ function getWorkbenchColumnWidth(column, stickyMeta) {
...
@@ -357,26 +331,23 @@ function getWorkbenchColumnWidth(column, stickyMeta) {
return
Math
.
min
(
readableWidth
,
280
);
return
Math
.
min
(
readableWidth
,
280
);
}
}
function
ColumnGroupControl
({
group
,
collapsed
,
onToggle
})
{
function
ColumnGroupControl
({
group
,
collapsed
,
index
,
onToggle
})
{
const
visible
=
!
collapsed
;
const
Icon
=
index
%
2
===
0
?
Group
:
ChartBarSquareIcon
;
const
actionLabel
=
`
${
collapsed
?
'显示'
:
'隐藏'
}${
group
.
name
}
列`
;
return
(
return
(
<
Tooltip
delay=
{
300
}
>
<
Tooltip
delay=
{
300
}
>
<
Switch
<
ToggleButton
size=
"sm"
size=
"sm"
isSelected=
{
visible
}
isSelected=
{
collapsed
}
onChange=
{
onToggle
}
onChange=
{
onToggle
}
aria
-
label=
{
`显示${group.name}列`
}
aria
-
label=
{
actionLabel
}
>
>
<
Switch
.
Content
>
<
Icon
size=
{
16
}
/>
<
Switch
.
Control
>
<
span
>
{
group
.
name
}
</
span
>
<
Switch
.
Thumb
/>
</
ToggleButton
>
</
Switch
.
Control
>
{
group
.
name
}
</
Switch
.
Content
>
</
Switch
>
<
Tooltip
.
Content
>
<
Tooltip
.
Content
>
显示或隐藏“
{
group
.
name
}
”的
{
group
.
children
.
length
}
个列表字段,不会筛选或修改数据
{
actionLabel
}
(共
{
group
.
children
.
length
}
个字段)
</
Tooltip
.
Content
>
</
Tooltip
.
Content
>
</
Tooltip
>
</
Tooltip
>
);
);
...
@@ -473,8 +444,7 @@ export default function RecordWorkbench({
...
@@ -473,8 +444,7 @@ export default function RecordWorkbench({
const
[
collapsedColumnGroups
,
setCollapsedColumnGroups
]
=
useState
({});
const
[
collapsedColumnGroups
,
setCollapsedColumnGroups
]
=
useState
({});
const
[
columnGroupAnnouncement
,
setColumnGroupAnnouncement
]
=
useState
(
''
);
const
[
columnGroupAnnouncement
,
setColumnGroupAnnouncement
]
=
useState
(
''
);
const
[
expandedRowIds
,
setExpandedRowIds
]
=
useState
({});
const
[
expandedRowIds
,
setExpandedRowIds
]
=
useState
({});
const
isDarkTheme
=
useRootDarkTheme
();
const
filterFieldVariant
=
'secondary'
;
const
filterFieldVariant
=
isDarkTheme
?
'secondary'
:
'primary'
;
const
loading
=
Boolean
(
states
?.
loading
);
const
loading
=
Boolean
(
states
?.
loading
);
const
error
=
states
?.
error
||
''
;
const
error
=
states
?.
error
||
''
;
const
totalRecords
=
pagination
?.
totalRecords
??
records
.
length
;
const
totalRecords
=
pagination
?.
totalRecords
??
records
.
length
;
...
@@ -841,16 +811,6 @@ export default function RecordWorkbench({
...
@@ -841,16 +811,6 @@ export default function RecordWorkbench({
const disabledDataGridRowKeys = dataGridRows
const disabledDataGridRowKeys = dataGridRows
.filter((row) => row.isExpandedRecord || row.isExpandedPanel)
.filter((row) => row.isExpandedRecord || row.isExpandedPanel)
.map((row) => String(row.key));
.map((row) => String(row.key));
const summaryGridClass =
summaryItems.length === 1
? 'md:grid-cols-1'
: summaryItems.length === 2
? 'md:grid-cols-2'
: summaryItems.length === 3
? 'md:grid-cols-3'
: summaryItems.length === 5 || summaryItems.length === 6
? 'md:grid-cols-2 xl:grid-cols-3'
: 'md:grid-cols-2 xl:grid-cols-4';
const toolbarSearchField = filterSchema.find((field) => field.icon === 'search');
const toolbarSearchField = filterSchema.find((field) => field.icon === 'search');
const structuredFilterFields = filterSchema.filter(
const structuredFilterFields = filterSchema.filter(
(field) => field !== toolbarSearchField,
(field) => field !== toolbarSearchField,
...
@@ -906,7 +866,7 @@ export default function RecordWorkbench({
...
@@ -906,7 +866,7 @@ export default function RecordWorkbench({
return (
return (
<div className="rc-page-stack">
<div className="rc-page-stack">
{summaryItems.length > 0 ? (
{summaryItems.length > 0 ? (
<section className=
{clsx('rc-kpi-grid grid', summaryGridClass)
}>
<section className=
"rc-kpi-grid" data-count={summaryItems.length
}>
{summaryItems.map((item) => (
{summaryItems.map((item) => (
<SummaryCard
<SummaryCard
key={item.label}
key={item.label}
...
@@ -945,8 +905,10 @@ export default function RecordWorkbench({
...
@@ -945,8 +905,10 @@ export default function RecordWorkbench({
) : null}
) : null}
{structuredFilterFields.length > 0 && !useInlineFilters ? (
{structuredFilterFields.length > 0 && !useInlineFilters ? (
<Card>
<Surface
<Card.Content>
className="rounded-3xl p-6"
variant="default"
>
<Disclosure
<Disclosure
isExpanded={filtersExpanded}
isExpanded={filtersExpanded}
onExpandedChange={setFiltersExpanded}
onExpandedChange={setFiltersExpanded}
...
@@ -1013,8 +975,7 @@ export default function RecordWorkbench({
...
@@ -1013,8 +975,7 @@ export default function RecordWorkbench({
</div>
</div>
</form>
</form>
</Disclosure>
</Disclosure>
</Card.Content>
</Surface>
</Card>
) : null}
) : null}
<section className={tableSectionClass}>
<section className={tableSectionClass}>
...
@@ -1053,16 +1014,19 @@ export default function RecordWorkbench({
...
@@ -1053,16 +1014,19 @@ export default function RecordWorkbench({
</form>
</form>
) : null}
) : null}
<div className="rc-page-toolbar">
<div className="flex min-w-0 items-center">
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-3">
<h2 className="text-base font-medium text-foreground">{title}</h2>
<h2 className="text-base font-medium text-foreground">{title}</h2>
</div>
<div className="rc-page-toolbar">
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">
{toolbarSearchField ? (
{toolbarSearchField ? (
<div className="w-full min-w-0 sm:w-80 lg:w-96">
<div className="w-full min-w-0
flex-none
sm:w-80 lg:w-96">
<SearchField
<SearchField
aria-label={toolbarSearchField.label || '关键词搜索'}
aria-label={toolbarSearchField.label || '关键词搜索'}
fullWidth
fullWidth
value={filters[toolbarSearchField.key] || ''}
value={filters[toolbarSearchField.key] || ''}
variant="
second
ary"
variant="
prim
ary"
onChange={(value) =>
onChange={(value) =>
onFiltersChange?.(toolbarSearchField.key, value)
onFiltersChange?.(toolbarSearchField.key, value)
}
}
...
@@ -1080,12 +1044,13 @@ export default function RecordWorkbench({
...
@@ -1080,12 +1044,13 @@ export default function RecordWorkbench({
</div>
</div>
) : null}
) : null}
{hasColumnGroups ? (
{hasColumnGroups ? (
<div className="flex flex-wrap items-center gap-
3
" aria-label="列分组显示控制">
<div className="flex flex-wrap items-center gap-
2
" aria-label="列分组显示控制">
{columnGroupMeta.groups.map((group) => (
{columnGroupMeta.groups.map((group
, index
) => (
<ColumnGroupControl
<ColumnGroupControl
key={group.id}
key={group.id}
group={group}
group={group}
collapsed={Boolean(collapsedColumnGroups[group.id])}
collapsed={Boolean(collapsedColumnGroups[group.id])}
index={index}
onToggle={() => toggleColumnGroup(group.id)}
onToggle={() => toggleColumnGroup(group.id)}
/>
/>
))}
))}
...
...
src/design-system/chartPalette.js
0 → 100644
View file @
f53d7e42
export
const
XINYUAN_CHART_PALETTE
=
Object
.
freeze
([
'var(--rc-chart-1)'
,
'var(--rc-chart-2)'
,
'var(--rc-chart-3)'
,
'var(--rc-chart-4)'
,
'var(--rc-chart-5)'
,
'var(--rc-chart-6)'
,
'var(--rc-chart-7)'
,
'var(--rc-chart-8)'
,
]);
export
const
XINYUAN_CHART_SEMANTIC
=
Object
.
freeze
({
primary
:
'var(--rc-chart-primary)'
,
risk
:
'var(--rc-chart-risk)'
,
warning
:
'var(--rc-chart-warning)'
,
positive
:
'var(--rc-chart-positive)'
,
comparison
:
'var(--rc-chart-comparison)'
,
neutral
:
'var(--rc-chart-neutral)'
,
});
function
normalizeChartKey
(
key
)
{
return
String
(
key
??
''
).
trim
();
}
function
hashChartKey
(
key
)
{
return
Array
.
from
(
key
).
reduce
(
(
value
,
character
)
=>
((
value
*
31
)
+
character
.
codePointAt
(
0
))
>>>
0
,
0
,
);
}
export
function
getXinyuanChartColor
(
key
,
fallbackIndex
=
0
)
{
const
stableKey
=
normalizeChartKey
(
key
);
if
(
!
stableKey
)
{
return
XINYUAN_CHART_PALETTE
[
fallbackIndex
%
XINYUAN_CHART_PALETTE
.
length
];
}
return
XINYUAN_CHART_PALETTE
[
hashChartKey
(
stableKey
)
%
XINYUAN_CHART_PALETTE
.
length
];
}
export
function
createXinyuanChartColorMap
(
keys
=
[])
{
const
stableKeys
=
[...
new
Set
(
keys
.
map
(
normalizeChartKey
).
filter
(
Boolean
))]
.
sort
();
const
colorMap
=
new
Map
();
const
usedIndexes
=
new
Set
();
stableKeys
.
forEach
((
key
,
keyIndex
)
=>
{
const
preferredIndex
=
hashChartKey
(
key
)
%
XINYUAN_CHART_PALETTE
.
length
;
let
paletteIndex
=
preferredIndex
;
if
(
keyIndex
<
XINYUAN_CHART_PALETTE
.
length
)
{
for
(
let
offset
=
0
;
offset
<
XINYUAN_CHART_PALETTE
.
length
;
offset
+=
1
)
{
const
candidateIndex
=
(
preferredIndex
+
offset
)
%
XINYUAN_CHART_PALETTE
.
length
;
if
(
!
usedIndexes
.
has
(
candidateIndex
))
{
paletteIndex
=
candidateIndex
;
usedIndexes
.
add
(
candidateIndex
);
break
;
}
}
}
colorMap
.
set
(
key
,
XINYUAN_CHART_PALETTE
[
paletteIndex
]);
});
return
colorMap
;
}
function
isVisibleChartColor
(
color
)
{
if
(
typeof
color
!==
'string'
)
{
return
false
;
}
const
normalizedColor
=
color
.
trim
().
toLowerCase
();
return
Boolean
(
normalizedColor
)
&&
normalizedColor
!==
'none'
&&
normalizedColor
!==
'transparent'
;
}
export
function
colorizeXinyuanChartTooltipPayload
(
payload
=
[],
resolveColor
,
)
{
return
payload
.
map
((
entry
,
index
)
=>
{
const
rechartsPayload
=
entry
?.
payload
||
{};
const
sourceDatum
=
rechartsPayload
?.
payload
||
rechartsPayload
;
const
key
=
sourceDatum
?.
name
??
rechartsPayload
?.
name
??
entry
?.
name
??
entry
?.
dataKey
??
''
;
const
candidateColors
=
[
resolveColor
?.({
entry
,
index
,
key
,
sourceDatum
}),
rechartsPayload
?.
fill
,
sourceDatum
?.
fill
,
entry
?.
fill
,
entry
?.
color
,
entry
?.
stroke
,
getXinyuanChartColor
(
key
,
index
),
];
const
color
=
candidateColors
.
find
(
isVisibleChartColor
);
return
{
...
entry
,
color
,
fill
:
color
,
name
:
entry
?.
name
??
sourceDatum
?.
name
??
entry
?.
dataKey
,
stroke
:
color
,
};
});
}
src/design-system/shell.css
View file @
f53d7e42
...
@@ -440,11 +440,202 @@
...
@@ -440,11 +440,202 @@
gap
:
var
(
--rc-page-module-gap
);
gap
:
var
(
--rc-page-module-gap
);
}
}
.rc-chart-grid
{
display
:
grid
;
grid-template-columns
:
minmax
(
0
,
1
fr
);
}
.rc-chart-grid
>
*
{
min-width
:
0
;
}
.rc-chart-grid
>
:only-child
{
grid-column
:
1
/
-1
;
}
/* Recharts keeps keyboard navigation while pointer focus stays visually quiet. */
.rc-chart-interaction
:focus:not
(
:focus-visible
),
.rc-chart-interaction
:focus:not
(
:focus-visible
)
{
outline
:
none
;
}
.rc-chart-interaction
:focus-visible
,
.rc-chart-interaction
:focus-visible
{
outline
:
2px
solid
var
(
--focus
);
outline-offset
:
2px
;
}
.rc-kpi-grid
{
.rc-kpi-grid
{
display
:
grid
;
grid-template-columns
:
minmax
(
0
,
1
fr
);
min-width
:
0
;
min-width
:
0
;
gap
:
var
(
--rc-related-item-gap
);
gap
:
var
(
--rc-related-item-gap
);
}
}
@media
(
min-width
:
48rem
)
{
.rc-kpi-grid
:not
([
data-count
=
'1'
])
{
grid-template-columns
:
repeat
(
2
,
minmax
(
0
,
1
fr
));
}
}
@media
(
min-width
:
80rem
)
{
.rc-kpi-grid
[
data-count
=
'1'
]
{
grid-template-columns
:
minmax
(
0
,
1
fr
);
}
.rc-kpi-grid
[
data-count
=
'2'
]
{
grid-template-columns
:
repeat
(
2
,
minmax
(
0
,
1
fr
));
}
.rc-kpi-grid
[
data-count
=
'3'
],
.rc-kpi-grid
[
data-count
=
'5'
],
.rc-kpi-grid
[
data-count
=
'6'
],
.rc-kpi-grid
[
data-count
=
'9'
]
{
grid-template-columns
:
repeat
(
3
,
minmax
(
0
,
1
fr
));
}
.rc-kpi-grid
[
data-count
=
'4'
],
.rc-kpi-grid
[
data-count
=
'7'
],
.rc-kpi-grid
[
data-count
=
'8'
]
{
grid-template-columns
:
repeat
(
4
,
minmax
(
0
,
1
fr
));
}
}
/* Shared HeroUI/HeroUI Pro business-card contract. */
.rc-card-surface
{
border
:
1px
solid
var
(
--rc-border-default
);
background-color
:
var
(
--rc-bg-surface
);
color
:
var
(
--rc-text-primary
);
box-shadow
:
var
(
--rc-shadow-card
);
}
.rc-dashboard-card
{
border-radius
:
1.5rem
;
}
/* HeroUI Pro KPI.Icon keeps its compound role; this class supplies Xinyuan tones. */
.rc-metric-icon-surface
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-neutral
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-neutral-soft
);
display
:
grid
;
width
:
3rem
;
min-width
:
3rem
;
max-width
:
3rem
;
height
:
3rem
;
min-height
:
3rem
;
max-height
:
3rem
;
flex
:
0
0
3rem
;
place-items
:
center
;
border-radius
:
1rem
;
color
:
var
(
--rc-metric-icon-color
);
background-color
:
var
(
--rc-metric-icon-background
);
line-height
:
1
;
}
.rc-metric-icon-surface
[
data-tone
=
'info'
]
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-info
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-info-soft
);
}
.rc-metric-icon-surface
[
data-tone
=
'success'
]
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-success
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-success-soft
);
}
.rc-metric-icon-surface
[
data-tone
=
'teal'
]
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-teal
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-teal-soft
);
}
.rc-metric-icon-surface
[
data-tone
=
'danger'
]
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-danger
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-danger-soft
);
}
.rc-metric-icon-surface
[
data-tone
=
'warning'
]
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-warning
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-warning-soft
);
}
.rc-metric-icon-surface
[
data-tone
=
'violet'
]
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-violet
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-violet-soft
);
}
.rc-metric-icon-surface
[
data-tone
=
'indigo'
]
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-indigo
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-indigo-soft
);
}
.rc-metric-icon-surface
[
data-tone
=
'coral'
]
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-coral
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-coral-soft
);
}
/* Untoned KPI items receive a unique ordered tone for boards of up to nine. */
.rc-kpi-grid
>
:nth-child
(
1
)
.rc-metric-icon-surface
:not
([
data-tone
])
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-info
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-info-soft
);
}
.rc-kpi-grid
>
:nth-child
(
2
)
.rc-metric-icon-surface
:not
([
data-tone
])
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-success
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-success-soft
);
}
.rc-kpi-grid
>
:nth-child
(
3
)
.rc-metric-icon-surface
:not
([
data-tone
])
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-warning
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-warning-soft
);
}
.rc-kpi-grid
>
:nth-child
(
4
)
.rc-metric-icon-surface
:not
([
data-tone
])
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-violet
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-violet-soft
);
}
.rc-kpi-grid
>
:nth-child
(
5
)
.rc-metric-icon-surface
:not
([
data-tone
])
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-danger
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-danger-soft
);
}
.rc-kpi-grid
>
:nth-child
(
6
)
.rc-metric-icon-surface
:not
([
data-tone
])
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-teal
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-teal-soft
);
}
.rc-kpi-grid
>
:nth-child
(
7
)
.rc-metric-icon-surface
:not
([
data-tone
])
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-indigo
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-indigo-soft
);
}
.rc-kpi-grid
>
:nth-child
(
8
)
.rc-metric-icon-surface
:not
([
data-tone
])
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-coral
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-coral-soft
);
}
.rc-kpi-grid
>
:nth-child
(
9
)
.rc-metric-icon-surface
:not
([
data-tone
])
{
--rc-metric-icon-color
:
var
(
--rc-metric-tone-neutral
);
--rc-metric-icon-background
:
var
(
--rc-metric-tone-neutral-soft
);
}
.rc-metric-icon-surface
>
.anticon
,
.rc-metric-icon-surface
>
svg
{
display
:
block
;
width
:
1.25rem
;
height
:
1.25rem
;
margin
:
0
;
color
:
inherit
;
font-size
:
1.25rem
;
line-height
:
1
;
}
.rc-metric-icon-surface
>
.anticon
>
svg
{
display
:
block
;
width
:
1em
;
height
:
1em
;
margin
:
0
;
}
.rc-page-toolbar
{
.rc-page-toolbar
{
display
:
flex
;
display
:
flex
;
min-width
:
0
;
min-width
:
0
;
...
@@ -464,6 +655,12 @@
...
@@ -464,6 +655,12 @@
gap
:
var
(
--rc-list-stack-gap
);
gap
:
var
(
--rc-list-stack-gap
);
}
}
@media
(
min-width
:
80rem
)
{
.rc-chart-grid
{
grid-template-columns
:
repeat
(
2
,
minmax
(
0
,
1
fr
));
}
}
.rc-mobile-sidebar-backdrop
{
.rc-mobile-sidebar-backdrop
{
position
:
fixed
;
position
:
fixed
;
inset
:
0
;
inset
:
0
;
...
@@ -728,7 +925,7 @@
...
@@ -728,7 +925,7 @@
padding
:
var
(
--rc-space-2
)
var
(
--rc-space-3
)
var
(
--rc-space-4
);
padding
:
var
(
--rc-space-2
)
var
(
--rc-space-3
)
var
(
--rc-space-4
);
}
}
.rc-sidebar-user-trigger.
button
{
.rc-sidebar-user-trigger.
dropdown__trigger
{
display
:
flex
;
display
:
flex
;
width
:
100%
;
width
:
100%
;
min-width
:
0
;
min-width
:
0
;
...
@@ -743,9 +940,9 @@
...
@@ -743,9 +940,9 @@
text-align
:
left
;
text-align
:
left
;
}
}
.rc-sidebar-user-trigger.
button
:hover
,
.rc-sidebar-user-trigger.
dropdown__trigger
:hover
,
.rc-sidebar-user-trigger.
button
[
data-hovered
=
'true'
],
.rc-sidebar-user-trigger.
dropdown__trigger
[
data-hovered
=
'true'
],
.rc-sidebar-user-trigger.
button
[
aria-expanded
=
'true'
]
{
.rc-sidebar-user-trigger.
dropdown__trigger
[
aria-expanded
=
'true'
]
{
background
:
var
(
--default
);
background
:
var
(
--default
);
}
}
...
@@ -1134,7 +1331,7 @@
...
@@ -1134,7 +1331,7 @@
padding
:
0.5rem
0.75rem
1rem
;
padding
:
0.5rem
0.75rem
1rem
;
}
}
.app-sidebar-user-trigger.
button
{
.app-sidebar-user-trigger.
dropdown__trigger
{
display
:
flex
;
display
:
flex
;
width
:
100%
;
width
:
100%
;
min-width
:
0
;
min-width
:
0
;
...
@@ -1150,13 +1347,13 @@
...
@@ -1150,13 +1347,13 @@
}
}
@media
(
hover
:
hover
)
{
@media
(
hover
:
hover
)
{
.app-sidebar-user-trigger.
button
:hover
,
.app-sidebar-user-trigger.
dropdown__trigger
:hover
,
.app-sidebar-user-trigger.
button
[
data-hovered
=
'true'
]
{
.app-sidebar-user-trigger.
dropdown__trigger
[
data-hovered
=
'true'
]
{
background
:
var
(
--default
);
background
:
var
(
--default
);
}
}
}
}
.app-sidebar-user-trigger.
button
[
aria-expanded
=
'true'
]
{
.app-sidebar-user-trigger.
dropdown__trigger
[
aria-expanded
=
'true'
]
{
background
:
var
(
--default
);
background
:
var
(
--default
);
}
}
...
...
src/design-system/theme.css
View file @
f53d7e42
...
@@ -13,34 +13,35 @@
...
@@ -13,34 +13,35 @@
/* Theme Colors (Light Mode) */
/* Theme Colors (Light Mode) */
--accent
:
oklch
(
45.32%
0.1661
27.03
);
--accent
:
oklch
(
45.32%
0.1661
27.03
);
--accent-foreground
:
oklch
(
99.11%
0
0
);
--accent-foreground
:
oklch
(
99.11%
0
0
);
--background
:
oklch
(
97.02%
0.00
19
27.03
);
--background
:
oklch
(
97.02%
0.00
45
27.03
);
--border
:
oklch
(
90.00%
0.00
19
27.03
);
--border
:
oklch
(
90.00%
0.00
45
27.03
);
--danger
:
oklch
(
65.32%
0.2337
358.52
);
--danger
:
oklch
(
45.32%
0.1661
27.03
);
--danger-foreground
:
oklch
(
99.11%
0
0
);
--danger-foreground
:
oklch
(
99.11%
0
0
);
--default
:
oklch
(
94.00%
0.0019
27.03
);
/* HeroUI control aliases: primary = L1, secondary = L2. */
--default-foreground
:
oklch
(
21.03%
0.0059
27.03
);
--default
:
var
(
--surface-secondary
);
--field-background
:
oklch
(
100.00%
0.0009
27.03
);
--default-foreground
:
var
(
--surface-secondary-foreground
);
--field-background
:
var
(
--surface
);
--field-border
:
transparent
;
--field-border
:
transparent
;
--field-foreground
:
oklch
(
21.03%
0.0019
27.03
);
--field-foreground
:
var
(
--surface-foreground
);
--field-placeholder
:
oklch
(
55.17%
0.00
38
27.03
);
--field-placeholder
:
oklch
(
55.17%
0.00
90
27.03
);
--focus
:
oklch
(
45.32%
0.1661
27.03
);
--focus
:
oklch
(
45.32%
0.1661
27.03
);
--foreground
:
oklch
(
21.03%
0.00
19
27.03
);
--foreground
:
oklch
(
21.03%
0.00
45
27.03
);
--muted
:
oklch
(
55.17%
0.00
38
27.03
);
--muted
:
oklch
(
55.17%
0.00
90
27.03
);
--overlay
:
oklch
(
100.00%
0.0006
27.03
);
--overlay
:
var
(
--surface
);
--overlay-foreground
:
oklch
(
21.03%
0.0019
27.03
);
--overlay-foreground
:
var
(
--surface-foreground
);
--scrollbar
:
oklch
(
87.10%
0.00
19
27.03
);
--scrollbar
:
oklch
(
87.10%
0.00
45
27.03
);
--segment
:
oklch
(
100.00%
0.00
19
27.03
);
--segment
:
oklch
(
100.00%
0.00
45
27.03
);
--segment-foreground
:
oklch
(
21.03%
0.00
19
27.03
);
--segment-foreground
:
oklch
(
21.03%
0.00
45
27.03
);
--separator
:
oklch
(
92.00%
0.00
19
27.03
);
--separator
:
oklch
(
92.00%
0.00
45
27.03
);
--success
:
oklch
(
73.29%
0.19
4
2
123.59
);
--success
:
oklch
(
73.29%
0.19
5
2
123.59
);
--success-foreground
:
oklch
(
21.03%
0.0059
123.59
);
--success-foreground
:
oklch
(
21.03%
0.0059
123.59
);
--surface
:
oklch
(
100.00%
0.00
09
27.03
);
--surface
:
oklch
(
100.00%
0.00
22
27.03
);
--surface-foreground
:
oklch
(
21.03%
0.00
19
27.03
);
--surface-foreground
:
oklch
(
21.03%
0.00
45
27.03
);
--surface-secondary
:
oklch
(
95.24%
0.00
15
27.03
);
--surface-secondary
:
oklch
(
95.24%
0.00
36
27.03
);
--surface-secondary-foreground
:
oklch
(
21.03%
0.00
19
27.03
);
--surface-secondary-foreground
:
oklch
(
21.03%
0.00
45
27.03
);
--surface-tertiary
:
oklch
(
93.73%
0.00
15
27.03
);
--surface-tertiary
:
oklch
(
93.73%
0.00
36
27.03
);
--surface-tertiary-foreground
:
oklch
(
21.03%
0.00
19
27.03
);
--surface-tertiary-foreground
:
oklch
(
21.03%
0.00
45
27.03
);
--warning
:
oklch
(
78.19%
0.159
1
45.11
);
--warning
:
oklch
(
78.19%
0.159
9
45.11
);
--warning-foreground
:
oklch
(
21.03%
0.0059
45.11
);
--warning-foreground
:
oklch
(
21.03%
0.0059
45.11
);
/* Border Radius */
/* Border Radius */
...
@@ -58,33 +59,34 @@
...
@@ -58,33 +59,34 @@
/* Theme Colors (Dark Mode) */
/* Theme Colors (Dark Mode) */
--accent
:
oklch
(
45.32%
0.1661
27.03
);
--accent
:
oklch
(
45.32%
0.1661
27.03
);
--accent-foreground
:
oklch
(
99.11%
0
0
);
--accent-foreground
:
oklch
(
99.11%
0
0
);
--background
:
oklch
(
12.00%
0.00
19
27.03
);
--background
:
oklch
(
12.00%
0.00
45
27.03
);
--border
:
oklch
(
28.00%
0.00
19
27.03
);
--border
:
oklch
(
28.00%
0.00
45
27.03
);
--danger
:
oklch
(
59.40%
0.1974
357.41
);
--danger
:
oklch
(
62.00%
0.1600
27.03
);
--danger-foreground
:
oklch
(
99.11%
0
0
);
--danger-foreground
:
oklch
(
99.11%
0
0
);
--default
:
oklch
(
27.40%
0.0019
27.03
);
/* HeroUI control aliases: primary = L1, secondary = L2. */
--default-foreground
:
oklch
(
99.11%
0
0
);
--default
:
var
(
--surface-secondary
);
--field-background
:
oklch
(
21.03%
0.0038
27.03
);
--default-foreground
:
var
(
--surface-secondary-foreground
);
--field-background
:
var
(
--surface
);
--field-border
:
transparent
;
--field-border
:
transparent
;
--field-foreground
:
oklch
(
99.11%
0.0019
27.03
);
--field-foreground
:
var
(
--surface-foreground
);
--field-placeholder
:
oklch
(
70.50%
0.00
38
27.03
);
--field-placeholder
:
oklch
(
70.50%
0.00
90
27.03
);
--focus
:
oklch
(
45.32%
0.1661
27.03
);
--focus
:
oklch
(
45.32%
0.1661
27.03
);
--foreground
:
oklch
(
99.11%
0.00
19
27.03
);
--foreground
:
oklch
(
99.11%
0.00
45
27.03
);
--muted
:
oklch
(
70.50%
0.00
38
27.03
);
--muted
:
oklch
(
70.50%
0.00
90
27.03
);
--overlay
:
oklch
(
21.03%
0.0038
27.03
);
--overlay
:
var
(
--surface
);
--overlay-foreground
:
oklch
(
99.11%
0.0019
27.03
);
--overlay-foreground
:
var
(
--surface-foreground
);
--scrollbar
:
oklch
(
70.50%
0.00
19
27.03
);
--scrollbar
:
oklch
(
70.50%
0.00
45
27.03
);
--segment
:
oklch
(
39.64%
0.00
19
27.03
);
--segment
:
oklch
(
39.64%
0.00
45
27.03
);
--segment-foreground
:
oklch
(
99.11%
0.00
19
27.03
);
--segment-foreground
:
oklch
(
99.11%
0.00
45
27.03
);
--separator
:
oklch
(
25.00%
0.00
19
27.03
);
--separator
:
oklch
(
25.00%
0.00
45
27.03
);
--success
:
oklch
(
73.29%
0.19
4
2
123.59
);
--success
:
oklch
(
73.29%
0.19
5
2
123.59
);
--success-foreground
:
oklch
(
21.03%
0.0059
123.59
);
--success-foreground
:
oklch
(
21.03%
0.0059
123.59
);
--surface
:
oklch
(
21.03%
0.00
38
27.03
);
--surface
:
oklch
(
21.03%
0.00
90
27.03
);
--surface-foreground
:
oklch
(
99.11%
0.00
19
27.03
);
--surface-foreground
:
oklch
(
99.11%
0.00
45
27.03
);
--surface-secondary
:
oklch
(
25.70%
0.00
29
27.03
);
--surface-secondary
:
oklch
(
25.70%
0.00
67
27.03
);
--surface-secondary-foreground
:
oklch
(
99.11%
0.00
19
27.03
);
--surface-secondary-foreground
:
oklch
(
99.11%
0.00
45
27.03
);
--surface-tertiary
:
oklch
(
27.21%
0.00
29
27.03
);
--surface-tertiary
:
oklch
(
27.21%
0.00
67
27.03
);
--surface-tertiary-foreground
:
oklch
(
99.11%
0.00
19
27.03
);
--surface-tertiary-foreground
:
oklch
(
99.11%
0.00
45
27.03
);
--warning
:
oklch
(
82.03%
0.1
393
49.12
);
--warning
:
oklch
(
82.03%
0.1
400
49.12
);
--warning-foreground
:
oklch
(
21.03%
0.0059
49.12
);
--warning-foreground
:
oklch
(
21.03%
0.0059
49.12
);
}
}
src/design-system/tokens.css
View file @
f53d7e42
...
@@ -5,7 +5,7 @@
...
@@ -5,7 +5,7 @@
[
data-theme
=
'default'
],
[
data-theme
=
'default'
],
.dark
,
.dark
,
[
data-theme
=
'dark'
]
{
[
data-theme
=
'dark'
]
{
/*
Shell aliases only. HeroUI component
tokens are owned by theme.css. */
/*
Xinyuan aliases and extension tokens. HeroUI core
tokens are owned by theme.css. */
--rc-brand-primary
:
var
(
--accent
);
--rc-brand-primary
:
var
(
--accent
);
--rc-brand-light
:
color-mix
(
in
oklab
,
var
(
--accent
)
84%
,
white
);
--rc-brand-light
:
color-mix
(
in
oklab
,
var
(
--accent
)
84%
,
white
);
--rc-brand-dark
:
color-mix
(
in
oklab
,
var
(
--accent
)
82%
,
black
);
--rc-brand-dark
:
color-mix
(
in
oklab
,
var
(
--accent
)
82%
,
black
);
...
@@ -78,6 +78,82 @@
...
@@ -78,6 +78,82 @@
--rc-shadow-raised
:
0
1px
2px
rgb
(
15
23
42
/
6%
),
--rc-shadow-raised
:
0
1px
2px
rgb
(
15
23
42
/
6%
),
0
8px
24px
rgb
(
15
23
42
/
8%
);
0
8px
24px
rgb
(
15
23
42
/
8%
);
--rc-shadow-overlay
:
0
16px
40px
rgb
(
15
23
42
/
14%
);
--rc-shadow-overlay
:
0
16px
40px
rgb
(
15
23
42
/
14%
);
--rc-shadow-card
:
0
1px
2px
rgb
(
15
23
42
/
2%
),
0
4px
12px
rgb
(
15
23
42
/
2%
);
/* KPI icon surfaces. Card bodies remain neutral; only the icon well carries tone. */
--rc-metric-tone-info
:
oklch
(
55%
0.14
255
);
--rc-metric-tone-success
:
oklch
(
54%
0.13
145
);
--rc-metric-tone-teal
:
oklch
(
55%
0.1
205
);
--rc-metric-tone-danger
:
oklch
(
52%
0.17
25
);
--rc-metric-tone-warning
:
oklch
(
62%
0.12
75
);
--rc-metric-tone-violet
:
oklch
(
56%
0.12
300
);
--rc-metric-tone-indigo
:
oklch
(
53%
0.13
275
);
--rc-metric-tone-coral
:
oklch
(
60%
0.12
48
);
--rc-metric-tone-neutral
:
var
(
--muted
);
--rc-metric-tone-info-soft
:
color-mix
(
in
oklab
,
var
(
--rc-metric-tone-info
)
9%
,
var
(
--surface
)
);
--rc-metric-tone-success-soft
:
color-mix
(
in
oklab
,
var
(
--rc-metric-tone-success
)
9%
,
var
(
--surface
)
);
--rc-metric-tone-teal-soft
:
color-mix
(
in
oklab
,
var
(
--rc-metric-tone-teal
)
9%
,
var
(
--surface
)
);
--rc-metric-tone-danger-soft
:
color-mix
(
in
oklab
,
var
(
--rc-metric-tone-danger
)
9%
,
var
(
--surface
)
);
--rc-metric-tone-warning-soft
:
color-mix
(
in
oklab
,
var
(
--rc-metric-tone-warning
)
9%
,
var
(
--surface
)
);
--rc-metric-tone-violet-soft
:
color-mix
(
in
oklab
,
var
(
--rc-metric-tone-violet
)
9%
,
var
(
--surface
)
);
--rc-metric-tone-indigo-soft
:
color-mix
(
in
oklab
,
var
(
--rc-metric-tone-indigo
)
9%
,
var
(
--surface
)
);
--rc-metric-tone-coral-soft
:
color-mix
(
in
oklab
,
var
(
--rc-metric-tone-coral
)
9%
,
var
(
--surface
)
);
--rc-metric-tone-neutral-soft
:
color-mix
(
in
oklab
,
var
(
--rc-metric-tone-neutral
)
8%
,
var
(
--surface
)
);
/* Data visualization only. Do not reuse UI status colors as a series palette. */
--rc-chart-1
:
oklch
(
45.32%
0.1661
27.03
);
--rc-chart-2
:
oklch
(
56%
0.135
255
);
--rc-chart-3
:
oklch
(
64%
0.11
220
);
--rc-chart-4
:
oklch
(
56%
0.115
280
);
--rc-chart-5
:
oklch
(
60%
0.095
200
);
--rc-chart-6
:
oklch
(
70%
0.12
80
);
--rc-chart-7
:
oklch
(
60%
0.1
295
);
--rc-chart-8
:
oklch
(
57%
0.035
250
);
--rc-chart-primary
:
var
(
--rc-chart-1
);
--rc-chart-risk
:
oklch
(
47%
0.145
27.03
);
--rc-chart-warning
:
var
(
--rc-chart-6
);
--rc-chart-positive
:
var
(
--rc-chart-2
);
--rc-chart-comparison
:
var
(
--rc-chart-4
);
--rc-chart-neutral
:
var
(
--rc-chart-8
);
--rc-chart-legend-font-size
:
0.75rem
;
--rc-chart-legend-line-height
:
1rem
;
}
}
.dark
,
.dark
,
...
@@ -96,4 +172,6 @@
...
@@ -96,4 +172,6 @@
--rc-shadow-raised
:
0
1px
2px
rgb
(
0
0
0
/
24%
),
--rc-shadow-raised
:
0
1px
2px
rgb
(
0
0
0
/
24%
),
0
8px
24px
rgb
(
0
0
0
/
24%
);
0
8px
24px
rgb
(
0
0
0
/
24%
);
--rc-shadow-overlay
:
0
16px
40px
rgb
(
0
0
0
/
38%
);
--rc-shadow-overlay
:
0
16px
40px
rgb
(
0
0
0
/
38%
);
--rc-shadow-card
:
0
1px
2px
rgb
(
0
0
0
/
20%
),
0
4px
12px
rgb
(
0
0
0
/
18%
);
}
}
src/pages/RecordDetailDrawer/components/SubjectViewContent.jsx
View file @
f53d7e42
import
clsx
from
'clsx'
;
import
clsx
from
'clsx'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
useRootDarkTheme
from
'@/hooks/useRootDarkTheme
'
;
import
{
Surface
}
from
'@heroui/react
'
;
import
TailwindEditableField
from
'./TailwindEditableField'
;
import
TailwindEditableField
from
'./TailwindEditableField'
;
import
{
import
{
buildViewColumns
,
buildViewColumns
,
...
@@ -62,7 +62,7 @@ export default function SubjectViewContent({
...
@@ -62,7 +62,7 @@ export default function SubjectViewContent({
const
contentFields
=
minimal
const
contentFields
=
minimal
?
(
displayFields
||
[]).
filter
((
field
)
=>
field
.
field
!==
'title'
)
?
(
displayFields
||
[]).
filter
((
field
)
=>
field
.
field
!==
'title'
)
:
displayFields
;
:
displayFields
;
const
fieldVariant
=
useRootDarkTheme
()
?
'secondary'
:
'prim
ary'
;
const
fieldVariant
=
'second
ary'
;
if
(
!
contentFields
||
contentFields
.
length
===
0
)
{
if
(
!
contentFields
||
contentFields
.
length
===
0
)
{
if
(
minimal
)
{
if
(
minimal
)
{
...
@@ -76,13 +76,14 @@ export default function SubjectViewContent({
...
@@ -76,13 +76,14 @@ export default function SubjectViewContent({
);
);
}
}
return
(
const
fields
=
(
<
section
<
section
className=
"grid min-w-0 grid-cols-1 gap-x-8 gap-y-2 md:grid-cols-2"
className=
"grid min-w-0 grid-cols-1 gap-x-8 gap-y-2 md:grid-cols-2"
>
>
{
contentFields
.
map
((
field
)
=>
{
{
contentFields
.
map
((
field
)
=>
{
const
value
=
getFieldValue
(
editableRecord
,
field
.
field
);
const
value
=
getFieldValue
(
editableRecord
,
field
.
field
);
const
fieldType
=
field
.
fieldDef
?.
type
||
'text'
;
const
fieldType
=
field
.
fieldDef
?.
type
||
'text'
;
const
fieldLabel
=
getFieldLabel
(
field
);
const
isFullWidth
=
FULL_WIDTH_FIELD_TYPES
.
has
(
fieldType
);
const
isFullWidth
=
FULL_WIDTH_FIELD_TYPES
.
has
(
fieldType
);
const
fieldDisabled
=
const
fieldDisabled
=
isFieldDisabled
?.(
field
.
field
,
view
)
||
isFieldDisabled
?.(
field
.
field
,
view
)
||
...
@@ -98,7 +99,7 @@ export default function SubjectViewContent({
...
@@ -98,7 +99,7 @@ export default function SubjectViewContent({
)
}
)
}
>
>
<
div
className=
{
minimal
?
'sr-only'
:
'text-sm font-medium text-muted'
}
>
<
div
className=
{
minimal
?
'sr-only'
:
'text-sm font-medium text-muted'
}
>
{
getFieldLabel
(
field
)
}
{
fieldLabel
}
</
div
>
</
div
>
<
div
className=
{
minimal
?
undefined
:
'mt-1.5 min-w-0'
}
>
<
div
className=
{
minimal
?
undefined
:
'mt-1.5 min-w-0'
}
>
<
TailwindEditableField
<
TailwindEditableField
...
@@ -107,6 +108,7 @@ export default function SubjectViewContent({
...
@@ -107,6 +108,7 @@ export default function SubjectViewContent({
fieldPath=
{
field
.
field
}
fieldPath=
{
field
.
field
}
editSessionId=
{
editSessionId
}
editSessionId=
{
editSessionId
}
fieldVariant=
{
fieldVariant
}
fieldVariant=
{
fieldVariant
}
placeholder=
{
`请输入${fieldLabel}`
}
mode=
{
mode
}
mode=
{
mode
}
onChange=
{
(
newValue
)
=>
onChange=
{
(
newValue
)
=>
onFieldChange
?.(
onFieldChange
?.(
...
@@ -126,4 +128,14 @@ export default function SubjectViewContent({
...
@@ -126,4 +128,14 @@ export default function SubjectViewContent({
})
}
})
}
</
section
>
</
section
>
);
);
if
(
mode
===
'view'
&&
!
minimal
)
{
return
(
<
Surface
className=
"rounded-2xl px-5 py-2 sm:px-6 sm:py-3"
variant=
"secondary"
>
{
fields
}
</
Surface
>
);
}
return
fields
;
}
}
src/pages/RecordDetailDrawer/components/TailwindEditableField.jsx
View file @
f53d7e42
...
@@ -74,9 +74,26 @@ function FieldError({ children, className }) {
...
@@ -74,9 +74,26 @@ function FieldError({ children, className }) {
);
);
}
}
function
isEmptyFieldValue
(
value
)
{
if
(
value
===
null
||
value
===
undefined
)
return
true
;
if
(
typeof
value
===
'string'
)
return
value
.
trim
()
===
''
;
if
(
Array
.
isArray
(
value
))
{
return
value
.
length
===
0
||
value
.
every
((
item
)
=>
isEmptyFieldValue
(
item
));
}
return
false
;
}
function
getTextPlaceholder
(
placeholder
,
fieldDef
)
{
const
explicitPlaceholder
=
String
(
placeholder
||
''
).
trim
();
if
(
explicitPlaceholder
)
return
explicitPlaceholder
;
const
fieldLabel
=
String
(
fieldDef
?.
label
||
''
).
trim
();
return
fieldLabel
?
`请输入
${
fieldLabel
}
`
:
'请输入内容'
;
}
function
FieldValue
({
value
,
fieldDef
})
{
function
FieldValue
({
value
,
fieldDef
})
{
if
(
value
===
null
||
value
===
undefined
||
value
===
''
||
(
Array
.
isArray
(
value
)
&&
value
.
length
===
0
))
{
if
(
isEmptyFieldValue
(
value
))
{
return
<
span
className=
"text-
muted"
>
未填写
</
span
>;
return
<
span
className=
"text-
sm text-muted"
>
暂无内容
</
span
>;
}
}
if
(
fieldDef
?.
type
===
'select'
)
{
if
(
fieldDef
?.
type
===
'select'
)
{
...
@@ -128,7 +145,7 @@ function FieldValue({ value, fieldDef }) {
...
@@ -128,7 +145,7 @@ function FieldValue({ value, fieldDef }) {
if
(
fieldDef
?.
type
===
'object'
||
fieldDef
?.
type
===
'form'
||
fieldDef
?.
type
===
'subject'
)
{
if
(
fieldDef
?.
type
===
'object'
||
fieldDef
?.
type
===
'form'
||
fieldDef
?.
type
===
'subject'
)
{
return
(
return
(
<
Card
variant=
"
second
ary"
>
<
Card
variant=
"
terti
ary"
>
<
Card
.
Content
>
<
Card
.
Content
>
<
pre
className=
"max-h-48 overflow-auto text-xs leading-5"
>
<
pre
className=
"max-h-48 overflow-auto text-xs leading-5"
>
{
JSON
.
stringify
(
value
,
null
,
2
)
}
{
JSON
.
stringify
(
value
,
null
,
2
)
}
...
@@ -139,7 +156,11 @@ function FieldValue({ value, fieldDef }) {
...
@@ -139,7 +156,11 @@ function FieldValue({ value, fieldDef }) {
}
}
const
text
=
formatFieldValue
(
value
,
fieldDef
);
const
text
=
formatFieldValue
(
value
,
fieldDef
);
return
<
span
className=
"whitespace-pre-wrap text-foreground"
>
{
text
||
'-'
}
</
span
>;
return
(
<
span
className=
{
clsx
(
'whitespace-pre-wrap'
,
isEmptyFieldValue
(
text
)
?
'text-sm text-muted'
:
'text-foreground'
)
}
>
{
isEmptyFieldValue
(
text
)
?
'暂无内容'
:
text
}
</
span
>
);
}
}
function
UserSelector
({
value
,
onChange
,
multiple
,
variant
})
{
function
UserSelector
({
value
,
onChange
,
multiple
,
variant
})
{
...
@@ -280,6 +301,7 @@ function ControlledFieldEditor({
...
@@ -280,6 +301,7 @@ function ControlledFieldEditor({
fieldPath
,
fieldPath
,
editSessionId
,
editSessionId
,
fieldVariant
,
fieldVariant
,
placeholder
,
onChange
,
onChange
,
onValidationChange
,
onValidationChange
,
})
{
})
{
...
@@ -288,6 +310,7 @@ function ControlledFieldEditor({
...
@@ -288,6 +310,7 @@ function ControlledFieldEditor({
const
fieldType
=
fieldDef
?.
type
||
'text'
;
const
fieldType
=
fieldDef
?.
type
||
'text'
;
const
isDateField
=
isDateFieldType
(
fieldType
);
const
isDateField
=
isDateFieldType
(
fieldType
);
const
isLongText
=
[
'long_text'
,
'rich_text'
].
includes
(
fieldType
);
const
isLongText
=
[
'long_text'
,
'rich_text'
].
includes
(
fieldType
);
const
textPlaceholder
=
getTextPlaceholder
(
placeholder
,
fieldDef
);
const
handleFileUpload
=
async
(
files
)
=>
{
const
handleFileUpload
=
async
(
files
)
=>
{
const
file
=
files
?.[
0
];
const
file
=
files
?.[
0
];
...
@@ -366,7 +389,7 @@ function ControlledFieldEditor({
...
@@ -366,7 +389,7 @@ function ControlledFieldEditor({
if
(
fieldType
===
'file'
)
{
if
(
fieldType
===
'file'
)
{
return
(
return
(
<
Card
>
<
Card
variant=
"secondary"
>
<
Card
.
Content
className=
"space-y-3"
>
<
Card
.
Content
className=
"space-y-3"
>
{
toArray
(
value
).
length
===
0
?
(
{
toArray
(
value
).
length
===
0
?
(
<
span
className=
"text-sm text-muted"
>
未上传
</
span
>
<
span
className=
"text-sm text-muted"
>
未上传
</
span
>
...
@@ -450,6 +473,7 @@ function ControlledFieldEditor({
...
@@ -450,6 +473,7 @@ function ControlledFieldEditor({
return
(
return
(
<
TextArea
<
TextArea
fullWidth
fullWidth
placeholder=
{
textPlaceholder
}
rows=
{
5
}
rows=
{
5
}
value=
{
value
||
''
}
value=
{
value
||
''
}
variant=
{
fieldVariant
}
variant=
{
fieldVariant
}
...
@@ -471,6 +495,7 @@ function ControlledFieldEditor({
...
@@ -471,6 +495,7 @@ function ControlledFieldEditor({
return
(
return
(
<
Input
<
Input
fullWidth
fullWidth
placeholder=
{
textPlaceholder
}
type=
{
fieldType
===
'number'
?
'number'
:
'text'
}
type=
{
fieldType
===
'number'
?
'number'
:
'text'
}
value=
{
value
??
''
}
value=
{
value
??
''
}
variant=
{
fieldVariant
}
variant=
{
fieldVariant
}
...
@@ -489,7 +514,8 @@ export default function TailwindEditableField({
...
@@ -489,7 +514,8 @@ export default function TailwindEditableField({
fieldDef
,
fieldDef
,
fieldPath
,
fieldPath
,
editSessionId
,
editSessionId
,
fieldVariant
=
'primary'
,
fieldVariant
=
'secondary'
,
placeholder
,
onSave
,
onSave
,
onChange
,
onChange
,
onValidationChange
,
onValidationChange
,
...
@@ -517,6 +543,7 @@ export default function TailwindEditableField({
...
@@ -517,6 +543,7 @@ export default function TailwindEditableField({
const
isDateField
=
isDateFieldType
(
fieldType
);
const
isDateField
=
isDateFieldType
(
fieldType
);
const
isLongText
=
[
'long_text'
,
'rich_text'
].
includes
(
fieldType
);
const
isLongText
=
[
'long_text'
,
'rich_text'
].
includes
(
fieldType
);
const
isJsonLike
=
[
'object'
,
'form'
,
'subject'
].
includes
(
fieldType
);
const
isJsonLike
=
[
'object'
,
'form'
,
'subject'
].
includes
(
fieldType
);
const
textPlaceholder
=
getTextPlaceholder
(
placeholder
,
fieldDef
);
if
(
mode
===
'view'
||
(
mode
===
'edit'
&&
(
readonly
||
disabled
)))
{
if
(
mode
===
'view'
||
(
mode
===
'edit'
&&
(
readonly
||
disabled
)))
{
return
<
ReadonlyFieldValue
value=
{
value
}
fieldDef=
{
fieldDef
}
/>;
return
<
ReadonlyFieldValue
value=
{
value
}
fieldDef=
{
fieldDef
}
/>;
...
@@ -530,6 +557,7 @@ export default function TailwindEditableField({
...
@@ -530,6 +557,7 @@ export default function TailwindEditableField({
fieldPath=
{
fieldPath
}
fieldPath=
{
fieldPath
}
editSessionId=
{
editSessionId
}
editSessionId=
{
editSessionId
}
fieldVariant=
{
fieldVariant
}
fieldVariant=
{
fieldVariant
}
placeholder=
{
textPlaceholder
}
onChange=
{
onChange
}
onChange=
{
onChange
}
onValidationChange=
{
onValidationChange
}
onValidationChange=
{
onValidationChange
}
/>
/>
...
@@ -734,7 +762,7 @@ export default function TailwindEditableField({
...
@@ -734,7 +762,7 @@ export default function TailwindEditableField({
if
(
fieldType
===
'file'
)
{
if
(
fieldType
===
'file'
)
{
return
(
return
(
<
Card
>
<
Card
variant=
"secondary"
>
<
Card
.
Content
className=
"space-y-3"
>
<
Card
.
Content
className=
"space-y-3"
>
<
div
className=
"space-y-2"
>
<
div
className=
"space-y-2"
>
{
toArray
(
draft
).
length
===
0
?
(
{
toArray
(
draft
).
length
===
0
?
(
...
@@ -822,6 +850,7 @@ export default function TailwindEditableField({
...
@@ -822,6 +850,7 @@ export default function TailwindEditableField({
<
TextArea
<
TextArea
autoFocus
autoFocus
fullWidth
fullWidth
placeholder=
{
textPlaceholder
}
rows=
{
5
}
rows=
{
5
}
value=
{
draft
||
''
}
value=
{
draft
||
''
}
variant=
"secondary"
variant=
"secondary"
...
@@ -848,6 +877,7 @@ export default function TailwindEditableField({
...
@@ -848,6 +877,7 @@ export default function TailwindEditableField({
<
Input
<
Input
autoFocus
autoFocus
fullWidth
fullWidth
placeholder=
{
textPlaceholder
}
type=
{
fieldType
===
'number'
?
'number'
:
'text'
}
type=
{
fieldType
===
'number'
?
'number'
:
'text'
}
value=
{
draft
??
''
}
value=
{
draft
??
''
}
variant=
"secondary"
variant=
"secondary"
...
...
src/pages/problem_rectification/ApplicationSwitcherDialog.jsx
View file @
f53d7e42
...
@@ -9,6 +9,7 @@ import {
...
@@ -9,6 +9,7 @@ import {
Description
,
Description
,
Modal
,
Modal
,
Skeleton
,
Skeleton
,
Surface
,
}
from
'@heroui/react'
;
}
from
'@heroui/react'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
...
@@ -124,7 +125,7 @@ function ApplicationCard({ application, current }) {
...
@@ -124,7 +125,7 @@ function ApplicationCard({ application, current }) {
const
initial
=
application
.
title
.
trim
().
slice
(
0
,
1
).
toUpperCase
()
||
'A'
;
const
initial
=
application
.
title
.
trim
().
slice
(
0
,
1
).
toUpperCase
()
||
'A'
;
return
(
return
(
<
Card
className=
"h-full"
variant=
{
current
?
'secondary'
:
'default'
}
>
<
Card
className=
"h-full"
variant=
"secondary"
>
<
Card
.
Content
>
<
Card
.
Content
>
<
a
<
a
href=
{
application
.
url
}
href=
{
application
.
url
}
...
@@ -133,10 +134,22 @@ function ApplicationCard({ application, current }) {
...
@@ -133,10 +134,22 @@ function ApplicationCard({ application, current }) {
aria
-
current=
{
current
?
'page'
:
undefined
}
aria
-
current=
{
current
?
'page'
:
undefined
}
className=
"flex min-w-0 items-center gap-3 no-underline"
className=
"flex min-w-0 items-center gap-3 no-underline"
>
>
<
Avatar
size=
"md"
>
<
Surface
{
application
.
logo
?
<
Avatar
.
Image
src=
{
application
.
logo
}
alt=
{
application
.
title
}
/>
:
null
}
aria
-
hidden=
"true"
className=
"grid size-9 shrink-0 place-items-center overflow-hidden rounded-[var(--rc-radius-control)]"
variant=
"tertiary"
>
<
Avatar
className=
"size-full rounded-[var(--rc-radius-control)] bg-transparent"
color=
"accent"
variant=
"soft"
>
{
application
.
logo
?
(
<
Avatar
.
Image
className=
"object-contain p-1.5"
src=
{
application
.
logo
}
alt=
""
/>
)
:
null
}
<
Avatar
.
Fallback
>
{
initial
}
</
Avatar
.
Fallback
>
<
Avatar
.
Fallback
>
{
initial
}
</
Avatar
.
Fallback
>
</
Avatar
>
</
Avatar
>
</
Surface
>
<
span
className=
"min-w-0 flex-1"
>
<
span
className=
"min-w-0 flex-1"
>
<
span
className=
"block break-words font-medium leading-5 text-foreground"
title=
{
application
.
title
}
>
<
span
className=
"block break-words font-medium leading-5 text-foreground"
title=
{
application
.
title
}
>
{
application
.
title
}
{
application
.
title
}
...
@@ -156,9 +169,9 @@ function LoadingSkeleton() {
...
@@ -156,9 +169,9 @@ function LoadingSkeleton() {
return
(
return
(
<
div
className=
"grid gap-3 sm:grid-cols-2"
aria
-
label=
"正在加载应用"
>
<
div
className=
"grid gap-3 sm:grid-cols-2"
aria
-
label=
"正在加载应用"
>
{
Array
.
from
({
length
:
6
}).
map
((
_
,
index
)
=>
(
{
Array
.
from
({
length
:
6
}).
map
((
_
,
index
)
=>
(
<
Card
key=
{
index
}
>
<
Card
key=
{
index
}
variant=
"secondary"
>
<
Card
.
Content
className=
"flex items-center gap-3"
>
<
Card
.
Content
className=
"flex items-center gap-3"
>
<
Skeleton
className=
"size-
10
"
/>
<
Skeleton
className=
"size-
9
"
/>
<
div
className=
"flex-1 space-y-2"
>
<
div
className=
"flex-1 space-y-2"
>
<
Skeleton
className=
"h-4 w-2/3"
/>
<
Skeleton
className=
"h-4 w-2/3"
/>
<
Skeleton
className=
"h-3 w-full"
/>
<
Skeleton
className=
"h-3 w-full"
/>
...
...
src/pages/problem_rectification/archive/index.jsx
View file @
f53d7e42
...
@@ -131,7 +131,7 @@ export default function ProblemRectificationArchivePage() {
...
@@ -131,7 +131,7 @@ export default function ProblemRectificationArchivePage() {
label
:
'关键词搜索'
,
label
:
'关键词搜索'
,
type
:
'text'
,
type
:
'text'
,
icon
:
'search'
,
icon
:
'search'
,
placeholder
:
'
搜索归档批次标题、编码或关联字段...
'
,
placeholder
:
'
请输入关键词
'
,
},
},
]
}
]
}
onFiltersChange=
{
(
key
,
value
)
=>
{
onFiltersChange=
{
(
key
,
value
)
=>
{
...
@@ -155,14 +155,14 @@ export default function ProblemRectificationArchivePage() {
...
@@ -155,14 +155,14 @@ export default function ProblemRectificationArchivePage() {
{
{
key
:
'archive-export'
,
key
:
'archive-export'
,
label
:
'导出批次'
,
label
:
'导出批次'
,
variant
:
'
outline
'
,
variant
:
'
secondary
'
,
disabled
:
true
,
disabled
:
true
,
onClick
:
()
=>
{},
onClick
:
()
=>
{},
},
},
{
{
key
:
'archive-compare'
,
key
:
'archive-compare'
,
label
:
'批次对比'
,
label
:
'批次对比'
,
variant
:
'
outline
'
,
variant
:
'
secondary
'
,
disabled
:
true
,
disabled
:
true
,
onClick
:
()
=>
{},
onClick
:
()
=>
{},
},
},
...
...
src/pages/problem_rectification/dashboard/index.jsx
View file @
f53d7e42
This diff is collapsed.
Click to expand it.
src/pages/problem_rectification/flow-table/index.jsx
View file @
f53d7e42
...
@@ -1288,7 +1288,7 @@ export default function ProblemRectificationFlowTablePage() {
...
@@ -1288,7 +1288,7 @@ export default function ProblemRectificationFlowTablePage() {
{
isDelayFlow
?
(
{
isDelayFlow
?
(
<>
<>
{
editable
?
(
{
editable
?
(
<
Button
isDisabled=
{
submitting
}
size=
"sm"
variant=
"
outline
"
onPress=
{
()
=>
handleDelaySave
(
row
)
}
>
<
Button
isDisabled=
{
submitting
}
size=
"sm"
variant=
"
secondary
"
onPress=
{
()
=>
handleDelaySave
(
row
)
}
>
保存申请
保存申请
</
Button
>
</
Button
>
)
:
null
}
)
:
null
}
...
@@ -1324,7 +1324,7 @@ export default function ProblemRectificationFlowTablePage() {
...
@@ -1324,7 +1324,7 @@ export default function ProblemRectificationFlowTablePage() {
)
:
(
)
:
(
<>
<>
{
editable
?
(
{
editable
?
(
<
Button
isDisabled=
{
submitting
}
size=
"sm"
variant=
"
outline
"
onPress=
{
()
=>
handleSave
(
row
)
}
>
<
Button
isDisabled=
{
submitting
}
size=
"sm"
variant=
"
secondary
"
onPress=
{
()
=>
handleSave
(
row
)
}
>
保存
保存
</
Button
>
</
Button
>
)
:
null
}
)
:
null
}
...
@@ -1573,7 +1573,7 @@ export default function ProblemRectificationFlowTablePage() {
...
@@ -1573,7 +1573,7 @@ export default function ProblemRectificationFlowTablePage() {
aria
-
label=
"刷新流程"
aria
-
label=
"刷新流程"
isDisabled=
{
loading
||
Boolean
(
submittingKey
)
}
isDisabled=
{
loading
||
Boolean
(
submittingKey
)
}
size=
"sm"
size=
"sm"
variant=
"
outline
"
variant=
"
secondary
"
onPress=
{
loadFlow
}
onPress=
{
loadFlow
}
>
>
<
ArrowPathIcon
size=
{
16
}
/>
<
ArrowPathIcon
size=
{
16
}
/>
...
...
src/pages/problem_rectification/form/index.jsx
View file @
f53d7e42
...
@@ -12,6 +12,7 @@ import { normalizeText } from '@/utils/subjectView';
...
@@ -12,6 +12,7 @@ import { normalizeText } from '@/utils/subjectView';
import
dayjs
from
'dayjs'
;
import
dayjs
from
'dayjs'
;
import
{
Alert
,
Button
,
Card
,
Chip
,
Spinner
}
from
'@heroui/react'
;
import
{
Alert
,
Button
,
Card
,
Chip
,
Spinner
}
from
'@heroui/react'
;
import
{
EmptyState
as
ProEmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
EmptyState
as
ProEmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
KPI
}
from
'@heroui-pro/react/kpi'
;
import
{
useContext
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
useContext
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
useSearchParams
}
from
'umi'
;
import
{
useSearchParams
}
from
'umi'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
...
@@ -73,22 +74,42 @@ function StepBadge({ title, status, active }) {
...
@@ -73,22 +74,42 @@ function StepBadge({ title, status, active }) {
);
);
}
}
function
SummaryCard
({
label
,
value
,
hint
,
icon
:
Icon
,
tone
=
'neutral'
})
{
function
SummaryCard
({
label
,
value
,
hint
,
icon
:
Icon
,
tone
,
variant
=
'default'
})
{
if
(
Icon
)
{
const
numericValue
=
Number
.
parseFloat
(
String
(
value
).
replace
(
/,/g
,
''
));
return
(
return
(
<
Card
>
<
KPI
className=
"rc-card-surface rc-dashboard-card"
>
<
KPI
.
Header
>
<
KPI
.
Title
>
{
label
}
</
KPI
.
Title
>
</
KPI
.
Header
>
<
KPI
.
Content
>
<
KPI
.
Value
value=
{
Number
.
isFinite
(
numericValue
)
?
numericValue
:
0
}
>
{
()
=>
value
}
</
KPI
.
Value
>
<
KPI
.
Icon
aria
-
label=
{
label
}
className=
"rc-metric-icon-surface"
data
-
tone=
{
tone
||
undefined
}
>
<
Icon
size=
{
20
}
/>
</
KPI
.
Icon
>
</
KPI
.
Content
>
{
hint
?
<
KPI
.
Footer
className=
"text-xs text-muted"
>
{
hint
}
</
KPI
.
Footer
>
:
null
}
</
KPI
>
);
}
return
(
<
Card
variant=
{
variant
}
>
<
Card
.
Content
className=
"flex items-start justify-between gap-4"
>
<
Card
.
Content
className=
"flex items-start justify-between gap-4"
>
<
div
className=
"min-w-0"
>
<
div
className=
"min-w-0"
>
<
div
className=
"text-sm font-medium text-muted"
>
{
label
}
</
div
>
<
div
className=
"text-sm font-medium text-muted"
>
{
label
}
</
div
>
<
div
className=
{
Icon
?
'mt-2 text-2xl font-medium text-foreground'
:
'mt-2 text-lg font-medium text-foreground'
}
>
<
div
className=
"mt-2 text-lg font-medium text-foreground"
>
{
value
}
{
value
}
</
div
>
</
div
>
{
hint
?
<
div
className=
"mt-1 text-xs text-muted"
>
{
hint
}
</
div
>
:
null
}
{
hint
?
<
div
className=
"mt-1 text-xs text-muted"
>
{
hint
}
</
div
>
:
null
}
</
div
>
</
div
>
{
Icon
?
(
<
Chip
color=
{
[
'danger'
,
'success'
,
'warning'
].
includes
(
tone
)
?
tone
:
undefined
}
size=
"lg"
variant=
"soft"
aria
-
label=
{
label
}
>
<
Icon
size=
{
20
}
/>
</
Chip
>
)
:
null
}
</
Card
.
Content
>
</
Card
.
Content
>
</
Card
>
</
Card
>
);
);
...
@@ -387,7 +408,7 @@ export default function ProblemRectificationFormPage() {
...
@@ -387,7 +408,7 @@ export default function ProblemRectificationFormPage() {
/>
/>
</
section
>
</
section
>
<
section
className=
"rc-
page-grid grid md:grid-cols-
3"
>
<
section
className=
"rc-
kpi-grid"
data
-
count=
"
3"
>
<
SummaryCard
label=
"关联记录"
value=
{
summary
.
total
}
icon=
{
Database
}
tone=
"info"
/>
<
SummaryCard
label=
"关联记录"
value=
{
summary
.
total
}
icon=
{
Database
}
tone=
"info"
/>
<
SummaryCard
<
SummaryCard
label=
"已整改"
label=
"已整改"
...
@@ -451,7 +472,7 @@ export default function ProblemRectificationFormPage() {
...
@@ -451,7 +472,7 @@ export default function ProblemRectificationFormPage() {
render
:
(
record
)
=>
(
render
:
(
record
)
=>
(
<
Button
<
Button
size=
"sm"
size=
"sm"
variant=
"
outline
"
variant=
"
secondary
"
onPress=
{
()
=>
setSelectedRecord
(
record
)
}
onPress=
{
()
=>
setSelectedRecord
(
record
)
}
>
>
{
canEditRecords
&&
!
record
.
isLocked
?
'查看/填写'
:
'查看详情'
}
{
canEditRecords
&&
!
record
.
isLocked
?
'查看/填写'
:
'查看详情'
}
...
@@ -491,7 +512,7 @@ export default function ProblemRectificationFormPage() {
...
@@ -491,7 +512,7 @@ export default function ProblemRectificationFormPage() {
</
Card
.
Header
>
</
Card
.
Header
>
<
Card
.
Content
>
<
Card
.
Content
>
<
div
className=
"flex flex-wrap gap-3"
>
<
div
className=
"flex flex-wrap gap-3"
>
<
Button
isDisabled=
{
submitting
}
size=
"sm"
variant=
"
outline
"
onPress=
{
reload
}
>
<
Button
isDisabled=
{
submitting
}
size=
"sm"
variant=
"
secondary
"
onPress=
{
reload
}
>
刷新流程
刷新流程
</
Button
>
</
Button
>
...
@@ -553,10 +574,10 @@ export default function ProblemRectificationFormPage() {
...
@@ -553,10 +574,10 @@ export default function ProblemRectificationFormPage() {
</
Alert
>
</
Alert
>
)
:
null
}
)
:
null
}
<
div
className=
"grid gap-4 md:grid-cols-2"
>
<
div
className=
"grid gap-4 md:grid-cols-2"
>
<
SummaryCard
label=
"问题标题"
value=
{
selectedRecord
.
title
||
'-'
}
/>
<
SummaryCard
label=
"问题标题"
value=
{
selectedRecord
.
title
||
'-'
}
variant=
"secondary"
/>
<
SummaryCard
label=
"反馈单位"
value=
{
selectedRecord
.
feedbackUnit
||
'-'
}
/>
<
SummaryCard
label=
"反馈单位"
value=
{
selectedRecord
.
feedbackUnit
||
'-'
}
variant=
"secondary"
/>
<
SummaryCard
label=
"整改进度"
value=
{
selectedRecord
.
progress
||
'-'
}
/>
<
SummaryCard
label=
"整改进度"
value=
{
selectedRecord
.
progress
||
'-'
}
variant=
"secondary"
/>
<
SummaryCard
label=
"预计完成时间"
value=
{
formatDateText
(
selectedRecord
.
raw
?.
metadata
?.
the_time_of_rectification
)
}
/>
<
SummaryCard
label=
"预计完成时间"
value=
{
formatDateText
(
selectedRecord
.
raw
?.
metadata
?.
the_time_of_rectification
)
}
variant=
"secondary"
/>
</
div
>
</
div
>
<
div
className=
"grid gap-4 lg:grid-cols-2"
>
<
div
className=
"grid gap-4 lg:grid-cols-2"
>
...
...
src/pages/problem_rectification/list/index.jsx
View file @
f53d7e42
...
@@ -14,7 +14,6 @@ import {
...
@@ -14,7 +14,6 @@ import {
TableActionButton
,
TableActionButton
,
}
from
'@/components/TableActionButton'
;
}
from
'@/components/TableActionButton'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
import
XinyuanModal
from
'@/components/XinyuanModal'
;
import
useRootDarkTheme
from
'@/hooks/useRootDarkTheme'
;
import
RecordWorkbench
from
'@/components/record-workbench'
;
import
RecordWorkbench
from
'@/components/record-workbench'
;
import
RecordDetailDrawer
from
'@/pages/RecordDetailDrawer'
;
import
RecordDetailDrawer
from
'@/pages/RecordDetailDrawer'
;
import
{
import
{
...
@@ -796,8 +795,7 @@ export default function ProblemRectificationListPage() {
...
@@ -796,8 +795,7 @@ export default function ProblemRectificationListPage() {
});
});
const [gridViewCounts, setGridViewCounts] = useState({});
const [gridViewCounts, setGridViewCounts] = useState({});
const [workflowUserLabels, setWorkflowUserLabels] = useState({});
const [workflowUserLabels, setWorkflowUserLabels] = useState({});
const isDarkTheme = useRootDarkTheme();
const createFieldVariant = 'secondary';
const createFieldVariant = isDarkTheme ? 'secondary' : 'primary';
const reloadResources = useCallback(async () => {
const reloadResources = useCallback(async () => {
setLoading(true);
setLoading(true);
...
@@ -1660,7 +1658,7 @@ export default function ProblemRectificationListPage() {
...
@@ -1660,7 +1658,7 @@ export default function ProblemRectificationListPage() {
label: '关键词搜索',
label: '关键词搜索',
type: 'text',
type: 'text',
icon: 'search',
icon: 'search',
placeholder: '
搜索项目名称、严重程度、分类、责任部门、问题原因...
',
placeholder: '
请输入关键词
',
},
},
{
{
key: 'inspectionReportMonth',
key: 'inspectionReportMonth',
...
@@ -1750,14 +1748,14 @@ export default function ProblemRectificationListPage() {
...
@@ -1750,14 +1748,14 @@ export default function ProblemRectificationListPage() {
{
{
key: 'export',
key: 'export',
label: '导出',
label: '导出',
variant: '
outline
',
variant: '
secondary
',
disabled: true,
disabled: true,
onClick: () => {},
onClick: () => {},
},
},
{
{
key: 'archive',
key: 'archive',
label: '一键归档',
label: '一键归档',
variant: '
outline
',
variant: '
secondary
',
disabled: true,
disabled: true,
onClick: () => {},
onClick: () => {},
},
},
...
@@ -1808,7 +1806,7 @@ export default function ProblemRectificationListPage() {
...
@@ -1808,7 +1806,7 @@ export default function ProblemRectificationListPage() {
key: 'view-delay-request',
key: 'view-delay-request',
label: '查看延期申请',
label: '查看延期申请',
icon: CalendarDaysIcon,
icon: CalendarDaysIcon,
variant: '
outline
',
variant: '
secondary
',
hidden: ({ record }) => !resolveDelayRequestDisplay(record),
hidden: ({ record }) => !resolveDelayRequestDisplay(record),
onClick: ({ record }) => setDelayDetailRecord(record),
onClick: ({ record }) => setDelayDetailRecord(record),
},
},
...
@@ -1816,7 +1814,7 @@ export default function ProblemRectificationListPage() {
...
@@ -1816,7 +1814,7 @@ export default function ProblemRectificationListPage() {
key: 'lock',
key: 'lock',
label: '锁定',
label: '锁定',
icon: LockClosedIcon,
icon: LockClosedIcon,
variant: '
outline
',
variant: '
secondary
',
hidden: ({ record }) => record.isLocked || record.progress !== '已整改',
hidden: ({ record }) => record.isLocked || record.progress !== '已整改',
disabled: () => actionLoading,
disabled: () => actionLoading,
onClick: ({ record }) => handleLockToggle(record, true),
onClick: ({ record }) => handleLockToggle(record, true),
...
@@ -1825,7 +1823,7 @@ export default function ProblemRectificationListPage() {
...
@@ -1825,7 +1823,7 @@ export default function ProblemRectificationListPage() {
key: 'unlock',
key: 'unlock',
label: '解锁',
label: '解锁',
icon: LockOpenIcon,
icon: LockOpenIcon,
variant: '
outline
',
variant: '
secondary
',
hidden: ({ record }) => !record.isLocked,
hidden: ({ record }) => !record.isLocked,
disabled: () => actionLoading,
disabled: () => actionLoading,
onClick: ({ record }) => handleLockToggle(record, false),
onClick: ({ record }) => handleLockToggle(record, false),
...
@@ -1833,14 +1831,14 @@ export default function ProblemRectificationListPage() {
...
@@ -1833,14 +1831,14 @@ export default function ProblemRectificationListPage() {
{
{
key: 'clone',
key: 'clone',
label: '复制',
label: '复制',
variant: '
outline
',
variant: '
secondary
',
disabled: ({ record }) => actionLoading || record.isLocked,
disabled: ({ record }) => actionLoading || record.isLocked,
onClick: ({ record }) => setCloneTargetRecord(record),
onClick: ({ record }) => setCloneTargetRecord(record),
},
},
{
{
key: 'delete',
key: 'delete',
label: '删除',
label: '删除',
variant: '
outline
',
variant: '
danger-soft
',
tone: 'danger',
tone: 'danger',
disabled: ({ record }) => actionLoading || record.isLocked,
disabled: ({ record }) => actionLoading || record.isLocked,
onClick: ({ record }) => handleDelete([record]),
onClick: ({ record }) => handleDelete([record]),
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment