Commit 1f155f90 authored by David Yang's avatar David Yang

feat: 迁移廉洁风险防控到通用工作台

parent c1d89af9
/node_modules
.DS_Store
.pnpm-debug.log
/.env.local
/.umirc.local.ts
/config/config.local.ts
......
......@@ -7,17 +7,19 @@ function resolvePackageRoot(packageName: string) {
const ROUTE_PATHS = {
login: '/login',
processRiskTable: '/risk_prevention_control_measures_table',
displayRiskTable: '/risk_prevention_control_measures_table_show',
measureDatabase: '/measure_description',
riskArchive: '/integrity_risk_control_archive',
processTable: '/risk_prevention_control_measures_table',
showTable: '/risk_prevention_control_measures_table_show',
measureDescription: '/measure_description',
archive: '/integrity_risk_control_archive',
controlMeasureForm: '/control_measure_form',
} as const;
const APP_BASE_PATH = '/integrity_risk_prevention_new/';
export default defineConfig({
layout: false,
fastRefresh: true,
base: '/integrity_risk_prevention/',
publicPath: '/integrity_risk_prevention/',
base: APP_BASE_PATH,
publicPath: APP_BASE_PATH,
alias: {
react: resolvePackageRoot('react'),
'react-dom': resolvePackageRoot('react-dom'),
......@@ -29,35 +31,35 @@ export default defineConfig({
component: './Login',
},
{
path: '/control_measure_form',
path: ROUTE_PATHS.controlMeasureForm,
layout: false,
wrappers: ['@/wrapper/Site', '@/wrapper/Auth'],
component: './integrity_risk/ControlMeasureWorkflow',
component: './integrity_risk_prevention/ControlMeasureFormPage',
},
{
path: '/',
wrappers: ['@/wrapper/Site', '@/wrapper/Auth'],
component: './project_manager_xy/Layout',
component: './integrity_risk_prevention/Layout',
routes: [
{
path: '/',
redirect: ROUTE_PATHS.processRiskTable,
redirect: ROUTE_PATHS.processTable,
},
{
path: ROUTE_PATHS.processRiskTable,
component: './integrity_risk/ProcessRiskTable',
path: ROUTE_PATHS.processTable,
component: './integrity_risk_prevention/RiskPreventionTablePage',
},
{
path: ROUTE_PATHS.displayRiskTable,
component: './integrity_risk/DisplayRiskTable',
path: ROUTE_PATHS.showTable,
component: './integrity_risk_prevention/RiskPreventionShowTablePage',
},
{
path: ROUTE_PATHS.measureDatabase,
component: './integrity_risk/MeasureDatabase',
path: ROUTE_PATHS.measureDescription,
component: './integrity_risk_prevention/MeasureDescriptionPage',
},
{
path: ROUTE_PATHS.riskArchive,
component: './integrity_risk/RiskArchive',
path: ROUTE_PATHS.archive,
component: './integrity_risk_prevention/IntegrityRiskArchivePage',
},
],
},
......@@ -89,6 +91,6 @@ export default defineConfig({
define: {
STREAMS_API_URL: '/api',
STREAMS_PARSER_URL: '/parser',
APP_BASE_PATH: '/integrity_risk_prevention',
INTEGRITY_RISK_PREVENTION_APP_BASE_PATH: APP_BASE_PATH,
},
});
......@@ -104,9 +104,8 @@ AI 在编写 API 调用或数据转换逻辑时,必须遵守:
## 业务文档索引
- 审计材料中心模块 - 请阅读 `/src/pages/project_manager_xy/audit-material-center.business-flow.md`
- 模板来源说明 - 如需确认旧项目上下文,可参考 `/src/pages/project_manager_xy/project-management.business-flow.md`
- 廉洁风险防控模块 - 请阅读 `/src/pages/integrity_risk/integrity-risk-prevention.business-flow.md`
- 项目管理模块 - 请阅读 `/src/pages/project_manager_xy/project-management.business-flow.md`
- 廉洁风险防控模块 - 请阅读 `/src/pages/integrity_risk_prevention/integrity-risk-prevention.business-flow.md`
## 获取当前登录用户信息
......
{
"name": "integrity_risk_prevention_new",
"name": "integrity_risk_prevention",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "integrity_risk_prevention",
"hasInstallScript": true,
"dependencies": {
"@headlessui/react": "^2.2.10",
......@@ -13,9 +14,10 @@
"clsx": "^2.1.1",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.20",
"echarts": "^6.1.0",
"echarts": "^6.0.0",
"echarts-for-react": "^3.0.6",
"jszip": "^3.10.1",
"motion": "^12.23.24",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwindcss": "^4.2.0",
......@@ -4184,12 +4186,12 @@
}
},
"node_modules/@tanstack/react-virtual": {
"version": "3.13.24",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.24.tgz",
"integrity": "sha512-aIJvz5OSkhNIhZIpYivrxrPTKYsjW9Uzy+sP/mx0S3sev2HyvPb7xmjbYvokzEpfgYHy/HjzJ2zFAETuUfgCpg==",
"version": "3.13.25",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.25.tgz",
"integrity": "sha512-bmNoqMu6gcAW9JGrKVB0Q1tN1i5RONZF8r1fW0bbE4Oyf3DwEGnzzQJ2OW+Ozg1P4s8PyugkHg2ULZoFQN+cqw==",
"license": "MIT",
"dependencies": {
"@tanstack/virtual-core": "3.14.0"
"@tanstack/virtual-core": "3.15.0"
},
"funding": {
"type": "github",
......@@ -4201,9 +4203,9 @@
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.14.0.tgz",
"integrity": "sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q==",
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.15.0.tgz",
"integrity": "sha512-0AwPGx0I8QxPYjAxShT/+z+ZOe9u8mW5rsXvivCTjRfRmz9a43+3mRyi4wwlyoUqOC56q/jatKa0Bh9M99BEHQ==",
"license": "MIT",
"funding": {
"type": "github",
......@@ -10278,13 +10280,13 @@
"license": "MIT"
},
"node_modules/echarts": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-6.1.0.tgz",
"integrity": "sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz",
"integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "2.3.0",
"zrender": "6.1.0"
"zrender": "6.0.0"
}
},
"node_modules/echarts-for-react": {
......@@ -11658,6 +11660,33 @@
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/framer-motion": {
"version": "12.43.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.43.0.tgz",
"integrity": "sha512-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.43.0",
"motion-utils": "^12.39.0",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
......@@ -14586,6 +14615,47 @@
"node": "*"
}
},
"node_modules/motion": {
"version": "12.43.0",
"resolved": "https://registry.npmjs.org/motion/-/motion-12.43.0.tgz",
"integrity": "sha512-BQgQbSa9Hn3/mtbib0MK53y6JSANa+YKUKlaYnWzAVDH424RYQ5LVpV3pNiWH00BA2z4ojsSdMzqT7g2FQwjuQ==",
"license": "MIT",
"dependencies": {
"framer-motion": "^12.43.0",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/motion-dom": {
"version": "12.43.0",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.43.0.tgz",
"integrity": "sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.39.0"
}
},
"node_modules/motion-utils": {
"version": "12.39.0",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz",
"integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
......@@ -21369,9 +21439,9 @@
}
},
"node_modules/zrender": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-6.1.0.tgz",
"integrity": "sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz",
"integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==",
"license": "BSD-3-Clause",
"dependencies": {
"tslib": "2.3.0"
......
{
"name": "integrity_risk_prevention",
"private": true,
"scripts": {
"build": "max build",
......@@ -15,9 +16,10 @@
"clsx": "^2.1.1",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.20",
"echarts": "^6.1.0",
"echarts": "^6.0.0",
"echarts-for-react": "^3.0.6",
"jszip": "^3.10.1",
"motion": "^12.23.24",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwindcss": "^4.2.0",
......
This diff is collapsed.
allowBuilds:
core-js: false
core-js-pure: false
es5-ext: false
esbuild: false
{
"name": "audit_material_manager",
"version": "1.0.2",
"name": "problem_rectification_new",
"version": "1.0.6",
"build": "abc",
"description": "merge master",
"date": "2025-07-14 19:27:20",
......
This diff is collapsed.
......@@ -4,7 +4,7 @@ import { defineApp } from 'umi';
export async function getInitialState() {
return {
name: 'project_manager_xy',
name: 'integrity_risk_prevention',
};
}
......
......@@ -43,7 +43,7 @@ export const AvatarButton = forwardRef(function AvatarButton(
let classes = clsx(
className,
square ? 'rounded-[20%]' : 'rounded-full',
'relative inline-grid focus:not-data-focus:outline-hidden data-focus:outline-2 data-focus:outline-offset-2 data-focus:outline-[var(--pm-brand)]'
'relative inline-grid focus:not-data-focus:outline-hidden data-focus:outline-2 data-focus:outline-offset-2 data-focus:outline-blue-500'
)
return typeof props.href === 'string' ? (
......
......@@ -54,7 +54,7 @@ export const BadgeButton = forwardRef(function BadgeButton(
) {
let classes = clsx(
className,
'group relative inline-flex rounded-md focus:not-data-focus:outline-hidden data-focus:outline-2 data-focus:outline-offset-2 data-focus:outline-[var(--pm-brand)]'
'group relative inline-flex rounded-md focus:not-data-focus:outline-hidden data-focus:outline-2 data-focus:outline-offset-2 data-focus:outline-blue-500'
)
return typeof props.href === 'string' ? (
......
......@@ -57,7 +57,7 @@ const base = [
'after:absolute after:inset-0 after:rounded-[calc(0.3125rem-1px)] after:shadow-[inset_0_1px_--theme(--color-white/15%)]',
,
// Focus ring
'group-data-focus:outline-2 group-data-focus:outline-offset-2 group-data-focus:outline-[var(--pm-brand)]',
'group-data-focus:outline-2 group-data-focus:outline-offset-2 group-data-focus:outline-blue-500',
// Disabled state
'group-data-disabled:opacity-50',
'group-data-disabled:border-zinc-950/25 group-data-disabled:bg-zinc-950/5 group-data-disabled:[--checkbox-check:var(--color-zinc-950)]/50 group-data-disabled:before:bg-transparent',
......
......@@ -38,7 +38,7 @@ export function Combobox({
// Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo
,
// Focus ring
'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-transparent after:ring-inset sm:focus-within:after:ring-2 sm:focus-within:after:ring-[var(--pm-brand)]',
'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-transparent after:ring-inset sm:focus-within:after:ring-2 sm:focus-within:after:ring-blue-500',
// Disabled state
'has-data-disabled:opacity-50 has-data-disabled:before:bg-zinc-950/5 has-data-disabled:before:shadow-none',
// Invalid state
......@@ -133,7 +133,7 @@ export function ComboboxOption({ children, className, ...props }) {
// Typography
'text-base/6 text-zinc-950 sm:text-sm/6 forced-colors:text-[CanvasText]',
// Focus
'outline-hidden data-focus:bg-[var(--pm-brand)] data-focus:text-white',
'outline-hidden data-focus:bg-blue-500 data-focus:text-white',
// Forced colors mode
'forced-color-adjust-none forced-colors:data-focus:bg-[Highlight] forced-colors:data-focus:text-[HighlightText]',
// Disabled
......
......@@ -51,7 +51,7 @@ export function DropdownItem({ className, ...props }) {
// Text styles
'text-left text-base/6 text-zinc-950 sm:text-sm/6 forced-colors:text-[CanvasText]',
// Focus
'data-focus:bg-[var(--pm-brand)] data-focus:text-white',
'data-focus:bg-blue-500 data-focus:text-white',
// Disabled state
'data-disabled:opacity-50',
// Forced colors mode
......
......@@ -38,7 +38,7 @@ export const Input = forwardRef(function Input(
// Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo
,
// Focus ring
'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-transparent after:ring-inset sm:focus-within:after:ring-2 sm:focus-within:after:ring-[var(--pm-brand)]',
'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-transparent after:ring-inset sm:focus-within:after:ring-2 sm:focus-within:after:ring-blue-500',
// Disabled state
'has-data-disabled:opacity-50 has-data-disabled:before:bg-zinc-950/5 has-data-disabled:before:shadow-none',
])}
......
......@@ -8,11 +8,12 @@
import * as Headless from '@headlessui/react'
import React, { forwardRef } from 'react'
import { Link as UmiLink } from 'umi'
export const Link = forwardRef(function Link(props, ref) {
export const Link = forwardRef(function Link({ href, ...props }, ref) {
return (
<Headless.DataInteractive>
<a {...props} ref={ref} />
<UmiLink {...props} to={href} ref={ref} />
</Headless.DataInteractive>
)
})
......@@ -22,7 +22,7 @@ export function Listbox({ className, placeholder, autoFocus, 'aria-label': ariaL
// Hide default focus styles
'focus:outline-hidden',
// Focus ring
'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-transparent after:ring-inset data-focus:after:ring-2 data-focus:after:ring-[var(--pm-brand)]',
'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-transparent after:ring-inset data-focus:after:ring-2 data-focus:after:ring-blue-500',
// Disabled state
'data-disabled:opacity-50 data-disabled:before:bg-zinc-950/5 data-disabled:before:shadow-none',
])}
......@@ -115,7 +115,7 @@ export function ListboxOption({ children, className, ...props }) {
// Typography
'text-base/6 text-zinc-950 sm:text-sm/6 forced-colors:text-[CanvasText]',
// Focus
'outline-hidden data-focus:bg-[var(--pm-brand)] data-focus:text-white',
'outline-hidden data-focus:bg-blue-500 data-focus:text-white',
// Forced colors mode
'forced-color-adjust-none forced-colors:data-focus:bg-[Highlight] forced-colors:data-focus:text-[HighlightText]',
// Disabled
......
......@@ -61,7 +61,7 @@ const base = [
// Indicator color (dark mode)
,
// Focus ring
'group-data-focus:outline group-data-focus:outline-2 group-data-focus:outline-offset-2 group-data-focus:outline-[var(--pm-brand)]',
'group-data-focus:outline group-data-focus:outline-2 group-data-focus:outline-offset-2 group-data-focus:outline-blue-500',
// Disabled state
'group-data-disabled:opacity-50',
'group-data-disabled:border-zinc-950/25 group-data-disabled:bg-zinc-950/5 group-data-disabled:[--radio-checked-indicator:var(--color-zinc-950)]/50 group-data-disabled:before:bg-transparent',
......
......@@ -15,7 +15,7 @@ export const Select = forwardRef(function Select({ className, multiple, ...props
// Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo
,
// Focus ring
'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-transparent after:ring-inset has-data-focus:after:ring-2 has-data-focus:after:ring-[var(--pm-brand)]',
'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-transparent after:ring-inset has-data-focus:after:ring-2 has-data-focus:after:ring-blue-500',
// Disabled state
'has-data-disabled:opacity-50 has-data-disabled:before:bg-zinc-950/5 has-data-disabled:before:shadow-none',
])}
......
'use client'
import * as Headless from '@headlessui/react'
import clsx from 'clsx'
import React, { useState } from 'react'
import { NavbarItem } from './navbar'
......@@ -30,6 +31,11 @@ function MobileSidebar({ open, close, children }) {
<Headless.DialogPanel
transition
className="fixed inset-y-0 w-full max-w-80 p-2 transition duration-300 ease-in-out data-closed:-translate-x-full"
onClick={(event) => {
if (event.target instanceof Element && event.target.closest('a[href]')) {
close()
}
}}
>
<div className="flex h-full flex-col rounded-lg bg-white shadow-xs ring-1 ring-zinc-950/5">
<div className="-mb-3 px-4 pt-3">
......@@ -44,7 +50,7 @@ function MobileSidebar({ open, close, children }) {
)
}
export function SidebarLayout({ navbar, sidebar, children }) {
export function SidebarLayout({ navbar, sidebar, children, contentClassName }) {
let [showSidebar, setShowSidebar] = useState(false)
return (
......@@ -70,7 +76,7 @@ export function SidebarLayout({ navbar, sidebar, children }) {
{/* Content */}
<main className="flex flex-1 flex-col pb-2 lg:min-w-0 lg:pt-2 lg:pr-2 lg:pl-64">
<div className="grow p-6 lg:rounded-lg lg:bg-white lg:p-10 lg:shadow-xs lg:ring-1 lg:ring-zinc-950/5">
<div className="mx-auto max-w-6xl">{children}</div>
<div className={clsx('mx-auto w-full max-w-6xl', contentClassName)}>{children}</div>
</div>
</main>
</div>
......
......@@ -4,8 +4,8 @@ import * as Headless from '@headlessui/react'
import clsx from 'clsx'
import { LayoutGroup, motion } from 'motion/react'
import React, { forwardRef, useId } from 'react'
import { Link as UmiLink } from 'umi'
import { TouchTarget } from './button'
import { Link } from './link'
export function Sidebar({ className, ...props }) {
return <nav {...props} className={clsx(className, 'flex h-full min-h-0 flex-col')} />
......@@ -67,7 +67,7 @@ export function SidebarSpacer({ className, ...props }) {
export function SidebarHeading({ className, ...props }) {
return (
<h3 {...props} className={clsx(className, 'mb-1 px-2 text-xs/6 font-medium text-zinc-500')} />
<h3 {...props} className={clsx(className, 'mb-1 px-2 text-xs/6 font-medium text-zinc-500 dark:text-zinc-400')} />
)
}
......@@ -76,6 +76,10 @@ export const SidebarItem = forwardRef(function SidebarItem(
ref
) {
let href = props.href
let interactiveProps = { ...props }
delete interactiveProps.href
let classes = clsx(
// Base
'flex w-full items-center gap-3 rounded-lg px-2 py-2.5 text-left text-base/6 font-medium text-zinc-950 sm:py-2 sm:text-sm/5',
......@@ -86,16 +90,16 @@ export const SidebarItem = forwardRef(function SidebarItem(
// Avatar
'*:data-[slot=avatar]:-m-0.5 *:data-[slot=avatar]:size-7 sm:*:data-[slot=avatar]:size-6',
// Hover
'data-hover:bg-zinc-950/5 data-hover:*:data-[slot=icon]:fill-zinc-950',
'hover:bg-zinc-950/5 hover:*:data-[slot=icon]:fill-zinc-950',
// Active
'data-active:bg-zinc-950/5 data-active:*:data-[slot=icon]:fill-zinc-950',
'active:bg-zinc-950/5 active:*:data-[slot=icon]:fill-zinc-950',
// Current
'data-current:*:data-[slot=icon]:fill-zinc-950',
'data-current:bg-blue-600 data-current:text-white data-current:*:data-[slot=icon]:fill-white',
// Dark mode
,
,
,
'dark:text-white dark:*:data-[slot=icon]:fill-zinc-400',
'dark:hover:bg-white/5 dark:hover:*:data-[slot=icon]:fill-white',
'dark:active:bg-white/5 dark:active:*:data-[slot=icon]:fill-white',
'dark:data-current:bg-blue-600 dark:data-current:text-white dark:data-current:*:data-[slot=icon]:fill-white'
)
return (
......@@ -103,19 +107,19 @@ export const SidebarItem = forwardRef(function SidebarItem(
{current && (
<motion.span
layoutId="current-indicator"
className="absolute inset-y-2 -left-4 w-0.5 rounded-full bg-zinc-950"
className="absolute inset-y-2 -left-4 w-0.5 rounded-full bg-zinc-950 dark:bg-white"
/>
)}
{typeof props.href === 'string' ? (
<Headless.CloseButton
as={Link}
{...props}
{typeof href === 'string' ? (
<UmiLink
{...interactiveProps}
to={href}
className={classes}
data-current={current ? 'true' : undefined}
ref={ref}
>
<TouchTarget>{children}</TouchTarget>
</Headless.CloseButton>
</UmiLink>
) : (
<Headless.Button
{...props}
......
......@@ -148,7 +148,7 @@ export function Switch({ color = 'dark/zinc', className, ...props }) {
// Checked
'data-checked:bg-(--switch-bg) data-checked:ring-(--switch-bg-ring)',
// Focus
'focus:not-data-focus:outline-hidden data-focus:outline-2 data-focus:outline-offset-2 data-focus:outline-[var(--pm-brand)]',
'focus:not-data-focus:outline-hidden data-focus:outline-2 data-focus:outline-offset-2 data-focus:outline-blue-500',
// Hover
'data-hover:ring-black/15 data-hover:data-checked:ring-(--switch-bg-ring)',
,
......
......@@ -16,6 +16,7 @@ export function Table({
dense = false,
grid = false,
striped = false,
scrollable = true,
className,
children,
...props
......@@ -27,7 +28,9 @@ export function Table({
{...props}
className={clsx(
className,
'-mx-(--gutter) overflow-x-auto whitespace-nowrap',
'-mx-(--gutter)',
scrollable && 'overflow-x-auto',
'whitespace-nowrap',
)}
>
<div
......@@ -70,7 +73,7 @@ export function TableRow({ href, target, title, className, ...props }) {
className={clsx(
className,
href &&
'has-[[data-row-link][data-focus]]:outline-2 has-[[data-row-link][data-focus]]:-outline-offset-2 has-[[data-row-link][data-focus]]:outline-[var(--pm-brand)]',
'has-[[data-row-link][data-focus]]:outline-2 has-[[data-row-link][data-focus]]:-outline-offset-2 has-[[data-row-link][data-focus]]:outline-blue-500',
striped && 'even:bg-zinc-950/2.5',
href && striped && 'hover:bg-zinc-950/5',
href && !striped && 'hover:bg-zinc-950/2.5',
......
......@@ -15,7 +15,7 @@ export const Textarea = forwardRef(function Textarea({ className, resizable = tr
// Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo
,
// Focus ring
'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-transparent after:ring-inset sm:focus-within:after:ring-2 sm:focus-within:after:ring-[var(--pm-brand)]',
'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-transparent after:ring-inset sm:focus-within:after:ring-2 sm:focus-within:after:ring-blue-500',
// Disabled state
'has-data-disabled:opacity-50 has-data-disabled:before:bg-zinc-950/5 has-data-disabled:before:shadow-none',
])}
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
import * as Headless from '@headlessui/react';
import {
CheckIcon,
ChevronUpDownIcon,
MagnifyingGlassIcon,
XMarkIcon,
} from '@heroicons/react/20/solid';
import clsx from 'clsx';
import { useMemo, useState } from 'react';
function serializeValue(value) {
if (Array.isArray(value)) {
return `[${value.map(serializeValue).join(',')}]`;
}
if (value && typeof value === 'object') {
return `{${Object.keys(value)
.sort()
.map((key) => `${JSON.stringify(key)}:${serializeValue(value[key])}`)
.join(',')}}`;
}
return JSON.stringify(value);
}
function getValueKey(value) {
try {
return `${typeof value}:${serializeValue(value)}`;
} catch (error) {
return `${typeof value}:${String(value)}`;
}
}
function getValueLabel(value) {
if (value && typeof value === 'object') {
return String(
value.label
|| value.title
|| value.name
|| value.display_name
|| value._id
|| serializeValue(value),
);
}
return String(value ?? '');
}
export default function MultiSelect({
value,
options = [],
onChange,
disabled = false,
placeholder = '请选择或搜索',
ariaLabel = '多选',
}) {
const [query, setQuery] = useState('');
const normalizedOptions = useMemo(() => options.map((option, index) => {
const optionValue = option?.value;
const valueKey = getValueKey(optionValue);
return {
...option,
value: optionValue,
valueKey,
reactKey: `${valueKey}-${index}`,
label: String(option?.label ?? optionValue ?? ''),
disabled: Boolean(option?.disabled),
};
}), [options]);
const optionByValue = useMemo(
() => new Map(normalizedOptions.map((option) => [option.valueKey, option])),
[normalizedOptions],
);
const selectedOptions = useMemo(() => {
const selectedValues = Array.isArray(value)
? value
: value === null || value === undefined || value === ''
? []
: [value];
return selectedValues.map((selectedValue, index) => {
const valueKey = getValueKey(selectedValue);
return optionByValue.get(valueKey) || {
value: selectedValue,
valueKey,
reactKey: `selected-${valueKey}-${index}`,
label: getValueLabel(selectedValue),
};
});
}, [optionByValue, value]);
const filteredOptions = useMemo(() => {
const normalizedQuery = query.trim().toLocaleLowerCase();
if (!normalizedQuery) {
return normalizedOptions;
}
return normalizedOptions.filter((option) => (
option.label.toLocaleLowerCase().includes(normalizedQuery)
|| String(option.value ?? '').toLocaleLowerCase().includes(normalizedQuery)
));
}, [normalizedOptions, query]);
const updateValue = (nextOptions) => {
onChange(nextOptions.map((option) => option.value));
setQuery('');
};
const removeOption = (optionToRemove) => {
updateValue(selectedOptions.filter(
(option) => option.valueKey !== optionToRemove.valueKey,
));
};
return (
<Headless.Combobox
value={selectedOptions}
onChange={updateValue}
by={(left, right) => left?.valueKey === right?.valueKey}
onClose={() => setQuery('')}
disabled={disabled}
multiple
immediate
>
<div data-slot="control" className="relative w-full">
<div
className={clsx(
'relative min-h-11 w-full rounded-lg border border-zinc-950/10 bg-white shadow-sm transition',
'focus-within:border-[var(--pm-brand)] focus-within:ring-2 focus-within:ring-[color-mix(in_srgb,var(--pm-brand)_18%,transparent)]',
disabled && 'cursor-not-allowed bg-zinc-100 opacity-60 shadow-none',
)}
>
{selectedOptions.length > 0 ? (
<div className="flex max-h-20 min-w-0 flex-wrap items-center gap-1.5 overflow-y-auto border-b border-zinc-100 px-3 py-2">
{selectedOptions.map((option) => (
<span
key={option.reactKey}
className="inline-flex max-w-full items-center gap-1 rounded-md bg-zinc-100 px-2 py-1 text-xs font-medium text-zinc-700 ring-1 ring-inset ring-zinc-200"
>
<span className="truncate">{option.label}</span>
<button
type="button"
disabled={disabled}
onClick={() => removeOption(option)}
className="shrink-0 rounded-sm text-zinc-400 outline-none transition hover:bg-zinc-200 hover:text-zinc-700 focus-visible:ring-2 focus-visible:ring-[var(--pm-brand)] disabled:pointer-events-none"
aria-label={`移除${option.label}`}
title={`移除${option.label}`}
>
<XMarkIcon className="size-3.5" aria-hidden="true" />
</button>
</span>
))}
</div>
) : null}
<div className="relative h-10 w-full">
<MagnifyingGlassIcon
className="pointer-events-none absolute left-3 top-1/2 z-10 size-4 -translate-y-1/2 text-zinc-400"
aria-hidden="true"
/>
<Headless.ComboboxInput
value={query}
onChange={(event) => setQuery(event.target.value)}
displayValue={() => query}
placeholder={selectedOptions.length > 0 ? '继续搜索' : placeholder}
aria-label={ariaLabel}
className="h-10 w-full rounded-lg border-0 bg-transparent py-0 pl-9 pr-20 text-sm text-zinc-950 outline-none placeholder:text-zinc-400 disabled:cursor-not-allowed"
/>
<div className="absolute inset-y-0 right-1.5 flex items-center gap-0.5">
{selectedOptions.length > 0 ? (
<button
type="button"
disabled={disabled}
onClick={() => updateValue([])}
className="rounded-md p-1.5 text-zinc-400 outline-none transition hover:bg-zinc-100 hover:text-zinc-700 focus-visible:ring-2 focus-visible:ring-[var(--pm-brand)] disabled:pointer-events-none"
aria-label="清空已选项"
title="清空已选项"
>
<XMarkIcon className="size-4" aria-hidden="true" />
</button>
) : null}
<Headless.ComboboxButton
className="rounded-md p-1.5 text-zinc-500 outline-none transition hover:bg-zinc-100 hover:text-zinc-700 focus-visible:ring-2 focus-visible:ring-[var(--pm-brand)] disabled:pointer-events-none"
aria-label="展开选项"
title="展开选项"
>
<ChevronUpDownIcon className="size-4" aria-hidden="true" />
</Headless.ComboboxButton>
</div>
</div>
</div>
<Headless.ComboboxOptions
anchor="bottom start"
transition
className={clsx(
'z-[100] max-h-56! w-[var(--input-width)] overflow-y-auto overscroll-contain rounded-lg border border-zinc-200 bg-white p-1 shadow-xl ring-1 ring-black/5 outline-none',
'[--anchor-gap:--spacing(2)] [--anchor-padding:--spacing(4)]',
'transition duration-100 ease-out data-closed:scale-95 data-closed:opacity-0',
)}
>
{filteredOptions.length > 0 ? filteredOptions.map((option) => (
<Headless.ComboboxOption
key={option.reactKey}
value={option}
disabled={option.disabled}
className="group flex cursor-default items-center gap-2.5 rounded-md px-3 py-2 text-sm text-zinc-700 outline-none data-disabled:opacity-45 data-focus:bg-zinc-100 data-focus:text-zinc-950"
>
{({ selected }) => (
<>
<span
className={clsx(
'flex size-4 shrink-0 items-center justify-center rounded border transition',
selected
? 'border-[var(--pm-brand)] bg-[var(--pm-brand)] text-white'
: 'border-zinc-300 bg-white text-transparent group-data-focus:border-zinc-400',
)}
aria-hidden="true"
>
<CheckIcon className="size-3" />
</span>
<span className="min-w-0 flex-1 truncate">{option.label}</span>
</>
)}
</Headless.ComboboxOption>
)) : (
<div className="px-3 py-6 text-center text-sm text-zinc-500">
{normalizedOptions.length === 0 ? '暂无可选项' : '未找到匹配选项'}
</div>
)}
{normalizedOptions.length > 0 ? (
<div className="sticky bottom-0 mt-1 flex items-center justify-between border-t border-zinc-100 bg-white px-3 py-2 text-xs text-zinc-500">
<span>已选择 {selectedOptions.length} 项</span>
{selectedOptions.length > 0 ? (
<button
type="button"
onMouseDown={(event) => event.preventDefault()}
onClick={() => updateValue([])}
className="font-medium text-[var(--pm-brand)] outline-none hover:underline focus-visible:underline"
>
清空
</button>
) : null}
</div>
) : null}
</Headless.ComboboxOptions>
</div>
</Headless.Combobox>
);
}
This diff is collapsed.
import { Checkbox } from '@/catalyst-ui-kit/javascript/checkbox';
import { Input } from '@/catalyst-ui-kit/javascript/input';
import { queryUsers } from '@/services/DataService';
import { normalizeUserLabel, toArray } from '@/utils/subjectView';
import { useEffect, useMemo, useState } from 'react';
export default function UserSearchSelect({ value, onChange, multiple = false }) {
const [keyword, setKeyword] = useState('');
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const selectedIds = useMemo(
() => toArray(value).map((item) => item?._id || item).filter(Boolean),
[value],
);
useEffect(() => {
let cancelled = false;
const loadUsers = async () => {
setLoading(true);
try {
const text = keyword.trim();
const result = await queryUsers(0, 30, text
? {
$or: [
{ username: { $regex: text, $options: 'i' } },
{ display_name: { $regex: text, $options: 'i' } },
],
}
: {});
if (!cancelled) {
setUsers(result?.users || []);
}
} catch (error) {
if (!cancelled) {
setUsers([]);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};
loadUsers();
return () => {
cancelled = true;
};
}, [keyword]);
const toggleUser = (userId, checked) => {
if (!multiple) {
onChange(checked ? userId : null);
return;
}
onChange(
checked
? Array.from(new Set([...selectedIds, userId]))
: selectedIds.filter((item) => item !== userId),
);
};
return (
<div className="space-y-3">
<Input
value={keyword}
onChange={(event) => setKeyword(event.target.value)}
placeholder="搜索用户名或显示名"
/>
<div className="max-h-56 overflow-y-auto rounded-lg border border-zinc-200 bg-zinc-50 p-2">
{loading ? (
<div className="px-3 py-4 text-sm text-zinc-500">正在加载用户...</div>
) : users.length === 0 ? (
<div className="px-3 py-4 text-sm text-zinc-500">暂无可选用户</div>
) : (
<div className="space-y-1">
{users.map((user) => {
const checked = selectedIds.includes(user._id);
return (
<label
key={user._id}
className="flex min-h-11 cursor-pointer items-center gap-3 rounded-md bg-white px-3 py-2 text-sm transition hover:bg-blue-50"
>
<Checkbox
checked={checked}
onChange={(nextChecked) => toggleUser(user._id, nextChecked)}
color="blue"
/>
<span className="min-w-0">
<span className="block truncate font-medium text-zinc-950">
{normalizeUserLabel(user)}
</span>
<span className="block truncate text-xs text-zinc-500">{user.username}</span>
</span>
</label>
);
})}
</div>
)}
</div>
</div>
);
}
This diff is collapsed.
:root {
--pm-brand: #c81e3a;
--pm-brand-strong: #9f1239;
--pm-brand: #b42318;
--pm-brand-strong: #8f1d14;
--pm-accent: #059669;
--pm-surface: #ffffff;
--pm-bg: #fff1f2;
--pm-sidebar: #0f172a;
--pm-sidebar-soft: #1e293b;
--pm-bg: #f8fafc;
--pm-sidebar: #18181b;
--pm-sidebar-soft: #27272a;
--pm-text: #0f172a;
--pm-muted: #64748b;
}
......@@ -16,8 +17,18 @@ body,
}
body {
--color-blue-50: #fef2f2;
--color-blue-100: #fee2e2;
--color-blue-200: #fecaca;
--color-blue-300: #fca5a5;
--color-blue-400: #f87171;
--color-blue-500: #dc2626;
--color-blue-600: #b42318;
--color-blue-700: #991b1b;
--color-blue-800: #7f1d1d;
--color-blue-900: #450a0a;
margin: 0;
background: linear-gradient(180deg, #fff7f7 0%, #fff1f2 100%);
background: var(--pm-bg);
color: var(--pm-text);
font-family:
"PingFang SC",
......
import { UserContext } from '@/wrapper/Auth';
import { useContext } from 'react';
export function useUserInfo() {
return useContext(UserContext);
}
export default useUserInfo;
import authService from '@/services/authService';
import { Button } from '@/catalyst-ui-kit/javascript/button';
import { Input } from '@/catalyst-ui-kit/javascript/input';
import { getCaptcha, userLogin } from '@/services/DataService';
import { SiteContext } from '@/wrapper/Site';
import CryptoJS from 'crypto-js';
......@@ -26,17 +28,17 @@ export default function LoginPage() {
const redirect = searchParams.get('redirect') || '/';
const siteTitle = site?.title || site?.name || '廉洁风险防控';
const siteTitle = site?.title || site?.name || '廉洁风险防控系统';
const siteSubtitle =
site?.subtitle || '风险识别、风险评估、防控措施与历史归档统一管理';
site?.subtitle || '风险点识别、措施维护、审批与归档统一协同';
const backgroundStyle = useMemo(() => {
const overlay =
'linear-gradient(135deg, rgba(28, 10, 16, 0.94) 0%, rgba(98, 20, 38, 0.9) 52%, rgba(190, 24, 93, 0.82) 100%)';
'linear-gradient(135deg, rgba(9, 30, 66, 0.96) 0%, rgba(30, 64, 175, 0.9) 56%, rgba(5, 150, 105, 0.82) 100%)';
if (!site?.background) {
return {
backgroundImage: `${overlay}, radial-gradient(circle at top, #57111f 0%, #21070f 55%, #120308 100%)`,
backgroundImage: `${overlay}, linear-gradient(145deg, #0f172a 0%, #1d4ed8 60%, #047857 100%)`,
};
}
......@@ -96,15 +98,15 @@ export default function LoginPage() {
return (
<div
className="min-h-screen bg-[#18060d] px-4 py-6 text-white md:px-8"
className="min-h-screen bg-[#061321] px-4 py-6 text-white md:px-8"
style={backgroundStyle}
>
<div className="mx-auto flex min-h-[calc(100vh-3rem)] w-full max-w-7xl overflow-hidden rounded-[32px] border border-white/10 bg-white/6 shadow-[0_28px_120px_rgba(0,0,0,0.38)] backdrop-blur-sm">
<div className="mx-auto flex min-h-[calc(100vh-3rem)] w-full max-w-7xl overflow-hidden rounded-lg border border-white/10 bg-white/6 shadow-[0_28px_120px_rgba(0,0,0,0.38)] backdrop-blur-sm">
<section className="relative hidden w-[52%] flex-col justify-between overflow-hidden px-10 py-12 lg:flex">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(255,255,255,0.2),transparent_36%),radial-gradient(circle_at_bottom_right,rgba(244,63,94,0.28),transparent_32%)]" />
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(255,255,255,0.2),transparent_36%),radial-gradient(circle_at_bottom_right,rgba(49,196,255,0.3),transparent_32%)]" />
<div className="relative z-10 max-w-xl">
<p className="text-xs font-medium uppercase tracking-[0.45em] text-white/60">
Integrity Risk Prevention
<p className="text-xs font-medium uppercase text-white/60">
Integrity Risk Access
</p>
<h1 className="mt-6 text-4xl font-semibold leading-tight text-white">
{siteTitle}
......@@ -115,20 +117,20 @@ export default function LoginPage() {
</div>
<div className="relative z-10 grid gap-4 xl:grid-cols-2">
<div className="rounded-3xl border border-white/10 bg-white/8 p-6">
<div className="text-sm text-white/65">协同视图</div>
<div className="rounded-lg border border-white/10 bg-white/8 p-6">
<div className="text-sm text-white/65">防控闭环</div>
<div className="mt-3 text-2xl font-semibold text-white">
风险识别 + 风险评估
填报 + 审核 + 归档
</div>
<p className="mt-3 text-sm leading-6 text-white/68">
统一维护风险点、涉及环节、岗位人员与风险等级。
风险点推送后依次完成填报、复核和最终审核,驳回后自动回到填报阶段。
</p>
</div>
<div className="rounded-3xl border border-white/10 bg-black/18 p-6">
<div className="text-sm text-white/65">状态追踪</div>
<div className="mt-3 text-2xl font-semibold text-white">填报与审批</div>
<div className="rounded-lg border border-white/10 bg-black/18 p-6">
<div className="text-sm text-white/65">风险台账</div>
<div className="mt-3 text-2xl font-semibold text-white">过程表与展示表</div>
<p className="mt-3 text-sm leading-6 text-white/68">
推送填报、审核状态、防控措施和历史快照全程留痕。
统一维护风险点、防控措施和制度依据,并保留每次归档形成的历史快照。
</p>
</div>
</div>
......@@ -137,14 +139,14 @@ export default function LoginPage() {
<section className="flex flex-1 items-center justify-center bg-white/92 px-6 py-10 text-slate-900 sm:px-10">
<div className="w-full max-w-md">
<div className="mb-8">
<p className="text-sm font-medium uppercase tracking-[0.35em] text-rose-600/80">
<p className="text-sm font-medium uppercase text-blue-600/80">
Sign In
</p>
<h2 className="mt-3 text-3xl font-semibold text-slate-900">
登录廉洁风险防控
登录廉洁风险防控系统
</h2>
<p className="mt-3 text-sm leading-6 text-slate-500">
使用统一账号登录后进入廉洁风险防控工作区。
使用现有账号和验证码进入廉洁风险工作台。
</p>
</div>
......@@ -153,8 +155,8 @@ export default function LoginPage() {
<label className="mb-2 block text-sm font-medium text-slate-700">
账号
</label>
<input
className="w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-900 outline-none transition focus:border-rose-500 focus:bg-white"
<Input
className="w-full"
value={identity}
onChange={(event) => setIdentity(event.target.value)}
disabled={loading}
......@@ -167,9 +169,9 @@ export default function LoginPage() {
<label className="mb-2 block text-sm font-medium text-slate-700">
密码
</label>
<input
<Input
type="password"
className="w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-900 outline-none transition focus:border-rose-500 focus:bg-white"
className="w-full"
value={password}
onChange={(event) => setPassword(event.target.value)}
disabled={loading}
......@@ -183,8 +185,8 @@ export default function LoginPage() {
验证码
</label>
<div className="flex gap-3">
<input
className="min-w-0 flex-1 rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-900 outline-none transition focus:border-rose-500 focus:bg-white"
<Input
className="min-w-0 flex-1"
value={captchaCode}
onChange={(event) => setCaptchaCode(event.target.value)}
disabled={loading}
......@@ -198,7 +200,7 @@ export default function LoginPage() {
refreshCaptcha();
}}
disabled={loading || captchaLoading}
className="flex h-[56px] w-[148px] shrink-0 items-center justify-center overflow-hidden rounded-2xl border border-slate-200 bg-white px-2 text-xs text-slate-500 transition hover:border-rose-300 hover:text-rose-600 disabled:cursor-not-allowed disabled:opacity-70"
className="flex h-[44px] w-[148px] shrink-0 items-center justify-center overflow-hidden rounded-lg border border-slate-200 bg-white px-2 text-xs text-slate-500 transition hover:border-blue-300 hover:text-blue-600 disabled:cursor-not-allowed disabled:opacity-70"
>
{captchaData?.image ? (
<span
......@@ -213,18 +215,19 @@ export default function LoginPage() {
</div>
{error ? (
<div className="rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-600">
<div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
{error}
</div>
) : null}
<button
<Button
type="submit"
color="blue"
disabled={loading || captchaLoading}
className="flex w-full items-center justify-center rounded-2xl bg-[linear-gradient(135deg,#9f1239_0%,#be123c_52%,#e11d48_100%)] px-4 py-3 text-sm font-medium text-white shadow-[0_16px_40px_rgba(190,24,93,0.28)] transition hover:brightness-105 disabled:cursor-not-allowed disabled:opacity-70"
className="flex w-full justify-center"
>
{loading ? '登录中...' : '登录'}
</button>
</Button>
</form>
</div>
</section>
......
import { useEffect, useMemo, useState } from 'react';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/catalyst-ui-kit/javascript/table';
import { loadSubject, queryRecords } from '@/services/DataService';
import {
formatFieldValue,
getFieldValue,
getReferenceSubjectName,
normalizeRecordId,
toArray,
} from '@/utils/subjectView';
const TITLE_COLUMN = {
field: 'title',
label: '标题',
fieldDef: { name: 'title', label: '标题', type: 'text' },
};
function hasRecordBody(value) {
return value && typeof value === 'object' && Boolean(value.metadata || value.title || value.name);
}
function getReferenceIds(value) {
return toArray(value).map(normalizeRecordId).filter(Boolean);
}
function inferColumnsFromRecords(records) {
const metadataKeys = [];
const seenKeys = new Set();
records.forEach((record) => {
Object.keys(record?.metadata || {}).forEach((key) => {
if (!seenKeys.has(key)) {
seenKeys.add(key);
metadataKeys.push(key);
}
});
});
const metadataColumns = metadataKeys.map((key) => ({
field: `metadata.${key}`,
label: key,
fieldDef: { name: key, label: key, type: 'text' },
}));
return metadataColumns.length > 0 ? metadataColumns : [TITLE_COLUMN];
}
function buildColumns(subject, records) {
if (!subject?.fields) {
return inferColumnsFromRecords(records);
}
const metadataColumns = toArray(subject?.fields).map((field) => ({
field: `metadata.${field.name}`,
label: field.label || field.name,
fieldDef: field,
}));
return metadataColumns.length > 0 ? metadataColumns : [TITLE_COLUMN];
}
function sortRecordsByReferenceOrder(records, referenceIds) {
const orderMap = new Map(referenceIds.map((id, index) => [id, index]));
return [...records].sort((left, right) => {
const leftOrder = orderMap.get(normalizeRecordId(left)) ?? Number.MAX_SAFE_INTEGER;
const rightOrder = orderMap.get(normalizeRecordId(right)) ?? Number.MAX_SAFE_INTEGER;
return leftOrder - rightOrder;
});
}
function CellValue({ value, fieldDef }) {
const text = formatFieldValue(value, fieldDef);
if (!text) {
return <span className="text-zinc-400">-</span>;
}
if (fieldDef?.type === 'object' || fieldDef?.type === 'form' || fieldDef?.type === 'subject') {
return (
<span className="block max-w-80 truncate font-mono text-xs text-zinc-600" title={text}>
{text}
</span>
);
}
return (
<span className="block max-w-80 whitespace-normal break-words text-zinc-700">
{text}
</span>
);
}
export default function ReferenceRecordsTable({ fieldDef, value, pageSize = 1000 }) {
const subjectName = getReferenceSubjectName(fieldDef);
const referenceIds = useMemo(() => getReferenceIds(value), [value]);
const populatedRecords = useMemo(
() => toArray(value).filter(hasRecordBody),
[value],
);
const [subject, setSubject] = useState(null);
const [records, setRecords] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
useEffect(() => {
let cancelled = false;
const loadReferenceData = async () => {
setError('');
setLoading(false);
setSubject(null);
setRecords([]);
if (referenceIds.length === 0) {
return;
}
if (!subjectName) {
setRecords(populatedRecords);
return;
}
setLoading(true);
try {
const shouldQueryRecords = populatedRecords.length < referenceIds.length;
const [subjectData, recordsResult] = await Promise.all([
loadSubject(subjectName),
shouldQueryRecords
? queryRecords({
subject: subjectName,
page: 0,
page_size: pageSize,
filter: {
_id: { $in: referenceIds },
},
mode: 'full',
})
: Promise.resolve({ records: populatedRecords }),
]);
if (cancelled) {
return;
}
const loadedRecords = recordsResult?.records || populatedRecords;
setSubject(subjectData || null);
setRecords(sortRecordsByReferenceOrder(loadedRecords, referenceIds));
} catch (requestError) {
if (!cancelled) {
setError(requestError?.message || '引用记录加载失败,请稍后重试。');
setSubject(null);
setRecords(populatedRecords);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};
loadReferenceData();
return () => {
cancelled = true;
};
}, [pageSize, populatedRecords, referenceIds, subjectName]);
if (referenceIds.length === 0) {
return (
<div className="rounded-lg border border-dashed border-zinc-200 bg-zinc-50 px-4 py-5 text-sm text-zinc-500">
暂无关联记录
</div>
);
}
if (!subjectName && populatedRecords.length === 0) {
return (
<div className="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-700">
引用字段缺少目标 Subject 配置,暂不能加载关联明细。
</div>
);
}
if (loading) {
return (
<div className="rounded-lg border border-zinc-200 bg-white px-4 py-5 text-sm text-zinc-500 shadow-sm">
正在加载关联明细...
</div>
);
}
const columns = buildColumns(subject, records);
return (
<div className="space-y-2">
{error ? (
<div className="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-700">
{error}
</div>
) : null}
<div className="overflow-hidden rounded-lg border border-zinc-200 bg-white shadow-sm">
<div className="border-b border-zinc-100 px-4 py-3 text-xs text-zinc-500">
共 {records.length} 条关联记录
</div>
{records.length === 0 ? (
<div className="px-4 py-5 text-sm text-zinc-500">暂无可展示的关联明细</div>
) : (
<Table dense grid className="max-h-96 overflow-auto">
<TableHead>
<TableRow>
{columns.map((column) => (
<TableHeader key={column.field} className="bg-zinc-50 text-xs font-semibold text-zinc-600">
{column.label}
</TableHeader>
))}
</TableRow>
</TableHead>
<TableBody>
{records.map((record, rowIndex) => (
<TableRow key={normalizeRecordId(record) || `${record?.name || 'reference'}-${rowIndex}`}>
{columns.map((column) => (
<TableCell key={column.field} className="align-top text-sm">
<CellValue
value={getFieldValue(record, column.field)}
fieldDef={column.fieldDef}
/>
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
)}
</div>
</div>
);
}
import { useEffect, useMemo, useState } from 'react';
import { Input } from '@/catalyst-ui-kit/javascript/input';
import { queryRecords, queryTags, queryViews } from '@/services/DataService';
import {
getReferenceSubjectName,
normalizeRecordId,
normalizeRecordLabel,
toArray,
} from '@/utils/subjectView';
export default function ReferenceSearchSelect({
fieldDef,
value,
onChange,
placeholder,
}) {
const [keyword, setKeyword] = useState('');
const [options, setOptions] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const fieldType = String(fieldDef?.type || 'reference').toLowerCase();
const subjectName = getReferenceSubjectName(fieldDef);
const multiple = Boolean(fieldDef?.multiple);
const selectedIds = useMemo(
() => toArray(value).map(normalizeRecordId).filter(Boolean),
[value],
);
useEffect(() => {
let cancelled = false;
const loadOptions = async () => {
if (!subjectName && !['view', 'tag'].includes(fieldType)) {
setOptions([]);
return;
}
setLoading(true);
setError('');
try {
const normalizedKeyword = keyword.trim();
let result;
let records;
if (fieldType === 'view') {
result = await queryViews(
0,
20,
normalizedKeyword ? { title: { $regex: normalizedKeyword } } : {},
);
records = result?.views || [];
} else if (fieldType === 'tag') {
result = await queryTags({
page: 0,
page_size: 20,
keyword: normalizedKeyword || undefined,
});
records = result?.tags || [];
} else {
result = await queryRecords({
subject: subjectName,
page: 0,
page_size: 20,
keyword: normalizedKeyword || undefined,
mode: 'basic',
});
records = result?.records || [];
}
if (!cancelled) {
setOptions(records);
}
} catch (requestError) {
if (!cancelled) {
setOptions([]);
setError(requestError?.message || '可选项加载失败');
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};
loadOptions();
return () => {
cancelled = true;
};
}, [fieldType, keyword, subjectName]);
const toggleRecord = (record, checked) => {
const recordId = normalizeRecordId(record);
if (!recordId) {
return;
}
if (!multiple) {
onChange(checked ? recordId : null);
return;
}
if (checked) {
onChange(Array.from(new Set([...selectedIds, recordId])));
return;
}
onChange(selectedIds.filter((item) => item !== recordId));
};
if (!subjectName && !['view', 'tag'].includes(fieldType)) {
return (
<div className="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-700">
引用字段缺少目标 Subject 配置,暂不能搜索选择。
</div>
);
}
return (
<div className="space-y-3">
<Input
value={keyword}
onChange={(event) => setKeyword(event.target.value)}
placeholder={placeholder || (fieldType === 'view'
? '搜索计算视图'
: fieldType === 'tag' ? '搜索标签' : '搜索引用记录标题')}
/>
<div className="max-h-56 space-y-2 overflow-y-auto rounded-lg border border-zinc-200 bg-zinc-50 p-2">
{loading ? (
<div className="rounded-md bg-white px-3 py-4 text-sm text-zinc-500">正在加载...</div>
) : error ? (
<div className="rounded-md bg-red-50 px-3 py-4 text-sm text-red-700">{error}</div>
) : options.length === 0 ? (
<div className="rounded-md bg-white px-3 py-4 text-sm text-zinc-500">暂无可选记录</div>
) : (
options.map((record) => {
const recordId = normalizeRecordId(record);
const checked = selectedIds.includes(recordId);
return (
<label
key={recordId}
className="flex cursor-pointer items-center gap-3 rounded-md bg-white px-3 py-2 text-sm text-zinc-700 shadow-sm transition hover:bg-blue-50"
>
<input
type={multiple ? 'checkbox' : 'radio'}
checked={checked}
onChange={(event) => toggleRecord(record, event.target.checked)}
/>
<span>
<span className="block font-medium text-zinc-950">{normalizeRecordLabel(record) || recordId}</span>
<span className="block text-xs text-zinc-500">{record.name || recordId}</span>
</span>
</label>
);
})
)}
</div>
</div>
);
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
import RiskDataWorkspace from './RiskDataWorkspace';
export default function DisplayRiskTable() {
return <RiskDataWorkspace workspaceKey="display" />;
}
import RiskDataWorkspace from './RiskDataWorkspace';
export default function MeasureDatabase() {
return <RiskDataWorkspace workspaceKey="measure" />;
}
import RiskDataWorkspace from './RiskDataWorkspace';
export default function ProcessRiskTable() {
return <RiskDataWorkspace workspaceKey="process" />;
}
import RiskDataWorkspace from './RiskDataWorkspace';
export default function RiskArchive() {
return <RiskDataWorkspace workspaceKey="archive" />;
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
import IntegrityRiskWorkbenchPage from './IntegrityRiskWorkbenchPage';
export default function IntegrityRiskArchivePage() {
return <IntegrityRiskWorkbenchPage pageType="archive" />;
}
This diff is collapsed.
This diff is collapsed.
import IntegrityRiskWorkbenchPage from './IntegrityRiskWorkbenchPage';
export default function MeasureDescriptionPage() {
return <IntegrityRiskWorkbenchPage pageType="measure" />;
}
import IntegrityRiskWorkbenchPage from './IntegrityRiskWorkbenchPage';
export default function RiskPreventionShowTablePage() {
return <IntegrityRiskWorkbenchPage pageType="show" />;
}
import IntegrityRiskWorkbenchPage from './IntegrityRiskWorkbenchPage';
export default function RiskPreventionTablePage() {
return <IntegrityRiskWorkbenchPage pageType="process" />;
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment