Initial commit: X-Server Mobile enterprise tool system
- Frontend: React 18 + TypeScript + Vite + Ant Design Mobile - Backend: PHP 8.5 + Webman 2.x + MySQL + Redis - i18n support: Chinese, English, Russian, Spanish, French - Approval workflow engine - Docker development environment - Feishu integration Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.approveBtn {
|
||||
background: #52c41a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.rejectBtn {
|
||||
background: #ff4d4f;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.cancelBtn {
|
||||
background: #8c8c8c;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.modalContent {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #e5e5e5;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.textarea:focus {
|
||||
outline: none;
|
||||
border-color: #1677ff;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { useApprovalAction } from '../hooks/useApprovalAction'
|
||||
import type { ApprovalStatus } from '../types'
|
||||
import { Modal } from 'antd-mobile'
|
||||
import { useState } from 'react'
|
||||
import styles from './ApprovalAction.module.css'
|
||||
|
||||
interface ApprovalActionProps {
|
||||
orderId: string
|
||||
status: ApprovalStatus
|
||||
}
|
||||
|
||||
export default function ApprovalAction({ orderId, status }: ApprovalActionProps) {
|
||||
const { approve, reject, cancel, isApproving, isRejecting, isCancelling } = useApprovalAction()
|
||||
const [comment, setComment] = useState('')
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [actionType, setActionType] = useState<'approve' | 'reject'>('approve')
|
||||
|
||||
const canAction = status === 'pending'
|
||||
const canCancel = status === 'pending' || status === 'draft'
|
||||
|
||||
const handleApprove = () => {
|
||||
approve({ orderId, comment: comment || undefined })
|
||||
setShowModal(false)
|
||||
setComment('')
|
||||
}
|
||||
|
||||
const handleReject = () => {
|
||||
reject({ orderId, comment: comment || '已拒绝' })
|
||||
setShowModal(false)
|
||||
setComment('')
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
if (confirm('确定要撤销此申请吗?')) {
|
||||
cancel(orderId)
|
||||
}
|
||||
}
|
||||
|
||||
const openModal = (action: 'approve' | 'reject') => {
|
||||
setActionType(action)
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
if (!canAction && !canCancel) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.actions}>
|
||||
{canAction && (
|
||||
<>
|
||||
<button
|
||||
className={`${styles.btn} ${styles.approveBtn}`}
|
||||
onClick={() => openModal('approve')}
|
||||
disabled={isApproving}
|
||||
>
|
||||
{isApproving ? '处理中...' : '通过'}
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.btn} ${styles.rejectBtn}`}
|
||||
onClick={() => openModal('reject')}
|
||||
disabled={isRejecting}
|
||||
>
|
||||
{isRejecting ? '处理中...' : '拒绝'}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{canCancel && !canAction && (
|
||||
<button
|
||||
className={`${styles.btn} ${styles.cancelBtn}`}
|
||||
onClick={handleCancel}
|
||||
disabled={isCancelling}
|
||||
>
|
||||
{isCancelling ? '处理中...' : '撤销申请'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
visible={showModal}
|
||||
title={actionType === 'approve' ? '审批通过' : '审批拒绝'}
|
||||
onClose={() => setShowModal(false)}
|
||||
content={
|
||||
<div className={styles.modalContent}>
|
||||
<textarea
|
||||
className={styles.textarea}
|
||||
placeholder={actionType === 'approve' ? '请输入审批意见(可选)' : '请输入拒绝原因'}
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
actions={[
|
||||
{
|
||||
key: 'cancel',
|
||||
text: '取消',
|
||||
onClick: () => setShowModal(false),
|
||||
},
|
||||
{
|
||||
key: 'confirm',
|
||||
text: actionType === 'approve' ? '确认通过' : '确认拒绝',
|
||||
primary: true,
|
||||
onClick: actionType === 'approve' ? handleApprove : handleReject,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
.loading,
|
||||
.error,
|
||||
.empty {
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
color: #8c8c8c;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.detail {
|
||||
padding: 16px;
|
||||
padding-bottom: calc(16px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 4px 12px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.formData {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.formRow {
|
||||
display: flex;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.formRow:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.formLabel {
|
||||
width: 100px;
|
||||
color: #8c8c8c;
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.formValue {
|
||||
flex: 1;
|
||||
color: #262626;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.timeline {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.timelineItem {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.timelineItem:not(:last-child)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 28px;
|
||||
bottom: -12px;
|
||||
width: 2px;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.timelineDot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #1677ff;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 0 0 2px #e6f4ff;
|
||||
flex-shrink: 0;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.timelineContent {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.timelineHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.timelineUser {
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.timelineAction {
|
||||
font-size: 13px;
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.timelineComment {
|
||||
font-size: 14px;
|
||||
color: #595959;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.timelineTime {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { getApprovalDetail } from '../services/approval'
|
||||
import ApprovalFlow from './ApprovalFlow'
|
||||
import ApprovalAction from './ApprovalAction'
|
||||
import styles from './ApprovalDetail.module.css'
|
||||
|
||||
interface ApprovalDetailProps {
|
||||
id: string
|
||||
}
|
||||
|
||||
export default function ApprovalDetail({ id }: ApprovalDetailProps) {
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ['approval-detail', id],
|
||||
queryFn: () => getApprovalDetail(id),
|
||||
})
|
||||
|
||||
if (isLoading) {
|
||||
return <div className={styles.loading}>加载中...</div>
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div className={styles.error}>加载失败: {error.message}</div>
|
||||
}
|
||||
|
||||
const order = data?.data
|
||||
|
||||
if (!order) {
|
||||
return <div className={styles.empty}>未找到审批详情</div>
|
||||
}
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
draft: '草稿',
|
||||
pending: '审批中',
|
||||
approved: '已通过',
|
||||
rejected: '已拒绝',
|
||||
cancelled: '已撤销',
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.detail}>
|
||||
{/* 标题 */}
|
||||
<div className={styles.header}>
|
||||
<h1 className={styles.title}>{order.title}</h1>
|
||||
<span className={styles.status}>{getStatusText(order.status)}</span>
|
||||
</div>
|
||||
|
||||
{/* 审批流程 */}
|
||||
{order.flow && (
|
||||
<div className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>审批流程</h2>
|
||||
<ApprovalFlow nodes={order.flow.nodes || []} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 表单数据 */}
|
||||
{order.formData && Object.keys(order.formData).length > 0 && (
|
||||
<div className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>申请内容</h2>
|
||||
<div className={styles.formData}>
|
||||
{Object.entries(order.formData).map(([key, value]) => (
|
||||
<div key={key} className={styles.formRow}>
|
||||
<span className={styles.formLabel}>{key}:</span>
|
||||
<span className={styles.formValue}>{String(value)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 审批记录 */}
|
||||
{order.timeline && order.timeline.length > 0 && (
|
||||
<div className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>审批记录</h2>
|
||||
<div className={styles.timeline}>
|
||||
{order.timeline.map((record: any) => (
|
||||
<div key={record.id} className={styles.timelineItem}>
|
||||
<div className={styles.timelineDot} />
|
||||
<div className={styles.timelineContent}>
|
||||
<div className={styles.timelineHeader}>
|
||||
<span className={styles.timelineUser}>{record.approver?.name}</span>
|
||||
<span className={styles.timelineAction}>
|
||||
{record.action === 'approve' && '通过'}
|
||||
{record.action === 'reject' && '拒绝'}
|
||||
{record.action === 'submit' && '提交'}
|
||||
{record.action === 'cancel' && '撤销'}
|
||||
</span>
|
||||
</div>
|
||||
{record.comment && (
|
||||
<div className={styles.timelineComment}>{record.comment}</div>
|
||||
)}
|
||||
<div className={styles.timelineTime}>
|
||||
{new Date(record.createdAt).toLocaleString('zh-CN')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<ApprovalAction orderId={order.id} status={order.status} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
.flow {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.nodeWrapper {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.nodeIcon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nodeContent {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nodeName {
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.nodeApprovers {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.approver {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
margin-right: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.connector {
|
||||
width: 28px;
|
||||
padding-left: 13px;
|
||||
}
|
||||
|
||||
.line {
|
||||
width: 2px;
|
||||
height: 20px;
|
||||
background: #e5e5e5;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { ApprovalNode, NodeStatus } from '../types'
|
||||
import styles from './ApprovalFlow.module.css'
|
||||
|
||||
interface ApprovalFlowProps {
|
||||
nodes: ApprovalNode[]
|
||||
currentStatus?: NodeStatus
|
||||
}
|
||||
|
||||
export default function ApprovalFlow({ nodes, currentStatus }: ApprovalFlowProps) {
|
||||
const getNodeStatus = (node: ApprovalNode): NodeStatus => {
|
||||
if (node.status) return node.status
|
||||
return 'pending'
|
||||
}
|
||||
|
||||
const getNodeIcon = (status: NodeStatus): string => {
|
||||
switch (status) {
|
||||
case 'approved':
|
||||
return '✓'
|
||||
case 'rejected':
|
||||
return '✕'
|
||||
case 'skipped':
|
||||
return '○'
|
||||
default:
|
||||
return '○'
|
||||
}
|
||||
}
|
||||
|
||||
const getNodeColor = (status: NodeStatus): string => {
|
||||
switch (status) {
|
||||
case 'approved':
|
||||
return '#52c41a'
|
||||
case 'rejected':
|
||||
return '#ff4d4f'
|
||||
case 'skipped':
|
||||
return '#d9d9d9'
|
||||
default:
|
||||
return '#d9d9d9'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.flow}>
|
||||
{nodes.map((node, index) => {
|
||||
const status = getNodeStatus(node)
|
||||
const isLast = index === nodes.length - 1
|
||||
|
||||
return (
|
||||
<div key={node.id} className={styles.nodeWrapper}>
|
||||
<div className={styles.node}>
|
||||
<div
|
||||
className={styles.nodeIcon}
|
||||
style={{ backgroundColor: getNodeColor(status) }}
|
||||
>
|
||||
{getNodeIcon(status)}
|
||||
</div>
|
||||
<div className={styles.nodeContent}>
|
||||
<div className={styles.nodeName}>{node.name}</div>
|
||||
<div className={styles.nodeApprovers}>
|
||||
{node.approvers.map((approver) => (
|
||||
<span key={approver.id} className={styles.approver}>
|
||||
{approver.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!isLast && (
|
||||
<div className={styles.connector}>
|
||||
<div className={styles.line} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
.loading,
|
||||
.error,
|
||||
.empty {
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
color: #8c8c8c;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.list {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.item {
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.item:active {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.itemHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
font-size: 15px;
|
||||
color: #262626;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 13px;
|
||||
padding: 2px 8px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.itemBody {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info {
|
||||
font-size: 13px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: #bfbfbf;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useApprovalList } from '../hooks/useApprovalList'
|
||||
import type { ApprovalListParams } from '../types'
|
||||
import styles from './ApprovalList.module.css'
|
||||
|
||||
interface ApprovalListProps {
|
||||
type?: ApprovalListParams['type']
|
||||
onItemClick?: (id: string) => void
|
||||
}
|
||||
|
||||
export default function ApprovalList({ type = 'my', onItemClick }: ApprovalListProps) {
|
||||
const { data, isLoading, error } = useApprovalList({ type })
|
||||
|
||||
if (isLoading) {
|
||||
return <div className={styles.loading}>加载中...</div>
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div className={styles.error}>加载失败: {error.message}</div>
|
||||
}
|
||||
|
||||
const list = data?.data?.list || []
|
||||
|
||||
if (list.length === 0) {
|
||||
return <div className={styles.empty}>暂无数据</div>
|
||||
}
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
draft: '草稿',
|
||||
pending: '审批中',
|
||||
approved: '已通过',
|
||||
rejected: '已拒绝',
|
||||
cancelled: '已撤销',
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
draft: '#8c8c8c',
|
||||
pending: '#1677ff',
|
||||
approved: '#52c41a',
|
||||
rejected: '#ff4d4f',
|
||||
cancelled: '#8c8c8c',
|
||||
}
|
||||
return colorMap[status] || '#8c8c8c'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.list}>
|
||||
{list.map((item: any) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={styles.item}
|
||||
onClick={() => onItemClick?.(item.id)}
|
||||
>
|
||||
<div className={styles.itemHeader}>
|
||||
<span className={styles.title}>{item.title}</span>
|
||||
<span
|
||||
className={styles.status}
|
||||
style={{ color: getStatusColor(item.status) }}
|
||||
>
|
||||
{getStatusText(item.status)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.itemBody}>
|
||||
<div className={styles.info}>
|
||||
申请人: {item.applicant?.name || '-'}
|
||||
</div>
|
||||
<div className={styles.time}>
|
||||
{new Date(item.createdAt).toLocaleString('zh-CN')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { approveOrder, rejectOrder, cancelOrder } from '../services/approval'
|
||||
import type { ApprovalActionParams } from '../types'
|
||||
|
||||
interface UseApprovalActionOptions {
|
||||
onSuccess?: (data: any) => void
|
||||
onError?: (error: Error) => void
|
||||
}
|
||||
|
||||
export function useApprovalAction(options?: UseApprovalActionOptions) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const approveMutation = useMutation({
|
||||
mutationFn: ({ orderId, comment }: { orderId: string; comment?: string }) =>
|
||||
approveOrder(orderId, comment),
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['approval-list'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['approval-detail'] })
|
||||
options?.onSuccess?.(data)
|
||||
},
|
||||
onError: (error) => {
|
||||
options?.onError?.(error as Error)
|
||||
},
|
||||
})
|
||||
|
||||
const rejectMutation = useMutation({
|
||||
mutationFn: ({ orderId, comment }: { orderId: string; comment?: string }) =>
|
||||
rejectOrder(orderId, comment),
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['approval-list'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['approval-detail'] })
|
||||
options?.onSuccess?.(data)
|
||||
},
|
||||
onError: (error) => {
|
||||
options?.onError?.(error as Error)
|
||||
},
|
||||
})
|
||||
|
||||
const cancelMutation = useMutation({
|
||||
mutationFn: (orderId: string) => cancelOrder(orderId),
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['approval-list'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['approval-detail'] })
|
||||
options?.onSuccess?.(data)
|
||||
},
|
||||
onError: (error) => {
|
||||
options?.onError?.(error as Error)
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
approve: approveMutation.mutate,
|
||||
reject: rejectMutation.mutate,
|
||||
cancel: cancelMutation.mutate,
|
||||
isApproving: approveMutation.isPending,
|
||||
isRejecting: rejectMutation.isPending,
|
||||
isCancelling: cancelMutation.isPending,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { getApprovalList } from '../services'
|
||||
import type { ApprovalListParams } from '../types'
|
||||
|
||||
export function useApprovalList(params: ApprovalListParams = {}) {
|
||||
const { type = 'my', page = 1, pageSize = 20 } = params
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['approval-list', type, page, pageSize],
|
||||
queryFn: () => getApprovalList({ type, page, pageSize }),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { submitApproval } from '../services/approval'
|
||||
import type { SubmitApprovalParams } from '../types'
|
||||
|
||||
interface UseSubmitApprovalOptions {
|
||||
onSuccess?: (data: any) => void
|
||||
onError?: (error: Error) => void
|
||||
}
|
||||
|
||||
export function useApprovalSubmit(options?: UseSubmitApprovalOptions) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (params: SubmitApprovalParams) => submitApproval(params),
|
||||
onSuccess: (data) => {
|
||||
// 刷新审批列表
|
||||
queryClient.invalidateQueries({ queryKey: ['approval-list'] })
|
||||
options?.onSuccess?.(data)
|
||||
},
|
||||
onError: (error) => {
|
||||
options?.onError?.(error as Error)
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
submitApproval: mutation.mutate,
|
||||
isSubmitting: mutation.isPending,
|
||||
error: mutation.error,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// 审批模块导出
|
||||
|
||||
// Types
|
||||
export type {
|
||||
ApprovalNode,
|
||||
Approver,
|
||||
ApprovalFlow,
|
||||
ApprovalOrder,
|
||||
Attachment,
|
||||
ApprovalTimeline,
|
||||
SubmitApprovalParams,
|
||||
ApprovalListParams,
|
||||
} from './types'
|
||||
|
||||
export { ApprovalStatus, ApprovalNodeType, NodeStatus } from './types'
|
||||
|
||||
// Components
|
||||
export { default as ApprovalFlow } from './components/ApprovalFlow'
|
||||
export { default as ApprovalList } from './components/ApprovalList'
|
||||
export { default as ApprovalDetail } from './components/ApprovalDetail'
|
||||
export { default as ApprovalAction } from './components/ApprovalAction'
|
||||
|
||||
// Hooks
|
||||
export { useApprovalSubmit } from './hooks/useApprovalSubmit'
|
||||
export { useApprovalAction } from './hooks/useApprovalAction'
|
||||
export { useApprovalList } from './hooks/useApprovalList'
|
||||
|
||||
// Services
|
||||
export * from './services/approval'
|
||||
@@ -0,0 +1,42 @@
|
||||
import { api } from '@/services/api'
|
||||
import type {
|
||||
ApprovalOrder,
|
||||
ApprovalFlow,
|
||||
SubmitApprovalParams,
|
||||
ApprovalListParams,
|
||||
} from '../types'
|
||||
|
||||
// 获取审批列表
|
||||
export function getApprovalList(params: ApprovalListParams) {
|
||||
return api.get<ApprovalOrder[]>('/approval/list', params)
|
||||
}
|
||||
|
||||
// 获取审批详情
|
||||
export function getApprovalDetail(id: string) {
|
||||
return api.get<ApprovalOrder>(`/approval/detail/${id}`)
|
||||
}
|
||||
|
||||
// 提交审批
|
||||
export function submitApproval(data: SubmitApprovalParams) {
|
||||
return api.post<{ order: ApprovalOrder; token?: string }>('/approval/submit', data)
|
||||
}
|
||||
|
||||
// 审批通过
|
||||
export function approveOrder(orderId: string, comment?: string) {
|
||||
return api.post<ApprovalOrder>(`/approval/approve/${orderId}`, { comment })
|
||||
}
|
||||
|
||||
// 审批拒绝
|
||||
export function rejectOrder(orderId: string, comment?: string) {
|
||||
return api.post<ApprovalOrder>(`/approval/reject/${orderId}`, { comment })
|
||||
}
|
||||
|
||||
// 撤销申请
|
||||
export function cancelOrder(orderId: string) {
|
||||
return api.post<ApprovalOrder>(`/approval/cancel/${orderId}`)
|
||||
}
|
||||
|
||||
// 获取审批流程
|
||||
export function getApprovalFlow(type: string) {
|
||||
return api.get<ApprovalFlow>(`/approval/flow/${type}`)
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
// 审批模块类型定义
|
||||
|
||||
export interface ApprovalNode {
|
||||
id: string
|
||||
type: ApprovalNodeType
|
||||
name: string
|
||||
approvers: Approver[]
|
||||
order: number
|
||||
status?: NodeStatus
|
||||
}
|
||||
|
||||
export interface Approver {
|
||||
id: string
|
||||
name: string
|
||||
avatar?: string
|
||||
}
|
||||
|
||||
export interface ApprovalFlow {
|
||||
id: string
|
||||
businessType: string
|
||||
name: string
|
||||
nodes: ApprovalNode[]
|
||||
}
|
||||
|
||||
export interface ApprovalOrder {
|
||||
id: string
|
||||
businessType: string
|
||||
businessId: string
|
||||
title: string
|
||||
applicant: Approver
|
||||
status: ApprovalStatus
|
||||
currentNodes: string[]
|
||||
flowId: string
|
||||
formData: Record<string, any>
|
||||
attachments: Attachment[]
|
||||
timeline: ApprovalTimeline[]
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface Attachment {
|
||||
id: string
|
||||
name: string
|
||||
url: string
|
||||
size?: number
|
||||
type?: string
|
||||
}
|
||||
|
||||
export interface ApprovalTimeline {
|
||||
id: string
|
||||
nodeId: string
|
||||
nodeType: string
|
||||
approver: Approver
|
||||
action: ApprovalAction
|
||||
comment?: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
// 类型常量
|
||||
export const ApprovalStatus = {
|
||||
DRAFT: 'draft',
|
||||
PENDING: 'pending',
|
||||
APPROVED: 'approved',
|
||||
REJECTED: 'rejected',
|
||||
CANCELLED: 'cancelled',
|
||||
} as const
|
||||
|
||||
export type ApprovalStatus = (typeof ApprovalStatus)[keyof typeof ApprovalStatus]
|
||||
|
||||
export const ApprovalNodeType = {
|
||||
APPROVER: 'approver',
|
||||
AND_APPROVE: 'and_approve',
|
||||
OR_APPROVE: 'or_approve',
|
||||
NOTIFY: 'notify',
|
||||
} as const
|
||||
|
||||
export type ApprovalNodeType = (typeof ApprovalNodeType)[keyof typeof ApprovalNodeType]
|
||||
|
||||
export const NodeStatus = {
|
||||
PENDING: 'pending',
|
||||
APPROVED: 'approved',
|
||||
REJECTED: 'rejected',
|
||||
SKIPPED: 'skipped',
|
||||
} as const
|
||||
|
||||
export type NodeStatus = (typeof NodeStatus)[keyof typeof NodeStatus]
|
||||
|
||||
export type ApprovalAction = 'submit' | 'approve' | 'reject' | 'cancel'
|
||||
|
||||
// 提交审批参数
|
||||
export interface SubmitApprovalParams {
|
||||
businessType: string
|
||||
businessId: string
|
||||
title: string
|
||||
flowType?: string
|
||||
formData?: Record<string, any>
|
||||
attachments?: Attachment[]
|
||||
}
|
||||
|
||||
// 审批操作参数
|
||||
export interface ApprovalActionParams {
|
||||
orderId: string
|
||||
action: 'approve' | 'reject'
|
||||
comment?: string
|
||||
}
|
||||
|
||||
// 列表查询参数
|
||||
export interface ApprovalListParams {
|
||||
type?: 'my' | 'pending' | 'approved'
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}
|
||||
Reference in New Issue
Block a user