first commit
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import React from 'react'
|
||||
import { Card, Divider } from 'antd'
|
||||
|
||||
function About() {
|
||||
return (
|
||||
<div className="about-page">
|
||||
<Card>
|
||||
<h1>影视星河 · 设备管理系统 v1.0.0</h1>
|
||||
<Divider />
|
||||
|
||||
<h2>软件概述</h2>
|
||||
<p>
|
||||
设备管理系统(Device Management System,简称DMS)是一款专为企事业单位设计的现代化设备资产管理软件。
|
||||
系统集成了设备全生命周期管理、人员管理、通知推送等核心功能,旨在帮助用户高效管理各类IT设备和办公资产。
|
||||
</p>
|
||||
|
||||
<h2>核心功能</h2>
|
||||
<ul>
|
||||
<li><strong>登录认证:</strong>支持账号密码登录,内置默认管理员账户,登录后根据时间智能问候</li>
|
||||
<li><strong>每日天气:</strong>顶部导航栏实时显示当前城市天气、温度、湿度等信息</li>
|
||||
<li><strong>设备管理:</strong>支持设备的添加、编辑、删除、查询等全生命周期管理,涵盖笔记本电脑、台式机、显示器、打印机、网络设备等各类IT资产</li>
|
||||
<li><strong>人员管理:</strong>维护组织架构中的人员信息,包括部门、职位、联系方式等,便于设备责任分配</li>
|
||||
<li><strong>通知管理:</strong>支持发布各类通知、公告和提醒,可设置优先级和发送范围,确保重要信息及时传达</li>
|
||||
<li><strong>数据概览:</strong>提供直观的仪表盘,实时展示设备状态、人员情况和通知统计</li>
|
||||
<li><strong>搜索过滤:</strong>支持按关键词、类型、状态等多维度快速检索</li>
|
||||
<li><strong>品牌标识:</strong>登录页、侧边栏、顶部导航均展示影视星河品牌 Logo</li>
|
||||
</ul>
|
||||
|
||||
<h2>技术架构</h2>
|
||||
<ul>
|
||||
<li><strong>前端:</strong>React 18 + Ant Design 5 + Vite 5</li>
|
||||
<li><strong>后端:</strong>Node.js + Express.js</li>
|
||||
<li><strong>数据库:</strong>SQLite(轻量级嵌入式数据库,无需额外安装)</li>
|
||||
<li><strong>桌面端:</strong>Electron(支持打包为Windows/Mac/Linux桌面应用)</li>
|
||||
</ul>
|
||||
|
||||
<h2>部署方式</h2>
|
||||
<ul>
|
||||
<li><strong>服务器部署:</strong>通过 Node.js 直接运行,支持任何服务器环境</li>
|
||||
<li><strong>桌面应用:</strong>打包为 Windows EXE 安装包,支持一键安装使用</li>
|
||||
<li><strong>手机网页:</strong>响应式设计,手机浏览器直接访问,自动适配移动端布局</li>
|
||||
<li><strong>手机App:</strong>支持 PWA 安装到桌面,或通过 Capacitor 打包原生 Android APK</li>
|
||||
<li><strong>局域网部署:</strong>支持在内网服务器上部署,供局域网内所有设备访问</li>
|
||||
</ul>
|
||||
|
||||
<h2>默认管理员账户</h2>
|
||||
<ul>
|
||||
<li>用户名:<strong>admin</strong></li>
|
||||
<li>密码:<strong>admin123</strong></li>
|
||||
<li>角色:<strong>系统管理员</strong></li>
|
||||
</ul>
|
||||
<p className="warning">提示:首次登录后请及时修改默认密码,确保系统安全。</p>
|
||||
|
||||
<h2>系统要求</h2>
|
||||
<ul>
|
||||
<li>操作系统:Windows 10/11、macOS 10.15+、Linux</li>
|
||||
<li>内存:最低 2GB,推荐 4GB 以上</li>
|
||||
<li>存储空间:最低 200MB 可用空间</li>
|
||||
<li>浏览器:Chrome 80+、Firefox 78+、Edge 80+、Safari 14+</li>
|
||||
<li>网络:天气功能需要访问互联网(Open-Meteo 免费天气接口)</li>
|
||||
</ul>
|
||||
|
||||
<h2>版本信息</h2>
|
||||
<ul>
|
||||
<li>当前版本:v1.0.0</li>
|
||||
<li>发布日期:2026年8月</li>
|
||||
<li>许可证:MIT License</li>
|
||||
</ul>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default About
|
||||
@@ -0,0 +1,140 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Card, Row, Col, Statistic, Spin } from 'antd'
|
||||
import {
|
||||
DesktopOutlined,
|
||||
TeamOutlined,
|
||||
BellOutlined,
|
||||
CheckCircleOutlined,
|
||||
ToolOutlined,
|
||||
CloseCircleOutlined,
|
||||
ExclamationCircleOutlined
|
||||
} from '@ant-design/icons'
|
||||
import axios from 'axios'
|
||||
|
||||
function Dashboard({ onNavigate }) {
|
||||
const [stats, setStats] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
fetchStats()
|
||||
}, [])
|
||||
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
const res = await axios.get('/api/dashboard/stats')
|
||||
if (res.data.success) {
|
||||
setStats(res.data.data)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取统计数据失败:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <div style={{ textAlign: 'center', padding: 60 }}><Spin size="large" tip="加载中..." /></div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 style={{ marginBottom: 24 }}>数据概览</h2>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card className="dashboard-card" onClick={() => onNavigate('devices')}>
|
||||
<Statistic
|
||||
title="设备总数"
|
||||
value={stats?.deviceTotal || 0}
|
||||
prefix={<DesktopOutlined />}
|
||||
valueStyle={{ color: '#1890ff' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card className="dashboard-card">
|
||||
<Statistic
|
||||
title="正常运行"
|
||||
value={stats?.deviceNormal || 0}
|
||||
prefix={<CheckCircleOutlined />}
|
||||
valueStyle={{ color: '#52c41a' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card className="dashboard-card">
|
||||
<Statistic
|
||||
title="维修中"
|
||||
value={stats?.deviceRepairing || 0}
|
||||
prefix={<ToolOutlined />}
|
||||
valueStyle={{ color: '#faad14' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card className="dashboard-card">
|
||||
<Statistic
|
||||
title="已报废"
|
||||
value={stats?.deviceScrapped || 0}
|
||||
prefix={<CloseCircleOutlined />}
|
||||
valueStyle={{ color: '#f5222d' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
||||
<Col xs={24} sm={12} lg={8}>
|
||||
<Card className="dashboard-card" onClick={() => onNavigate('personnel')}>
|
||||
<Statistic
|
||||
title="人员总数"
|
||||
value={stats?.personnelTotal || 0}
|
||||
prefix={<TeamOutlined />}
|
||||
valueStyle={{ color: '#722ed1' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={8}>
|
||||
<Card className="dashboard-card">
|
||||
<Statistic
|
||||
title="在职人员"
|
||||
value={stats?.personnelActive || 0}
|
||||
prefix={<CheckCircleOutlined />}
|
||||
valueStyle={{ color: '#13c2c2' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={8}>
|
||||
<Card className="dashboard-card" onClick={() => onNavigate('notifications')}>
|
||||
<Statistic
|
||||
title="未读通知"
|
||||
value={stats?.notifUnread || 0}
|
||||
prefix={<ExclamationCircleOutlined />}
|
||||
valueStyle={{ color: '#fa541c' }}
|
||||
suffix={<span style={{ fontSize: 14, color: '#999' }}>/ {stats?.notifTotal || 0}</span>}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Card style={{ marginTop: 24 }}>
|
||||
<h3>系统信息</h3>
|
||||
<Row gutter={16} style={{ marginTop: 12 }}>
|
||||
<Col span={8}>
|
||||
<p><strong>系统版本:</strong>v1.0.0</p>
|
||||
<p><strong>系统名称:</strong>设备管理系统</p>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<p><strong>数据库:</strong>SQLite</p>
|
||||
<p><strong>前端框架:</strong>React + Ant Design</p>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<p><strong>后端框架:</strong>Express.js</p>
|
||||
<p><strong>部署方式:</strong>服务器 / 桌面应用</p>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Dashboard
|
||||
@@ -0,0 +1,317 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Table, Button, Input, Select, Space, Tag, Modal, Form, DatePicker, message, Popconfirm, Card } from 'antd'
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, ReloadOutlined, ToolOutlined, SyncOutlined } from '@ant-design/icons'
|
||||
import axios from 'axios'
|
||||
import dayjs from 'dayjs'
|
||||
import { can } from '../permissions'
|
||||
|
||||
const { Option } = Select
|
||||
const { TextArea } = Input
|
||||
|
||||
const deviceTypes = ['笔记本电脑', '台式机', '显示器', '打印机', '网络设备', '服务器', '投影仪', '其他']
|
||||
const deviceStatuses = ['正常', '维修中', '已报废', '闲置']
|
||||
|
||||
function DeviceManagement({ user }) {
|
||||
const isAdminUser = can(user, 'device:edit')
|
||||
const canRepair = can(user, 'device:repair')
|
||||
const canAdd = can(user, 'device:add')
|
||||
const [devices, setDevices] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [modalVisible, setModalVisible] = useState(false)
|
||||
const [editingDevice, setEditingDevice] = useState(null)
|
||||
const [keyword, setKeyword] = useState('')
|
||||
const [filterType, setFilterType] = useState('')
|
||||
const [filterStatus, setFilterStatus] = useState('')
|
||||
const [statusModalVisible, setStatusModalVisible] = useState(false)
|
||||
const [statusDevice, setStatusDevice] = useState(null)
|
||||
const [form] = Form.useForm()
|
||||
const [statusForm] = Form.useForm()
|
||||
|
||||
useEffect(() => {
|
||||
fetchDevices()
|
||||
}, [keyword, filterType, filterStatus])
|
||||
|
||||
const fetchDevices = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const params = {}
|
||||
if (keyword) params.keyword = keyword
|
||||
if (filterType) params.type = filterType
|
||||
if (filterStatus) params.status = filterStatus
|
||||
|
||||
const res = await axios.get('/api/devices', { params })
|
||||
if (res.data.success) {
|
||||
setDevices(res.data.data)
|
||||
}
|
||||
} catch (err) {
|
||||
message.error('获取设备列表失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
setEditingDevice(null)
|
||||
form.resetFields()
|
||||
setModalVisible(true)
|
||||
}
|
||||
|
||||
const handleEdit = (record) => {
|
||||
setEditingDevice(record)
|
||||
form.setFieldsValue({
|
||||
...record,
|
||||
purchase_date: record.purchase_date ? dayjs(record.purchase_date) : null,
|
||||
warranty_date: record.warranty_date ? dayjs(record.warranty_date) : null
|
||||
})
|
||||
setModalVisible(true)
|
||||
}
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
try {
|
||||
const res = await axios.delete(`/api/devices/${id}`)
|
||||
if (res.data.success) {
|
||||
message.success('设备删除成功')
|
||||
fetchDevices()
|
||||
}
|
||||
} catch (err) {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleRepair = async (record) => {
|
||||
Modal.confirm({
|
||||
title: `设备报修:${record.name}`,
|
||||
content: '确认将该设备标记为“维修中”吗?可填写报修说明。',
|
||||
okText: '确认报修',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
const res = await axios.patch(`/api/devices/${record.id}/repair`, { remark: '' })
|
||||
if (res.data.success) {
|
||||
message.success(res.data.message)
|
||||
fetchDevices()
|
||||
}
|
||||
} catch (err) {
|
||||
message.error(err.response?.data?.message || '报修失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 管理员:打开修改维修状态弹窗
|
||||
const handleOpenStatus = (record) => {
|
||||
setStatusDevice(record)
|
||||
statusForm.setFieldsValue({ status: record.status })
|
||||
setStatusModalVisible(true)
|
||||
}
|
||||
|
||||
// 管理员:提交维修状态修改
|
||||
const handleStatusSubmit = async () => {
|
||||
try {
|
||||
const values = await statusForm.validateFields()
|
||||
const res = await axios.patch(`/api/devices/${statusDevice.id}/status`, values)
|
||||
if (res.data.success) {
|
||||
message.success(res.data.message)
|
||||
setStatusModalVisible(false)
|
||||
fetchDevices()
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.response) message.error(err.response.data?.message || '修改失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields()
|
||||
const data = {
|
||||
...values,
|
||||
purchase_date: values.purchase_date ? values.purchase_date.format('YYYY-MM-DD') : '',
|
||||
warranty_date: values.warranty_date ? values.warranty_date.format('YYYY-MM-DD') : ''
|
||||
}
|
||||
|
||||
if (editingDevice) {
|
||||
const res = await axios.put(`/api/devices/${editingDevice.id}`, data)
|
||||
if (res.data.success) {
|
||||
message.success('设备更新成功')
|
||||
}
|
||||
} else {
|
||||
const res = await axios.post('/api/devices', data)
|
||||
if (res.data.success) {
|
||||
message.success('设备添加成功')
|
||||
}
|
||||
}
|
||||
setModalVisible(false)
|
||||
fetchDevices()
|
||||
} catch (err) {
|
||||
if (err.response) {
|
||||
message.error(err.response.data.message || '操作失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
setKeyword('')
|
||||
setFilterType('')
|
||||
setFilterStatus('')
|
||||
}
|
||||
|
||||
const getStatusTag = (status) => {
|
||||
const colorMap = {
|
||||
'正常': 'green',
|
||||
'维修中': 'orange',
|
||||
'已报废': 'red',
|
||||
'闲置': 'default'
|
||||
}
|
||||
return <Tag color={colorMap[status] || 'default'}>{status}</Tag>
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ title: '设备名称', dataIndex: 'name', key: 'name', width: 180, ellipsis: true },
|
||||
{ title: '设备类型', dataIndex: 'type', key: 'type', width: 110 },
|
||||
{ title: '型号', dataIndex: 'model', key: 'model', width: 140, ellipsis: true },
|
||||
{ title: '序列号', dataIndex: 'serial_number', key: 'serial_number', width: 130, ellipsis: true },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 90, render: getStatusTag },
|
||||
{ title: '位置', dataIndex: 'location', key: 'location', width: 110, ellipsis: true },
|
||||
{ title: '购买日期', dataIndex: 'purchase_date', key: 'purchase_date', width: 110 },
|
||||
{ title: '负责人', dataIndex: 'responsible_person', key: 'responsible_person', width: 90 },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: isAdminUser ? 280 : 100,
|
||||
fixed: 'right',
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
{canRepair && record.status !== '维修中' && record.status !== '已报废' && (
|
||||
<Button type="link" icon={<ToolOutlined />} size="small" onClick={() => handleRepair(record)}>报修</Button>
|
||||
)}
|
||||
{isAdminUser && (
|
||||
<Button type="link" icon={<SyncOutlined />} size="small" onClick={() => handleOpenStatus(record)}>维修状态</Button>
|
||||
)}
|
||||
{isAdminUser && (
|
||||
<Button type="link" icon={<EditOutlined />} size="small" onClick={() => handleEdit(record)}>编辑</Button>
|
||||
)}
|
||||
{isAdminUser && (
|
||||
<Popconfirm title="确定要删除此设备吗?" onConfirm={() => handleDelete(record.id)} okText="确定" cancelText="取消">
|
||||
<Button type="link" danger icon={<DeleteOutlined />} size="small">删除</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<div className="search-bar">
|
||||
<Input
|
||||
placeholder="搜索设备名称/序列号/位置"
|
||||
prefix={<SearchOutlined />}
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
style={{ width: 240 }}
|
||||
allowClear
|
||||
/>
|
||||
<Select
|
||||
placeholder="设备类型"
|
||||
value={filterType || undefined}
|
||||
onChange={(val) => setFilterType(val || '')}
|
||||
style={{ width: 140 }}
|
||||
allowClear
|
||||
>
|
||||
{deviceTypes.map(t => <Option key={t} value={t}>{t}</Option>)}
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="设备状态"
|
||||
value={filterStatus || undefined}
|
||||
onChange={(val) => setFilterStatus(val || '')}
|
||||
style={{ width: 120 }}
|
||||
allowClear
|
||||
>
|
||||
{deviceStatuses.map(s => <Option key={s} value={s}>{s}</Option>)}
|
||||
</Select>
|
||||
<Button icon={<ReloadOutlined />} onClick={handleReset}>重置</Button>
|
||||
</div>
|
||||
<div className="table-toolbar">
|
||||
<span>共 {devices.length} 条记录</span>
|
||||
{canAdd && <Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>添加设备</Button>}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={devices}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
scroll={{ x: 1200 }}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条`
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={editingDevice ? '编辑设备' : '添加设备'}
|
||||
open={modalVisible}
|
||||
onOk={handleSubmit}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
width={720}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<Form.Item name="name" label="设备名称" rules={[{ required: true, message: '请输入设备名称' }]}>
|
||||
<Input placeholder="请输入设备名称" />
|
||||
</Form.Item>
|
||||
<Form.Item name="type" label="设备类型" rules={[{ required: true, message: '请选择设备类型' }]}>
|
||||
<Select placeholder="请选择设备类型">
|
||||
{deviceTypes.map(t => <Option key={t} value={t}>{t}</Option>)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<div className="form-grid">
|
||||
<Form.Item name="model" label="型号"><Input placeholder="请输入型号" /></Form.Item>
|
||||
<Form.Item name="serial_number" label="序列号"><Input placeholder="请输入序列号" /></Form.Item>
|
||||
<Form.Item name="status" label="状态" initialValue="正常">
|
||||
<Select>
|
||||
{deviceStatuses.map(s => <Option key={s} value={s}>{s}</Option>)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item name="location" label="位置"><Input placeholder="请输入位置" /></Form.Item>
|
||||
<Form.Item name="purchase_date" label="购买日期"><DatePicker style={{ width: '100%' }} /></Form.Item>
|
||||
<Form.Item name="warranty_date" label="保修日期"><DatePicker style={{ width: '100%' }} /></Form.Item>
|
||||
<Form.Item name="responsible_person" label="负责人"><Input placeholder="请输入负责人" /></Form.Item>
|
||||
</div>
|
||||
<Form.Item name="description" label="描述"><TextArea rows={3} placeholder="请输入描述信息" /></Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="修改维修状态"
|
||||
open={statusModalVisible}
|
||||
onOk={handleStatusSubmit}
|
||||
onCancel={() => setStatusModalVisible(false)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
width={420}
|
||||
>
|
||||
{statusDevice && (
|
||||
<>
|
||||
<p style={{ color: '#666' }}>设备:<strong>{statusDevice.name}</strong> 当前状态:{getStatusTag(statusDevice.status)}</p>
|
||||
<Form form={statusForm} layout="vertical">
|
||||
<Form.Item name="status" label="新的维修状态" rules={[{ required: true, message: '请选择状态' }]}>
|
||||
<Select>
|
||||
{deviceStatuses.map(s => <Option key={s} value={s}>{s}</Option>)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<div className="tip" style={{ fontSize: 12, color: '#999' }}>例如:维修完成后,将状态从“维修中”改回“正常”。</div>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DeviceManagement
|
||||
@@ -0,0 +1,252 @@
|
||||
import React from 'react'
|
||||
import { Card, Divider } from 'antd'
|
||||
|
||||
function Guide() {
|
||||
return (
|
||||
<div className="guide-page">
|
||||
<Card>
|
||||
<h1>使用说明</h1>
|
||||
<Divider />
|
||||
|
||||
<h2>一、登录系统</h2>
|
||||
<ol>
|
||||
<li>打开系统后进入登录页面</li>
|
||||
<li>输入用户名和密码</li>
|
||||
<li>点击"登录"按钮进入系统</li>
|
||||
</ol>
|
||||
<p>默认管理员账号:</p>
|
||||
<ul>
|
||||
<li>用户名:<strong>admin</strong></li>
|
||||
<li>密码:<strong>admin123</strong></li>
|
||||
</ul>
|
||||
<div className="warning">注意:首次登录后建议立即修改默认密码。</div>
|
||||
|
||||
<h2>二、每日天气</h2>
|
||||
<p>登录成功后,页面顶部导航栏右侧会显示当前城市天气信息,包括:</p>
|
||||
<ul>
|
||||
<li>当前温度</li>
|
||||
<li>天气状况(晴、多云、雨、雪等)</li>
|
||||
<li>今日最高/最低温度</li>
|
||||
<li>湿度和风速(鼠标悬停查看详情)</li>
|
||||
</ul>
|
||||
<div className="tip">天气数据来自 Open-Meteo 免费天气接口,需要联网才能正常显示。</div>
|
||||
|
||||
<h2>三、问候语</h2>
|
||||
<p>登录后,顶部导航栏会根据当前时间自动显示问候语:</p>
|
||||
<ul>
|
||||
<li>06:00 - 09:00:早上好</li>
|
||||
<li>09:00 - 12:00:上午好</li>
|
||||
<li>12:00 - 14:00:中午好</li>
|
||||
<li>14:00 - 18:00:下午好</li>
|
||||
<li>18:00 - 06:00:晚上好</li>
|
||||
</ul>
|
||||
|
||||
<h2>四、退出登录</h2>
|
||||
<p>点击右上角用户名,在下拉菜单中选择"退出登录"即可安全退出系统。</p>
|
||||
|
||||
<h2>五、系统首页(仪表盘)</h2>
|
||||
<p>系统首页提供数据的整体概览,包括:</p>
|
||||
<ul>
|
||||
<li>设备总数及状态分布(正常、维修中、已报废)</li>
|
||||
<li>人员总数及在职情况</li>
|
||||
<li>未读通知数量</li>
|
||||
</ul>
|
||||
<p>点击卡片可快速跳转到对应的管理页面。</p>
|
||||
|
||||
<h2>六、设备管理</h2>
|
||||
<h3>6.1 查看设备列表</h3>
|
||||
<p>进入"设备管理"页面后,可以看到所有设备的列表,包含设备名称、类型、型号、序列号、状态、位置等信息。</p>
|
||||
|
||||
<h3>6.2 添加设备</h3>
|
||||
<ol>
|
||||
<li>点击页面右上角的"添加设备"按钮</li>
|
||||
<li>在弹出的表单中填写设备信息(名称和类型为必填项)</li>
|
||||
<li>点击"确定"保存</li>
|
||||
</ol>
|
||||
|
||||
<h3>6.3 编辑设备</h3>
|
||||
<ol>
|
||||
<li>在设备列表中找到需要编辑的设备</li>
|
||||
<li>点击操作列的"编辑"按钮</li>
|
||||
<li>修改设备信息后点击"确定"保存</li>
|
||||
</ol>
|
||||
|
||||
<h3>6.4 删除设备</h3>
|
||||
<ol>
|
||||
<li>在设备列表中找到需要删除的设备</li>
|
||||
<li>点击操作列的"删除"按钮</li>
|
||||
<li>在确认对话框中点击"确定"</li>
|
||||
</ol>
|
||||
<div className="warning">注意:删除操作不可恢复,请谨慎操作。</div>
|
||||
|
||||
<h3>6.5 搜索和筛选</h3>
|
||||
<ul>
|
||||
<li>在搜索框中输入关键词可按设备名称、序列号、位置搜索</li>
|
||||
<li>通过“设备类型”下拉框按类型筛选</li>
|
||||
<li>通过“设备状态”下拉框按状态筛选</li>
|
||||
<li>点击“重置”按钮清除所有筛选条件</li>
|
||||
</ul>
|
||||
|
||||
<h3>6.6 设备报修与维修状态修改</h3>
|
||||
<ul>
|
||||
<li><strong>设备报修(普通账户+管理员):</strong>点击操作列的“报修”按钮,确认后设备状态即变为“维修中”。</li>
|
||||
<li><strong>修改维修状态(仅管理员):</strong>点击操作列的“维修状态”按钮,在弹窗中选择新状态(正常/维修中/已报废/闲置)后确定。例如维修完成后,可将状态从“维修中”改回“正常”。</li>
|
||||
</ul>
|
||||
|
||||
<h2>七、人员管理</h2>
|
||||
<h3>7.1 查看人员列表</h3>
|
||||
<p>进入"人员管理"页面后,可以看到所有人员的信息,包含姓名、工号、部门、职位、联系方式等。</p>
|
||||
|
||||
<h3>7.2 添加人员</h3>
|
||||
<ol>
|
||||
<li>点击页面右上角的"添加人员"按钮</li>
|
||||
<li>在弹出的表单中填写人员信息(姓名为必填项)</li>
|
||||
<li>点击"确定"保存</li>
|
||||
</ol>
|
||||
|
||||
<h3>7.3 编辑和删除人员</h3>
|
||||
<p>操作方式与设备管理类似,点击对应操作列的按钮即可。</p>
|
||||
|
||||
<h3>7.4 搜索和筛选</h3>
|
||||
<ul>
|
||||
<li>搜索框支持按姓名、工号、手机号搜索</li>
|
||||
<li>可按部门和在职状态进行筛选</li>
|
||||
</ul>
|
||||
|
||||
<h3>7.5 创建登录账户(多用户管理)</h3>
|
||||
<p>添加/编辑人员时,打开"创建/启用登录账户"开关,即可为该人员同步创建登录账户:</p>
|
||||
<ol>
|
||||
<li>填写登录用户名和密码</li>
|
||||
<li>选择账户角色:普通账户 或 管理员账户</li>
|
||||
<li>保存后,该人员即可用此账户登录系统</li>
|
||||
</ol>
|
||||
<div className="tip">人员与账户一一对应。删除人员时会同时删除其登录账户;关闭开关则停用账户。</div>
|
||||
|
||||
<h3>7.6 账户权限说明</h3>
|
||||
<ul>
|
||||
<li><strong>普通账户:</strong>添加设备、设备报修、查看人员、查看通知</li>
|
||||
<li><strong>管理员账户:</strong>拥有全部权限(编辑/删除设备、管理人员与账户、发布/删除通知等)</li>
|
||||
</ul>
|
||||
<div className="warning">注意:普通账户登录后,界面上仅显示其有权限的操作按钮,后端也会做权限校验。</div>
|
||||
|
||||
<h2>八、通知管理</h2>
|
||||
<h3>8.1 查看通知列表</h3>
|
||||
<p>进入"通知管理"页面后,可以看到所有发布的通知,包含标题、类型、优先级、阅读状态等信息。</p>
|
||||
|
||||
<h3>8.2 发布通知</h3>
|
||||
<ol>
|
||||
<li>点击页面右上角的"发布通知"按钮</li>
|
||||
<li>填写通知标题、内容</li>
|
||||
<li>选择通知类型(通知、公告、提醒)</li>
|
||||
<li>选择优先级(普通、高)</li>
|
||||
<li>选择目标范围(全部、技术部、行政部等)</li>
|
||||
<li>点击"确定"发布</li>
|
||||
</ol>
|
||||
|
||||
<h3>8.3 标记已读</h3>
|
||||
<ul>
|
||||
<li>点击单条通知的"标记已读"按钮可将该通知标为已读</li>
|
||||
<li>点击"全部已读"按钮可一键标记所有通知为已读</li>
|
||||
</ul>
|
||||
|
||||
<h3>8.4 筛选通知</h3>
|
||||
<ul>
|
||||
<li>可按关键词搜索标题和内容</li>
|
||||
<li>可按类型、优先级、阅读状态筛选</li>
|
||||
</ul>
|
||||
|
||||
<h2>九、服务器部署指南</h2>
|
||||
<h3>9.1 环境准备</h3>
|
||||
<ol>
|
||||
<li>安装 Node.js 18 或以上版本</li>
|
||||
<li>将项目文件上传到服务器</li>
|
||||
</ol>
|
||||
|
||||
<h3>9.2 安装依赖</h3>
|
||||
<pre style={{ background: '#f5f5f5', padding: 12, borderRadius: 4 }}>
|
||||
{`cd device-management-system
|
||||
npm install
|
||||
cd client
|
||||
npm install
|
||||
npm run build
|
||||
cd ..`}
|
||||
</pre>
|
||||
|
||||
<h3>9.3 启动服务</h3>
|
||||
<pre style={{ background: '#f5f5f5', padding: 12, borderRadius: 4 }}>
|
||||
{`# 启动服务(默认端口3001)
|
||||
npm start
|
||||
|
||||
# 或指定端口
|
||||
PORT=8080 npm start`}
|
||||
</pre>
|
||||
|
||||
<h3>9.4 访问系统</h3>
|
||||
<p>浏览器打开 http://服务器IP:3001 即可使用系统。</p>
|
||||
|
||||
<div className="tip">
|
||||
提示:建议使用 PM2 等进程管理工具保持服务后台运行:
|
||||
<pre style={{ marginTop: 8 }}>
|
||||
{`npm install -g pm2
|
||||
pm2 start server.js --name device-manager`}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<h2>十、桌面应用打包</h2>
|
||||
<h3>10.1 Windows EXE 打包</h3>
|
||||
<pre style={{ background: '#f5f5f5', padding: 12, borderRadius: 4 }}>
|
||||
{`# 安装依赖
|
||||
npm install
|
||||
cd client && npm install && npm run build && cd ..
|
||||
|
||||
# 打包 Windows 安装包
|
||||
npm run electron:build:win`}
|
||||
</pre>
|
||||
<p>打包完成后,EXE安装包位于 <code>dist-electron</code> 目录中。</p>
|
||||
|
||||
<h2>十一、手机端使用</h2>
|
||||
<h3>11.1 手机浏览器访问(响应式网页)</h3>
|
||||
<ol>
|
||||
<li>确保手机与服务器在同一局域网</li>
|
||||
<li>手机浏览器打开 http://服务器IP:3001</li>
|
||||
<li>页面会自动切换为移动端布局(底部导航 + 抽屉菜单)</li>
|
||||
</ol>
|
||||
|
||||
<h3>11.2 安装为 PWA 应用(推荐)</h3>
|
||||
<ol>
|
||||
<li>用手机 Chrome / Edge 浏览器打开系统</li>
|
||||
<li>点击浏览器菜单中的"添加到主屏幕" / "安装应用"</li>
|
||||
<li>安装后从桌面图标打开,体验接近原生App</li>
|
||||
</ol>
|
||||
<div className="tip">PWA 方式无需应用商店,安装简单,支持离线缓存。</div>
|
||||
|
||||
<h3>11.3 打包原生 Android App(APK)</h3>
|
||||
<pre style={{ background: '#f5f5f5', padding: 12, borderRadius: 4 }}>
|
||||
{`# 前置: 安装 Android Studio 及 SDK
|
||||
|
||||
# 双击 build-app.bat 或手动执行:
|
||||
cd client
|
||||
npm install
|
||||
npm run build
|
||||
npx capacitor add android
|
||||
npx capacitor sync android
|
||||
npx capacitor open android
|
||||
|
||||
# 在 Android Studio 中 Build APK
|
||||
# APK位置: android/app/build/outputs/apk/debug/app-debug.apk`}
|
||||
</pre>
|
||||
|
||||
<h3>11.4 App 配置服务器地址</h3>
|
||||
<ol>
|
||||
<li>打开App进入登录页</li>
|
||||
<li>点击"服务器设置"展开配置</li>
|
||||
<li>输入服务器地址,如 http://192.168.1.100:3001</li>
|
||||
<li>输入账号密码登录</li>
|
||||
</ol>
|
||||
<div className="warning">注意:App 必须能访问到服务器,请确保手机与服务器网络互通。</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Guide
|
||||
@@ -0,0 +1,90 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Form, Input, Button, Card, message, Collapse } from 'antd'
|
||||
import { UserOutlined, LockOutlined, LinkOutlined, SettingOutlined } from '@ant-design/icons'
|
||||
import axios from 'axios'
|
||||
import { setApiBase, getApiBase, isNative } from '../apiConfig'
|
||||
|
||||
function Login({ onLogin }) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [serverUrl, setServerUrl] = useState(getApiBase())
|
||||
const [form] = Form.useForm()
|
||||
|
||||
const handleSubmit = async (values) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
// 应用服务器地址
|
||||
setApiBase(serverUrl)
|
||||
|
||||
const res = await axios.post('/api/auth/login', {
|
||||
username: values.username,
|
||||
password: values.password
|
||||
})
|
||||
if (res.data.success) {
|
||||
const { token, ...user } = res.data.data
|
||||
localStorage.setItem('token', token)
|
||||
localStorage.setItem('user', JSON.stringify(user))
|
||||
message.success(res.data.message)
|
||||
onLogin(user)
|
||||
}
|
||||
} catch (err) {
|
||||
message.error(err.response?.data?.message || '登录失败,请检查服务器地址和网络')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="login-page">
|
||||
<div className="login-bg">
|
||||
<div className="login-shape shape-1"></div>
|
||||
<div className="login-shape shape-2"></div>
|
||||
<div className="login-shape shape-3"></div>
|
||||
</div>
|
||||
|
||||
<div className="login-container">
|
||||
<div className="login-brand">
|
||||
<img src="/assets/logo-white.png" alt="影视星河" className="login-logo" />
|
||||
<p className="login-subtitle">设备管理系统</p>
|
||||
</div>
|
||||
|
||||
<Card className="login-card" bordered={false}>
|
||||
<h2 className="login-title">欢迎登录</h2>
|
||||
<p className="login-desc">请输入您的账号和密码</p>
|
||||
|
||||
<Form form={form} name="login" onFinish={handleSubmit} size="large">
|
||||
<Form.Item name="username" rules={[{ required: true, message: '请输入用户名' }]}>
|
||||
<Input prefix={<UserOutlined />} placeholder="用户名" autoComplete="username" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="password" rules={[{ required: true, message: '请输入密码' }]}>
|
||||
<Input.Password prefix={<LockOutlined />} placeholder="密码" autoComplete="current-password" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" loading={loading} block className="login-btn">
|
||||
登 录
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<Collapse ghost items={[{
|
||||
key: 'server',
|
||||
label: <span style={{ color: '#6b7280' }}><SettingOutlined /> 服务器设置{isNative() ? '(App必填)' : ''}</span>,
|
||||
children: (
|
||||
<Input
|
||||
prefix={<LinkOutlined />}
|
||||
placeholder="http://服务器IP:3001"
|
||||
value={serverUrl}
|
||||
onChange={(e) => setServerUrl(e.target.value)}
|
||||
/>
|
||||
)
|
||||
}]} />
|
||||
</Card>
|
||||
|
||||
<p className="login-copyright">© 2026 影视星河 设备管理系统</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Login
|
||||
@@ -0,0 +1,307 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Table, Button, Input, Select, Space, Tag, Modal, Form, message, Popconfirm, Card, Badge, Tooltip } from 'antd'
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, ReloadOutlined, CheckOutlined, CheckCircleOutlined } from '@ant-design/icons'
|
||||
import axios from 'axios'
|
||||
import { can } from '../permissions'
|
||||
|
||||
const { Option } = Select
|
||||
const { TextArea } = Input
|
||||
|
||||
const notifTypes = ['通知', '公告', '提醒']
|
||||
const priorities = ['普通', '高']
|
||||
const targets = ['全部', '技术部', '行政部', '财务部', '市场部', '人事部', '运营部']
|
||||
|
||||
function NotificationManagement({ user }) {
|
||||
const manageAllowed = can(user, 'notification:manage')
|
||||
const [notifications, setNotifications] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [modalVisible, setModalVisible] = useState(false)
|
||||
const [editingNotif, setEditingNotif] = useState(null)
|
||||
const [keyword, setKeyword] = useState('')
|
||||
const [filterType, setFilterType] = useState('')
|
||||
const [filterPriority, setFilterPriority] = useState('')
|
||||
const [filterRead, setFilterRead] = useState('')
|
||||
const [form] = Form.useForm()
|
||||
|
||||
useEffect(() => {
|
||||
fetchNotifications()
|
||||
}, [keyword, filterType, filterPriority, filterRead])
|
||||
|
||||
const fetchNotifications = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const params = {}
|
||||
if (keyword) params.keyword = keyword
|
||||
if (filterType) params.type = filterType
|
||||
if (filterPriority) params.priority = filterPriority
|
||||
if (filterRead !== '') params.is_read = filterRead
|
||||
|
||||
const res = await axios.get('/api/notifications', { params })
|
||||
if (res.data.success) {
|
||||
setNotifications(res.data.data)
|
||||
}
|
||||
} catch (err) {
|
||||
message.error('获取通知列表失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
setEditingNotif(null)
|
||||
form.resetFields()
|
||||
setModalVisible(true)
|
||||
}
|
||||
|
||||
const handleEdit = (record) => {
|
||||
setEditingNotif(record)
|
||||
form.setFieldsValue(record)
|
||||
setModalVisible(true)
|
||||
}
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
try {
|
||||
const res = await axios.delete(`/api/notifications/${id}`)
|
||||
if (res.data.success) {
|
||||
message.success('通知删除成功')
|
||||
fetchNotifications()
|
||||
}
|
||||
} catch (err) {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleMarkRead = async (id) => {
|
||||
try {
|
||||
const res = await axios.patch(`/api/notifications/${id}/read`)
|
||||
if (res.data.success) {
|
||||
message.success('已标记为已读')
|
||||
fetchNotifications()
|
||||
}
|
||||
} catch (err) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleMarkAllRead = async () => {
|
||||
try {
|
||||
const res = await axios.patch('/api/notifications/read-all')
|
||||
if (res.data.success) {
|
||||
message.success('全部标记为已读')
|
||||
fetchNotifications()
|
||||
}
|
||||
} catch (err) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields()
|
||||
|
||||
if (editingNotif) {
|
||||
const res = await axios.put(`/api/notifications/${editingNotif.id}`, {
|
||||
...values,
|
||||
is_read: values.is_read || false
|
||||
})
|
||||
if (res.data.success) {
|
||||
message.success('通知更新成功')
|
||||
}
|
||||
} else {
|
||||
const res = await axios.post('/api/notifications', values)
|
||||
if (res.data.success) {
|
||||
message.success('通知发布成功')
|
||||
}
|
||||
}
|
||||
setModalVisible(false)
|
||||
fetchNotifications()
|
||||
} catch (err) {
|
||||
if (err.response) {
|
||||
message.error(err.response.data.message || '操作失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
setKeyword('')
|
||||
setFilterType('')
|
||||
setFilterPriority('')
|
||||
setFilterRead('')
|
||||
}
|
||||
|
||||
const getTypeTag = (type) => {
|
||||
const colorMap = { '通知': 'blue', '公告': 'purple', '提醒': 'cyan' }
|
||||
return <Tag color={colorMap[type] || 'default'}>{type}</Tag>
|
||||
}
|
||||
|
||||
const getPriorityTag = (priority) => {
|
||||
return <Tag color={priority === '高' ? 'red' : 'default'}>{priority}</Tag>
|
||||
}
|
||||
|
||||
const unreadCount = notifications.filter(n => n.is_read === 0).length
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '标题',
|
||||
dataIndex: 'title',
|
||||
key: 'title',
|
||||
width: 220,
|
||||
ellipsis: true,
|
||||
render: (text, record) => (
|
||||
<span>
|
||||
{record.is_read === 0 && <Badge status="processing" />}
|
||||
{' '}{text}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '内容',
|
||||
dataIndex: 'content',
|
||||
key: 'content',
|
||||
width: 300,
|
||||
ellipsis: { showTitle: false },
|
||||
render: (text) => <Tooltip placement="topLeft" title={text}>{text}</Tooltip>
|
||||
},
|
||||
{ title: '类型', dataIndex: 'type', key: 'type', width: 80, render: getTypeTag },
|
||||
{ title: '优先级', dataIndex: 'priority', key: 'priority', width: 80, render: getPriorityTag },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'is_read',
|
||||
key: 'is_read',
|
||||
width: 80,
|
||||
render: (val) => val ? <Tag color="green">已读</Tag> : <Tag color="orange">未读</Tag>
|
||||
},
|
||||
{ title: '目标', dataIndex: 'target', key: 'target', width: 90 },
|
||||
{ title: '发布时间', dataIndex: 'created_at', key: 'created_at', width: 170 },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
fixed: 'right',
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
{record.is_read === 0 && (
|
||||
<Button type="link" icon={<CheckOutlined />} size="small" onClick={() => handleMarkRead(record.id)}>已读</Button>
|
||||
)}
|
||||
{manageAllowed && (
|
||||
<Button type="link" icon={<EditOutlined />} size="small" onClick={() => handleEdit(record)}>编辑</Button>
|
||||
)}
|
||||
{manageAllowed && (
|
||||
<Popconfirm title="确定要删除此通知吗?" onConfirm={() => handleDelete(record.id)} okText="确定" cancelText="取消">
|
||||
<Button type="link" danger icon={<DeleteOutlined />} size="small">删除</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<div className="search-bar">
|
||||
<Input
|
||||
placeholder="搜索标题/内容"
|
||||
prefix={<SearchOutlined />}
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
style={{ width: 200 }}
|
||||
allowClear
|
||||
/>
|
||||
<Select
|
||||
placeholder="类型"
|
||||
value={filterType || undefined}
|
||||
onChange={(val) => setFilterType(val || '')}
|
||||
style={{ width: 110 }}
|
||||
allowClear
|
||||
>
|
||||
{notifTypes.map(t => <Option key={t} value={t}>{t}</Option>)}
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="优先级"
|
||||
value={filterPriority || undefined}
|
||||
onChange={(val) => setFilterPriority(val || '')}
|
||||
style={{ width: 110 }}
|
||||
allowClear
|
||||
>
|
||||
{priorities.map(p => <Option key={p} value={p}>{p}</Option>)}
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="阅读状态"
|
||||
value={filterRead}
|
||||
onChange={(val) => setFilterRead(val)}
|
||||
style={{ width: 120 }}
|
||||
allowClear
|
||||
>
|
||||
<Option value="0">未读</Option>
|
||||
<Option value="1">已读</Option>
|
||||
</Select>
|
||||
<Button icon={<ReloadOutlined />} onClick={handleReset}>重置</Button>
|
||||
</div>
|
||||
<div className="table-toolbar">
|
||||
<Space>
|
||||
<span>共 {notifications.length} 条记录</span>
|
||||
{unreadCount > 0 && (
|
||||
<Button icon={<CheckCircleOutlined />} onClick={handleMarkAllRead} size="small">
|
||||
全部已读 ({unreadCount})
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
{manageAllowed && <Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>发布通知</Button>}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={notifications}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
scroll={{ x: 1300 }}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条`
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={editingNotif ? '编辑通知' : '发布通知'}
|
||||
open={modalVisible}
|
||||
onOk={handleSubmit}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
width={640}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<Form.Item name="title" label="标题" rules={[{ required: true, message: '请输入标题' }]}>
|
||||
<Input placeholder="请输入通知标题" />
|
||||
</Form.Item>
|
||||
<Form.Item name="content" label="内容" rules={[{ required: true, message: '请输入内容' }]}>
|
||||
<TextArea rows={4} placeholder="请输入通知内容" />
|
||||
</Form.Item>
|
||||
<div className="form-grid-3">
|
||||
<Form.Item name="type" label="类型" initialValue="通知">
|
||||
<Select>
|
||||
{notifTypes.map(t => <Option key={t} value={t}>{t}</Option>)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item name="priority" label="优先级" initialValue="普通">
|
||||
<Select>
|
||||
{priorities.map(p => <Option key={p} value={p}>{p}</Option>)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item name="target" label="发送范围" initialValue="全部">
|
||||
<Select>
|
||||
{targets.map(t => <Option key={t} value={t}>{t}</Option>)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotificationManagement
|
||||
@@ -0,0 +1,303 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Table, Button, Input, Select, Space, Tag, Modal, Form, DatePicker, message, Popconfirm, Card, Switch } from 'antd'
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, ReloadOutlined, PhoneOutlined, MailOutlined } from '@ant-design/icons'
|
||||
import axios from 'axios'
|
||||
import dayjs from 'dayjs'
|
||||
import { can, roleLabel } from '../permissions'
|
||||
|
||||
const { Option } = Select
|
||||
const { TextArea } = Input
|
||||
|
||||
const departments = ['拍摄组', '后勤组', '监督组']
|
||||
|
||||
function PersonnelManagement({ user }) {
|
||||
const manageAllowed = can(user, 'personnel:manage')
|
||||
const [personnel, setPersonnel] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [modalVisible, setModalVisible] = useState(false)
|
||||
const [editingPerson, setEditingPerson] = useState(null)
|
||||
const [keyword, setKeyword] = useState('')
|
||||
const [filterDept, setFilterDept] = useState('')
|
||||
const [filterStatus, setFilterStatus] = useState('')
|
||||
const [form] = Form.useForm()
|
||||
|
||||
useEffect(() => {
|
||||
fetchPersonnel()
|
||||
}, [keyword, filterDept, filterStatus])
|
||||
|
||||
const fetchPersonnel = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const params = {}
|
||||
if (keyword) params.keyword = keyword
|
||||
if (filterDept) params.department = filterDept
|
||||
if (filterStatus) params.status = filterStatus
|
||||
|
||||
const res = await axios.get('/api/personnel', { params })
|
||||
if (res.data.success) {
|
||||
setPersonnel(res.data.data)
|
||||
}
|
||||
} catch (err) {
|
||||
message.error('获取人员列表失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
setEditingPerson(null)
|
||||
form.resetFields()
|
||||
setModalVisible(true)
|
||||
}
|
||||
|
||||
const handleEdit = (record) => {
|
||||
setEditingPerson(record)
|
||||
form.setFieldsValue({
|
||||
...record,
|
||||
entry_date: record.entry_date ? dayjs(record.entry_date) : null,
|
||||
createAccount: !!record.user_id,
|
||||
account_username: record.account_username || '',
|
||||
account_role: record.account_role || 'user',
|
||||
account_password: ''
|
||||
})
|
||||
setModalVisible(true)
|
||||
}
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
try {
|
||||
const res = await axios.delete(`/api/personnel/${id}`)
|
||||
if (res.data.success) {
|
||||
message.success('人员删除成功')
|
||||
fetchPersonnel()
|
||||
}
|
||||
} catch (err) {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields()
|
||||
const data = {
|
||||
...values,
|
||||
entry_date: values.entry_date ? values.entry_date.format('YYYY-MM-DD') : '',
|
||||
account: {
|
||||
createAccount: !!values.createAccount,
|
||||
username: values.account_username,
|
||||
password: values.account_password,
|
||||
role: values.account_role || 'user'
|
||||
}
|
||||
}
|
||||
delete data.createAccount
|
||||
delete data.account_username
|
||||
delete data.account_password
|
||||
delete data.account_role
|
||||
|
||||
if (editingPerson) {
|
||||
const res = await axios.put(`/api/personnel/${editingPerson.id}`, data)
|
||||
if (res.data.success) {
|
||||
message.success('人员更新成功')
|
||||
}
|
||||
} else {
|
||||
const res = await axios.post('/api/personnel', data)
|
||||
if (res.data.success) {
|
||||
message.success('人员添加成功')
|
||||
}
|
||||
}
|
||||
setModalVisible(false)
|
||||
fetchPersonnel()
|
||||
} catch (err) {
|
||||
if (err.response) {
|
||||
message.error(err.response.data.message || '操作失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
setKeyword('')
|
||||
setFilterDept('')
|
||||
setFilterStatus('')
|
||||
}
|
||||
|
||||
const getStatusTag = (status) => {
|
||||
return <Tag color={status === '在职' ? 'green' : status === '离职' ? 'red' : 'default'}>{status}</Tag>
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ title: '姓名', dataIndex: 'name', key: 'name', width: 100 },
|
||||
{ title: '工号', dataIndex: 'employee_id', key: 'employee_id', width: 100 },
|
||||
{ title: '部门', dataIndex: 'department', key: 'department', width: 100 },
|
||||
{ title: '职位', dataIndex: 'position', key: 'position', width: 130 },
|
||||
{
|
||||
title: '电话',
|
||||
dataIndex: 'phone',
|
||||
key: 'phone',
|
||||
width: 130,
|
||||
render: (text) => text ? <span><PhoneOutlined style={{ marginRight: 4 }} />{text}</span> : '-'
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
dataIndex: 'email',
|
||||
key: 'email',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
render: (text) => text ? <span><MailOutlined style={{ marginRight: 4 }} />{text}</span> : '-'
|
||||
},
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 80, render: getStatusTag },
|
||||
{
|
||||
title: '登录账户',
|
||||
key: 'account',
|
||||
width: 140,
|
||||
render: (_, record) => record.account_username
|
||||
? <Space direction="vertical" size={0}>
|
||||
<span>{record.account_username}</span>
|
||||
<Tag color={record.account_role === 'admin' ? 'gold' : 'blue'} style={{ marginRight: 0 }}>{roleLabel(record.account_role)}</Tag>
|
||||
</Space>
|
||||
: <Tag>无账户</Tag>
|
||||
},
|
||||
{ title: '入职日期', dataIndex: 'entry_date', key: 'entry_date', width: 110 },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 140,
|
||||
fixed: 'right',
|
||||
render: (_, record) => manageAllowed ? (
|
||||
<Space>
|
||||
<Button type="link" icon={<EditOutlined />} size="small" onClick={() => handleEdit(record)}>编辑</Button>
|
||||
<Popconfirm title="确定要删除此人员吗?" onConfirm={() => handleDelete(record.id)} okText="确定" cancelText="取消">
|
||||
<Button type="link" danger icon={<DeleteOutlined />} size="small">删除</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
) : <Tag color="default">仅查看</Tag>
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<div className="search-bar">
|
||||
<Input
|
||||
placeholder="搜索姓名/工号/手机号"
|
||||
prefix={<SearchOutlined />}
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
style={{ width: 220 }}
|
||||
allowClear
|
||||
/>
|
||||
<Select
|
||||
placeholder="部门"
|
||||
value={filterDept || undefined}
|
||||
onChange={(val) => setFilterDept(val || '')}
|
||||
style={{ width: 130 }}
|
||||
allowClear
|
||||
>
|
||||
{departments.map(d => <Option key={d} value={d}>{d}</Option>)}
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="在职状态"
|
||||
value={filterStatus || undefined}
|
||||
onChange={(val) => setFilterStatus(val || '')}
|
||||
style={{ width: 120 }}
|
||||
allowClear
|
||||
>
|
||||
<Option value="在职">在职</Option>
|
||||
<Option value="离职">离职</Option>
|
||||
</Select>
|
||||
<Button icon={<ReloadOutlined />} onClick={handleReset}>重置</Button>
|
||||
</div>
|
||||
<div className="table-toolbar">
|
||||
<span>共 {personnel.length} 条记录{!manageAllowed && '(当前为普通账户,仅可查看)'}</span>
|
||||
{manageAllowed && <Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>添加人员</Button>}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={personnel}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
scroll={{ x: 1100 }}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条`
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={editingPerson ? '编辑人员' : '添加人员'}
|
||||
open={modalVisible}
|
||||
onOk={handleSubmit}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
width={720}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<div className="form-grid">
|
||||
<Form.Item name="name" label="姓名" rules={[{ required: true, message: '请输入姓名' }]}>
|
||||
<Input placeholder="请输入姓名" />
|
||||
</Form.Item>
|
||||
<Form.Item name="employee_id" label="工号">
|
||||
<Input placeholder="请输入工号" />
|
||||
</Form.Item>
|
||||
<Form.Item name="department" label="部门">
|
||||
<Select placeholder="请选择部门" allowClear>
|
||||
{departments.map(d => <Option key={d} value={d}>{d}</Option>)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item name="position" label="职位">
|
||||
<Input placeholder="请输入职位" />
|
||||
</Form.Item>
|
||||
<Form.Item name="phone" label="电话">
|
||||
<Input placeholder="请输入电话" />
|
||||
</Form.Item>
|
||||
<Form.Item name="email" label="邮箱">
|
||||
<Input placeholder="请输入邮箱" />
|
||||
</Form.Item>
|
||||
<Form.Item name="status" label="状态" initialValue="在职">
|
||||
<Select>
|
||||
<Option value="在职">在职</Option>
|
||||
<Option value="离职">离职</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item name="entry_date" label="入职日期">
|
||||
<DatePicker style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<Card size="small" title="登录账户(可选)" style={{ marginBottom: 16, background: '#fafafa' }}>
|
||||
<Form.Item name="createAccount" label="创建/启用登录账户" valuePropName="checked">
|
||||
<Switch checkedChildren="开" unCheckedChildren="关" />
|
||||
</Form.Item>
|
||||
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.createAccount !== cur.createAccount}>
|
||||
{({ getFieldValue }) => getFieldValue('createAccount') ? (
|
||||
<div className="form-grid">
|
||||
<Form.Item name="account_username" label="用户名" rules={[{ required: true, message: '请输入用户名' }]}>
|
||||
<Input placeholder="登录用户名" />
|
||||
</Form.Item>
|
||||
<Form.Item name="account_password" label="密码" rules={[{ required: !editingPerson, message: '请输入密码' }]}>
|
||||
<Input.Password placeholder={editingPerson ? '留空则不修改' : '登录密码'} />
|
||||
</Form.Item>
|
||||
<Form.Item name="account_role" label="账户角色" initialValue="user">
|
||||
<Select>
|
||||
<Option value="user">普通账户</Option>
|
||||
<Option value="admin">管理员账户</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</div>
|
||||
) : null}
|
||||
</Form.Item>
|
||||
</Card>
|
||||
|
||||
<Form.Item name="description" label="备注">
|
||||
<TextArea rows={3} placeholder="请输入备注信息" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PersonnelManagement
|
||||
Reference in New Issue
Block a user