feat: 实现默认审批人和抄送人功能
- 后端新增 getDefaultApprovers 接口,从审批流程配置中获取默认审批人和抄送人 - 前端申请页面加载时自动获取并显示默认审批人和抄送人 - 添加默认审批流程配置:1个审批人(闫继光) + 2个抄送人(孟启、程丽敏) - 移除后端 support 目录下的自定义 Db 和 Model 类,使用 Webman 官方实现 - 前端选择页面优化:优先从 sessionStorage 恢复,否则加载后端默认配置 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -205,4 +205,19 @@ class BusinessTripController extends BaseController
|
|||||||
return $this->error($e->getMessage());
|
return $this->error($e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取默认审批人和抄送人
|
||||||
|
*/
|
||||||
|
public function getDefaultApprovers(Request $request): Response
|
||||||
|
{
|
||||||
|
$userId = $this->getUserId($request);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $this->service->getDefaultApprovers($userId);
|
||||||
|
return $this->success($result);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -457,4 +457,71 @@ class BusinessTripService
|
|||||||
return $businessTrip->fresh(['itineraries', 'approvalOrder']);
|
return $businessTrip->fresh(['itineraries', 'approvalOrder']);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取默认审批人和抄送人
|
||||||
|
* 从审批流程配置表中获取
|
||||||
|
*/
|
||||||
|
public function getDefaultApprovers(int $userId): array
|
||||||
|
{
|
||||||
|
// 获取出差审批流程
|
||||||
|
$flow = ApprovalFlow::where('business_type', 'business_trip')
|
||||||
|
->where('status', 1)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$flow) {
|
||||||
|
return [
|
||||||
|
'approvers' => [],
|
||||||
|
'cc_users' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取流程节点
|
||||||
|
$nodes = ApprovalNode::where('flow_id', $flow->id)
|
||||||
|
->orderBy('order')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$approvers = [];
|
||||||
|
$ccUsers = [];
|
||||||
|
|
||||||
|
foreach ($nodes as $node) {
|
||||||
|
$nodeApprovers = $node->approvers ?? [];
|
||||||
|
|
||||||
|
if (empty($nodeApprovers)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据节点类型获取用户信息
|
||||||
|
$users = User::whereIn('id', $nodeApprovers)
|
||||||
|
->where('status', 1)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$userList = [];
|
||||||
|
foreach ($nodeApprovers as $userId) {
|
||||||
|
$user = $users->firstWhere('id', $userId);
|
||||||
|
if ($user) {
|
||||||
|
$userList[] = [
|
||||||
|
'id' => $user->id,
|
||||||
|
'name' => $user->name,
|
||||||
|
'avatar' => $user->avatar,
|
||||||
|
'position' => $user->position,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 审批节点(approver, and_approve, or_approve)作为审批人
|
||||||
|
if ($node->isApprovalNode()) {
|
||||||
|
$approvers = array_merge($approvers, $userList);
|
||||||
|
}
|
||||||
|
// 通知节点(notify)作为抄送人
|
||||||
|
elseif ($node->type === ApprovalNode::TYPE_NOTIFY) {
|
||||||
|
$ccUsers = array_merge($ccUsers, $userList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'approvers' => $approvers,
|
||||||
|
'cc_users' => $ccUsers,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ Route::group('/api/v1/approval', function () {
|
|||||||
Route::group('/api/v1/business-trip', function () {
|
Route::group('/api/v1/business-trip', function () {
|
||||||
Route::get('/list', [app\controller\BusinessTripController::class, 'list']);
|
Route::get('/list', [app\controller\BusinessTripController::class, 'list']);
|
||||||
Route::get('/detail/{id}', [app\controller\BusinessTripController::class, 'detail']);
|
Route::get('/detail/{id}', [app\controller\BusinessTripController::class, 'detail']);
|
||||||
|
Route::get('/default-approvers', [app\controller\BusinessTripController::class, 'getDefaultApprovers']);
|
||||||
Route::post('/create', [app\controller\BusinessTripController::class, 'create']);
|
Route::post('/create', [app\controller\BusinessTripController::class, 'create']);
|
||||||
Route::post('/update/{id}', [app\controller\BusinessTripController::class, 'update']);
|
Route::post('/update/{id}', [app\controller\BusinessTripController::class, 'update']);
|
||||||
Route::post('/submit/{id}', [app\controller\BusinessTripController::class, 'submit']);
|
Route::post('/submit/{id}', [app\controller\BusinessTripController::class, 'submit']);
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
-- 添加默认出差审批流程节点
|
||||||
|
-- 1个审批人 + 2个抄送人
|
||||||
|
|
||||||
|
USE xserver;
|
||||||
|
|
||||||
|
-- 清空 business_trip 流程的现有节点
|
||||||
|
DELETE FROM approval_node WHERE flow_id = 2;
|
||||||
|
|
||||||
|
-- 添加审批节点(用户ID 2: 闫继光)
|
||||||
|
INSERT INTO approval_node (flow_id, type, name, `order`, approvers) VALUES
|
||||||
|
(2, "approver", "主管审批", 1, '[2]');
|
||||||
|
|
||||||
|
-- 添加抄送节点(用户ID 3: 孟启, 5: 程丽敏)
|
||||||
|
INSERT INTO approval_node (flow_id, type, name, `order`, approvers) VALUES
|
||||||
|
(2, "notify", "抄送人-孟启", 2, '[3]'),
|
||||||
|
(2, "notify", "抄送人-程丽敏", 3, '[5]');
|
||||||
|
|
||||||
|
-- 验证结果
|
||||||
|
SELECT "=== 默认审批流程节点 ===" AS "";
|
||||||
|
SELECT an.id, an.type, an.name, an.`order`, an.approvers, u.name AS approver_name
|
||||||
|
FROM approval_node an
|
||||||
|
LEFT JOIN users u ON JSON_CONTAINS(an.approvers, CAST(u.id AS JSON))
|
||||||
|
WHERE an.flow_id = 2
|
||||||
|
ORDER BY an.`order`;
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace support;
|
|
||||||
|
|
||||||
use Webman\App;
|
|
||||||
use Workerman\Worker;
|
|
||||||
|
|
||||||
class Bootstrap
|
|
||||||
{
|
|
||||||
public static function start(?Worker $worker): void
|
|
||||||
{
|
|
||||||
// 应用启动时的初始化
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
ini_set('display_errors', 'on');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace support;
|
|
||||||
|
|
||||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
|
||||||
|
|
||||||
class Db
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var Capsule
|
|
||||||
*/
|
|
||||||
protected static $capsule = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化
|
|
||||||
*/
|
|
||||||
public static function init(): void
|
|
||||||
{
|
|
||||||
if (self::$capsule !== null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$config = config('database');
|
|
||||||
$connections = $config['connections'] ?? [];
|
|
||||||
|
|
||||||
self::$capsule = new Capsule();
|
|
||||||
|
|
||||||
foreach ($connections as $name => $connection) {
|
|
||||||
self::$capsule->addConnection($connection, $name);
|
|
||||||
}
|
|
||||||
|
|
||||||
self::$capsule->setAsGlobal();
|
|
||||||
self::$capsule->bootEloquent();
|
|
||||||
|
|
||||||
if ($default = $config['default'] ?? null) {
|
|
||||||
self::$capsule->getDatabaseManager()->setDefaultConnection($default);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取连接
|
|
||||||
*/
|
|
||||||
public static function connection(?string $name = null): \Illuminate\Database\Connection
|
|
||||||
{
|
|
||||||
if (self::$capsule === null) {
|
|
||||||
self::init();
|
|
||||||
}
|
|
||||||
return self::$capsule->connection($name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取表查询构造器
|
|
||||||
*/
|
|
||||||
public static function table(string $table, ?string $connection = null): \Illuminate\Database\Query\Builder
|
|
||||||
{
|
|
||||||
return self::connection($connection)->table($table);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 原始SQL表达式
|
|
||||||
*/
|
|
||||||
public static function raw(string $value): \Illuminate\Database\Query\Expression
|
|
||||||
{
|
|
||||||
return self::connection()->raw($value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 Capsule 实例
|
|
||||||
*/
|
|
||||||
public static function getCapsule(): Capsule
|
|
||||||
{
|
|
||||||
if (self::$capsule === null) {
|
|
||||||
self::init();
|
|
||||||
}
|
|
||||||
return self::$capsule;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace support;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model as EloquentModel;
|
|
||||||
|
|
||||||
class Model extends EloquentModel
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* 获取数据库连接
|
|
||||||
*/
|
|
||||||
public function getConnection()
|
|
||||||
{
|
|
||||||
return Db::connection($this->getConnectionName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,8 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { DatePicker, Input, Toast, Dialog, TextArea, Selector } from 'antd-mobile'
|
import { DatePicker, Input, Toast, Dialog, TextArea, Selector } from 'antd-mobile'
|
||||||
import { AddOutline, DeleteOutline } from 'antd-mobile-icons'
|
import { AddOutline, DeleteOutline } from 'antd-mobile-icons'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { businessTripApi } from '../../services/api'
|
import { businessTripApi, userApi } from '../../services/api'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
import styles from './apply.module.css'
|
import styles from './apply.module.css'
|
||||||
|
|
||||||
interface ItineraryItem {
|
interface ItineraryItem {
|
||||||
@@ -64,6 +65,7 @@ export default function BusinessTripApply() {
|
|||||||
])
|
])
|
||||||
const [approvers, setApprovers] = useState<ApproverItem[]>([])
|
const [approvers, setApprovers] = useState<ApproverItem[]>([])
|
||||||
const [ccUsers, setCcUsers] = useState<ApproverItem[]>([])
|
const [ccUsers, setCcUsers] = useState<ApproverItem[]>([])
|
||||||
|
const [defaultLoaded, setDefaultLoaded] = useState(false)
|
||||||
|
|
||||||
// 手动保存草稿的函数
|
// 手动保存草稿的函数
|
||||||
const saveDraft = () => {
|
const saveDraft = () => {
|
||||||
@@ -84,6 +86,40 @@ export default function BusinessTripApply() {
|
|||||||
// 使用 ref 来跟踪是否已经恢复过草稿
|
// 使用 ref 来跟踪是否已经恢复过草稿
|
||||||
const hasRestoredRef = useRef(false)
|
const hasRestoredRef = useRef(false)
|
||||||
|
|
||||||
|
// 加载默认审批人和抄送人(仅在首次加载且没有草稿数据时)
|
||||||
|
useEffect(() => {
|
||||||
|
const loadDefaultApprovers = async () => {
|
||||||
|
// 如果已经有存储的草稿数据,不加载默认值
|
||||||
|
if (sessionStorage.getItem('businessTripDraft')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 如果已经从选择页面返回,不加载默认值
|
||||||
|
if (location.state?.fromSelection) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 如果已经有存储的人员数据,不加载默认值
|
||||||
|
if (sessionStorage.getItem('selectedApprovers') || sessionStorage.getItem('selectedCc')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const defaultData = await businessTripApi.getDefaultApprovers()
|
||||||
|
if (defaultData.approvers && defaultData.approvers.length > 0) {
|
||||||
|
setApprovers(defaultData.approvers)
|
||||||
|
sessionStorage.setItem('selectedApprovers', JSON.stringify(defaultData.approvers))
|
||||||
|
}
|
||||||
|
if (defaultData.cc_users && defaultData.cc_users.length > 0) {
|
||||||
|
setCcUsers(defaultData.cc_users)
|
||||||
|
sessionStorage.setItem('selectedCc', JSON.stringify(defaultData.cc_users))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load default approvers:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadDefaultApprovers()
|
||||||
|
}, [])
|
||||||
|
|
||||||
// 处理从选择页面返回的数据和草稿恢复
|
// 处理从选择页面返回的数据和草稿恢复
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fromSelection = location.state?.fromSelection
|
const fromSelection = location.state?.fromSelection
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useNavigate, useLocation } from 'react-router-dom'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { NavBar, SearchBar, Button, Toast, Empty } from 'antd-mobile'
|
import { NavBar, SearchBar, Button, Toast, Empty } from 'antd-mobile'
|
||||||
import { CheckOutline } from 'antd-mobile-icons'
|
import { CheckOutline } from 'antd-mobile-icons'
|
||||||
import { userApi } from '../../services/api'
|
import { userApi, businessTripApi } from '../../services/api'
|
||||||
import styles from './select-approver.module.css'
|
import styles from './select-approver.module.css'
|
||||||
|
|
||||||
interface UserItem {
|
interface UserItem {
|
||||||
@@ -34,16 +34,45 @@ export default function SelectUserPage() {
|
|||||||
const state = location.state as LocationState
|
const state = location.state as LocationState
|
||||||
|
|
||||||
const [searchValue, setSearchValue] = useState('')
|
const [searchValue, setSearchValue] = useState('')
|
||||||
// 只从 sessionStorage 获取对应类型的数据
|
const [selectedUsers, setSelectedUsers] = useState<UserItem[]>([])
|
||||||
const getInitialSelected = () => {
|
|
||||||
const data = sessionStorage.getItem('selectedApprovers')
|
|
||||||
return data ? JSON.parse(data) : []
|
|
||||||
}
|
|
||||||
const [selectedUsers, setSelectedUsers] = useState<UserItem[]>(getInitialSelected())
|
|
||||||
const [allUsers, setAllUsers] = useState<UserItem[]>([])
|
const [allUsers, setAllUsers] = useState<UserItem[]>([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [hasMore, setHasMore] = useState(true)
|
const [hasMore, setHasMore] = useState(true)
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
|
const [defaultLoaded, setDefaultLoaded] = useState(false)
|
||||||
|
|
||||||
|
// 获取初始选中的用户(从 sessionStorage 或后端默认配置)
|
||||||
|
const loadInitialSelected = async () => {
|
||||||
|
const type = location.state?.type || 'approver'
|
||||||
|
const storageKey = type === 'approver' ? 'selectedApprovers' : 'selectedCc'
|
||||||
|
|
||||||
|
// 先尝试从 sessionStorage 获取
|
||||||
|
const storedData = sessionStorage.getItem(storageKey)
|
||||||
|
if (storedData) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(storedData)
|
||||||
|
if (parsed.length > 0) {
|
||||||
|
setSelectedUsers(parsed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse stored data:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有存储的数据,从后端获取默认配置
|
||||||
|
try {
|
||||||
|
const defaultData = await businessTripApi.getDefaultApprovers()
|
||||||
|
const defaultUsers = type === 'approver'
|
||||||
|
? (defaultData.approvers || [])
|
||||||
|
: (defaultData.cc_users || [])
|
||||||
|
setSelectedUsers(defaultUsers)
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to load default approvers:', error)
|
||||||
|
// 获取失败时设置为空数组
|
||||||
|
setSelectedUsers([])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 加载用户列表
|
// 加载用户列表
|
||||||
const loadUsers = async (pageNum: number = 1, isRefresh: boolean = false) => {
|
const loadUsers = async (pageNum: number = 1, isRefresh: boolean = false) => {
|
||||||
@@ -89,6 +118,7 @@ export default function SelectUserPage() {
|
|||||||
// 初始加载
|
// 初始加载
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadUsers(1, true)
|
loadUsers(1, true)
|
||||||
|
loadInitialSelected()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const isUserSelected = (userId: number) => {
|
const isUserSelected = (userId: number) => {
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ export const businessTripApi = {
|
|||||||
|
|
||||||
getDetail: (id: number) => api.get(`/business-trip/detail/${id}`),
|
getDetail: (id: number) => api.get(`/business-trip/detail/${id}`),
|
||||||
|
|
||||||
|
// 获取默认审批人和抄送人
|
||||||
|
getDefaultApprovers: () => api.get('/business-trip/default-approvers'),
|
||||||
|
|
||||||
create: (data: any) => api.post('/business-trip/create', data),
|
create: (data: any) => api.post('/business-trip/create', data),
|
||||||
|
|
||||||
update: (id: number, data: any) =>
|
update: (id: number, data: any) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user