fix: 修复页面间状态丢失问题并优化选择页面

- 修复选择审批人后再选择抄送人导致状态丢失的问题
- 使用独立的 sessionStorage key (selectedApprovers, selectedCc)
- 组件每次挂载时从 sessionStorage 恢复状态
- 只在提交/保存成功后清除 sessionStorage
- 优化人员选择页面布局和样式
- 更新 CLAUDE.md 文档记录问题和解决方案

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-02-25 15:22:12 +08:00
parent 2d39357cc3
commit 09d4cb8054
51 changed files with 2944 additions and 647 deletions
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace App\Model;
use Illuminate\Database\Eloquent\Model;
class ApprovalFlow extends Model
{
protected $table = 'approval_flow';
protected $fillable = [
'business_type',
'name',
'description',
'status',
];
protected $casts = [
'status' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* 关联审批节点
*/
public function nodes()
{
return $this->hasMany(ApprovalNode::class, 'flow_id')->orderBy('order');
}
/**
* 获取启用的流程
*/
public function scopeActive($query)
{
return $query->where('status', 1);
}
/**
* 根据业务类型获取流程
*/
public function scopeByBusinessType($query, string $businessType)
{
return $query->where('business_type', $businessType);
}
}
+79
View File
@@ -0,0 +1,79 @@
<?php
namespace App\Model;
use Illuminate\Database\Eloquent\Model;
class ApprovalNode extends Model
{
protected $table = 'approval_node';
protected $fillable = [
'flow_id',
'type',
'name',
'order',
'approvers',
'conditions',
];
protected $casts = [
'approvers' => 'array',
'conditions' => 'array',
'order' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* 节点类型常量
*/
const TYPE_APPROVER = 'approver'; // 审批人(单人)
const TYPE_AND_APPROVE = 'and_approve'; // 会签(需所有人通过)
const TYPE_OR_APPROVE = 'or_approve'; // 或签(任一人通过)
const TYPE_NOTIFY = 'notify'; // 抄送(通知用)
/**
* 关联流程
*/
public function flow()
{
return $this->belongsTo(ApprovalFlow::class, 'flow_id');
}
/**
* 获取指定顺序的节点
*/
public function scopeByOrder($query, int $order)
{
return $query->where('order', $order);
}
/**
* 判断是否是审批节点
*/
public function isApprovalNode(): bool
{
return in_array($this->type, [
self::TYPE_APPROVER,
self::TYPE_AND_APPROVE,
self::TYPE_OR_APPROVE,
]);
}
/**
* 判断是否是会签
*/
public function isAndApprove(): bool
{
return $this->type === self::TYPE_AND_APPROVE;
}
/**
* 判断是否是或签
*/
public function isOrApprove(): bool
{
return $this->type === self::TYPE_OR_APPROVE;
}
}
+91
View File
@@ -0,0 +1,91 @@
<?php
namespace App\Model;
use Illuminate\Database\Eloquent\Model;
class ApprovalOrder extends Model
{
protected $table = 'approval_order';
protected $fillable = [
'business_type',
'business_id',
'title',
'applicant_id',
'status',
'current_nodes',
'flow_id',
'form_data',
'attachments',
];
protected $casts = [
'current_nodes' => 'array',
'form_data' => 'array',
'attachments' => 'array',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* 审批状态常量
*/
const STATUS_DRAFT = 'draft'; // 草稿
const STATUS_PENDING = 'pending'; // 审批中
const STATUS_APPROVED = 'approved'; // 已通过
const STATUS_REJECTED = 'rejected'; // 已拒绝
const STATUS_CANCELLED = 'cancelled'; // 已撤销
/**
* 关联流程
*/
public function flow()
{
return $this->belongsTo(ApprovalFlow::class, 'flow_id');
}
/**
* 关联申请人
*/
public function applicant()
{
return $this->belongsTo(User::class, 'applicant_id');
}
/**
* 关联审批记录
*/
public function records()
{
return $this->hasMany(ApprovalRecord::class, 'order_id')->orderBy('created_at');
}
/**
* 判断是否可以编辑
*/
public function canEdit(): bool
{
return $this->status === self::STATUS_DRAFT;
}
/**
* 判断是否可以审批
*/
public function canApprove(): bool
{
return $this->status === self::STATUS_PENDING;
}
/**
* 判断是否已结束
*/
public function isEnded(): bool
{
return in_array($this->status, [
self::STATUS_APPROVED,
self::STATUS_REJECTED,
self::STATUS_CANCELLED,
]);
}
}
+63
View File
@@ -0,0 +1,63 @@
<?php
namespace App\Model;
use Illuminate\Database\Eloquent\Model;
class ApprovalRecord extends Model
{
protected $table = 'approval_record';
protected $fillable = [
'order_id',
'node_id',
'node_type',
'approver_id',
'action',
'comment',
];
protected $casts = [
'created_at' => 'datetime',
];
/**
* 操作类型常量
*/
const ACTION_SUBMIT = 'submit'; // 提交
const ACTION_APPROVE = 'approve'; // 通过
const ACTION_REJECT = 'reject'; // 拒绝
const ACTION_CANCEL = 'cancel'; // 撤销
/**
* 关联审批单
*/
public function order()
{
return $this->belongsTo(ApprovalOrder::class, 'order_id');
}
/**
* 关联审批人
*/
public function approver()
{
return $this->belongsTo(User::class, 'approver_id');
}
/**
* 判断是否是批准操作
*/
public function isApproved(): bool
{
return $this->action === self::ACTION_APPROVE;
}
/**
* 判断是否是拒绝操作
*/
public function isRejected(): bool
{
return $this->action === self::ACTION_REJECT;
}
}
+101
View File
@@ -0,0 +1,101 @@
<?php
namespace App\Model;
use Illuminate\Database\Eloquent\Model;
class BusinessTrip extends Model
{
protected $table = 'business_trip';
protected $fillable = [
'user_id',
'reason',
'start_date',
'end_date',
'destination',
'estimated_cost',
'status',
'approval_order_id',
];
protected $casts = [
'user_id' => 'integer',
'estimated_cost' => 'decimal:2',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* 状态常量
*/
const STATUS_DRAFT = 'draft'; // 草稿
const STATUS_PENDING = 'pending'; // 审批中
const STATUS_APPROVED = 'approved'; // 已通过
const STATUS_REJECTED = 'rejected'; // 已拒绝
const STATUS_CANCELLED = 'cancelled'; // 已撤销
/**
* 关联用户
*/
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
/**
* 关联行程明细
*/
public function itineraries()
{
return $this->hasMany(BusinessTripItinerary::class, 'business_trip_id');
}
/**
* 关联审批单
*/
public function approvalOrder()
{
return $this->belongsTo(ApprovalOrder::class, 'approval_order_id');
}
/**
* 计算总天数
*/
public function getDurationDays(): int
{
if (!$this->start_date || !$this->end_date) {
return 0;
}
$start = \DateTime::createFromFormat('Y-m-d', $this->start_date);
$end = \DateTime::createFromFormat('Y-m-d', $this->end_date);
if (!$start || !$end) {
return 0;
}
return $end->diff($start)->days + 1;
}
/**
* 判断是否可以编辑
*/
public function canEdit(): bool
{
return $this->status === self::STATUS_DRAFT;
}
/**
* 判断是否可以提交
*/
public function canSubmit(): bool
{
return $this->status === self::STATUS_DRAFT;
}
/**
* 判断是否可以取消
*/
public function canCancel(): bool
{
return $this->status === self::STATUS_PENDING;
}
}
@@ -0,0 +1,59 @@
<?php
namespace App\Model;
use Illuminate\Database\Eloquent\Model;
class BusinessTripItinerary extends Model
{
protected $table = 'business_trip_itinerary';
protected $fillable = [
'business_trip_id',
'start_time',
'end_time',
'departure',
'destination',
'transport',
'trip_type',
'remark',
];
protected $casts = [
'business_trip_id' => 'integer',
'start_time' => 'datetime',
'end_time' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* 行程类型常量
*/
const TRIP_TYPE_ONEWAY = 'oneway'; // 单程
const TRIP_TYPE_ROUNDTRIP = 'roundtrip'; // 往返
/**
* 关联出差申请
*/
public function businessTrip()
{
return $this->belongsTo(BusinessTrip::class, 'business_trip_id');
}
/**
* 计算天数
*/
public function getDurationDays(): int
{
if (!$this->start_time || !$this->end_time) {
return 0;
}
$start = \DateTime::createFromFormat('Y-m-d H:i:s', $this->start_time);
$end = \DateTime::createFromFormat('Y-m-d H:i:s', $this->end_time);
if (!$start || !$end) {
return 0;
}
return $end->diff($start)->days + 1;
}
}
+62
View File
@@ -0,0 +1,62 @@
<?php
namespace App\Model;
use Illuminate\Database\Eloquent\Model;
class Company extends Model
{
protected $table = 'companies';
protected $fillable = [
'name',
'abbr',
'tenant_key',
'sort',
'status',
'feishu_enabled',
];
protected $casts = [
'sort' => 'integer',
'status' => 'integer',
'feishu_enabled' => 'boolean',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* 获取启用的公司
*/
public function scopeActive($query)
{
return $query->where('status', 1);
}
/**
* 根据 tenant_key 查找
*/
public function scopeByTenantKey($query, ?string $tenantKey)
{
if ($tenantKey === null) {
return $query;
}
return $query->where('tenant_key', $tenantKey);
}
/**
* 关联飞书应用配置
*/
public function feishuApps()
{
return $this->hasMany(CompanyFeishuApp::class, 'company_id');
}
/**
* 关联用户
*/
public function users()
{
return $this->hasMany(User::class, 'company_id');
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace App\Model;
use Illuminate\Database\Eloquent\Model;
class CompanyFeishuApp extends Model
{
protected $table = 'company_feishu_apps';
protected $fillable = [
'company_id',
'system_id',
'name',
'tenant_key',
'app_id',
'app_secret',
'base_uri',
'callback_url',
'mobile_callback_url',
'status',
];
protected $casts = [
'company_id' => 'integer',
'system_id' => 'integer',
'status' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* 获取启用的应用
*/
public function scopeActive($query)
{
return $query->where('status', 1);
}
/**
* 关联公司
*/
public function company()
{
return $this->belongsTo(Company::class, 'company_id');
}
}
+134
View File
@@ -0,0 +1,134 @@
<?php
namespace App\Model;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
protected $table = 'users';
protected $fillable = [
'company_id',
'sn',
'report_to',
'name',
'avatar',
'sex',
'email',
'phone',
'password',
'status',
'user_type',
'union_id',
'feishu_user_id',
'feishu_open_id',
'tenant_key',
'position',
'sign_img',
];
protected $casts = [
'company_id' => 'integer',
'report_to' => 'integer',
'sex' => 'integer',
'status' => 'integer',
'user_type' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
protected $hidden = [
'password',
];
/**
* 关联公司
*/
public function company()
{
return $this->belongsTo(Company::class, 'company_id');
}
/**
* 上级用户
*/
public function parent()
{
return $this->belongsTo(User::class, 'report_to');
}
/**
* 下级用户
*/
public function children()
{
return $this->hasMany(User::class, 'report_to');
}
/**
* 获取启用的用户
*/
public function scopeActive($query)
{
return $query->where('status', 1);
}
/**
* 根据公司ID查找
*/
public function scopeByCompany($query, int $companyId)
{
return $query->where('company_id', $companyId);
}
/**
* 根据飞书用户ID查找
*/
public function scopeByFeishuUserId($query, ?string $feishuUserId)
{
if ($feishuUserId === null) {
return $query;
}
return $query->where('feishu_user_id', $feishuUserId);
}
/**
* 根据飞书 UnionID 查找
*/
public function scopeByFeishuUnionId($query, ?string $feishuUnionId)
{
if ($feishuUnionId === null) {
return $query;
}
return $query->where('union_id', $feishuUnionId);
}
/**
* 根据OpenID查找
*/
public function scopeByFeishuOpenId($query, ?string $openId)
{
if ($openId === null) {
return $query;
}
return $query->where('feishu_open_id', $openId);
}
/**
* 搜索用户
*/
public function scopeSearch($query, ?string $keyword)
{
if (!$keyword) {
return $query;
}
return $query->where(function ($q) use ($keyword) {
$q->where('name', 'like', "%{$keyword}%")
->orWhere('email', 'like', "%{$keyword}%")
->orWhere('phone', 'like', "%{$keyword}%")
->orWhere('sn', 'like', "%{$keyword}%");
});
}
}