```
feat(DeviceLoan): 添加设备搜索功能并优化借用流程 - 新增设备关键词搜索状态和输入框,支持按设备编码/名称搜索 - 优化借用设备选项过滤逻辑,排除已报废和借用中的设备 - 在打开借用弹窗时清空设备搜索关键词 - 更新借用设备选择下拉框提示文案 feat(DeviceManagement): 添加借用中设备状态 - 在设备状态列表中添加"借用中"状态选项 - 为"借用中"状态配置蓝色标签样式 feat(Messages): 实现群组邀请成员功能 - 添加邀请成员弹窗组件和相关状态管理 - 实现群主邀请成员加入群聊的功能接口调用 - 过滤可邀请用户列表,排除已在群内的成员 - 添加邀请成员按钮,仅群主可见 ```
This commit is contained in:
@@ -23,6 +23,7 @@ function DeviceLoan({ user }) {
|
|||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [statusFilter, setStatusFilter] = useState('')
|
const [statusFilter, setStatusFilter] = useState('')
|
||||||
const [keyword, setKeyword] = useState('')
|
const [keyword, setKeyword] = useState('')
|
||||||
|
const [deviceKw, setDeviceKw] = useState('')
|
||||||
|
|
||||||
// 借用弹窗
|
// 借用弹窗
|
||||||
const [loanOpen, setLoanOpen] = useState(false)
|
const [loanOpen, setLoanOpen] = useState(false)
|
||||||
@@ -74,6 +75,7 @@ function DeviceLoan({ user }) {
|
|||||||
// ============ 借用 ============
|
// ============ 借用 ============
|
||||||
const openLoanModal = () => {
|
const openLoanModal = () => {
|
||||||
loanForm.resetFields()
|
loanForm.resetFields()
|
||||||
|
setDeviceKw('')
|
||||||
setLoanPhoto('')
|
setLoanPhoto('')
|
||||||
setLoanPhotoPreview('')
|
setLoanPhotoPreview('')
|
||||||
setLoanOpen(true)
|
setLoanOpen(true)
|
||||||
@@ -216,9 +218,16 @@ function DeviceLoan({ user }) {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const deviceOptions = devices
|
// 借用选项:排除已报废与借用中(已借用设备不能二次借用),支持按设备编码/名称搜索
|
||||||
|
const kw = deviceKw.trim().toLowerCase()
|
||||||
|
const loanDeviceOptions = devices
|
||||||
|
.filter(d => d.status !== '已报废' && d.status !== '借用中')
|
||||||
|
.filter(d => !kw || (d.serial_number || '').toLowerCase().includes(kw) || (d.name || '').toLowerCase().includes(kw))
|
||||||
|
.map(d => ({ value: d.id, label: `${d.name}(编号 ${d.serial_number || '-'} / ${d.status})` }))
|
||||||
|
|
||||||
|
const returnDeviceOptions = devices
|
||||||
.filter(d => d.status !== '已报废')
|
.filter(d => d.status !== '已报废')
|
||||||
.map(d => ({ value: d.id, label: `${d.name}(${d.type || '未分类'} / ${d.status})` }))
|
.map(d => ({ value: d.id, label: `${d.name}(编号 ${d.serial_number || '-'} / ${d.status})` }))
|
||||||
|
|
||||||
const personnelOptions = personnel.map(p => ({
|
const personnelOptions = personnel.map(p => ({
|
||||||
value: p.name,
|
value: p.name,
|
||||||
@@ -299,8 +308,16 @@ function DeviceLoan({ user }) {
|
|||||||
width={520}
|
width={520}
|
||||||
>
|
>
|
||||||
<Form form={loanForm} layout="vertical" style={{ marginTop: 12 }}>
|
<Form form={loanForm} layout="vertical" style={{ marginTop: 12 }}>
|
||||||
|
<Form.Item label="搜索设备编码">
|
||||||
|
<Input.Search
|
||||||
|
placeholder="输入或扫描设备编码 / 名称查找设备"
|
||||||
|
allowClear
|
||||||
|
value={deviceKw}
|
||||||
|
onChange={e => setDeviceKw(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
<Form.Item name="device_id" label="借用设备" rules={[{ required: true, message: '请选择设备' }]}>
|
<Form.Item name="device_id" label="借用设备" rules={[{ required: true, message: '请选择设备' }]}>
|
||||||
<Select showSearch optionFilterProp="label" placeholder="从已有设备中选择" options={deviceOptions} />
|
<Select showSearch optionFilterProp="label" placeholder="从可借设备中选择(借用中设备不可二次借用)" options={loanDeviceOptions} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="borrower" label="借用人" rules={[{ required: true, message: '请选择借用人' }]}>
|
<Form.Item name="borrower" label="借用人" rules={[{ required: true, message: '请选择借用人' }]}>
|
||||||
<Select showSearch optionFilterProp="label" placeholder="从已有成员中选择" options={personnelOptions} />
|
<Select showSearch optionFilterProp="label" placeholder="从已有成员中选择" options={personnelOptions} />
|
||||||
@@ -328,7 +345,7 @@ function DeviceLoan({ user }) {
|
|||||||
>
|
>
|
||||||
<Form form={returnForm} layout="vertical" style={{ marginTop: 12 }}>
|
<Form form={returnForm} layout="vertical" style={{ marginTop: 12 }}>
|
||||||
<Form.Item name="device_id" label="归还设备" rules={[{ required: true, message: '请选择设备' }]}>
|
<Form.Item name="device_id" label="归还设备" rules={[{ required: true, message: '请选择设备' }]}>
|
||||||
<Select showSearch optionFilterProp="label" placeholder="从已有设备中选择" options={deviceOptions} />
|
<Select showSearch optionFilterProp="label" placeholder="从已有设备中选择" options={returnDeviceOptions} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="borrower" label="归还人" rules={[{ required: true, message: '请选择归还人' }]}>
|
<Form.Item name="borrower" label="归还人" rules={[{ required: true, message: '请选择归还人' }]}>
|
||||||
<Select showSearch optionFilterProp="label" placeholder="从已有成员中选择" options={personnelOptions} />
|
<Select showSearch optionFilterProp="label" placeholder="从已有成员中选择" options={personnelOptions} />
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const { Option } = Select
|
|||||||
const { TextArea } = Input
|
const { TextArea } = Input
|
||||||
|
|
||||||
const deviceTypes = ['笔记本电脑', '台式机', '显示器', '打印机', '网络设备', '服务器', '投影仪', '其他']
|
const deviceTypes = ['笔记本电脑', '台式机', '显示器', '打印机', '网络设备', '服务器', '投影仪', '其他']
|
||||||
const deviceStatuses = ['正常', '维修中', '已报废', '闲置']
|
const deviceStatuses = ['正常', '维修中', '已报废', '闲置', '借用中']
|
||||||
|
|
||||||
function DeviceManagement({ user }) {
|
function DeviceManagement({ user }) {
|
||||||
const isAdminUser = can(user, 'device:edit')
|
const isAdminUser = can(user, 'device:edit')
|
||||||
@@ -229,7 +229,8 @@ function DeviceManagement({ user }) {
|
|||||||
'正常': 'green',
|
'正常': 'green',
|
||||||
'维修中': 'orange',
|
'维修中': 'orange',
|
||||||
'已报废': 'red',
|
'已报废': 'red',
|
||||||
'闲置': 'default'
|
'闲置': 'default',
|
||||||
|
'借用中': 'blue'
|
||||||
}
|
}
|
||||||
return <Tag color={colorMap[status] || 'default'}>{status}</Tag>
|
return <Tag color={colorMap[status] || 'default'}>{status}</Tag>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react'
|
import React, { useState, useEffect, useRef } from 'react'
|
||||||
import { Card, Button, Input, List, Badge, Modal, Form, Select, Tabs, Avatar, Empty, message, Popconfirm, Space, Tag, Tooltip, Popover, Image } from 'antd'
|
import { Card, Button, Input, List, Badge, Modal, Form, Select, Tabs, Avatar, Empty, message, Popconfirm, Space, Tag, Tooltip, Popover, Image } from 'antd'
|
||||||
import { PlusOutlined, SendOutlined, TeamOutlined, UserOutlined, ArrowLeftOutlined, DeleteOutlined, SettingOutlined, UserDeleteOutlined, CheckCircleFilled, ClockCircleOutlined, SmileOutlined, PictureOutlined, VideoCameraOutlined, PaperClipOutlined, FileOutlined, DownloadOutlined } from '@ant-design/icons'
|
import { PlusOutlined, SendOutlined, TeamOutlined, UserOutlined, ArrowLeftOutlined, DeleteOutlined, SettingOutlined, UserDeleteOutlined, UserAddOutlined, CheckCircleFilled, ClockCircleOutlined, SmileOutlined, PictureOutlined, VideoCameraOutlined, PaperClipOutlined, FileOutlined, DownloadOutlined } from '@ant-design/icons'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
const { TextArea } = Input
|
const { TextArea } = Input
|
||||||
@@ -45,6 +45,8 @@ function Messages({ user }) {
|
|||||||
const [sending, setSending] = useState(false)
|
const [sending, setSending] = useState(false)
|
||||||
const [createVisible, setCreateVisible] = useState(false)
|
const [createVisible, setCreateVisible] = useState(false)
|
||||||
const [membersVisible, setMembersVisible] = useState(false)
|
const [membersVisible, setMembersVisible] = useState(false)
|
||||||
|
const [inviteVisible, setInviteVisible] = useState(false)
|
||||||
|
const [inviteIds, setInviteIds] = useState([])
|
||||||
const [createForm] = Form.useForm()
|
const [createForm] = Form.useForm()
|
||||||
const [creating, setCreating] = useState(false)
|
const [creating, setCreating] = useState(false)
|
||||||
const [uploading, setUploading] = useState(false)
|
const [uploading, setUploading] = useState(false)
|
||||||
@@ -242,6 +244,38 @@ function Messages({ user }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 群主邀请成员加入群聊(拉人)
|
||||||
|
const handleInviteMembers = async () => {
|
||||||
|
if (!inviteIds.length) {
|
||||||
|
message.warning('请选择要邀请的成员')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await axios.post(`/api/chat/conversations/${activeConv.id}/members`, { user_ids: inviteIds })
|
||||||
|
if (res.data.success) {
|
||||||
|
message.success(res.data.message)
|
||||||
|
setInviteVisible(false)
|
||||||
|
setInviteIds([])
|
||||||
|
const list = await axios.get('/api/chat/conversations')
|
||||||
|
if (list.data.success) {
|
||||||
|
setConversations(list.data.data)
|
||||||
|
const conv = list.data.data.find(c => c.id === activeConv.id)
|
||||||
|
if (conv) {
|
||||||
|
setActiveConv(conv)
|
||||||
|
activeConvRef.current = conv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
message.error(err.response?.data?.message || '邀请失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可邀请的用户(尚未在群内)
|
||||||
|
const inviteOptions = allUsers
|
||||||
|
.filter(u => !(activeConv?.members || []).some(m => m.id === u.id))
|
||||||
|
.map(u => ({ value: u.id, label: `${u.display_name || u.username}${u.role === 'admin' ? '(管理员)' : ''}` }))
|
||||||
|
|
||||||
const otherOptions = allUsers.filter(u => u.id !== user?.id).map(u => ({
|
const otherOptions = allUsers.filter(u => u.id !== user?.id).map(u => ({
|
||||||
value: u.id,
|
value: u.id,
|
||||||
label: (
|
label: (
|
||||||
@@ -507,6 +541,17 @@ function Messages({ user }) {
|
|||||||
footer={null}
|
footer={null}
|
||||||
width={420}
|
width={420}
|
||||||
>
|
>
|
||||||
|
{activeConv?.creator_id === user?.id && (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<UserAddOutlined />}
|
||||||
|
block
|
||||||
|
style={{ marginBottom: 12 }}
|
||||||
|
onClick={() => { setInviteIds([]); setInviteVisible(true) }}
|
||||||
|
>
|
||||||
|
邀请成员(拉人入群)
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<List
|
<List
|
||||||
dataSource={activeConv?.members || []}
|
dataSource={activeConv?.members || []}
|
||||||
renderItem={(m) => (
|
renderItem={(m) => (
|
||||||
@@ -543,6 +588,31 @@ function Messages({ user }) {
|
|||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ============ 邀请成员弹窗(仅群主,拉人) ============
|
||||||
|
const inviteModal = (
|
||||||
|
<Modal
|
||||||
|
title="邀请成员加入群聊"
|
||||||
|
open={inviteVisible}
|
||||||
|
onCancel={() => setInviteVisible(false)}
|
||||||
|
onOk={handleInviteMembers}
|
||||||
|
okText="邀请"
|
||||||
|
cancelText="取消"
|
||||||
|
width={420}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
mode="multiple"
|
||||||
|
placeholder="选择要拉入群聊的成员"
|
||||||
|
value={inviteIds}
|
||||||
|
onChange={setInviteIds}
|
||||||
|
options={inviteOptions}
|
||||||
|
showSearch
|
||||||
|
optionFilterProp="label"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
/>
|
||||||
|
<p style={{ color: '#999', fontSize: 12, marginTop: 8 }}>被邀请成员将收到系统通知,并立即加入群聊。</p>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
|
||||||
// ============ 布局 ============
|
// ============ 布局 ============
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
return (
|
return (
|
||||||
@@ -553,6 +623,7 @@ function Messages({ user }) {
|
|||||||
|
|
||||||
{createModal}
|
{createModal}
|
||||||
{membersModal}
|
{membersModal}
|
||||||
|
{inviteModal}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -568,6 +639,7 @@ function Messages({ user }) {
|
|||||||
|
|
||||||
{createModal}
|
{createModal}
|
||||||
{membersModal}
|
{membersModal}
|
||||||
|
{inviteModal}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user