Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
A
audit_project_manager
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
audit_project_manager
Commits
fd8348b3
Commit
fd8348b3
authored
Aug 26, 2026
by
Andy-bubu
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add cross-application record features
parent
f8a83c57
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
23 changed files
with
787 additions
and
86 deletions
+787
-86
.umirc.ts
.umirc.ts
+5
-0
ui-architecture.md
docs/ui-architecture.md
+27
-0
ui-migration-checklist.md
docs/ui-migration-checklist.md
+13
-0
xinyuan-design-system.md
docs/xinyuan-design-system.md
+14
-0
xinyuan-ui-audit.md
docs/xinyuan-ui-audit.md
+114
-3
AppIcons.jsx
src/components/AppIcons.jsx
+2
-0
AppShell.jsx
src/components/AppShell.jsx
+4
-0
DashboardNavbar.jsx
src/components/DashboardNavbar.jsx
+36
-3
FileUploadField.jsx
src/components/FileUploadField.jsx
+7
-3
SubjectRecordWorkbench.jsx
src/components/SubjectRecordWorkbench.jsx
+14
-10
application.js
src/config/application.js
+3
-0
SubjectViewContent.jsx
...ages/RecordDetailDrawer/components/SubjectViewContent.jsx
+5
-1
TailwindEditableField.jsx
...s/RecordDetailDrawer/components/TailwindEditableField.jsx
+21
-0
index.jsx
src/pages/RecordDetailDrawer/index.jsx
+65
-3
AuditProjectListPage.jsx
src/pages/project_manager_xy/AuditProjectListPage.jsx
+1
-3
Layout.jsx
src/pages/project_manager_xy/Layout.jsx
+28
-0
SubjectWorkbenchService.js
src/pages/project_manager_xy/SubjectWorkbenchService.js
+15
-15
auditProjectMaterialService.js
src/pages/project_manager_xy/auditProjectMaterialService.js
+119
-0
AuditProjectMaterialField.jsx
...oject_manager_xy/components/AuditProjectMaterialField.jsx
+0
-0
NotificationListPage.jsx
...project_manager_xy/notifications/NotificationListPage.jsx
+0
-0
notificationService.js
...s/project_manager_xy/notifications/notificationService.js
+154
-0
project-management.business-flow.md
...es/project_manager_xy/project-management.business-flow.md
+35
-5
subjectView.js
src/utils/subjectView.js
+105
-40
No files found.
.umirc.ts
View file @
fd8348b3
...
...
@@ -12,6 +12,7 @@ const ROUTE_PATHS = {
auditProjectArchive
:
'/audit_project_archive'
,
projectForm
:
'/project_form'
,
auditProjectForm
:
'/audit_project_form'
,
notifications
:
'/notifications'
,
}
as
const
;
const
APP_BASE_PATH
=
'/audit_project_manager_new/'
;
...
...
@@ -65,6 +66,10 @@ export default defineConfig({
path
:
ROUTE_PATHS
.
auditProjectForm
,
component
:
'./project_manager_xy/AuditProjectFormPage'
,
},
{
path
:
ROUTE_PATHS
.
notifications
,
component
:
'./project_manager_xy/notifications/NotificationListPage'
,
},
],
},
],
...
...
docs/ui-architecture.md
View file @
fd8348b3
...
...
@@ -135,3 +135,30 @@ CSS,并用显式 `@source` 仅扫描入口、组件、页面和 wrapper;`src
动态表单缺少字段定义时不从数据对象 key 猜测表头,改为 Pro EmptyState,避免把内部字段
标识或存储结构作为业务界面。
关联记录在缺少 Subject 字段定义时同样只保留标题列,不从
`metadata`
key 生成未知表头。
## 跨应用功能数据流
```
text
UserContext
-> user.applications[name = audit_project_manager]
-> Grid viewAuth / field auth(Detail View 只负责编排与 shortTitle)
Navbar notification badge
-> platform notice APIs -> /notifications -> base-aware internal navigation
Editable file field
-> material-center selection -> file snapshot in detail draft
-> local /upload -> pending material draft
-> detail Save -> create am_material (idempotent retry marker)
-> all created -> existing record update callback
```
Subject/View 资源必须先加载并完成 Grid View 授权过滤;只有存在可访问 View 才允许查询记录。
`RecordDetailDrawer`
接收激活 Grid View 作为唯一
`authorizationView`
,激活 Detail View 只决定字段
布局、条件和记录标题是否显示。通知与材料服务只封装既有接口参数,不拥有 Shell、记录更新或
业务状态流。
通知路由解析以
`ROUTE_TITLES`
的真实注册路径为本应用站内白名单,并只剥离
`/audit_project_manager_new`
自身 base;不能把其它应用的裸路径交给当前 Umi Router。
`FileUploadField`
直接组合 HeroUI Pro
`DropZone.Input`
,将运行时
`multiple`
同时应用于点击选择和
拖放;材料字段继续负责单值替换、多值追加、URL 去重及详情编辑会话的 pending 建档状态。
docs/ui-migration-checklist.md
View file @
fd8348b3
...
...
@@ -93,6 +93,19 @@
-
[
x
]
四份项目 UI 文档更新为最终事实
-
[
x
]
未提交、未推送、未合并
## 2026-08-26 五项功能
-
[
x
]
Grid
`shortTitle`
按激活 View 生效,title 仍参与搜索且业务入口保留
-
[
x
]
Detail
`shortTitle`
随激活 Detail View 即时更新
-
[
x
]
权限只读取
`audit_project_manager`
应用项,缺省拒绝且管理旁路正确
-
[
x
]
无可访问 Grid View 时不查询记录、不建 fallback、不显示新增/批量/行操作
-
[
x
]
只有一个可访问 Grid View 时隐藏 Tabs 轨道
-
[
x
]
`/notifications`
、Navbar Badge、消息/待办、全部/未读和动态操作完成
-
[
x
]
通知未读 0/1/99/100、站内外跳转、纯文本正文完成浏览器验收
-
[
x
]
所有 editable file 字段支持材料中心和本地上传
-
[
x
]
单值/多值、材料选择、URL 去重、取消、失败与重试事务完成浏览器验收
-
[
x
]
四视口浅深色完成;最终构建、审计脚本、定向扫描和干净控制台复验通过
说明:扩展的全项目
`npm ls ... react react-dom`
会报告两个 Pro peer 包仍声明 React 18
peer,以及 Umi 工具链锁定的 React 18 子图;应用与 HeroUI/React Aria 子图实际统一解析
React 19.2.8,构建和浏览器验收通过。详见
`docs/xinyuan-ui-audit.md`
。
docs/xinyuan-design-system.md
View file @
fd8348b3
...
...
@@ -92,3 +92,17 @@ Inter 与系统回退字体之间切换。
Portal、错误/空/加载、键盘 focus 和移动响应式。合入前运行
`npm run build`
、
`git diff --check`
、鑫元审计脚本、旧组件/私有视觉/本机路径扫描、依赖树和 Pro 子路径检查;
不得以构建成功替代浏览器验收。
## 配置化权限、通知与材料
-
`settings.shortTitle === true`
只隐藏当前 View 的
`title`
展示;title 数据、关键词搜索、排序、
保存与详情入口不得丢失。Detail 标题显示只跟随当前激活 Detail View。
-
View/字段权限只能读取正式应用
`audit_project_manager`
在
`user.applications[]`
中的授权项。
系统管理员及该应用“管理”角色可旁路;普通用户缺失配置时默认拒绝,
`disableColumns`
不旁路。
-
Navbar 通知使用 HeroUI
`Badge.Anchor + Badge + Button + Tooltip`
,位于主题按钮左侧;通知页面
使用原平台接口,正文按纯文本呈现,站内路径剥离
`/audit_project_manager_new`
后交给 Umi。
-
editable file 字段统一提供“材料中心 / 本地上传”。选择材料只写文件快照;本地文件先上传到
详情草稿,保存时逐个创建
`am_material`
,全部成功后才能更新业务记录,失败重试不得重复建档。
-
材料来源切换使用内容宽度 HeroUI
`Tabs`
的
`secondary`
variant;选择材料使用 Overlay 内
`secondary`
Autocomplete,本地上传使用 HeroUI Pro
`DropZone`
。运行时
`multiple`
必须传给
`DropZone.Input`
和拖放处理,不能只在业务层把返回值当数组。
docs/xinyuan-ui-audit.md
View file @
fd8348b3
...
...
@@ -7,9 +7,9 @@
-
分支:
`codex/ui-ux-xinyuan-migration`
-
唯一实现基准:
`problem_rectification`
-
应用基路径:
`/audit_project_manager_new/`
-
本轮启动命令:
`npm run dev -- --port 80
14`
(Umi 自动选择实际空闲端口
`8005`
)
-
目标验收地址:
`http://127.0.0.1:800
5
/audit_project_manager_new/`
-
基准验收地址:
`http://127.0.0.1:800
6/problem_rectification_new/problem_rectification
`
-
本轮启动命令:
`npm run dev -- --port 80
00`
-
目标验收地址:
`http://127.0.0.1:800
0
/audit_project_manager_new/`
-
基准验收地址:
`http://127.0.0.1:800
1/problem_rectification_new/
`
迁移前已启动未修改项目并记录业务基线。原登录页为蓝色营销式左右分栏,Shell、表格、
弹窗和表单混用 Catalyst、Headless UI、Heroicons、ECharts 与可见原生控件。浏览器检查的
...
...
@@ -258,6 +258,43 @@ HeroUI Context。`@heroui/styles@3.2.4` 的两个 pnpm peer 虚拟目录仅分
`oklch(1 0.0022 27.03)`
、
`oklch(0.9524 0.0036 27.03)`
、
`oklch(0.9373 0.0036 27.03)`
、L2、L1;深色对应
`0.2103`
、
`0.257`
、
`0.2721`
、L2、L1。
-
目标与“问题整改”同时打开对照,所有 Tabs 均由 HeroUI 官方 variant 决定外观。
## 2026-08-26 跨应用五项功能审计
-
功能白名单判定:本应用为“审计项目管理”,属于完整功能应用,实施 Grid/Detail
`shortTitle`
、视图权限、消息通知、文件与审计材料中心五项功能。
-
运行标识:Subject 为
`audit_project`
,正式应用授权名与材料
`source_app`
为
`audit_project_manager`
;Umi 路由 base 为
`/audit_project_manager_new/`
。站点配置同时存在
演示入口
`audit_project_manager_new`
,但它不作为权限或材料来源标识。
-
登录样本:Andy 的
`type`
为
`admin`
、
`applications`
为空,只能验证系统管理员旁路;普通
用户必须仅从
`user.applications[]`
中名称为
`audit_project_manager`
的项目读取 group、role
和 permission,禁止合并顶层或其他应用授权。
-
View 现状:七个 Grid View 均配置
`shortTitle: true`
;“计划”额外配置 group
`viewAuth`
与字段
`auth`
。页面此前通过常量无条件隐藏
`title`
,搜索也随之丢失 title;
View 权限缺省允许、无权时创建 fallback,并在鉴权前查询数据,均不符合基准。
-
Detail 现状:详情视图未统一配置
`shortTitle`
;抽屉始终显示记录标题,字段权限同时受 Detail
View 影响。目标行为应随当前激活 Detail View 即时切换标题显示,但字段权限只读取激活 Grid
View,
`disableColumns`
永远只读。
-
通知现状:原
`DataService`
已提供
`queryUserNotice`
、
`readUserNotice`
、
`unReadNoticeCount`
和
`exacteActionFunction`
,但没有独立页面、路由或 Navbar 入口。
-
文件现状:editable file 字段只执行
`/upload`
并立即写入详情草稿,没有材料中心选择,也没有
“保存详情时建档”的事务边界。材料 Subject 为
`am_material`
,快照协议必须保持
`{name,type,size,url}`
,不得保存材料 ID。
### 本轮浏览器验收记录
-
Andy 管理员、目标应用“管理”角色、普通用户、无权限用户及单 View 用户均通过浏览器响应夹具
验证;无权限状态没有
`/record/query`
,也没有新增、批量、行操作或兜底 View。
-
Grid
`shortTitle`
的 true/false 和 Detail View 动态切换均已验证;隐藏 title 后项目名称和固定
操作列仍可打开详情,关键词搜索仍包含记录 title。
-
通知验证 0/1/99/100(100 显示
`99+`
)、纯文本正文、
`status = "unread"`
、正则搜索转义、
单条/全部已读、动态 action、站内
`/project_form`
跳转和外部无 opener 新窗口。
-
材料验证单值保存顺序、两文件多值选择、已有材料 URL 去重、取消不建档及部分失败重试;失败后
第一个成功材料未重复创建,全部材料成功后才出现业务记录 PUT。所有夹具只拦截浏览器请求,
没有向真实后端写测试数据。
-
目标与“问题整改”同时打开,在 1440x900、1280x720、768x1024、390x844 的浅色和深色检查
通知页、列表、详情及材料控件;Navbar 动作均为 36px 且坐标一致,页面横向溢出为零。390px
详情面板为 358x812,四角、内部滚动、Tabs、Autocomplete 和 DropZone 在两种主题下可辨。
1440x900、1280x720、768x1024、390x844 浅色及相关深色状态均无页面横向溢出;390px
长分组只在官方 ScrollShadow 内滚动。点击“标准化业务制度修订情况”后仅该分组选中,表格
正确切换为“问题一 / 符合 / 王五”,控制台 0 errors / 0 warnings。
...
...
@@ -282,6 +319,80 @@ HeroUI Context。`@heroui/styles@3.2.4` 的两个 pnpm peer 虚拟目录仅分
-
Auth/Site 路由加载态删除
`text-slate-500`
,改用 HeroUI Spinner;详情全屏图标统一收口到
`AppIcons`
,页面直接导入图标包归零。
### 2026-08-26 最终硬门槛复核
以下命令均在
`/Users/liuchenlong/Desktop/job/audit_project_manager`
执行:
```
bash
npm run build
git diff
--check
bash /Users/liuchenlong/.codex/skills/xinyuan-design-system/scripts/audit_project.sh
\
/Users/liuchenlong/Desktop/job/audit_project_manager
cmp src/design-system/theme.css
\
/Users/liuchenlong/.codex/skills/xinyuan-design-system/assets/baseline/design-system/theme.css
npm
ls
@heroui-pro/react @heroui/react @heroui/styles
\
react-aria-components @react-aria/utils
--depth
=
1
```
结果:Umi/Webpack 在 6.86 秒内编译成功;
`git diff --check`
、鑫元审计脚本、权威主题
字节比较与定向依赖树均以退出码 0 完成。依赖树解析为
`@heroui-pro/react@1.0.0-beta.8`
、
`@heroui/react@3.2.4`
、
`@heroui/styles@3.2.4`
、
`react-aria-components@1.20.0`
和
`@react-aria/utils@3.34.1`
。
HeroUI Pro 使用 Node ESM 动态
`import()`
检查根包及以下子路径:
```
bash
node
--input-type
=
module
<<
'
NODE
'
import fs from 'node:fs';
import {createRequire} from 'node:module';
const require = createRequire(import.meta.url);
const packagePath = require.resolve('@heroui-pro/react/package.json');
const packageDirectory = packagePath.replace(/package\.json
$/
, '');
for (const relativePath of ['dist/index.js', 'dist/css/index.css']) {
if (!fs.existsSync(`
${
packageDirectory
}${
relativePath
}
`)) process.exitCode = 1;
}
for (const specifier of [
'@heroui-pro/react',
'@heroui-pro/react/sidebar',
'@heroui-pro/react/navbar',
'@heroui-pro/react/data-grid',
'@heroui-pro/react/drop-zone',
'@heroui-pro/react/action-bar',
'@heroui-pro/react/empty-state',
'@heroui-pro/react/list-view',
]) await import(specifier);
console.log(import.meta.resolve('@heroui-pro/react/css'));
NODE
```
根包与八个 JS 子路径全部成功;CSS export 解析到本仓库 pnpm virtual store 内的
`dist/css/index.css`
,
`dist/index.js`
和
`dist/css/index.css`
均存在。审计脚本自身仍会对六个
ESM-only 子路径打印 CommonJS
`require`
WARN,但其退出码为 0,且上述 ESM 检查逐项通过。
```
bash
rg
-n
'catalyst-ui-kit|@headlessui/react|@heroicons'
src
--glob
'*.{js,jsx,ts,tsx}'
rg
-n
'/compat/|\\\\compat\\\\'
src
--glob
'*.{js,jsx,ts,tsx}'
rg
-n
"<(select|datalist)
\\
b|<input[^>]+type=['
\"
](date|datetime-local|file)['
\"
]"
\
src
--glob
'*.{js,jsx,ts,tsx}'
rg
-n
"className=.*(bg-|border|rounded|shadow|ring|outline|hover:|focus:|active:)"
\
src/pages src/components
--glob
'*.{js,jsx,ts,tsx}'
rg
-n
"sky-|blue-|cyan-|rose-|pink-|zinc-|slate-|bg-gradient|#[0-9A-Fa-f]{6}"
\
src/pages
--glob
'*.{js,jsx,ts,tsx,css,less,scss}'
find src/pages
-type
f
\(
-name
'*.css'
-o
-name
'*.less'
-o
-name
'*.scss'
\)
-print
rg
-n
"file:(
\\
.
\\
.?/|/Users/)|/Users/.+/node_modules|project_manager_new/node_modules"
\
package.json pnpm-lock.yaml pnpm-workspace.yaml
rg
-n
'hp_[A-Za-z0-9]+'
.
--glob
'!node_modules/**'
--glob
'!dist/**'
--glob
'!.git/**'
```
结果:以上旧组件、compat、可见原生选择/日期/文件控件、页面私有视觉 utility、页面旧色调、
页面私有样式文件、
`file:`
/本机依赖路径及授权字面值扫描均为 0 命中。
最终浏览器控制台使用
`$playwright-cli`
在既有目标/基准对照 Context 中新建页面,先绑定
`console`
与
`pageerror`
监听器,再依次访问
`/audit_project`
和
`/notifications`
。两个页面均
返回 200,标题正确,页面级横向溢出均为 0,捕获结果为
`0 errors / 0 warnings`
;此前为验证
材料失败重试而故意模拟的 500 日志未计入本次干净结果。
### 2026-08-24 DataGrid 长文本边界复核
-
全局三个 HeroUI Pro DataGrid 消费入口均复用
`DataTable`
:主业务列表、动态表单数据表和
...
...
src/components/AppIcons.jsx
View file @
fd8348b3
import
BankOutlined
from
'@ant-design/icons/BankOutlined'
;
import
BarChartOutlined
from
'@ant-design/icons/BarChartOutlined'
;
import
BellOutlined
from
'@ant-design/icons/BellOutlined'
;
import
AppstoreOutlined
from
'@ant-design/icons/AppstoreOutlined'
;
import
CalendarOutlined
from
'@ant-design/icons/CalendarOutlined'
;
import
CheckOutlined
from
'@ant-design/icons/CheckOutlined'
;
...
...
@@ -65,6 +66,7 @@ export const Bank = withSize(BankOutlined);
export
const
Archive
=
withSize
(
InboxOutlined
);
export
const
AppGrid
=
withSize
(
AppstoreOutlined
);
export
const
BarChart
=
withSize
(
BarChartOutlined
);
export
const
Bell
=
withSize
(
BellOutlined
);
export
const
Calendar
=
withSize
(
CalendarOutlined
);
export
const
Check
=
withSize
(
CheckOutlined
);
export
const
ChevronDown
=
withSize
(
DownOutlined
);
...
...
src/components/AppShell.jsx
View file @
fd8348b3
...
...
@@ -20,6 +20,8 @@ function getInitialTheme() {
export
default
function
AppShell
({
children
,
notificationCount
,
notificationPath
,
open
,
onOpenChange
,
title
,
...
...
@@ -52,6 +54,8 @@ export default function AppShell({
<
DashboardSidebar
{
...
sidebarProps
}
/>
<
Sidebar
.
Main
className=
"app-main-glass rc-app-main"
>
<
DashboardNavbar
notificationCount=
{
notificationCount
}
onOpenNotifications=
{
notificationPath
?
()
=>
history
.
push
(
notificationPath
)
:
undefined
}
theme=
{
theme
}
title=
{
title
}
onThemeChange=
{
setTheme
}
...
...
src/components/DashboardNavbar.jsx
View file @
fd8348b3
import
{
Navbar
}
from
'@heroui-pro/react/navbar'
;
import
{
Sidebar
,
useSidebar
}
from
'@heroui-pro/react/sidebar'
;
import
{
Button
,
Tooltip
}
from
'@heroui/react'
;
import
{
Moon
,
SidebarPanel
,
Sun
}
from
'./AppIcons'
;
import
{
B
adge
,
B
utton
,
Tooltip
}
from
'@heroui/react'
;
import
{
Bell
,
Moon
,
SidebarPanel
,
Sun
}
from
'./AppIcons'
;
export
default
function
DashboardNavbar
({
onThemeChange
,
theme
,
title
})
{
export
default
function
DashboardNavbar
({
notificationCount
=
0
,
onOpenNotifications
,
onThemeChange
,
theme
,
title
,
})
{
const
{
isMobile
,
isOpen
}
=
useSidebar
();
const
isDark
=
theme
===
'dark'
;
const
themeLabel
=
isDark
?
'切换至浅色模式'
:
'切换至深色模式'
;
const
normalizedNotificationCount
=
Math
.
max
(
0
,
Number
(
notificationCount
)
||
0
);
return
(
<
Navbar
className=
"app-navbar rc-navbar"
maxWidth=
"full"
>
...
...
@@ -20,6 +27,32 @@ export default function DashboardNavbar({ onThemeChange, theme, title }) {
<
h1
className=
"app-navbar-page-title rc-navbar-title"
>
{
title
}
</
h1
>
<
Navbar
.
Spacer
/>
<
div
className=
"rc-navbar-actions"
>
{
onOpenNotifications
?
(
<
Tooltip
delay=
{
0
}
>
<
Badge
.
Anchor
>
<
Button
isIconOnly
aria
-
label=
"通知消息"
className=
"app-navbar-icon-button rc-navbar-icon-button"
size=
"sm"
variant=
"ghost"
onPress=
{
onOpenNotifications
}
>
<
Bell
size=
{
16
}
/>
</
Button
>
{
normalizedNotificationCount
>
0
?
(
<
Badge
aria
-
label=
{
`${normalizedNotificationCount}条未读通知`
}
color=
"danger"
size=
"sm"
>
{
normalizedNotificationCount
>
99
?
'99+'
:
normalizedNotificationCount
}
</
Badge
>
)
:
null
}
</
Badge
.
Anchor
>
<
Tooltip
.
Content
>
通知消息
</
Tooltip
.
Content
>
</
Tooltip
>
)
:
null
}
<
Tooltip
delay=
{
0
}
>
<
Button
isIconOnly
...
...
src/components/FileUploadField.jsx
View file @
fd8348b3
...
...
@@ -9,6 +9,7 @@ export default function FileUploadField({
description
=
'将文件拖到此处,或从本地选择。'
,
isDisabled
=
false
,
fileName
=
''
,
multiple
=
false
,
onFilesChange
,
triggerLabel
=
'选择文件'
,
})
{
...
...
@@ -29,9 +30,9 @@ export default function FileUploadField({
if
(
fileItems
.
length
===
0
)
return
;
const
files
=
await
Promise
.
all
(
fileItems
.
map
((
item
)
=>
item
.
getFile
()));
emitFiles
(
files
.
slice
(
0
,
1
));
emitFiles
(
multiple
?
files
:
files
.
slice
(
0
,
1
));
},
[
emitFiles
],
[
emitFiles
,
multiple
],
);
return
(
...
...
@@ -44,6 +45,7 @@ export default function FileUploadField({
<
DropZone
.
Input
accept=
{
accept
}
disabled=
{
isDisabled
}
multiple=
{
multiple
}
onSelect=
{
emitFiles
}
/>
<
DropZone
.
Icon
>
...
...
@@ -51,7 +53,9 @@ export default function FileUploadField({
</
DropZone
.
Icon
>
<
DropZone
.
Label
>
{
fileName
||
'将文件拖到此处'
}
</
DropZone
.
Label
>
<
DropZone
.
Description
>
{
fileName
?
'已选择文件,可重新选择进行替换。'
:
description
}
{
fileName
?
`已选择${multiple ? '文件,可继续添加。' : '文件,可重新选择进行替换。'}`
:
description
}
</
DropZone
.
Description
>
<
DropZone
.
Trigger
isDisabled=
{
isDisabled
}
>
<
Upload
aria
-
hidden=
"true"
/>
...
...
src/components/SubjectRecordWorkbench.jsx
View file @
fd8348b3
...
...
@@ -104,6 +104,7 @@ export default function SubjectRecordWorkbench({
()
=>
gridViews
.
find
((
view
)
=>
view
.
name
===
activeViewName
)
||
gridViews
[
0
]
||
null
,
[
gridViews
,
activeViewName
],
);
const
hasAccessibleView
=
Boolean
(
activeView
);
const
configuredView
=
useMemo
(
()
=>
buildConfiguredWorkbenchView
(
records
,
filters
,
activeView
,
subject
),
[
records
,
filters
,
activeView
,
subject
],
...
...
@@ -219,7 +220,7 @@ export default function SubjectRecordWorkbench({
{
label
:
'当前已选'
,
value
:
selectedIds
.
length
,
hint
:
'可执行批量操作'
,
tone
:
'amber'
},
];
const
builtToolbarActions
=
[
const
builtToolbarActions
=
hasAccessibleView
?
[
...
toolbarActions
.
map
(
wrapAction
),
...(
allowCreate
&&
!
readonly
?
[{
...
...
@@ -234,9 +235,9 @@ export default function SubjectRecordWorkbench({
},
}]
:
[]),
];
]
:
[]
;
const
builtBulkActions
=
[
const
builtBulkActions
=
hasAccessibleView
?
[
...(
allowDelete
&&
!
readonly
?
[{
key
:
'bulk-delete'
,
...
...
@@ -251,9 +252,9 @@ export default function SubjectRecordWorkbench({
),
}]
:
[]),
];
]
:
[]
;
const
builtRowActions
=
[
const
builtRowActions
=
hasAccessibleView
?
[
{
key
:
'view-detail'
,
label
:
'查看详情'
,
...
...
@@ -292,7 +293,7 @@ export default function SubjectRecordWorkbench({
onClick
:
({
record
})
=>
setDeleteTargets
([
record
.
raw
||
record
]),
}]
:
[]),
];
]
:
[]
;
return
(
<>
...
...
@@ -322,7 +323,7 @@ export default function SubjectRecordWorkbench({
toolbarActions=
{
builtToolbarActions
}
bulkActions=
{
builtBulkActions
}
rowActions=
{
builtRowActions
}
detailPanel=
{
{
detailPanel=
{
hasAccessibleView
?
{
getRecordName
:
(
record
)
=>
record
.
name
||
record
.
id
,
onDirtyClose
:
reload
,
render
:
({
open
,
recordName
,
onClose
,
onDataChange
})
=>
(
...
...
@@ -334,17 +335,20 @@ export default function SubjectRecordWorkbench({
onClose=
{
onClose
}
onDataChange=
{
onDataChange
}
preferredViewName=
{
activeView
?.
title
||
activeView
?.
name
}
authorizationView=
{
activeView
}
/>
),
}
}
}
:
null
}
states=
{
{
loading
,
error
,
success
,
loadingTitle
:
`正在加载${title}`
,
loadingDescription
:
'正在读取对象结构、视图配置和记录数据,请稍候。'
,
emptyTitle
:
'当前视图暂无数据'
,
emptyDescription
:
emptyDescription
||
'可以切换其他视图或调整筛选条件。'
,
emptyTitle
:
hasAccessibleView
?
'当前视图暂无数据'
:
'暂无可访问的视图'
,
emptyDescription
:
hasAccessibleView
?
emptyDescription
||
'可以切换其他视图或调整筛选条件。'
:
'当前账号未获得该对象 Grid 视图的查看权限。'
,
}
}
pagination=
{
{
page
:
safePage
,
...
...
src/config/application.js
View file @
fd8348b3
export
const
APPLICATION_DISPLAY_NAME
=
'审计项目管理'
;
export
const
APPLICATION_BASE_PATH
=
'/audit_project_manager_new/'
;
export
const
AUTHORIZATION_APPLICATION_NAME
=
'audit_project_manager'
;
export
const
SOURCE_APPLICATION_NAME
=
'audit_project_manager'
;
export
const
THEME_STORAGE_KEY
=
'xinyuan_color_theme'
;
export
const
ROUTE_TITLES
=
Object
.
freeze
({
...
...
@@ -8,4 +10,5 @@ export const ROUTE_TITLES = Object.freeze({
'/audit_project_archive'
:
'往期项目归档'
,
'/project_form'
:
'项目表单'
,
'/audit_project_form'
:
'审计项目填写表'
,
'/notifications'
:
'通知消息'
,
});
src/pages/RecordDetailDrawer/components/SubjectViewContent.jsx
View file @
fd8348b3
...
...
@@ -54,13 +54,15 @@ export default function SubjectViewContent({
editSessionId
,
mode
=
'view'
,
onFieldChange
,
onPendingLocalMaterialChange
,
onFieldValidation
,
readonly
,
isFieldDisabled
,
minimal
=
false
,
useAuditMaterialCenter
=
false
,
})
{
const
displayFields
=
buildViewColumns
(
view
,
subject
);
const
contentFields
=
m
ode
===
'view'
||
m
inimal
const
contentFields
=
minimal
?
(
displayFields
||
[]).
filter
((
field
)
=>
field
.
field
!==
'title'
)
:
displayFields
;
const
fieldVariant
=
'secondary'
;
...
...
@@ -117,8 +119,10 @@ export default function SubjectViewContent({
)
}
onValidationChange=
{
onFieldValidation
}
onPendingLocalMaterialChange=
{
onPendingLocalMaterialChange
}
readonly=
{
readonly
}
disabled=
{
fieldDisabled
}
useAuditMaterialCenter=
{
useAuditMaterialCenter
}
/>
</
div
>
</>
...
...
src/pages/RecordDetailDrawer/components/TailwindEditableField.jsx
View file @
fd8348b3
...
...
@@ -17,6 +17,7 @@ import { queryUsers, uploadFile } from '@/services/DataService';
import
ReferenceSearchSelect
from
'@/components/ReferenceSearchSelect'
;
import
XinyuanAutocomplete
from
'@/components/XinyuanAutocomplete'
;
import
DynamicFormValueEditor
from
'@/components/DynamicFormValueEditor'
;
import
AuditProjectMaterialField
from
'@/pages/project_manager_xy/components/AuditProjectMaterialField'
;
import
ReferenceRecordsTable
from
'./ReferenceRecordsTable'
;
import
{
dateInputValue
,
...
...
@@ -315,7 +316,9 @@ function ControlledFieldEditor({
fieldVariant
,
placeholder
,
onChange
,
onPendingLocalMaterialChange
,
onValidationChange
,
useAuditMaterialCenter
,
})
{
const
[
error
,
setError
]
=
useState
(
''
);
const
[
uploading
,
setUploading
]
=
useState
(
false
);
...
...
@@ -402,6 +405,20 @@ function ControlledFieldEditor({
}
if
(
fieldType
===
'file'
)
{
if
(
useAuditMaterialCenter
)
{
return
(
<
AuditProjectMaterialField
editSessionId=
{
editSessionId
}
fieldPath=
{
fieldPath
}
multiple=
{
Boolean
(
fieldDef
?.
multiple
)
}
onChange=
{
onChange
}
onPendingLocalMaterialChange=
{
onPendingLocalMaterialChange
}
onValidationChange=
{
onValidationChange
}
value=
{
value
}
/>
);
}
return
(
<
Card
variant=
"secondary"
>
<
Card
.
Content
className=
"space-y-3"
>
...
...
@@ -548,7 +565,9 @@ export default function TailwindEditableField({
placeholder
,
onSave
,
onChange
,
onPendingLocalMaterialChange
,
onValidationChange
,
useAuditMaterialCenter
=
false
,
mode
=
'inline'
,
readonly
=
false
,
disabled
=
false
,
...
...
@@ -591,7 +610,9 @@ export default function TailwindEditableField({
fieldVariant=
{
fieldVariant
}
placeholder=
{
textPlaceholder
}
onChange=
{
onChange
}
onPendingLocalMaterialChange=
{
onPendingLocalMaterialChange
}
onValidationChange=
{
onValidationChange
}
useAuditMaterialCenter=
{
useAuditMaterialCenter
}
/>
);
}
...
...
src/pages/RecordDetailDrawer/index.jsx
View file @
fd8348b3
...
...
@@ -2,6 +2,7 @@ import { Close, Fullscreen, FullscreenExit } from '@/components/AppIcons';
import
{
Alert
,
Button
,
Modal
,
Spinner
,
Tabs
,
Tooltip
,
toast
}
from
'@heroui/react'
;
import
React
,
{
useCallback
,
useContext
,
forwardRef
,
useEffect
,
useImperativeHandle
,
...
...
@@ -11,13 +12,15 @@ import React, {
}
from
'react'
;
import
{
useSubjectRecord
}
from
'./hooks/useSubjectRecord'
;
import
SubjectViewContent
from
'./components/SubjectViewContent'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
{
createAuditProjectMaterial
}
from
'@/pages/project_manager_xy/auditProjectMaterialService'
;
import
{
buildViewColumns
,
checkViewCondition
,
getFieldValue
,
isFieldDisabledByView
,
isViewAccessible
,
setFieldValue
,
shouldHideViewTitleColumn
,
}
from
'@/utils/subjectView'
;
const
EMPTY_ARRAY
=
[];
...
...
@@ -99,8 +102,11 @@ const RecordDetailDrawer = (props, ref) => {
footerActions
=
EMPTY_ARRAY
,
autoSwitchToLastTab
=
false
,
isViewDisabled
:
isViewDisabledProp
,
useAuditMaterialCenter
=
false
,
}
=
props
;
const
currentUser
=
useContext
(
UserContext
);
const
[
refreshKey
,
setRefreshKey
]
=
useState
(()
=>
Date
.
now
());
const
[
isFullscreen
,
setIsFullscreen
]
=
useState
(
false
);
const
[
activeTabKey
,
setActiveTabKey
]
=
useState
();
...
...
@@ -114,6 +120,7 @@ const RecordDetailDrawer = (props, ref) => {
const
[
actionLoadingKey
,
setActionLoadingKey
]
=
useState
(
''
);
const
[
lastTabSwitchRevision
,
setLastTabSwitchRevision
]
=
useState
(
''
);
const
editSessionRef
=
useRef
(
0
);
const
pendingLocalMaterialsRef
=
useRef
({});
const
{
editableRecord
,
...
...
@@ -147,6 +154,7 @@ const RecordDetailDrawer = (props, ref) => {
setValidationErrors
({});
setSaveError
(
''
);
setLastTabSwitchRevision
(
''
);
pendingLocalMaterialsRef
.
current
=
{};
editSessionRef
.
current
+=
1
;
},
[
open
,
recordName
]);
...
...
@@ -160,7 +168,6 @@ const RecordDetailDrawer = (props, ref) => {
const
filtered
=
(
views
||
[])
.
filter
((
view
)
=>
view
.
type
===
'detail'
)
.
filter
((
view
)
=>
(
filterView
.
length
===
0
?
true
:
filterView
.
includes
(
view
.
name
)))
.
filter
((
view
)
=>
isViewAccessible
(
view
))
.
filter
((
view
)
=>
checkViewCondition
(
view
.
settings
?.
visabled
,
remoteRecord
));
if
(
filtered
.
length
>
0
)
{
...
...
@@ -248,6 +255,7 @@ const RecordDetailDrawer = (props, ref) => {
const
activeView
=
enabledDetailViews
.
find
((
view
)
=>
view
.
name
===
activeTabKey
)
||
enabledDetailViews
[
enabledDetailViews
.
length
-
1
]
||
detailViews
[
0
];
const
hideRecordTitle
=
shouldHideViewTitleColumn
(
activeView
);
const
isRecordLocked
=
remoteRecord
?.
metadata
?.
is_locked
===
true
;
const
effectiveReadonly
=
readonly
||
isRecordLocked
;
const
disabledFieldSet
=
useMemo
(
...
...
@@ -330,6 +338,7 @@ const RecordDetailDrawer = (props, ref) => {
setDirtyFields
({});
setValidationErrors
({});
setSaveError
(
''
);
pendingLocalMaterialsRef
.
current
=
{};
};
const
handleRequestClose
=
()
=>
{
...
...
@@ -345,6 +354,7 @@ const RecordDetailDrawer = (props, ref) => {
setDirtyFields
({});
setValidationErrors
({});
setSaveError
(
''
);
pendingLocalMaterialsRef
.
current
=
{};
setIsEditing
(
true
);
};
...
...
@@ -387,6 +397,35 @@ const RecordDetailDrawer = (props, ref) => {
});
},
[]);
const
handlePendingLocalMaterialChange
=
useCallback
((
fieldPath
,
change
,
editSessionId
)
=>
{
if
(
editSessionId
!==
editSessionRef
.
current
)
return
;
const
nextPendingMaterials
=
{
...
pendingLocalMaterialsRef
.
current
};
const
currentFieldMaterials
=
Array
.
isArray
(
nextPendingMaterials
[
fieldPath
])
?
nextPendingMaterials
[
fieldPath
]
:
[];
let
nextFieldMaterials
=
currentFieldMaterials
;
if
(
change
?.
type
===
'replace'
)
{
nextFieldMaterials
=
change
.
materials
||
[];
}
else
if
(
change
?.
type
===
'add'
)
{
const
materialsById
=
new
Map
(
currentFieldMaterials
.
map
((
material
)
=>
[
material
.
clientId
,
material
]),
);
(
change
.
materials
||
[]).
forEach
((
material
)
=>
{
materialsById
.
set
(
material
.
clientId
,
material
);
});
nextFieldMaterials
=
[...
materialsById
.
values
()];
}
else
if
(
change
?.
type
===
'remove'
)
{
nextFieldMaterials
=
currentFieldMaterials
.
filter
(
(
material
)
=>
material
.
clientId
!==
change
.
clientId
,
);
}
if
(
nextFieldMaterials
.
length
>
0
)
nextPendingMaterials
[
fieldPath
]
=
nextFieldMaterials
;
else
delete
nextPendingMaterials
[
fieldPath
];
pendingLocalMaterialsRef
.
current
=
nextPendingMaterials
;
},
[]);
const
handleSave
=
async
()
=>
{
if
(
!
hasDirtyFields
||
hasValidationErrors
||
isSaving
)
return
;
...
...
@@ -394,10 +433,31 @@ const RecordDetailDrawer = (props, ref) => {
setSaveError
(
''
);
try
{
const
pendingMaterials
=
Object
.
entries
(
pendingLocalMaterialsRef
.
current
)
.
filter
(([
fieldPath
])
=>
dirtyFields
[
fieldPath
]);
for
(
const
[
fieldPath
,
fieldMaterials
]
of
pendingMaterials
)
{
for
(
const
pendingMaterial
of
fieldMaterials
)
{
if
(
pendingMaterial
.
materialRecordId
)
continue
;
const
createdMaterial
=
await
createAuditProjectMaterial
({
fileAsset
:
pendingMaterial
.
fileAsset
,
currentUser
,
});
const
currentFieldMaterials
=
pendingLocalMaterialsRef
.
current
[
fieldPath
]
||
[];
pendingLocalMaterialsRef
.
current
=
{
...
pendingLocalMaterialsRef
.
current
,
[
fieldPath
]:
currentFieldMaterials
.
map
((
material
)
=>
material
.
clientId
===
pendingMaterial
.
clientId
?
{
...
material
,
materialRecordId
:
createdMaterial
.
id
}
:
material
),
};
}
}
await
handleFieldsUpdate
(
Object
.
values
(
dirtyFields
));
setIsEditing
(
false
);
setDirtyFields
({});
setValidationErrors
({});
pendingLocalMaterialsRef
.
current
=
{};
onDataChange
?.(
true
);
}
catch
(
saveFailure
)
{
setSaveError
(
saveFailure
?.
message
||
'保存失败,请稍后重试。'
);
...
...
@@ -412,7 +472,7 @@ const RecordDetailDrawer = (props, ref) => {
displayedRecord
?.
name
||
recordName
||
'记录'
;
const
showRecordTitle
=
loading
||
!
isPlaceholderTitle
(
recordTitle
);
const
showRecordTitle
=
!
hideRecordTitle
&&
(
loading
||
!
isPlaceholderTitle
(
recordTitle
)
);
return
(
<
Modal
.
Root
>
...
...
@@ -547,9 +607,11 @@ const RecordDetailDrawer = (props, ref) => {
mode=
{
isEditing
?
'edit'
:
'view'
}
onFieldChange=
{
handleDraftFieldChange
}
onFieldValidation=
{
handleFieldValidation
}
onPendingLocalMaterialChange=
{
handlePendingLocalMaterialChange
}
readonly=
{
effectiveReadonly
}
isFieldDisabled=
{
isDrawerFieldDisabled
}
minimal=
{
minimalContent
}
useAuditMaterialCenter=
{
useAuditMaterialCenter
}
/>
</
div
>
)
}
...
...
src/pages/project_manager_xy/AuditProjectListPage.jsx
View file @
fd8348b3
...
...
@@ -12,8 +12,6 @@ import {
transitionAuditProject
,
}
from
'./AuditProjectService'
;
const
HIDDEN_LIST_COLUMNS
=
[
'title'
];
function
hasStatus
(
record
,
statuses
)
{
return
statuses
.
includes
(
record
?.
metadata
?.
project_status
);
}
...
...
@@ -92,7 +90,6 @@ export default function AuditProjectListPage() {
description=
"按线上视图配置展示审计项目,支持详情维护、状态流转、导出和批量归档。"
allowEdit=
{
false
}
allowClone=
{
false
}
hiddenColumnFields=
{
HIDDEN_LIST_COLUMNS
}
buildSummaryItems=
{
()
=>
[]
}
toolbarActions=
{
[
{
...
...
@@ -118,6 +115,7 @@ export default function AuditProjectListPage() {
headerActions
:
HEADER_ACTIONS
,
footerActions
:
FOOTER_ACTIONS
,
autoSwitchToLastTab
:
true
,
useAuditMaterialCenter
:
true
,
}
}
emptyDescription=
"当前视图暂无审计项目,可以切换视图、清空筛选或新建记录。"
/>
...
...
src/pages/project_manager_xy/Layout.jsx
View file @
fd8348b3
...
...
@@ -10,6 +10,10 @@ import {
Project
,
}
from
'@/components/AppIcons'
;
import
ApplicationSwitcherDialog
from
'./ApplicationSwitcherDialog'
;
import
{
loadUnreadNotificationCount
,
NOTIFICATION_STATE_EVENT
,
}
from
'./notifications/notificationService'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
{
SiteContext
}
from
'@/wrapper/Site'
;
import
{
useContext
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
...
...
@@ -50,6 +54,7 @@ export default function ProjectManagerLayout() {
const
siteInfo
=
useContext
(
SiteContext
);
const
[
sidebarOpen
,
setSidebarOpen
]
=
useState
(
true
);
const
[
isApplicationDialogOpen
,
setIsApplicationDialogOpen
]
=
useState
(
false
);
const
[
unreadNotificationCount
,
setUnreadNotificationCount
]
=
useState
(
0
);
const
currentTitle
=
useMemo
(
()
=>
ROUTE_TITLES
[
location
.
pathname
]
||
APPLICATION_DISPLAY_NAME
,
...
...
@@ -60,10 +65,33 @@ export default function ProjectManagerLayout() {
document
.
title
=
`
${
currentTitle
}
-
${
APPLICATION_DISPLAY_NAME
}
`
;
},
[
currentTitle
]);
useEffect
(()
=>
{
let
cancelled
=
false
;
const
refreshUnreadCount
=
async
()
=>
{
try
{
const
count
=
await
loadUnreadNotificationCount
();
if
(
!
cancelled
)
setUnreadNotificationCount
(
count
);
}
catch
(
error
)
{
if
(
!
cancelled
)
setUnreadNotificationCount
(
0
);
}
};
refreshUnreadCount
();
window
.
addEventListener
(
NOTIFICATION_STATE_EVENT
,
refreshUnreadCount
);
const
timer
=
window
.
setInterval
(
refreshUnreadCount
,
60000
);
return
()
=>
{
cancelled
=
true
;
window
.
removeEventListener
(
NOTIFICATION_STATE_EVENT
,
refreshUnreadCount
);
window
.
clearInterval
(
timer
);
};
},
[
location
.
pathname
]);
return
(
<>
<
AppShell
menuGroups=
{
MENU_GROUPS
}
notificationCount=
{
unreadNotificationCount
}
notificationPath=
"/notifications"
onLogout=
{
userInfo
?.
logout
}
onOpenApplicationSwitcher=
{
()
=>
setIsApplicationDialogOpen
(
true
)
}
open=
{
sidebarOpen
}
...
...
src/pages/project_manager_xy/SubjectWorkbenchService.js
View file @
fd8348b3
...
...
@@ -5,6 +5,7 @@ import {
}
from
'@/services/DataService'
;
import
{
buildViewColumns
,
buildViewSearchColumns
,
checkViewCondition
,
formatFieldValue
,
getFieldValue
,
...
...
@@ -56,11 +57,14 @@ export async function queryAllSubjectRecords(subjectName, query = {}) {
}
export
async
function
loadSubjectWorkbenchResources
(
subjectName
,
query
=
{})
{
const
[
records
,
subject
,
views
]
=
await
Promise
.
all
([
queryAllSubjectRecords
(
subjectName
,
query
),
const
[
subject
,
views
]
=
await
Promise
.
all
([
loadSubject
(
subjectName
),
getSubjectView
(
subjectName
),
]);
const
accessibleGridViews
=
buildGridViews
(
views
||
[]);
const
records
=
accessibleGridViews
.
length
>
0
?
await
queryAllSubjectRecords
(
subjectName
,
query
)
:
[];
return
{
records
:
records
.
map
(
normalizeWorkbenchRecord
),
...
...
@@ -70,29 +74,25 @@ export async function loadSubjectWorkbenchResources(subjectName, query = {}) {
}
export
function
buildGridViews
(
views
=
[])
{
const
gridViews
=
views
.
filter
((
view
)
=>
view
.
type
===
'grid'
)
.
filter
((
view
)
=>
isViewAccessible
(
view
));
const
configuredGridViews
=
views
.
filter
((
view
)
=>
view
.
type
===
'grid'
);
const
gridViews
=
configuredGridViews
.
filter
((
view
)
=>
isViewAccessible
(
view
));
if
(
gridViews
.
length
>
0
)
{
if
(
gridViews
.
length
>
0
||
configuredGridViews
.
length
>
0
)
{
return
gridViews
;
}
return
[
{
title
:
'全部记录'
,
type
:
'grid'
,
name
:
'all'
,
settings
:
{},
},
];
const
fallbackView
=
{
title
:
'全部记录'
,
type
:
'grid'
,
name
:
'all'
,
settings
:
{}
};
return
isViewAccessible
(
fallbackView
)
?
[
fallbackView
]
:
[];
}
export
function
buildConfiguredWorkbenchView
(
records
,
filters
,
view
,
subject
)
{
if
(
!
view
)
return
{
columns
:
[],
filteredRecords
:
[]
};
const
keyword
=
normalizeText
(
filters
?.
keyword
).
toLowerCase
();
const
status
=
normalizeText
(
filters
?.
status
);
const
year
=
normalizeText
(
filters
?.
year
);
const
columns
=
buildViewColumns
(
view
,
subject
);
const
searchColumns
=
buildViewSearchColumns
(
view
,
subject
);
const
filteredRecords
=
records
.
filter
((
record
)
=>
{
const
rawRecord
=
record
.
raw
||
record
;
const
metadata
=
rawRecord
?.
metadata
||
{};
...
...
@@ -125,7 +125,7 @@ export function buildConfiguredWorkbenchView(records, filters, view, subject) {
return
true
;
}
const
searchText
=
c
olumns
const
searchText
=
searchC
olumns
.
map
((
column
)
=>
formatFieldValue
(
getFieldValue
(
rawRecord
,
column
.
field
),
column
.
fieldDef
))
.
join
(
' '
)
.
toLowerCase
();
...
...
src/pages/project_manager_xy/auditProjectMaterialService.js
0 → 100644
View file @
fd8348b3
import
dayjs
from
'dayjs'
;
import
{
createRecord
,
queryRecords
,
uploadFile
}
from
'@/services/DataService'
;
import
{
SOURCE_APPLICATION_NAME
}
from
'@/config/application'
;
export
const
AUDIT_MATERIAL_SUBJECT
=
'am_material'
;
const
MATERIAL_PAGE_SIZE
=
20
;
function
extractRecords
(
response
)
{
return
[
response
,
response
?.
data
,
response
?.
data
?.
list
,
response
?.
data
?.
records
,
response
?.
list
,
response
?.
records
,
response
?.
items
,
response
?.
result
,
response
?.
result
?.
records
,
].
find
(
Array
.
isArray
)
||
[];
}
function
escapeRegExp
(
value
=
''
)
{
return
String
(
value
).
replace
(
/
[
.*+?^${}()|[
\]\\]
/g
,
'
\\
$&'
);
}
function
inferMaterialTitle
(
fileName
=
''
)
{
return
String
(
fileName
||
''
).
replace
(
/
\.[^
.
]
+$/
,
''
).
trim
()
||
'未命名材料'
;
}
function
normalizeFileAsset
(
uploadResult
,
file
)
{
const
candidates
=
[
uploadResult
?.
file
,
uploadResult
?.
data
?.
file
,
uploadResult
?.
data
,
uploadResult
?.
result
?.
file
,
uploadResult
?.
result
,
uploadResult
,
];
for
(
const
candidate
of
candidates
)
{
if
(
!
candidate
||
typeof
candidate
!==
'object'
)
continue
;
const
url
=
candidate
.
url
||
candidate
.
path
||
candidate
.
location
||
candidate
?.
data
?.
url
||
''
;
if
(
!
url
)
continue
;
return
{
name
:
candidate
.
name
||
candidate
.
filename
||
file
?.
name
||
'未命名文件'
,
type
:
candidate
.
type
||
candidate
.
mimetype
||
file
?.
type
||
'application/octet-stream'
,
size
:
Number
(
candidate
.
size
||
file
?.
size
||
0
),
url
,
};
}
throw
new
Error
(
'文件上传成功,但未获取到可用的文件地址。'
);
}
function
normalizeMaterial
(
record
=
{})
{
const
metadata
=
record
?.
metadata
||
{};
const
fileAsset
=
metadata
.
file_asset
||
{};
return
{
id
:
record
.
_id
||
record
.
id
||
record
.
name
||
''
,
materialTitle
:
metadata
.
material_title
||
record
.
title
||
fileAsset
.
name
||
'未命名材料'
,
fileAsset
,
archiveCategory
:
metadata
.
archive_category
||
''
,
archiveYear
:
metadata
.
archive_year
||
''
,
};
}
export
async
function
queryAuditMaterials
(
keyword
=
''
)
{
const
escapedKeyword
=
escapeRegExp
(
String
(
keyword
||
''
).
trim
());
const
filter
=
escapedKeyword
?
{
$or
:
[
{
'metadata.material_title'
:
{
$regex
:
escapedKeyword
,
$options
:
'i'
}
},
{
'metadata.file_asset.name'
:
{
$regex
:
escapedKeyword
,
$options
:
'i'
}
},
],
}
:
{};
const
response
=
await
queryRecords
({
subject
:
AUDIT_MATERIAL_SUBJECT
,
page
:
0
,
page_size
:
MATERIAL_PAGE_SIZE
,
filter
,
sort
:
'-updated_at -created_at'
,
mode
:
'full'
,
});
return
extractRecords
(
response
)
.
map
(
normalizeMaterial
)
.
filter
((
material
)
=>
material
.
id
&&
material
.
fileAsset
?.
url
);
}
export
async
function
uploadAuditProjectFile
(
file
)
{
if
(
!
file
)
throw
new
Error
(
'请选择要上传的本地文件。'
);
return
normalizeFileAsset
(
await
uploadFile
(
file
),
file
);
}
export
async
function
createAuditProjectMaterial
({
fileAsset
,
currentUser
})
{
if
(
!
fileAsset
?.
url
)
throw
new
Error
(
'缺少有效的文件地址,无法写入审计材料中心。'
);
const
uploaderId
=
currentUser
?.
_id
||
currentUser
?.
id
||
''
;
if
(
!
uploaderId
)
throw
new
Error
(
'未获取到当前登录用户,无法写入审计材料中心。'
);
const
materialTitle
=
inferMaterialTitle
(
fileAsset
.
name
);
const
response
=
await
createRecord
({
subject
:
AUDIT_MATERIAL_SUBJECT
,
title
:
materialTitle
,
content_type
:
'text'
,
metadata
:
{
material_title
:
materialTitle
,
file_asset
:
{
name
:
fileAsset
.
name
,
type
:
fileAsset
.
type
||
'application/octet-stream'
,
size
:
Number
(
fileAsset
.
size
||
0
),
url
:
fileAsset
.
url
,
},
source_app
:
SOURCE_APPLICATION_NAME
,
uploader
:
uploaderId
,
uploaded_at
:
dayjs
().
toISOString
(),
},
});
const
createdRecord
=
response
?.
data
||
response
?.
result
||
response
;
return
{
id
:
createdRecord
?.
_id
||
createdRecord
?.
id
||
createdRecord
?.
name
||
true
,
record
:
createdRecord
,
};
}
src/pages/project_manager_xy/components/AuditProjectMaterialField.jsx
0 → 100644
View file @
fd8348b3
This diff is collapsed.
Click to expand it.
src/pages/project_manager_xy/notifications/NotificationListPage.jsx
0 → 100644
View file @
fd8348b3
This diff is collapsed.
Click to expand it.
src/pages/project_manager_xy/notifications/notificationService.js
0 → 100644
View file @
fd8348b3
import
{
exacteActionFunction
,
queryUserNotice
,
readUserNotice
,
unReadNoticeCount
,
}
from
'@/services/DataService'
;
import
{
APPLICATION_BASE_PATH
,
ROUTE_TITLES
}
from
'@/config/application'
;
export
const
NOTIFICATION_PAGE_SIZE
=
10
;
export
const
NOTIFICATION_STATE_EVENT
=
'audit-project-manager:notification-state'
;
const
INTERNAL_ROUTE_PATHS
=
Object
.
keys
(
ROUTE_TITLES
);
const
INTERNAL_ROUTE_PATTERN
=
INTERNAL_ROUTE_PATHS
.
map
((
path
)
=>
path
.
replace
(
/
[
.*+?^${}()|[
\]\\]
/g
,
'
\\
$&'
))
.
sort
((
left
,
right
)
=>
right
.
length
-
left
.
length
)
.
join
(
'|'
);
function
toArray
(
value
)
{
return
Array
.
isArray
(
value
)
?
value
:
[];
}
function
toId
(
value
)
{
if
(
value
&&
typeof
value
===
'object'
)
return
String
(
value
.
_id
||
value
.
id
||
value
.
name
||
''
);
return
String
(
value
||
''
);
}
function
escapeRegExp
(
value
)
{
return
String
(
value
||
''
).
replace
(
/
[
.*+?^${}()|[
\]\\]
/g
,
'
\\
$&'
);
}
export
async
function
loadNotificationPage
({
page
=
0
,
pageSize
=
NOTIFICATION_PAGE_SIZE
,
category
=
'message'
,
readStatus
=
'all'
,
keyword
=
''
,
}
=
{})
{
const
filter
=
{
category
};
const
normalizedKeyword
=
String
(
keyword
||
''
).
trim
();
if
(
normalizedKeyword
)
{
filter
.
content
=
{
$regex
:
escapeRegExp
(
normalizedKeyword
),
$options
:
'i'
};
}
const
response
=
await
queryUserNotice
(
page
,
pageSize
,
filter
,
readStatus
===
'unread'
?
'unread'
:
null
,
);
return
{
list
:
toArray
(
response
?.
list
),
page
:
Number
(
response
?.
current_page
??
page
)
||
0
,
pageSize
:
Number
(
response
?.
page_size
??
pageSize
)
||
pageSize
,
total
:
Number
(
response
?.
total_notices
??
response
?.
total
??
0
)
||
0
,
totalPages
:
Number
(
response
?.
total_pages
??
0
)
||
0
,
};
}
export
async
function
loadUnreadNotificationCount
()
{
const
response
=
await
unReadNoticeCount
();
return
Math
.
max
(
0
,
Number
(
response
?.
total_notices
??
response
?.
count
??
response
?.
total
??
response
??
0
,
)
||
0
);
}
export
async
function
markNotificationRead
(
name
)
{
if
(
!
name
)
throw
new
Error
(
'通知缺少已读标识'
);
await
readUserNotice
(
name
);
notifyNotificationStateChanged
();
}
export
async
function
markAllNotificationsRead
()
{
await
readUserNotice
();
notifyNotificationStateChanged
();
}
export
async
function
executeNotificationAction
(
notification
,
action
)
{
const
actionName
=
typeof
action
?.
action
===
'string'
?
action
.
action
:
action
?.
action
?.
name
||
action
?.
name
;
const
methodName
=
action
?.
method_name
||
action
?.
methodName
;
if
(
!
actionName
||
!
methodName
)
throw
new
Error
(
'该待办操作缺少执行配置'
);
const
result
=
await
exacteActionFunction
(
actionName
,
methodName
,
{
notice_id
:
notification
?.
_id
,
});
notifyNotificationStateChanged
();
return
result
;
}
export
function
isNotificationRead
(
notification
,
userId
)
{
const
normalizedUserId
=
toId
(
userId
);
return
normalizedUserId
?
toArray
(
notification
?.
data
?.
logs
).
some
((
log
)
=>
toId
(
log
?.
user_id
)
===
normalizedUserId
)
:
false
;
}
export
function
getNotificationSender
(
notification
)
{
return
notification
?.
from
?.
display_name
||
notification
?.
from
?.
username
||
notification
?.
created_by
?.
display_name
||
notification
?.
created_by
?.
username
||
'系统'
;
}
export
function
getNotificationContent
(
notification
)
{
return
String
(
notification
?.
content
||
notification
?.
content_compile
||
''
).
trim
();
}
function
trimTarget
(
target
)
{
return
String
(
target
||
''
).
replace
(
/
[
),.;!?,。;!?)
]
+$/
u
,
''
);
}
export
function
extractNotificationTarget
(
notification
)
{
const
configuredTarget
=
notification
?.
settings
?.
url
||
notification
?.
settings
?.
link
||
notification
?.
data
?.
url
||
notification
?.
data
?.
link
;
if
(
configuredTarget
)
return
trimTarget
(
configuredTarget
);
const
prefix
=
APPLICATION_BASE_PATH
.
replace
(
/
\/
$/
,
''
);
const
escapedPrefix
=
prefix
.
replace
(
/
[
.*+?^${}()|[
\]\\]
/g
,
'
\\
$&'
);
const
content
=
getNotificationContent
(
notification
);
const
match
=
content
.
match
(
new
RegExp
(
`https?:\\/\\/[^\\s<>"']+|
${
escapedPrefix
}
\\/[^\\s<>"']+|(?:
${
INTERNAL_ROUTE_PATTERN
}
)(?:[/?#][^\\s<>"']*)?`
,
));
return
trimTarget
(
match
?.[
0
]);
}
export
function
resolveNotificationTarget
(
target
)
{
const
normalizedTarget
=
trimTarget
(
target
);
if
(
!
normalizedTarget
)
return
null
;
const
appPrefix
=
APPLICATION_BASE_PATH
.
replace
(
/
\/
$/
,
''
);
if
(
/^https
?
:
\/\/
/i
.
test
(
normalizedTarget
))
{
try
{
const
url
=
new
URL
(
normalizedTarget
);
if
(
url
.
pathname
.
startsWith
(
`
${
appPrefix
}
/`
))
{
return
resolveNotificationTarget
(
`
${
url
.
pathname
}${
url
.
search
}${
url
.
hash
}
`
);
}
return
{
href
:
url
.
toString
(),
external
:
true
};
}
catch
(
error
)
{
return
null
;
}
}
if
(
normalizedTarget
.
startsWith
(
`
${
appPrefix
}
/`
))
{
return
{
href
:
normalizedTarget
.
slice
(
appPrefix
.
length
),
external
:
false
};
}
return
normalizedTarget
.
startsWith
(
'/'
)
?
{
href
:
normalizedTarget
,
external
:
!
INTERNAL_ROUTE_PATHS
.
some
((
path
)
=>
normalizedTarget
===
path
||
normalizedTarget
.
startsWith
(
`
${
path
}
?`
))
}
:
null
;
}
export
function
notifyNotificationStateChanged
()
{
if
(
typeof
window
!==
'undefined'
)
{
window
.
dispatchEvent
(
new
CustomEvent
(
NOTIFICATION_STATE_EVENT
));
}
}
src/pages/project_manager_xy/project-management.business-flow.md
View file @
fd8348b3
...
...
@@ -12,6 +12,7 @@
| 往期项目归档 |
`/audit_project_archive`
| 只读查看审计项目历史快照 |
| 项目表单 |
`/project_form`
| 维护动态表单及字段 |
| 审计项目填写表 |
`/audit_project_form`
| 兼容外部填写和复核链接 |
| 通知消息 |
`/notifications`
| 查看消息和待办,并处理已读、动态动作及关联流程跳转 |
配置中的
`audit_indicator`
、
`risk_indicator`
、
`audit_process`
、
`audit_report`
没有菜单入口,属于复制配置残留,不注册为本应用路由。
...
...
@@ -26,15 +27,17 @@
## 2. 项目列表与详情
1.
页面通过
`loadSubject`
、
`getSubjectView`
和
`POST /record/query`
读取字段、视图和记录。
2.
表格列、视图限制、排序、选择字段选项和字段分组来自服务端配置;字段分组读取当前 Grid View 的
`settings.groups`
,按配置展示分组表头并支持展开或收起。
审计项目管理列表不展示通用
`title`
列,项目名称及其他业务字段仍按 View 配置展示
。
2.
表格列、视图限制、排序、选择字段选项和字段分组来自服务端配置;字段分组读取当前 Grid View 的
`settings.groups`
,按配置展示分组表头并支持展开或收起。
当前 Grid View 的
`settings.shortTitle === true`
时只隐藏通用
`title`
展示列;
`false`
或未配置时正常展示。
`title`
数据仍参与关键词搜索、排序、详情和保存,详情入口保留在项目名称和固定操作列
。
View 中兼容历史字段
`metadata.project_finish_time`
时按数据定义显示为“项目完成时间”;
内部
`metadata.*`
路径不作为可见表头,字段取值路径和服务端配置保持不变。
3.
新建和编辑时业务字段统一写入
`metadata`
;引用和用户字段只提交 ID,日期提交 ISO 8601。
4.
编辑模式下主键字段禁用;必填字段按 Subject 配置校验。
5.
新建与编辑弹窗的空控件按可见字段标签显示输入、选择或搜索提示;提示仅用于界面引导,不参与校验,也不写入提交数据。
6.
删除调用
`POST /record/batch/remove`
,默认软删除;列表行内不提供编辑、复制或归档按钮,字段维护统一从详情侧栏进入。
7.
详情弹窗按可访问的 detail 视图显示字段,并支持字段内联维护;标题、阶段 Tabs 和顶部业务
操作固定在 Header,字段内容只在 Body 内纵向滚动,滚动内容不得穿过或遮挡阶段 Tabs。
7.
详情弹窗按符合条件的 detail 视图显示字段,并支持字段内联维护;当前 Detail View 的
`settings.shortTitle === true`
时隐藏记录标题和
`title`
字段,但始终保留“记录详情”功能标题和
Modal 可访问名称。标题、阶段 Tabs 和顶部业务操作固定在 Header,字段内容只在 Body 内纵向
滚动,滚动内容不得穿过或遮挡阶段 Tabs。
8.
列表页不展示前端写死的项目总数、完成率等摘要卡片,仅展示由服务端 Subject/View 配置驱动的视图、筛选和表格内容;项目统计统一在驾驶舱查看。
9.
业务列表、动态表单数据表和关联记录表中的长文本最多展示两行,内容始终限制在所属列内;超出内容通过 HeroUI Tooltip 或项目详情查看,避免遮挡相邻列或由单条长文本过度拉高整行。弹窗内只读关联记录表按实际数据行高度收缩,不提供无业务价值的列宽拖拽。
...
...
@@ -108,7 +111,8 @@ Word 导出分别读取 `audit_plain_template`、`audit_verification_template`
-
`settings`
必须是合法 JSON;选择字段选项存放在
`settings.options`
。
-
审计项目中的
`form`
字段在项目详情侧栏内默认完整展开:左侧按
`settings.group`
展示默认分组和自定义分组,右侧展示当前分组下的全部表单及数据表格。
-
表单字段支持在侧栏内添加/删除分组、添加/删除表单、切换表单模板和调整所属分组;删除自定义分组时,其中已配置的表单移入默认分组。
-
表单数据行使用通用表格展示,通过 Catalyst 弹窗新增或编辑;侧栏中的修改先作为草稿保留,点击“保存表单”后统一写回
`{ form, data, settings }`
,不向业务用户暴露 JSON。
-
表单数据行使用 HeroUI Pro DataGrid 展示,通过 HeroUI Modal 新增或编辑;详情中的修改先作为
草稿保留,点击详情“保存”后统一写回
`{ form, data, settings }`
,不向业务用户暴露 JSON。
-
关联的表单模板不存在或加载失败时,在当前表单内容内显示“表单模板不可用”,不展示内部
模板标识,也不继续展示缺少定义的表格;编辑态可以重新选择有效模板,现有数据不会自动改写。
-
模板存在但缺少字段定义时显示“暂无可展示字段”,不使用已有数据对象的内部 key 临时生成
...
...
@@ -122,6 +126,32 @@ Word 导出分别读取 `audit_plain_template`、`audit_verification_template`
-
登录页首次打开时调用
`GET /auth/captcha`
获取验证码图片和 token;登录请求在既有账号、
加密密码与
`type=password`
基础上提交
`captcha`
和
`token`
。用户可点击验证码刷新;登录
失败时清空验证码输入并重新获取验证码,成功后的 token 保存和 redirect 流程保持不变。
-
Subject 视图继续执行服务端配置的访问限制、条件过滤和字段禁用规则。
-
Grid View 的
`settings.viewAuth`
控制视图可见性,当前激活 Grid View 的
`settings.auth`
控制
详情字段编辑权限,
`settings.disableColumns`
对所有用户始终只读。授权只读取
`user.applications[]`
中
`name = "audit_project_manager"`
的应用项;系统管理员、系统账号及该
应用“管理”角色可旁路 View/字段 auth,但不能旁路
`disableColumns`
。普通用户缺少配置时默认
拒绝;没有可访问 Grid View 时不查询记录、不创建兜底视图,也不展示新增、批量和行操作;只有
一个可访问 View 时隐藏 Tabs 轨道。
-
历史快照列表只读,不允许新建、复制或删除。
-
批量归档未返回任何历史记录 ID 时按失败处理,并保留原审计项目数据。
## 10. 消息通知
-
Navbar 的通知按钮位于主题按钮左侧,未读数来自
`GET /notice/customer/count`
;0 不显示,
1–99 显示真实数量,100 及以上显示
`99+`
,并通过通知状态事件和 60 秒轮询刷新。
-
`/notifications`
使用原平台通知接口,支持消息/待办、全部/未读、300ms 搜索、固定每页 10 条、
单条已读、全部已读和服务端配置的动态动作;未读筛选向服务端提交
`status = "unread"`
。
-
通知正文按纯文本显示,不执行 HTML。以
`/audit_project_manager_new`
开头的站内链接剥离自身
base 后交给 Umi;本应用已注册的裸业务路由按站内处理,其它应用和外部 HTTP(S) 链接在无
`window.opener`
的新窗口打开。
## 11. 文件与审计材料中心
-
审计项目详情中所有运行时可编辑的
`file`
字段提供“材料中心 / 本地上传”,单值或多值取自
Subject 字段的
`multiple`
配置。材料中心查询
`am_material`
最近 20 条,并按材料标题和文件名
远程搜索;选择后只把
`{name,type,size,url}`
快照写入业务草稿。
-
本地文件先调用
`/upload`
并进入当前详情草稿。取消编辑不创建材料主档;单值替换,多值追加并
按 URL 去重,移除或清空业务附件不删除材料中心主档或底层文件。
-
用户点击详情“保存”后,系统才用
`source_app = "audit_project_manager"`
、当前用户、字段快照和
ISO 时间逐个创建
`am_material`
。全部建档成功后才调用既有业务记录更新回调;部分失败会在当前
编辑会话保留已建档标记,重试只创建尚未成功的材料,避免重复主档。
src/utils/subjectView.js
View file @
fd8348b3
import
dayjs
from
'dayjs'
;
import
{
AUTHORIZATION_APPLICATION_NAME
}
from
'@/config/application'
;
export
function
normalizeText
(
value
)
{
return
String
(
value
??
''
).
trim
();
...
...
@@ -62,60 +63,88 @@ export function getCurrentUserFromStorage() {
}
}
function
collectUserGroups
(
user
)
{
return
toArray
(
user
?.
groups
)
.
flatMap
((
group
)
=>
[
group
?.
_id
,
group
?.
name
,
group
?.
display_name
,
group
])
function
collectIdentityValues
(
values
)
{
return
toArray
(
values
)
.
flatMap
((
value
)
=>
value
&&
typeof
value
===
'object'
?
[
value
.
_id
,
value
.
name
,
value
.
display_name
,
value
.
displayName
,
value
.
username
]
:
value
)
.
map
(
normalizeText
)
.
filter
(
Boolean
);
}
function
collectUserRoles
(
user
)
{
const
directRoles
=
toArray
(
user
?.
roles
);
const
appRoles
=
toArray
(
user
?.
applications
).
flatMap
((
app
)
=>
app
?.
roles
||
[]);
return
[...
directRoles
,
...
appRoles
].
map
(
normalizeText
).
filter
(
Boolean
)
;
function
getUserApplication
(
user
,
applicationName
=
AUTHORIZATION_APPLICATION_NAME
)
{
return
toArray
(
user
?.
applications
).
find
(
(
application
)
=>
normalizeText
(
application
?.
name
)
===
normalizeText
(
applicationName
),
)
||
null
;
}
function
collectUserPermissions
(
user
)
{
const
directPermissions
=
toArray
(
user
?.
permissions
);
const
appPermissions
=
toArray
(
user
?.
applications
).
flatMap
((
app
)
=>
app
?.
permissions
||
[]);
return
[...
directPermissions
,
...
appPermissions
].
map
(
normalizeText
).
filter
(
Boolean
);
function
collectApplicationAuthorization
(
user
,
applicationName
)
{
const
application
=
getUserApplication
(
user
,
applicationName
);
return
{
groups
:
collectIdentityValues
(
application
?.
groups
),
roles
:
collectIdentityValues
(
application
?.
roles
),
permissions
:
collectIdentityValues
(
application
?.
permissions
),
};
}
function
intersects
(
left
,
right
)
{
const
rightSet
=
new
Set
(
toArray
(
right
).
map
(
normalizeText
).
filter
(
Boolean
));
return
toArray
(
left
).
some
((
item
)
=>
rightSet
.
has
(
normalizeText
(
item
)
));
const
rightSet
=
new
Set
(
collectIdentityValues
(
right
));
return
collectIdentityValues
(
left
).
some
((
item
)
=>
rightSet
.
has
(
item
));
}
export
function
hasAuthAccess
(
authRule
,
user
=
getCurrentUserFromStorage
())
{
if
(
!
authRule
)
{
function
getAuthorizationRule
(
authRule
)
{
return
[
'group'
,
'role'
,
'permission'
,
'username'
].
includes
(
authRule
?.
type
)
?
authRule
:
'管理'
;
}
export
function
isSystemAdministrator
(
user
=
getCurrentUserFromStorage
())
{
return
user
?.
type
===
'admin'
||
user
?.
type
===
'system'
;
}
export
function
hasApplicationManagementAccess
(
user
=
getCurrentUserFromStorage
(),
applicationName
=
AUTHORIZATION_APPLICATION_NAME
,
)
{
if
(
isSystemAdministrator
(
user
))
{
return
true
;
}
if
(
!
user
)
{
return
false
;
}
const
{
roles
}
=
collectApplicationAuthorization
(
user
,
applicationName
);
return
intersects
(
roles
,
[
'管理'
])
;
}
if
(
user
.
type
===
'admin'
)
{
return
true
;
export
function
hasAuthAccess
(
authRule
,
user
=
getCurrentUserFromStorage
(),
applicationName
=
AUTHORIZATION_APPLICATION_NAME
,
)
{
if
(
!
user
)
return
false
;
if
(
hasApplicationManagementAccess
(
user
,
applicationName
))
return
true
;
const
{
groups
,
roles
,
permissions
}
=
collectApplicationAuthorization
(
user
,
applicationName
);
if
(
typeof
authRule
===
'string'
)
{
return
intersects
(
roles
,
[
authRule
]);
}
if
(
authRule
.
type
===
'group'
)
{
return
intersects
(
collectUserGroups
(
user
)
,
authRule
.
groups
);
if
(
authRule
?
.
type
===
'group'
)
{
return
intersects
(
groups
,
authRule
.
groups
);
}
if
(
authRule
.
type
===
'role'
)
{
return
intersects
(
collectUserRoles
(
user
)
,
authRule
.
roles
);
if
(
authRule
?
.
type
===
'role'
)
{
return
intersects
(
roles
,
authRule
.
roles
);
}
if
(
authRule
.
type
===
'permission'
)
{
return
intersects
(
collectUserPermissions
(
user
)
,
authRule
.
permissions
);
if
(
authRule
?
.
type
===
'permission'
)
{
return
intersects
(
permissions
,
authRule
.
permissions
);
}
if
(
authRule
.
type
===
'username'
)
{
return
intersects
([
user
.
username
,
user
.
display_name
,
user
.
_id
],
authRule
.
usernames
);
if
(
authRule
?
.
type
===
'username'
)
{
return
intersects
([
user
.
username
],
authRule
.
usernames
);
}
return
tru
e
;
return
fals
e
;
}
export
function
getFieldValue
(
record
,
fieldPath
)
{
...
...
@@ -249,12 +278,18 @@ export function normalizeRecordLabel(value) {
return
normalizeText
(
value
);
}
export
function
shouldHideViewTitleColumn
(
view
)
{
return
view
?.
settings
?.
shortTitle
===
true
;
}
export
function
buildViewColumns
(
view
,
subject
,
{
includeHidden
=
false
}
=
{})
{
const
configuredColumns
=
view
?.
settings
?.
columns
||
[];
const
hideTitleColumn
=
!
includeHidden
&&
shouldHideViewTitleColumn
(
view
);
if
(
configuredColumns
.
length
>
0
)
{
return
configuredColumns
.
filter
((
column
)
=>
includeHidden
||
!
column
.
hide
)
.
filter
((
column
)
=>
(
includeHidden
||
!
column
.
hide
)
&&
(
!
hideTitleColumn
||
column
.
field
!==
'title'
))
.
map
((
column
)
=>
{
const
fieldName
=
getFieldNameFromPath
(
column
.
field
);
const
configuredFieldDef
=
getFieldDefinition
(
column
.
field
,
subject
);
...
...
@@ -274,12 +309,14 @@ export function buildViewColumns(view, subject, { includeHidden = false } = {})
}
const
defaultColumns
=
[];
defaultColumns
.
push
({
field
:
'title'
,
width
:
180
,
label
:
'标题'
,
fieldDef
:
{
name
:
'title'
,
label
:
'标题'
,
type
:
'text'
},
});
if
(
!
hideTitleColumn
)
{
defaultColumns
.
push
({
field
:
'title'
,
width
:
180
,
label
:
'标题'
,
fieldDef
:
{
name
:
'title'
,
label
:
'标题'
,
type
:
'text'
},
});
}
toArray
(
subject
?.
fields
).
forEach
((
field
)
=>
{
const
fieldPath
=
`metadata.
${
field
.
name
}
`
;
...
...
@@ -298,6 +335,21 @@ export function buildViewColumns(view, subject, { includeHidden = false } = {})
return
defaultColumns
;
}
export
function
buildViewSearchColumns
(
view
,
subject
)
{
const
displayColumns
=
buildViewColumns
(
view
,
subject
);
if
(
displayColumns
.
some
((
column
)
=>
column
.
field
===
'title'
))
return
displayColumns
;
return
[
{
field
:
'title'
,
width
:
180
,
label
:
'标题'
,
fieldDef
:
getFieldDefinition
(
'title'
,
subject
),
},
...
displayColumns
,
];
}
function
compareRuleValue
(
recordValue
,
comparator
,
expectedValue
)
{
const
expectedValues
=
toArray
(
expectedValue
).
map
(
normalizeText
);
const
recordValues
=
toArray
(
recordValue
).
map
((
item
)
=>
{
...
...
@@ -356,18 +408,31 @@ export function checkViewCondition(condition, record) {
:
results
.
every
(
Boolean
);
}
export
function
isViewAccessible
(
view
,
user
=
getCurrentUserFromStorage
())
{
return
hasAuthAccess
(
view
?.
settings
?.
viewAuth
,
user
);
export
function
isViewAccessible
(
view
,
user
=
getCurrentUserFromStorage
(),
applicationName
=
AUTHORIZATION_APPLICATION_NAME
,
)
{
return
hasAuthAccess
(
getAuthorizationRule
(
view
?.
settings
?.
viewAuth
),
user
,
applicationName
);
}
export
function
isFieldDisabledByView
(
fieldPath
,
view
,
user
=
getCurrentUserFromStorage
())
{
export
function
isFieldDisabledByView
(
fieldPath
,
view
,
user
=
getCurrentUserFromStorage
(),
applicationName
=
AUTHORIZATION_APPLICATION_NAME
,
)
{
const
disabledColumn
=
toArray
(
view
?.
settings
?.
disableColumns
).
some
((
column
)
=>
column
.
field
===
fieldPath
);
if
(
disabledColumn
)
{
return
true
;
}
if
(
hasApplicationManagementAccess
(
user
,
applicationName
))
{
return
false
;
}
const
fieldAuthRule
=
toArray
(
view
?.
settings
?.
auth
).
find
((
rule
)
=>
rule
.
field
===
fieldPath
);
return
fieldAuthRule
?
!
hasAuthAccess
(
fieldAuthRule
,
user
)
:
false
;
return
!
hasAuthAccess
(
getAuthorizationRule
(
fieldAuthRule
),
user
,
applicationName
)
;
}
export
function
recordMatchesRestrictions
(
record
,
restrictions
=
[])
{
...
...
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