merge: 合并远程审批中心,保留本地加氢订单/记录与数量统计并同步导航。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
57
src/common/oneos-web-approval/ApprovalApp.tsx
Normal file
57
src/common/oneos-web-approval/ApprovalApp.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import '../oneosWebLegacy/legacyGlobals';
|
||||
import '../../prototypes/vehicle-management/style.css';import './styles/index.css';
|
||||
|
||||
import React from 'react';
|
||||
import { ConfigProvider } from 'antd';
|
||||
import type { ApprovalTabKey } from './types';
|
||||
import { ApprovalCenterPage } from './pages/ApprovalCenterPage';
|
||||
|
||||
const approvalTheme = {
|
||||
token: {
|
||||
colorPrimary: '#165dff',
|
||||
borderRadius: 8,
|
||||
fontFamily:
|
||||
'Inter, -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif',
|
||||
colorBgLayout: '#f5f7fa',
|
||||
},
|
||||
components: {
|
||||
Tabs: {
|
||||
inkBarColor: '#165dff',
|
||||
itemSelectedColor: '#165dff',
|
||||
itemHoverColor: '#165dff',
|
||||
},
|
||||
Button: {
|
||||
controlHeight: 32,
|
||||
},
|
||||
Select: {
|
||||
controlHeight: 32,
|
||||
},
|
||||
Input: {
|
||||
controlHeight: 32,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export interface ApprovalAppProps {
|
||||
tabKey?: ApprovalTabKey;
|
||||
pageTitle?: string;
|
||||
showTabs?: boolean;
|
||||
}
|
||||
|
||||
export function ApprovalApp({
|
||||
tabKey,
|
||||
pageTitle = '审批中心',
|
||||
showTabs = false,
|
||||
}: ApprovalAppProps) {
|
||||
return (
|
||||
<ConfigProvider theme={approvalTheme}>
|
||||
<ApprovalCenterPage
|
||||
tabKey={tabKey}
|
||||
pageTitle={pageTitle}
|
||||
showTabs={showTabs}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default ApprovalApp;
|
||||
96
src/common/oneos-web-approval/components/ApprovalCard.tsx
Normal file
96
src/common/oneos-web-approval/components/ApprovalCard.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import React from 'react';
|
||||
import { RightOutlined } from '@ant-design/icons';
|
||||
import type { ApprovalCardItem } from '../types';
|
||||
import {
|
||||
formatStatusLabel,
|
||||
getApprovalTypeColor,
|
||||
getApprovalTypeLabel,
|
||||
} from '../config/approvalTypeConfig';
|
||||
|
||||
export interface ApprovalCardProps {
|
||||
item: ApprovalCardItem;
|
||||
selected?: boolean;
|
||||
onClick?: (item: ApprovalCardItem) => void;
|
||||
}
|
||||
|
||||
function statusClassName(status: ApprovalCardItem['status']): string {
|
||||
if (status === 'approved') return 'ap-status ap-status--approved';
|
||||
if (status === 'rejected') return 'ap-status ap-status--rejected';
|
||||
return 'ap-status ap-status--pending';
|
||||
}
|
||||
|
||||
export function ApprovalCard({ item, selected = false, onClick }: ApprovalCardProps) {
|
||||
const typeColor = getApprovalTypeColor(item.type);
|
||||
const statusLabel = formatStatusLabel(item);
|
||||
|
||||
return (
|
||||
<article
|
||||
className={`ap-card${selected ? ' ap-card--selected' : ''}`}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => onClick?.(item)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
onClick?.(item);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="ap-card__header">
|
||||
<span
|
||||
className="ap-card__type"
|
||||
style={{ backgroundColor: `${typeColor}14`, color: typeColor, borderColor: `${typeColor}33` }}
|
||||
>
|
||||
{item.typeLabel ?? getApprovalTypeLabel(item.type)}
|
||||
</span>
|
||||
<span className={statusClassName(item.status)}>{statusLabel}</span>
|
||||
</div>
|
||||
|
||||
<div className="ap-card__body">
|
||||
<h3 className="ap-card__title">{item.title}</h3>
|
||||
{item.subtitle ? <p className="ap-card__subtitle">{item.subtitle}</p> : null}
|
||||
|
||||
{item.keyFacts && item.keyFacts.length > 0 ? (
|
||||
<dl className="ap-card__facts">
|
||||
{item.keyFacts.map((fact) => (
|
||||
<div key={`${item.id}-${fact.label}`} className="ap-card__fact">
|
||||
<dt>{fact.label}</dt>
|
||||
<dd className={fact.emphasis ? 'ap-card__fact-value--emphasis' : undefined}>
|
||||
{fact.value}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
) : null}
|
||||
|
||||
{item.risks && item.risks.length > 0 ? (
|
||||
<div className="ap-card__risks">
|
||||
{item.risks.map((risk) => (
|
||||
<span
|
||||
key={`${item.id}-${risk.label}`}
|
||||
className={`ap-card__risk ap-card__risk--${risk.level ?? 'default'}`}
|
||||
>
|
||||
{risk.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="ap-card__footer">
|
||||
<div className="ap-card__footer-meta">
|
||||
{item.showInitiatorInFooter ? (
|
||||
<span className="ap-card__footer-text">发起人 {item.initiatedBy}</span>
|
||||
) : null}
|
||||
<span className="ap-card__footer-text">
|
||||
{item.footerText ?? `发起时间 ${item.initiatedAt}`}
|
||||
</span>
|
||||
</div>
|
||||
<span className="ap-card__action">
|
||||
查看详情
|
||||
<RightOutlined />
|
||||
</span>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { Empty, Spin } from 'antd';
|
||||
import type { ApprovalCardItem } from '../types';
|
||||
import { ApprovalCard } from './ApprovalCard';
|
||||
|
||||
export interface ApprovalCardListProps {
|
||||
items: ApprovalCardItem[];
|
||||
loading?: boolean;
|
||||
selectedId?: string | null;
|
||||
onItemClick?: (item: ApprovalCardItem) => void;
|
||||
}
|
||||
|
||||
export function ApprovalCardList({
|
||||
items,
|
||||
loading = false,
|
||||
selectedId,
|
||||
onItemClick,
|
||||
}: ApprovalCardListProps) {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="ap-list ap-list--loading">
|
||||
<Spin tip="加载中..." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<div className="ap-list ap-list--empty">
|
||||
<Empty description="暂无审批事项" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ap-list">
|
||||
{items.map((item) => (
|
||||
<ApprovalCard
|
||||
key={item.id}
|
||||
item={item}
|
||||
selected={item.id === selectedId}
|
||||
onClick={onItemClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Empty,
|
||||
Space,
|
||||
Tabs,
|
||||
Timeline,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
CopyOutlined,
|
||||
ReloadOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { ApprovalCardItem } from '../types';
|
||||
import {
|
||||
formatStatusLabel,
|
||||
getApprovalTypeLabel,
|
||||
} from '../config/approvalTypeConfig';
|
||||
import { InsuranceComparisonDetail } from './InsuranceComparisonDetail';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
export interface ApprovalDetailPlaceholderProps {
|
||||
item: ApprovalCardItem | null;
|
||||
onRefresh?: () => void;
|
||||
}
|
||||
|
||||
function statusBadgeClass(status: ApprovalCardItem['status']): string {
|
||||
if (status === 'approved') return 'ap-detail__badge ap-detail__badge--approved';
|
||||
if (status === 'rejected') return 'ap-detail__badge ap-detail__badge--rejected';
|
||||
return 'ap-detail__badge ap-detail__badge--pending';
|
||||
}
|
||||
|
||||
const MOCK_TIMELINE = [
|
||||
{ role: '发起人', name: '提交申请', time: '', color: 'blue' as const },
|
||||
{ role: '部门主管', name: '审批通过', time: '2026-07-08 10:30', color: 'green' as const },
|
||||
{ role: '财务审核', name: '审批中', time: '', color: 'gray' as const },
|
||||
];
|
||||
|
||||
export function ApprovalDetailPlaceholder({ item, onRefresh }: ApprovalDetailPlaceholderProps) {
|
||||
const [activeTab, setActiveTab] = useState('detail');
|
||||
|
||||
if (!item) {
|
||||
return (
|
||||
<div className="ap-detail ap-detail--empty">
|
||||
<Empty description="请从左侧选择一条审批事项" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (item.typeLabel === '保险比价采购') {
|
||||
return <InsuranceComparisonDetail item={item} onRefresh={onRefresh} />;
|
||||
}
|
||||
|
||||
const statusLabel = formatStatusLabel(item);
|
||||
const typeLabel = getApprovalTypeLabel(item.type);
|
||||
|
||||
return (
|
||||
<div className="ap-detail">
|
||||
<div className="ap-detail__toolbar">
|
||||
<Space size={8}>
|
||||
<Text type="secondary">编号:</Text>
|
||||
<Text copyable={{ icon: <CopyOutlined />, text: item.id }}>{item.id}</Text>
|
||||
</Space>
|
||||
<Button type="text" icon={<ReloadOutlined />} onClick={onRefresh}>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="ap-detail__header">
|
||||
<div className="ap-detail__header-main">
|
||||
<Title level={4} className="ap-detail__title">
|
||||
{item.title}
|
||||
</Title>
|
||||
<span className={statusBadgeClass(item.status)}>{statusLabel}</span>
|
||||
</div>
|
||||
<div className="ap-detail__meta">
|
||||
<Space size={16} wrap>
|
||||
<Space size={6}>
|
||||
<Avatar size={24} icon={<UserOutlined />} />
|
||||
<Text>{item.initiatedBy}</Text>
|
||||
</Space>
|
||||
<Space size={6}>
|
||||
<Text type="secondary">流程分类</Text>
|
||||
<Text>{typeLabel}</Text>
|
||||
</Space>
|
||||
<Space size={6}>
|
||||
<ClockCircleOutlined style={{ color: '#8c8c8c' }} />
|
||||
<Text type="secondary">提交时间</Text>
|
||||
<Text>{item.initiatedAt}</Text>
|
||||
</Space>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
className="ap-detail__tabs"
|
||||
items={[
|
||||
{
|
||||
key: 'detail',
|
||||
label: '审批详情',
|
||||
children: (
|
||||
<div className="ap-detail__content">
|
||||
{item.subtitle ? (
|
||||
<p className="ap-detail__subtitle">{item.subtitle}</p>
|
||||
) : null}
|
||||
|
||||
{item.keyFacts && item.keyFacts.length > 0 ? (
|
||||
<dl className="ap-detail__facts">
|
||||
{item.keyFacts.map((fact) => (
|
||||
<div key={fact.label} className="ap-detail__fact">
|
||||
<dt>{fact.label}</dt>
|
||||
<dd className={fact.emphasis ? 'ap-detail__fact-value--emphasis' : undefined}>
|
||||
{fact.value}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
) : null}
|
||||
|
||||
{item.risks && item.risks.length > 0 ? (
|
||||
<div className="ap-detail__risks">
|
||||
{item.risks.map((risk) => (
|
||||
<span
|
||||
key={risk.label}
|
||||
className={`ap-card__risk ap-card__risk--${risk.level ?? 'default'}`}
|
||||
>
|
||||
{risk.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="ap-detail__timeline-section">
|
||||
<Text strong className="ap-detail__section-title">
|
||||
审批进度
|
||||
</Text>
|
||||
<Timeline
|
||||
className="ap-detail__timeline"
|
||||
items={MOCK_TIMELINE.map((step, index) => ({
|
||||
color: step.color,
|
||||
dot:
|
||||
step.color === 'green' ? (
|
||||
<CheckCircleOutlined style={{ fontSize: 14 }} />
|
||||
) : undefined,
|
||||
children: (
|
||||
<div className="ap-detail__timeline-item">
|
||||
<div className="ap-detail__timeline-head">
|
||||
<Text strong>
|
||||
{step.role}
|
||||
{step.name ? ` · ${step.name}` : ''}
|
||||
</Text>
|
||||
{step.time ? (
|
||||
<Text type="secondary" className="ap-detail__timeline-time">
|
||||
{step.time}
|
||||
</Text>
|
||||
) : null}
|
||||
</div>
|
||||
{index === 0 ? (
|
||||
<Text type="secondary">{item.initiatedBy}</Text>
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'flow',
|
||||
label: '审批流程图',
|
||||
children: (
|
||||
<div className="ap-detail__flow-placeholder">
|
||||
<Empty description="流程图占位,后续接入 BPM 流程引擎" />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="ap-detail__actions">
|
||||
<Button>评论</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Empty,
|
||||
Space,
|
||||
Table,
|
||||
Tabs,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import {
|
||||
ClockCircleOutlined,
|
||||
CopyOutlined,
|
||||
ReloadOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { ApprovalCardItem } from '../types';
|
||||
import { formatStatusLabel } from '../config/approvalTypeConfig';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
export interface InsuranceComparisonDetailProps {
|
||||
item: ApprovalCardItem;
|
||||
onRefresh?: () => void;
|
||||
}
|
||||
|
||||
function getFact(item: ApprovalCardItem, label: string): string {
|
||||
return item.keyFacts?.find((fact) => fact.label === label)?.value ?? '-';
|
||||
}
|
||||
|
||||
interface VehicleRow {
|
||||
key: string;
|
||||
plateNo: string;
|
||||
vin: string;
|
||||
insuranceType: string;
|
||||
latestPaymentDate: string;
|
||||
finalQuote: string;
|
||||
status: string;
|
||||
approver: string;
|
||||
remark: string;
|
||||
}
|
||||
|
||||
interface QuoteRow {
|
||||
key: string;
|
||||
company: string;
|
||||
amount: string;
|
||||
isFinal: boolean;
|
||||
remark: string;
|
||||
}
|
||||
|
||||
function buildComparisonNo(item: ApprovalCardItem): string {
|
||||
const datePart = item.initiatedAt.replace(/[-: ]/g, '').slice(0, 8);
|
||||
const seq = item.id.replace(/\D/g, '').padStart(4, '0');
|
||||
return `BXBJ${datePart}${seq}`;
|
||||
}
|
||||
|
||||
function buildSummaryRows(item: ApprovalCardItem, comparisonNo: string) {
|
||||
const finalQuote = getFact(item, '最终报价').replace('¥', '');
|
||||
const insuranceType = getFact(item, '采购险种');
|
||||
const companyCount = getFact(item, '保险公司数量');
|
||||
|
||||
return [
|
||||
{ label: '比价单号', value: comparisonNo },
|
||||
{ label: '采购状态', value: <Tag color="processing">审批中</Tag> },
|
||||
{ label: '创建人', value: item.initiatedBy },
|
||||
{ label: '创建时间', value: item.initiatedAt },
|
||||
{ label: '车辆数', value: '1' },
|
||||
{ label: '险种数', value: '1' },
|
||||
{ label: '已确认报价数', value: '1' },
|
||||
{ label: '确认报价总额', value: finalQuote },
|
||||
{ label: '当前审批人', value: item.currentApprover ?? '-' },
|
||||
{ label: '保险公司数量', value: companyCount },
|
||||
{ label: '采购险种', value: insuranceType },
|
||||
{ label: '中选保险公司', value: item.subtitle?.replace(/^中选:/, '') ?? '-' },
|
||||
];
|
||||
}
|
||||
|
||||
function buildVehicleRows(item: ApprovalCardItem): VehicleRow[] {
|
||||
const finalQuote = getFact(item, '最终报价').replace('¥', '');
|
||||
|
||||
return [
|
||||
{
|
||||
key: '1',
|
||||
plateNo: item.title,
|
||||
vin: '-',
|
||||
insuranceType: getFact(item, '采购险种'),
|
||||
latestPaymentDate: getFact(item, '最晚付费日'),
|
||||
finalQuote,
|
||||
status: '审批中',
|
||||
approver: item.currentApprover ?? '-',
|
||||
remark: item.subtitle ?? '-',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function buildQuoteRows(item: ApprovalCardItem): QuoteRow[] {
|
||||
const winner = item.subtitle?.replace(/^中选:/, '') ?? '国元农业保险上海分公司';
|
||||
const finalAmount = getFact(item, '最终报价').replace('¥', '');
|
||||
|
||||
return [
|
||||
{ key: '1', company: '国任财产保险股份有限公司广东分公司', amount: '111.00', isFinal: false, remark: '-' },
|
||||
{ key: '2', company: winner, amount: finalAmount, isFinal: true, remark: '-' },
|
||||
{ key: '3', company: '中国平安财产保险股份有限公司上海分公司', amount: '888.00', isFinal: false, remark: '-' },
|
||||
];
|
||||
}
|
||||
|
||||
export function InsuranceComparisonDetail({ item, onRefresh }: InsuranceComparisonDetailProps) {
|
||||
const [activeTab, setActiveTab] = useState('detail');
|
||||
const comparisonNo = buildComparisonNo(item);
|
||||
const statusLabel = formatStatusLabel(item);
|
||||
const typeLabel = item.typeLabel ?? '保险比价采购';
|
||||
const summaryRows = buildSummaryRows(item, comparisonNo);
|
||||
const vehicleRows = buildVehicleRows(item);
|
||||
const quoteRows = buildQuoteRows(item);
|
||||
|
||||
const vehicleColumns: ColumnsType<VehicleRow> = [
|
||||
{ title: '车牌号', dataIndex: 'plateNo', width: 110 },
|
||||
{ title: 'VIN', dataIndex: 'vin', width: 120 },
|
||||
{ title: '险种', dataIndex: 'insuranceType', width: 90 },
|
||||
{ title: '最晚付费日', dataIndex: 'latestPaymentDate', width: 120 },
|
||||
{ title: '最终报价', dataIndex: 'finalQuote', width: 100 },
|
||||
{
|
||||
title: '采购状态',
|
||||
dataIndex: 'status',
|
||||
width: 90,
|
||||
render: (value: string) => <Tag color="processing">{value}</Tag>,
|
||||
},
|
||||
{ title: '当前审批人', dataIndex: 'approver', width: 120 },
|
||||
{ title: '备注', dataIndex: 'remark', ellipsis: true },
|
||||
];
|
||||
|
||||
const quoteColumns: ColumnsType<QuoteRow> = [
|
||||
{ title: '保险公司', dataIndex: 'company', ellipsis: true },
|
||||
{ title: '报价金额', dataIndex: 'amount', width: 110 },
|
||||
{
|
||||
title: '最终',
|
||||
dataIndex: 'isFinal',
|
||||
width: 72,
|
||||
render: (isFinal: boolean) =>
|
||||
isFinal ? <Tag color="success">是</Tag> : <Text type="secondary">-</Text>,
|
||||
},
|
||||
{ title: '备注', dataIndex: 'remark', width: 80 },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="ap-detail ap-detail--insurance">
|
||||
<div className="ap-detail__toolbar">
|
||||
<Space size={8}>
|
||||
<Text type="secondary">编号:</Text>
|
||||
<Text copyable={{ icon: <CopyOutlined />, text: comparisonNo }}>{comparisonNo}</Text>
|
||||
</Space>
|
||||
<Button type="text" icon={<ReloadOutlined />} onClick={onRefresh}>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="ap-detail__header">
|
||||
<div className="ap-detail__header-main">
|
||||
<Title level={4} className="ap-detail__title">
|
||||
{typeLabel}审批-{comparisonNo}
|
||||
</Title>
|
||||
<span className="ap-detail__badge ap-detail__badge--pending">{statusLabel}</span>
|
||||
</div>
|
||||
<div className="ap-detail__meta">
|
||||
<Space size={16} wrap>
|
||||
<Space size={6}>
|
||||
<Avatar size={24} icon={<UserOutlined />} />
|
||||
<Text>{item.initiatedBy}</Text>
|
||||
</Space>
|
||||
<Space size={6}>
|
||||
<Text type="secondary">流程分类</Text>
|
||||
<Text>{typeLabel}</Text>
|
||||
</Space>
|
||||
<Space size={6}>
|
||||
<ClockCircleOutlined style={{ color: '#8c8c8c' }} />
|
||||
<Text type="secondary">提交时间</Text>
|
||||
<Text>{item.initiatedAt}</Text>
|
||||
</Space>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
className="ap-detail__tabs"
|
||||
items={[
|
||||
{
|
||||
key: 'detail',
|
||||
label: '审批详情',
|
||||
children: (
|
||||
<div className="ap-detail__content">
|
||||
<table className="ap-insurance-summary">
|
||||
<tbody>
|
||||
{Array.from({ length: Math.ceil(summaryRows.length / 2) }).map((_, rowIndex) => {
|
||||
const left = summaryRows[rowIndex * 2];
|
||||
const right = summaryRows[rowIndex * 2 + 1];
|
||||
return (
|
||||
<tr key={left.label}>
|
||||
<th>{left.label}</th>
|
||||
<td>{left.value}</td>
|
||||
{right ? (
|
||||
<>
|
||||
<th>{right.label}</th>
|
||||
<td>{right.value}</td>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<th />
|
||||
<td />
|
||||
</>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div className="ap-insurance-table-section">
|
||||
<Text strong className="ap-detail__section-title">
|
||||
车辆明细
|
||||
</Text>
|
||||
<Table
|
||||
className="ap-insurance-table"
|
||||
columns={vehicleColumns}
|
||||
dataSource={vehicleRows}
|
||||
pagination={false}
|
||||
size="small"
|
||||
expandable={{
|
||||
expandedRowRender: () => (
|
||||
<Table
|
||||
className="ap-insurance-quote-table"
|
||||
columns={quoteColumns}
|
||||
dataSource={quoteRows}
|
||||
pagination={false}
|
||||
size="small"
|
||||
/>
|
||||
),
|
||||
defaultExpandedRowKeys: ['1'],
|
||||
}}
|
||||
scroll={{ x: 900 }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'flow',
|
||||
label: '审批流程图',
|
||||
children: (
|
||||
<div className="ap-detail__flow-placeholder">
|
||||
<Empty description="流程图占位,后续接入 BPM 流程引擎" />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="ap-detail__actions">
|
||||
<Button>评论</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
71
src/common/oneos-web-approval/config/approvalTypeConfig.ts
Normal file
71
src/common/oneos-web-approval/config/approvalTypeConfig.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { ApprovalCardItem, ApprovalStatus, ApprovalTypeKey, StatusFilterValue } from '../types';
|
||||
|
||||
export interface ApprovalTypeOption {
|
||||
value: ApprovalTypeKey;
|
||||
label: string;
|
||||
shortLabel: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export const APPROVAL_TYPE_CONFIG: Record<ApprovalTypeKey, ApprovalTypeOption> = {
|
||||
parking: { value: 'parking', label: '停车场', shortLabel: '停车场', color: '#6366f1' },
|
||||
vehicle_procurement: { value: 'vehicle_procurement', label: '车辆采购', shortLabel: '采购', color: '#8b5cf6' },
|
||||
lease: { value: 'lease', label: '租赁合同', shortLabel: '租赁', color: '#2563eb' },
|
||||
charter: { value: 'charter', label: '包车合同', shortLabel: '包车', color: '#0ea5e9' },
|
||||
transfer: { value: 'transfer', label: '调拨', shortLabel: '调拨', color: '#14b8a6' },
|
||||
vehicle_change: { value: 'vehicle_change', label: '异动', shortLabel: '异动', color: '#f59e0b' },
|
||||
replacement: { value: 'replacement', label: '替换车', shortLabel: '替换', color: '#f97316' },
|
||||
delivery: { value: 'delivery', label: '交车', shortLabel: '交车', color: '#10b981' },
|
||||
return_settlement: { value: 'return_settlement', label: '还车应结款', shortLabel: '还车结款', color: '#ef4444' },
|
||||
pickup_receivable: { value: 'pickup_receivable', label: '提车应收款', shortLabel: '提车应收', color: '#ec4899' },
|
||||
billing: { value: 'billing', label: '租赁账单', shortLabel: '账单', color: '#a855f7' },
|
||||
insurance: { value: 'insurance', label: '保险采购', shortLabel: '保险', color: '#22c55e' },
|
||||
supplier: { value: 'supplier', label: '供应商准入', shortLabel: '供应商', color: '#64748b' },
|
||||
customer_risk: { value: 'customer_risk', label: '客户风险标签', shortLabel: '风险标签', color: '#dc2626' },
|
||||
third_party_return: { value: 'third_party_return', label: '三方退租', shortLabel: '退租', color: '#d97706' },
|
||||
contract_template: { value: 'contract_template', label: '合同模板', shortLabel: '模板', color: '#7c3aed' },
|
||||
annual_inspection: { value: 'annual_inspection', label: '年审', shortLabel: '年审', color: '#0891b2' },
|
||||
finance_payment: { value: 'finance_payment', label: '付款申请', shortLabel: '付款', color: '#be185d' },
|
||||
clearing: { value: 'clearing', label: '清分结算', shortLabel: '清分', color: '#4f46e5' },
|
||||
energy_account: { value: 'energy_account', label: '能源账户', shortLabel: '能源', color: '#059669' },
|
||||
};
|
||||
|
||||
export const APPROVAL_TYPE_OPTIONS = Object.values(APPROVAL_TYPE_CONFIG);
|
||||
|
||||
export const STATUS_OPTIONS: { value: StatusFilterValue; label: string }[] = [
|
||||
{ value: 'all', label: '全部' },
|
||||
{ value: 'pending', label: '审批中' },
|
||||
{ value: 'approved', label: '已通过' },
|
||||
{ value: 'rejected', label: '已驳回' },
|
||||
];
|
||||
|
||||
const STATUS_LABEL_MAP: Record<ApprovalStatus, string> = {
|
||||
pending: '审批中',
|
||||
processing: '审批中',
|
||||
approved: '已通过',
|
||||
rejected: '已驳回',
|
||||
};
|
||||
|
||||
export function getApprovalTypeLabel(type: ApprovalTypeKey): string {
|
||||
return APPROVAL_TYPE_CONFIG[type]?.label ?? type;
|
||||
}
|
||||
|
||||
export function getApprovalTypeColor(type: ApprovalTypeKey): string {
|
||||
return APPROVAL_TYPE_CONFIG[type]?.color ?? '#64748b';
|
||||
}
|
||||
|
||||
export function formatStatusLabel(item: Pick<ApprovalCardItem, 'status' | 'currentApprover'>): string {
|
||||
const base = STATUS_LABEL_MAP[item.status] ?? item.status;
|
||||
if ((item.status === 'pending' || item.status === 'processing') && item.currentApprover) {
|
||||
return `审批中:${item.currentApprover}`;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
export function matchesStatusFilter(status: ApprovalStatus, filter: StatusFilterValue): boolean {
|
||||
if (filter === 'all') return true;
|
||||
if (filter === 'pending') return status === 'pending' || status === 'processing';
|
||||
if (filter === 'approved') return status === 'approved';
|
||||
if (filter === 'rejected') return status === 'rejected';
|
||||
return true;
|
||||
}
|
||||
50
src/common/oneos-web-approval/data/mockApprovalCases.ts
Normal file
50
src/common/oneos-web-approval/data/mockApprovalCases.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { ApprovalCardItem } from '../types';
|
||||
import { CURRENT_USER } from '../types';
|
||||
|
||||
export const MOCK_APPROVAL_CASES: ApprovalCardItem[] = [
|
||||
{
|
||||
id: 'ac-22',
|
||||
type: 'lease',
|
||||
typeLabel: '合同审批',
|
||||
status: 'rejected',
|
||||
listTab: 'initiated',
|
||||
title: '杭州迅达运输有限公司',
|
||||
subtitle: '杭州城配项目',
|
||||
keyFacts: [
|
||||
{ label: '租赁车辆数', value: '5 辆' },
|
||||
{ label: '租金及服务费合计', value: '192000.00 元' },
|
||||
{ label: '保证金总额', value: '40000.00 元' },
|
||||
{ label: '氢气预付款金额', value: '20000.00 元' },
|
||||
],
|
||||
showInitiatorInFooter: true,
|
||||
initiatedBy: CURRENT_USER,
|
||||
initiatedAt: '2026-06-10 14:00',
|
||||
},
|
||||
{
|
||||
id: 'ac-23',
|
||||
type: 'insurance',
|
||||
typeLabel: '保险比价采购',
|
||||
status: 'processing',
|
||||
listTab: 'initiated',
|
||||
title: '沪A51676F',
|
||||
subtitle: '中选:国元农业保险上海分公司',
|
||||
keyFacts: [
|
||||
{ label: '采购险种', value: '商业险' },
|
||||
{ label: '最终报价', value: '¥1000.00' },
|
||||
{ label: '保险公司数量', value: '3家' },
|
||||
{ label: '最晚付费日', value: '2026-08-09' },
|
||||
],
|
||||
currentApprover: '财务部-赵总',
|
||||
showInitiatorInFooter: true,
|
||||
initiatedBy: CURRENT_USER,
|
||||
initiatedAt: '2026-06-10 14:00',
|
||||
},
|
||||
];
|
||||
|
||||
export function filterCasesByTab(
|
||||
cases: ApprovalCardItem[],
|
||||
tabKey?: string,
|
||||
): ApprovalCardItem[] {
|
||||
if (!tabKey) return cases;
|
||||
return cases.filter((item) => item.listTab === tabKey);
|
||||
}
|
||||
156
src/common/oneos-web-approval/pages/ApprovalCenterPage.tsx
Normal file
156
src/common/oneos-web-approval/pages/ApprovalCenterPage.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Input, Select, Tabs } from 'antd';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import type { ApprovalCardItem, ApprovalListTab, ApprovalStatus } from '../types';
|
||||
import { ApprovalCardList } from '../components/ApprovalCardList';
|
||||
import { ApprovalDetailPlaceholder } from '../components/ApprovalDetailPlaceholder';
|
||||
import {
|
||||
filterCasesByTab,
|
||||
MOCK_APPROVAL_CASES,
|
||||
} from '../data/mockApprovalCases';
|
||||
import {
|
||||
APPROVAL_TYPE_OPTIONS,
|
||||
formatStatusLabel,
|
||||
} from '../config/approvalTypeConfig';
|
||||
|
||||
const TAB_ITEMS: { key: ApprovalListTab; label: string }[] = [
|
||||
{ key: 'todo', label: '我的待办' },
|
||||
{ key: 'initiated', label: '我发起的' },
|
||||
{ key: 'done', label: '我的已办' },
|
||||
{ key: 'cc', label: '我的抄送' },
|
||||
];
|
||||
|
||||
const STATUS_OPTIONS: { value: ApprovalStatus | 'all'; label: string }[] = [
|
||||
{ value: 'all', label: '全部状态' },
|
||||
{ value: 'pending', label: '审批中' },
|
||||
{ value: 'approved', label: '已通过' },
|
||||
{ value: 'rejected', label: '已驳回' },
|
||||
];
|
||||
|
||||
export interface ApprovalCenterPageProps {
|
||||
tabKey?: ApprovalListTab;
|
||||
pageTitle?: string;
|
||||
showTabs?: boolean;
|
||||
}
|
||||
|
||||
export function ApprovalCenterPage({
|
||||
tabKey = 'todo',
|
||||
pageTitle,
|
||||
showTabs = false,
|
||||
}: ApprovalCenterPageProps) {
|
||||
const [activeTab, setActiveTab] = useState<ApprovalListTab>(tabKey);
|
||||
const [statusFilter, setStatusFilter] = useState<ApprovalStatus | 'all'>('all');
|
||||
const [typeFilter, setTypeFilter] = useState<string>('all');
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveTab(tabKey);
|
||||
}, [tabKey]);
|
||||
|
||||
const tabItems = useMemo(() => {
|
||||
let list = filterCasesByTab(MOCK_APPROVAL_CASES, activeTab);
|
||||
|
||||
if (statusFilter !== 'all') {
|
||||
list = list.filter((item) => item.status === statusFilter);
|
||||
}
|
||||
if (typeFilter !== 'all') {
|
||||
list = list.filter((item) => item.type === typeFilter);
|
||||
}
|
||||
if (keyword.trim()) {
|
||||
const q = keyword.trim().toLowerCase();
|
||||
list = list.filter(
|
||||
(item) =>
|
||||
item.title.toLowerCase().includes(q) ||
|
||||
(item.subtitle?.toLowerCase().includes(q) ?? false) ||
|
||||
item.initiatedBy.toLowerCase().includes(q) ||
|
||||
formatStatusLabel(item).toLowerCase().includes(q),
|
||||
);
|
||||
}
|
||||
return list;
|
||||
}, [activeTab, statusFilter, typeFilter, keyword]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tabItems.length === 0) {
|
||||
setSelectedId(null);
|
||||
return;
|
||||
}
|
||||
if (!selectedId || !tabItems.some((item) => item.id === selectedId)) {
|
||||
setSelectedId(tabItems[0].id);
|
||||
}
|
||||
}, [tabItems, selectedId]);
|
||||
|
||||
const selectedItem = useMemo(
|
||||
() => tabItems.find((item) => item.id === selectedId) ?? null,
|
||||
[tabItems, selectedId],
|
||||
);
|
||||
|
||||
const handleItemClick = (item: ApprovalCardItem) => {
|
||||
setSelectedId(item.id);
|
||||
};
|
||||
|
||||
const currentTabMeta = TAB_ITEMS.find((t) => t.key === activeTab);
|
||||
const displayTitle = pageTitle ?? currentTabMeta?.label ?? '审批中心';
|
||||
|
||||
return (
|
||||
<div className="ap-page">
|
||||
{showTabs ? (
|
||||
<div className="ap-page__tabs-bar">
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={(key) => setActiveTab(key as ApprovalListTab)}
|
||||
items={TAB_ITEMS.map((t) => ({ key: t.key, label: t.label }))}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="ap-page__shell">
|
||||
<aside className="ap-page__sidebar">
|
||||
<div className="ap-sidebar__head">
|
||||
<h1 className="ap-sidebar__title">{displayTitle}</h1>
|
||||
<p className="ap-sidebar__desc">按状态、流程类型与关键词筛选审批事项</p>
|
||||
</div>
|
||||
|
||||
<div className="ap-filter ap-filter--sidebar">
|
||||
<Select
|
||||
className="ap-filter__control"
|
||||
value={statusFilter}
|
||||
onChange={setStatusFilter}
|
||||
options={STATUS_OPTIONS}
|
||||
placeholder="审批状态"
|
||||
/>
|
||||
<Select
|
||||
className="ap-filter__control"
|
||||
value={typeFilter}
|
||||
onChange={setTypeFilter}
|
||||
options={[{ value: 'all', label: '全部类型' }, ...APPROVAL_TYPE_OPTIONS]}
|
||||
placeholder="流程类型"
|
||||
/>
|
||||
<Input
|
||||
className="ap-filter__search"
|
||||
allowClear
|
||||
prefix={<SearchOutlined />}
|
||||
placeholder="关键词搜索"
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="ap-sidebar__list">
|
||||
<ApprovalCardList
|
||||
items={tabItems}
|
||||
selectedId={selectedId}
|
||||
onItemClick={handleItemClick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="ap-sidebar__foot">共 {tabItems.length} 条</div>
|
||||
</aside>
|
||||
|
||||
<main className="ap-page__detail">
|
||||
<ApprovalDetailPlaceholder item={selectedItem} />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
603
src/common/oneos-web-approval/styles/index.css
Normal file
603
src/common/oneos-web-approval/styles/index.css
Normal file
@@ -0,0 +1,603 @@
|
||||
.ap-page {
|
||||
--ap-primary: #165dff;
|
||||
--ap-primary-soft: rgba(22, 93, 255, 0.08);
|
||||
--ap-border: #e5e7eb;
|
||||
--ap-surface: #ffffff;
|
||||
--ap-muted: #64748b;
|
||||
--ap-text: #0f172a;
|
||||
--ap-radius: 12px;
|
||||
--ap-shadow: 0 1px 2px rgba(15, 23, 42, 0.06), 0 4px 16px rgba(15, 23, 42, 0.04);
|
||||
--ap-sidebar-width: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
padding: 0;
|
||||
background: var(--vm-bg, #f5f7fa);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ap-page__tabs-bar {
|
||||
flex-shrink: 0;
|
||||
padding: 12px 20px 0;
|
||||
background: var(--ap-surface);
|
||||
border-bottom: 1px solid var(--ap-border);
|
||||
}
|
||||
|
||||
.ap-page__tabs-bar .ant-tabs-nav {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.ap-page__shell {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.ap-page__sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
width: var(--ap-sidebar-width);
|
||||
background: var(--ap-surface);
|
||||
border-right: 1px solid var(--ap-border);
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.ap-sidebar__head {
|
||||
flex-shrink: 0;
|
||||
padding: 20px 16px 12px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.ap-sidebar__title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--ap-text);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.ap-sidebar__desc {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--ap-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.ap-filter {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.ap-filter--sidebar {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.ap-filter__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.ap-filter__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ap-filter__label {
|
||||
font-size: 13px;
|
||||
color: var(--ap-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ap-filter__control {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ap-filter__search {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ap-sidebar__list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.ap-sidebar__foot {
|
||||
flex-shrink: 0;
|
||||
padding: 10px 16px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
font-size: 12px;
|
||||
color: var(--ap-muted);
|
||||
text-align: center;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.ap-page__detail {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: var(--ap-surface);
|
||||
}
|
||||
|
||||
.ap-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ap-list--loading,
|
||||
.ap-list--empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 200px;
|
||||
background: transparent;
|
||||
border: 1px dashed var(--ap-border);
|
||||
border-radius: var(--ap-radius);
|
||||
}
|
||||
|
||||
.ap-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--ap-surface);
|
||||
border: 1px solid var(--ap-border);
|
||||
border-radius: var(--ap-radius);
|
||||
box-shadow: var(--ap-shadow);
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ap-card:hover,
|
||||
.ap-card:focus-visible {
|
||||
border-color: rgba(22, 93, 255, 0.35);
|
||||
box-shadow: 0 4px 16px rgba(22, 93, 255, 0.08);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ap-card--selected {
|
||||
border-color: rgba(22, 93, 255, 0.5);
|
||||
background: var(--ap-primary-soft);
|
||||
box-shadow: 0 4px 16px rgba(22, 93, 255, 0.1);
|
||||
}
|
||||
|
||||
.ap-card--selected::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
background: var(--ap-primary);
|
||||
border-radius: var(--ap-radius) 0 0 var(--ap-radius);
|
||||
}
|
||||
|
||||
.ap-card__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 16px 0;
|
||||
}
|
||||
|
||||
.ap-card__type {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid transparent;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.ap-status {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.ap-status--pending {
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.ap-status--approved {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.ap-status--rejected {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.ap-card__body {
|
||||
flex: 1;
|
||||
padding: 12px 16px 0;
|
||||
}
|
||||
|
||||
.ap-card__title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--ap-text);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.ap-card__subtitle {
|
||||
margin: 6px 0 0;
|
||||
font-size: 13px;
|
||||
color: var(--ap-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.ap-card__facts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px 12px;
|
||||
margin: 14px 0 0;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.ap-card--selected .ap-card__facts {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.ap-card__fact {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ap-card__fact dt {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--ap-muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.ap-card__fact dd {
|
||||
margin: 4px 0 0;
|
||||
font-size: 13px;
|
||||
color: var(--ap-text);
|
||||
font-weight: 500;
|
||||
line-height: 1.45;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.ap-card__fact-value--emphasis {
|
||||
color: var(--ap-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.ap-card__risks {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.ap-card__risk {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
background: #fff7ed;
|
||||
color: #c2410c;
|
||||
border: 1px solid #fed7aa;
|
||||
}
|
||||
|
||||
.ap-card__risk--high {
|
||||
background: #fef2f2;
|
||||
color: #b91c1c;
|
||||
border-color: #fecaca;
|
||||
}
|
||||
|
||||
.ap-card__risk--medium {
|
||||
background: #fffbeb;
|
||||
color: #b45309;
|
||||
border-color: #fde68a;
|
||||
}
|
||||
|
||||
.ap-card__footer {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: 10px;
|
||||
padding: 8px 16px 10px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.ap-card__footer-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.ap-card__footer-text {
|
||||
font-size: 12px;
|
||||
color: var(--ap-muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.ap-card__action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--ap-primary);
|
||||
line-height: 1.4;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Detail panel */
|
||||
.ap-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.ap-detail--empty {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ap-detail__toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
padding: 12px 24px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.ap-detail__header {
|
||||
flex-shrink: 0;
|
||||
padding: 20px 24px 0;
|
||||
}
|
||||
|
||||
.ap-detail__header-main {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.ap-detail__title {
|
||||
margin: 0 !important;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ap-detail__badge {
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.ap-detail__badge--pending {
|
||||
background: #fff7ed;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.ap-detail__badge--approved {
|
||||
background: #f0fdf4;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.ap-detail__badge--rejected {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.ap-detail__meta {
|
||||
margin-top: 12px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.ap-detail__tabs {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.ap-detail__tabs .ant-tabs-content-holder {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.ap-detail__tabs .ant-tabs-content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ap-detail__content {
|
||||
padding: 8px 0 24px;
|
||||
}
|
||||
|
||||
.ap-detail__subtitle {
|
||||
margin: 0 0 16px;
|
||||
color: var(--ap-muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ap-detail__facts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px 16px;
|
||||
margin: 0 0 16px;
|
||||
padding: 16px;
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.ap-detail__fact dt {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--ap-muted);
|
||||
}
|
||||
|
||||
.ap-detail__fact dd {
|
||||
margin: 4px 0 0;
|
||||
font-size: 14px;
|
||||
color: var(--ap-text);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ap-detail__fact-value--emphasis {
|
||||
color: var(--ap-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.ap-detail__risks {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.ap-detail__timeline-section {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.ap-detail__section-title {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.ap-detail__timeline {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.ap-detail__timeline-item {
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.ap-detail__timeline-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.ap-detail__timeline-time {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ap-detail__flow-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 280px;
|
||||
}
|
||||
|
||||
.ap-detail__actions {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 12px 24px 20px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
background: var(--ap-surface);
|
||||
}
|
||||
|
||||
.ap-insurance-summary {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid var(--ap-border);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.ap-insurance-summary th,
|
||||
.ap-insurance-summary td {
|
||||
padding: 10px 14px;
|
||||
border: 1px solid #f1f5f9;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.ap-insurance-summary th {
|
||||
width: 14%;
|
||||
background: #fafbfc;
|
||||
color: var(--ap-muted);
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ap-insurance-summary td {
|
||||
width: 36%;
|
||||
color: var(--ap-text);
|
||||
}
|
||||
|
||||
.ap-insurance-table-section {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.ap-insurance-table .ant-table,
|
||||
.ap-insurance-quote-table .ant-table {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.ap-insurance-quote-table {
|
||||
margin: 0;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.ap-insurance-quote-table .ant-table-thead > tr > th {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.ap-page {
|
||||
height: auto;
|
||||
min-height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.ap-page__shell {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ap-page__sidebar {
|
||||
width: 100%;
|
||||
max-height: 50vh;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--ap-border);
|
||||
}
|
||||
|
||||
.ap-page__detail {
|
||||
min-height: 50vh;
|
||||
}
|
||||
|
||||
.ap-filter__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.ap-detail__facts {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
73
src/common/oneos-web-approval/types.ts
Normal file
73
src/common/oneos-web-approval/types.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
export type ApprovalStatus = 'pending' | 'processing' | 'approved' | 'rejected';
|
||||
|
||||
export type ApprovalTabKey = 'todo' | 'done' | 'initiated' | 'cc';
|
||||
|
||||
export type ApprovalTypeKey =
|
||||
| 'parking'
|
||||
| 'vehicle_procurement'
|
||||
| 'lease'
|
||||
| 'charter'
|
||||
| 'transfer'
|
||||
| 'vehicle_change'
|
||||
| 'replacement'
|
||||
| 'delivery'
|
||||
| 'return_settlement'
|
||||
| 'pickup_receivable'
|
||||
| 'billing'
|
||||
| 'insurance'
|
||||
| 'supplier'
|
||||
| 'customer_risk'
|
||||
| 'third_party_return'
|
||||
| 'contract_template'
|
||||
| 'annual_inspection'
|
||||
| 'finance_payment'
|
||||
| 'clearing'
|
||||
| 'energy_account';
|
||||
|
||||
export type ApprovalListTab = ApprovalTabKey;
|
||||
|
||||
export interface ApprovalKeyFact {
|
||||
label: string;
|
||||
value: string;
|
||||
emphasis?: boolean;
|
||||
}
|
||||
|
||||
export interface ApprovalRisk {
|
||||
label: string;
|
||||
level?: 'high' | 'medium' | 'low';
|
||||
}
|
||||
|
||||
export interface ApprovalCardItem {
|
||||
id: string;
|
||||
type: ApprovalTypeKey;
|
||||
status: ApprovalStatus;
|
||||
listTab: ApprovalListTab;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
keyFacts?: ApprovalKeyFact[];
|
||||
risks?: ApprovalRisk[];
|
||||
currentApprover?: string;
|
||||
initiatedBy: string;
|
||||
initiatedAt: string;
|
||||
typeLabel?: string;
|
||||
footerText?: string;
|
||||
showInitiatorInFooter?: boolean;
|
||||
ccUsers?: string[];
|
||||
handledBy?: string;
|
||||
}
|
||||
|
||||
export const CURRENT_USER = '张明辉';
|
||||
|
||||
export type StatusFilterValue = 'all' | 'pending' | 'approved' | 'rejected';
|
||||
|
||||
export interface ApprovalFilters {
|
||||
status: StatusFilterValue;
|
||||
type: ApprovalTypeKey | 'all';
|
||||
keyword: string;
|
||||
}
|
||||
|
||||
export const EMPTY_APPROVAL_FILTERS: ApprovalFilters = {
|
||||
status: 'all',
|
||||
type: 'all',
|
||||
keyword: '',
|
||||
};
|
||||
@@ -11,6 +11,37 @@
|
||||
"title": "工作台",
|
||||
"itemKey": "prototypes/oneos-web-workbench"
|
||||
},
|
||||
{
|
||||
"id": "folder-oneos-approval",
|
||||
"kind": "folder",
|
||||
"title": "审批中心",
|
||||
"children": [
|
||||
{
|
||||
"id": "item:prototypes:oneos-web-approval-initiated",
|
||||
"kind": "item",
|
||||
"title": "我发起的",
|
||||
"itemKey": "prototypes/oneos-web-approval-initiated"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:oneos-web-approval-todo",
|
||||
"kind": "item",
|
||||
"title": "我的待办",
|
||||
"itemKey": "prototypes/oneos-web-approval-todo"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:oneos-web-approval-done",
|
||||
"kind": "item",
|
||||
"title": "我的已办",
|
||||
"itemKey": "prototypes/oneos-web-approval-done"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:oneos-web-approval-cc",
|
||||
"kind": "item",
|
||||
"title": "我的抄送",
|
||||
"itemKey": "prototypes/oneos-web-approval-cc"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "folder-1782874599304-roj8fd",
|
||||
"kind": "folder",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": 1,
|
||||
"updatedAt": "2026-07-14T21:21:56.182Z",
|
||||
"updatedAt": "2026-07-16T01:04:29.807Z",
|
||||
"title": "小羚羚",
|
||||
"sectionId": "folder-prototypes-xll-miniapp",
|
||||
"description": "氢能车辆运营移动端原型;菜单与小羚羚「小程序」项目目录同步。",
|
||||
|
||||
8
src/prototypes/oneos-web-approval-cc/index.tsx
Normal file
8
src/prototypes/oneos-web-approval-cc/index.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @name 我的抄送
|
||||
*/
|
||||
import { ApprovalApp } from '../../common/oneos-web-approval/ApprovalApp';
|
||||
|
||||
export default function OneosWebApprovalCc() {
|
||||
return <ApprovalApp tabKey="cc" pageTitle="我的抄送" />;
|
||||
}
|
||||
8
src/prototypes/oneos-web-approval-done/index.tsx
Normal file
8
src/prototypes/oneos-web-approval-done/index.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @name 我的已办
|
||||
*/
|
||||
import { ApprovalApp } from '../../common/oneos-web-approval/ApprovalApp';
|
||||
|
||||
export default function OneosWebApprovalDone() {
|
||||
return <ApprovalApp tabKey="done" pageTitle="我的已办" />;
|
||||
}
|
||||
8
src/prototypes/oneos-web-approval-initiated/index.tsx
Normal file
8
src/prototypes/oneos-web-approval-initiated/index.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @name 我发起的
|
||||
*/
|
||||
import { ApprovalApp } from '../../common/oneos-web-approval/ApprovalApp';
|
||||
|
||||
export default function OneosWebApprovalInitiated() {
|
||||
return <ApprovalApp tabKey="initiated" pageTitle="我发起的" />;
|
||||
}
|
||||
8
src/prototypes/oneos-web-approval-todo/index.tsx
Normal file
8
src/prototypes/oneos-web-approval-todo/index.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @name 我的待办
|
||||
*/
|
||||
import { ApprovalApp } from '../../common/oneos-web-approval/ApprovalApp';
|
||||
|
||||
export default function OneosWebApprovalTodo() {
|
||||
return <ApprovalApp tabKey="todo" pageTitle="我的待办" />;
|
||||
}
|
||||
8
src/prototypes/oneos-web-approval/index.tsx
Normal file
8
src/prototypes/oneos-web-approval/index.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @name 审批中心
|
||||
*/
|
||||
import { ApprovalApp } from '../../common/oneos-web-approval/ApprovalApp';
|
||||
|
||||
export default function OneosWebApproval() {
|
||||
return <ApprovalApp pageTitle="审批中心" showTabs />;
|
||||
}
|
||||
Reference in New Issue
Block a user