diff --git a/client/src/pages/DeviceLoan.jsx b/client/src/pages/DeviceLoan.jsx
index 9a4cb24..1b5032f 100644
--- a/client/src/pages/DeviceLoan.jsx
+++ b/client/src/pages/DeviceLoan.jsx
@@ -23,6 +23,7 @@ function DeviceLoan({ user }) {
const [loading, setLoading] = useState(false)
const [statusFilter, setStatusFilter] = useState('')
const [keyword, setKeyword] = useState('')
+ const [deviceKw, setDeviceKw] = useState('')
// 借用弹窗
const [loanOpen, setLoanOpen] = useState(false)
@@ -74,6 +75,7 @@ function DeviceLoan({ user }) {
// ============ 借用 ============
const openLoanModal = () => {
loanForm.resetFields()
+ setDeviceKw('')
setLoanPhoto('')
setLoanPhotoPreview('')
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 !== '已报废')
- .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 => ({
value: p.name,
@@ -299,8 +308,16 @@ function DeviceLoan({ user }) {
width={520}
>
+ setDeviceKw(e.target.value)}
+ />
+
-
+
@@ -328,7 +345,7 @@ function DeviceLoan({ user }) {
>
-
+
diff --git a/client/src/pages/DeviceManagement.jsx b/client/src/pages/DeviceManagement.jsx
index f41a135..4c4bceb 100644
--- a/client/src/pages/DeviceManagement.jsx
+++ b/client/src/pages/DeviceManagement.jsx
@@ -9,7 +9,7 @@ const { Option } = Select
const { TextArea } = Input
const deviceTypes = ['笔记本电脑', '台式机', '显示器', '打印机', '网络设备', '服务器', '投影仪', '其他']
-const deviceStatuses = ['正常', '维修中', '已报废', '闲置']
+const deviceStatuses = ['正常', '维修中', '已报废', '闲置', '借用中']
function DeviceManagement({ user }) {
const isAdminUser = can(user, 'device:edit')
@@ -229,7 +229,8 @@ function DeviceManagement({ user }) {
'正常': 'green',
'维修中': 'orange',
'已报废': 'red',
- '闲置': 'default'
+ '闲置': 'default',
+ '借用中': 'blue'
}
return {status}
}
diff --git a/client/src/pages/Messages.jsx b/client/src/pages/Messages.jsx
index 0b62d8b..d868aae 100644
--- a/client/src/pages/Messages.jsx
+++ b/client/src/pages/Messages.jsx
@@ -1,6 +1,6 @@
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 { 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'
const { TextArea } = Input
@@ -45,6 +45,8 @@ function Messages({ user }) {
const [sending, setSending] = useState(false)
const [createVisible, setCreateVisible] = useState(false)
const [membersVisible, setMembersVisible] = useState(false)
+ const [inviteVisible, setInviteVisible] = useState(false)
+ const [inviteIds, setInviteIds] = useState([])
const [createForm] = Form.useForm()
const [creating, setCreating] = 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 => ({
value: u.id,
label: (
@@ -507,6 +541,17 @@ function Messages({ user }) {
footer={null}
width={420}
>
+ {activeConv?.creator_id === user?.id && (
+ }
+ block
+ style={{ marginBottom: 12 }}
+ onClick={() => { setInviteIds([]); setInviteVisible(true) }}
+ >
+ 邀请成员(拉人入群)
+
+ )}
(
@@ -543,6 +588,31 @@ function Messages({ user }) {
)
+ // ============ 邀请成员弹窗(仅群主,拉人) ============
+ const inviteModal = (
+ setInviteVisible(false)}
+ onOk={handleInviteMembers}
+ okText="邀请"
+ cancelText="取消"
+ width={420}
+ >
+
+ 被邀请成员将收到系统通知,并立即加入群聊。
+
+ )
+
// ============ 布局 ============
if (isMobile) {
return (
@@ -553,6 +623,7 @@ function Messages({ user }) {
{createModal}
{membersModal}
+ {inviteModal}
)
}
@@ -568,6 +639,7 @@ function Messages({ user }) {
{createModal}
{membersModal}
+ {inviteModal}
)
}