init
This commit is contained in:
parent
abbc1dd7a6
commit
aca88d1214
|
|
@ -0,0 +1,474 @@
|
|||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const searchQuery = ref('')
|
||||
const selectedCategory = ref('all')
|
||||
const selectedStatus = ref('all')
|
||||
|
||||
const categories = [
|
||||
{ value: 'all', label: '全部类型' },
|
||||
{ value: 'sensor', label: '监测传感器' },
|
||||
{ value: 'camera', label: '监控摄像头' },
|
||||
{ value: 'collector', label: '数据采集器' },
|
||||
{ value: 'monitor', label: '环境监测仪' }
|
||||
]
|
||||
|
||||
const statuses = [
|
||||
{ value: 'all', label: '全部状态' },
|
||||
{ value: 'online', label: '在线' },
|
||||
{ value: 'offline', label: '离线' },
|
||||
{ value: 'error', label: '故障' }
|
||||
]
|
||||
|
||||
const devices = ref([
|
||||
{ id: 'DEV001', name: '温度传感器-001', type: '监测传感器', location: '大殿-东厢房', status: 'online', lastActive: '2分钟前', battery: '85%' },
|
||||
{ id: 'DEV002', name: '监控摄像头-012', type: '监控摄像头', location: '山门-入口', status: 'offline', lastActive: '12分钟前', battery: '92%' },
|
||||
{ id: 'DEV003', name: '湿度传感器-008', type: '监测传感器', location: '大殿-西厢房', status: 'warning', lastActive: '25分钟前', battery: '78%' },
|
||||
{ id: 'DEV004', name: '振动监测仪-003', type: '数据采集器', location: '钟楼-二楼', status: 'online', lastActive: '32分钟前', battery: '65%' },
|
||||
{ id: 'DEV005', name: '环境监测仪-015', type: '环境监测仪', location: '鼓楼-一楼', status: 'online', lastActive: '45分钟前', battery: '88%' },
|
||||
{ id: 'DEV006', name: '监控摄像头-008', type: '监控摄像头', location: '大殿-正门', status: 'online', lastActive: '1小时前', battery: '95%' },
|
||||
{ id: 'DEV007', name: '数据采集器-022', type: '数据采集器', location: '藏经阁-地下室', status: 'error', lastActive: '2小时前', battery: '42%' },
|
||||
{ id: 'DEV008', name: '温度传感器-015', type: '监测传感器', location: '方丈室-后院', status: 'online', lastActive: '3小时前', battery: '72%' }
|
||||
])
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
online: '在线',
|
||||
offline: '离线',
|
||||
warning: '异常',
|
||||
error: '故障'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="device-list">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">设备管理</h1>
|
||||
<p class="page-subtitle">管理所有设备的运行状态与配置信息</p>
|
||||
</div>
|
||||
|
||||
<section class="filter-section">
|
||||
<div class="filter-row">
|
||||
<div class="search-box">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
class="search-input"
|
||||
placeholder="搜索设备名称、编号或位置..."
|
||||
>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<select v-model="selectedCategory" class="filter-select">
|
||||
<option v-for="cat in categories" :key="cat.value" :value="cat.value">
|
||||
{{ cat.label }}
|
||||
</option>
|
||||
</select>
|
||||
<select v-model="selectedStatus" class="filter-select">
|
||||
<option v-for="stat in statuses" :key="stat.value" :value="stat.value">
|
||||
{{ stat.label }}
|
||||
</option>
|
||||
</select>
|
||||
<button class="btn btn-primary">添加设备</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="table-section">
|
||||
<div class="table-container">
|
||||
<table class="device-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>设备编号</th>
|
||||
<th>设备名称</th>
|
||||
<th>设备类型</th>
|
||||
<th>安装位置</th>
|
||||
<th>状态</th>
|
||||
<th>最后活跃</th>
|
||||
<th>电量</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="device in devices" :key="device.id">
|
||||
<td class="table-cell--code">{{ device.id }}</td>
|
||||
<td class="table-cell--name">{{ device.name }}</td>
|
||||
<td>{{ device.type }}</td>
|
||||
<td>{{ device.location }}</td>
|
||||
<td>
|
||||
<span class="status-badge" :class="`status-badge--${device.status}`">
|
||||
{{ getStatusText(device.status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="table-cell--time">{{ device.lastActive }}</td>
|
||||
<td class="table-cell--battery">
|
||||
<div class="battery-indicator">
|
||||
<div class="battery-bar" :style="{ width: device.battery }"></div>
|
||||
</div>
|
||||
<span>{{ device.battery }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="btn-icon" title="查看详情">详情</button>
|
||||
<button class="btn-icon" title="编辑">编辑</button>
|
||||
<button class="btn-icon" title="删除">删除</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="pagination">
|
||||
<div class="pagination__info">显示 1-8 共 1,248 条</div>
|
||||
<div class="pagination__controls">
|
||||
<button class="pagination-btn" disabled>上一页</button>
|
||||
<button class="pagination-btn pagination-btn--active">1</button>
|
||||
<button class="pagination-btn">2</button>
|
||||
<button class="pagination-btn">3</button>
|
||||
<button class="pagination-btn">...</button>
|
||||
<button class="pagination-btn">156</button>
|
||||
<button class="pagination-btn">下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.device-list {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 18px;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
flex: 1;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
border-color: rgba(180, 128, 64, 0.5);
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.btn {
|
||||
height: 40px;
|
||||
padding: 0 18px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #b48040, #205c40);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.device-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.device-table thead {
|
||||
background: rgba(31, 35, 40, 0.04);
|
||||
}
|
||||
|
||||
.device-table th {
|
||||
padding: 14px 16px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
border-bottom: 1px solid var(--border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.device-table td {
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.device-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.device-table tbody tr:hover {
|
||||
background: rgba(31, 35, 40, 0.02);
|
||||
}
|
||||
|
||||
.table-cell--code {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.table-cell--name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.table-cell--time {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.table-cell--battery {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.battery-indicator {
|
||||
width: 40px;
|
||||
height: 6px;
|
||||
background: rgba(31, 35, 40, 0.1);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.battery-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #b48040, #205c40);
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
padding: 0 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-badge--online {
|
||||
background: rgba(32, 92, 64, 0.12);
|
||||
color: #205c40;
|
||||
}
|
||||
|
||||
.status-badge--offline {
|
||||
background: rgba(180, 128, 64, 0.12);
|
||||
color: #b48040;
|
||||
}
|
||||
|
||||
.status-badge--warning {
|
||||
background: rgba(245, 158, 11, 0.12);
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.status-badge--error {
|
||||
background: rgba(220, 38, 38, 0.12);
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
border-color: rgba(180, 128, 64, 0.4);
|
||||
background: rgba(180, 128, 64, 0.08);
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.pagination__info {
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.pagination__controls {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pagination-btn {
|
||||
min-width: 36px;
|
||||
height: 36px;
|
||||
padding: 0 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.pagination-btn:hover:not(:disabled) {
|
||||
border-color: rgba(180, 128, 64, 0.4);
|
||||
background: rgba(180, 128, 64, 0.08);
|
||||
}
|
||||
|
||||
.pagination-btn--active {
|
||||
background: linear-gradient(135deg, #b48040, #205c40);
|
||||
color: #fff;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.pagination-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.filter-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.device-list {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pagination__controls {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.device-table th,
|
||||
.device-table td {
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,389 @@
|
|||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const stats = ref([
|
||||
{ label: '设备总数', value: '1,248', trend: '+12%', status: 'up' },
|
||||
{ label: '在线设备', value: '1,156', trend: '+8%', status: 'up' },
|
||||
{ label: '离线设备', value: '68', trend: '-3%', status: 'down' },
|
||||
{ label: '故障设备', value: '24', trend: '-5%', status: 'down' }
|
||||
])
|
||||
|
||||
const categories = ref([
|
||||
{ name: '监测传感器', count: 456, percentage: 36.5 },
|
||||
{ name: '监控摄像头', count: 312, percentage: 25.0 },
|
||||
{ name: '数据采集器', count: 280, percentage: 22.4 },
|
||||
{ name: '环境监测仪', count: 120, percentage: 9.6 },
|
||||
{ name: '其他设备', count: 80, percentage: 6.5 }
|
||||
])
|
||||
|
||||
const recentActivities = ref([
|
||||
{ device: '温度传感器-001', action: '上线', time: '5分钟前', status: 'online' },
|
||||
{ device: '监控摄像头-012', action: '离线', time: '12分钟前', status: 'offline' },
|
||||
{ device: '湿度传感器-008', action: '数据异常', time: '25分钟前', status: 'warning' },
|
||||
{ device: '振动监测仪-003', action: '恢复正常', time: '32分钟前', status: 'success' },
|
||||
{ device: '数据采集器-015', action: '上线', time: '45分钟前', status: 'online' }
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="device-summary">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">设备概述</h1>
|
||||
<p class="page-subtitle">实时监控设备状态与运行情况</p>
|
||||
</div>
|
||||
|
||||
<section class="stats-grid">
|
||||
<div v-for="stat in stats" :key="stat.label" class="stat-card">
|
||||
<div class="stat__label">{{ stat.label }}</div>
|
||||
<div class="stat__value">{{ stat.value }}</div>
|
||||
<div class="stat__trend" :class="`stat__trend--${stat.status}`">
|
||||
{{ stat.trend }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-grid">
|
||||
<div class="content-card">
|
||||
<h2 class="card-title">设备分类统计</h2>
|
||||
<div class="category-list">
|
||||
<div v-for="cat in categories" :key="cat.name" class="category-item">
|
||||
<div class="category__info">
|
||||
<div class="category__name">{{ cat.name }}</div>
|
||||
<div class="category__count">{{ cat.count }} 台</div>
|
||||
</div>
|
||||
<div class="category__bar">
|
||||
<div class="category__progress" :style="{ width: cat.percentage + '%' }"></div>
|
||||
</div>
|
||||
<div class="category__percentage">{{ cat.percentage }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-card">
|
||||
<h2 class="card-title">最近活动</h2>
|
||||
<div class="activity-list">
|
||||
<div v-for="activity in recentActivities" :key="activity.device" class="activity-item">
|
||||
<div class="activity__dot" :class="`activity__dot--${activity.status}`"></div>
|
||||
<div class="activity__content">
|
||||
<div class="activity__device">{{ activity.device }}</div>
|
||||
<div class="activity__action">{{ activity.action }}</div>
|
||||
</div>
|
||||
<div class="activity__time">{{ activity.time }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="status-overview">
|
||||
<h2 class="card-title">设备状态分布</h2>
|
||||
<div class="status-bars">
|
||||
<div class="status-bar">
|
||||
<div class="status-bar__label">在线</div>
|
||||
<div class="status-bar__track">
|
||||
<div class="status-bar__fill status-bar__fill--online" style="width: 92.6%"></div>
|
||||
</div>
|
||||
<div class="status-bar__value">1,156</div>
|
||||
</div>
|
||||
<div class="status-bar">
|
||||
<div class="status-bar__label">离线</div>
|
||||
<div class="status-bar__track">
|
||||
<div class="status-bar__fill status-bar__fill--offline" style="width: 5.4%"></div>
|
||||
</div>
|
||||
<div class="status-bar__value">68</div>
|
||||
</div>
|
||||
<div class="status-bar">
|
||||
<div class="status-bar__label">故障</div>
|
||||
<div class="status-bar__track">
|
||||
<div class="status-bar__fill status-bar__fill--error" style="width: 2.0%"></div>
|
||||
</div>
|
||||
<div class="status-bar__value">24</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.device-summary {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.stat__label {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat__value {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat__trend {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stat__trend--up {
|
||||
color: #205c40;
|
||||
}
|
||||
|
||||
.stat__trend--down {
|
||||
color: #b48040;
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.content-card {
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin: 0 0 16px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.category-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.category__info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.category__name {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.category__count {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.category__bar {
|
||||
height: 8px;
|
||||
background: rgba(31, 35, 40, 0.08);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.category__progress {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #b48040, #205c40);
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.category__percentage {
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.activity-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.activity-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.activity__dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.activity__dot--online {
|
||||
background: #205c40;
|
||||
}
|
||||
|
||||
.activity__dot--offline {
|
||||
background: #b48040;
|
||||
}
|
||||
|
||||
.activity__dot--warning {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.activity__dot--success {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
.activity__content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.activity__device {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.activity__action {
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.activity__time {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-overview {
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.status-bars {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.status-bar__label {
|
||||
width: 60px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-bar__track {
|
||||
flex: 1;
|
||||
height: 12px;
|
||||
background: rgba(31, 35, 40, 0.08);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.status-bar__fill {
|
||||
height: 100%;
|
||||
border-radius: 6px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.status-bar__fill--online {
|
||||
background: #205c40;
|
||||
}
|
||||
|
||||
.status-bar__fill--offline {
|
||||
background: #b48040;
|
||||
}
|
||||
|
||||
.status-bar__fill--error {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.status-bar__value {
|
||||
width: 80px;
|
||||
text-align: right;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.device-summary {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stat__value {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,467 @@
|
|||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const searchQuery = ref('')
|
||||
const selectedPosition = ref('all')
|
||||
const selectedOrg = ref('all')
|
||||
const selectedStatus = ref('all')
|
||||
|
||||
const positions = [
|
||||
{ value: 'all', label: '全部职位' },
|
||||
{ value: 'director', label: '主任' },
|
||||
{ value: 'deputy', label: '副主任' },
|
||||
{ value: 'section', label: '科长' },
|
||||
{ value: 'staff', label: '工作人员' }
|
||||
]
|
||||
|
||||
const organizations = [
|
||||
{ value: 'all', label: '全部机构' },
|
||||
{ value: 'heritage', label: '文物保护局' },
|
||||
{ value: 'museum', label: '博物馆' },
|
||||
{ value: 'research', label: '研究所' },
|
||||
{ value: 'maintenance', label: '维护中心' }
|
||||
]
|
||||
|
||||
const statuses = [
|
||||
{ value: 'all', label: '全部状态' },
|
||||
{ value: 'active', label: '在职' },
|
||||
{ value: 'leave', label: '休假' },
|
||||
{ value: 'resigned', label: '离职' }
|
||||
]
|
||||
|
||||
const persons = ref([
|
||||
{ id: 'P001', name: '张明远', position: '主任', organization: '文物保护局', phone: '138-0000-0001', email: 'zhangmy@example.com', status: 'active', joinDate: '2020-03-15' },
|
||||
{ id: 'P002', name: '李华', position: '副主任', organization: '文物保护局', phone: '138-0000-0002', email: 'lihua@example.com', status: 'active', joinDate: '2020-05-20' },
|
||||
{ id: 'P003', name: '王建国', position: '科长', organization: '博物馆', phone: '138-0000-0003', email: 'wangjg@example.com', status: 'active', joinDate: '2021-01-10' },
|
||||
{ id: 'P004', name: '赵丽', position: '工作人员', organization: '研究所', phone: '138-0000-0004', email: 'zhaoli@example.com', status: 'leave', joinDate: '2021-06-18' },
|
||||
{ id: 'P005', name: '陈志强', position: '副主任', organization: '维护中心', phone: '138-0000-0005', email: 'chenzq@example.com', status: 'active', joinDate: '2020-08-25' },
|
||||
{ id: 'P006', name: '刘芳', position: '科长', organization: '文物保护局', phone: '138-0000-0006', email: 'liufang@example.com', status: 'active', joinDate: '2021-03-30' },
|
||||
{ id: 'P007', name: '孙伟', position: '工作人员', organization: '博物馆', phone: '138-0000-0007', email: 'sunwei@example.com', status: 'resigned', joinDate: '2021-09-12' },
|
||||
{ id: 'P008', name: '周敏', position: '工作人员', organization: '维护中心', phone: '138-0000-0008', email: 'zhoumin@example.com', status: 'active', joinDate: '2022-01-05' }
|
||||
])
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
active: '在职',
|
||||
leave: '休假',
|
||||
resigned: '离职'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="person-list">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">机构负责人</h1>
|
||||
<p class="page-subtitle">管理机构负责人信息与职责分配</p>
|
||||
</div>
|
||||
|
||||
<section class="filter-section">
|
||||
<div class="filter-row">
|
||||
<div class="search-box">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
class="search-input"
|
||||
placeholder="搜索姓名、职位或联系方式..."
|
||||
>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<select v-model="selectedPosition" class="filter-select">
|
||||
<option v-for="pos in positions" :key="pos.value" :value="pos.value">
|
||||
{{ pos.label }}
|
||||
</option>
|
||||
</select>
|
||||
<select v-model="selectedOrg" class="filter-select">
|
||||
<option v-for="org in organizations" :key="org.value" :value="org.value">
|
||||
{{ org.label }}
|
||||
</option>
|
||||
</select>
|
||||
<select v-model="selectedStatus" class="filter-select">
|
||||
<option v-for="stat in statuses" :key="stat.value" :value="stat.value">
|
||||
{{ stat.label }}
|
||||
</option>
|
||||
</select>
|
||||
<button class="btn btn-primary">添加人员</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="table-section">
|
||||
<div class="table-container">
|
||||
<table class="person-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>人员编号</th>
|
||||
<th>姓名</th>
|
||||
<th>职位</th>
|
||||
<th>所属机构</th>
|
||||
<th>联系电话</th>
|
||||
<th>邮箱</th>
|
||||
<th>状态</th>
|
||||
<th>入职日期</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="person in persons" :key="person.id">
|
||||
<td class="table-cell--code">{{ person.id }}</td>
|
||||
<td class="table-cell--name">{{ person.name }}</td>
|
||||
<td>{{ person.position }}</td>
|
||||
<td>{{ person.organization }}</td>
|
||||
<td class="table-cell--phone">{{ person.phone }}</td>
|
||||
<td class="table-cell--email">{{ person.email }}</td>
|
||||
<td>
|
||||
<span class="status-badge" :class="`status-badge--${person.status}`">
|
||||
{{ getStatusText(person.status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="table-cell--date">{{ person.joinDate }}</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="btn-icon" title="查看详情">详情</button>
|
||||
<button class="btn-icon" title="编辑">编辑</button>
|
||||
<button class="btn-icon" title="删除">删除</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="pagination">
|
||||
<div class="pagination__info">显示 1-8 共 156 条</div>
|
||||
<div class="pagination__controls">
|
||||
<button class="pagination-btn" disabled>上一页</button>
|
||||
<button class="pagination-btn pagination-btn--active">1</button>
|
||||
<button class="pagination-btn">2</button>
|
||||
<button class="pagination-btn">3</button>
|
||||
<button class="pagination-btn">...</button>
|
||||
<button class="pagination-btn">20</button>
|
||||
<button class="pagination-btn">下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.person-list {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 18px;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
flex: 1;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
border-color: rgba(180, 128, 64, 0.5);
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.btn {
|
||||
height: 40px;
|
||||
padding: 0 18px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #b48040, #205c40);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.person-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.person-table thead {
|
||||
background: rgba(31, 35, 40, 0.04);
|
||||
}
|
||||
|
||||
.person-table th {
|
||||
padding: 14px 16px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
border-bottom: 1px solid var(--border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.person-table td {
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.person-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.person-table tbody tr:hover {
|
||||
background: rgba(31, 35, 40, 0.02);
|
||||
}
|
||||
|
||||
.table-cell--code {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.table-cell--name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.table-cell--phone {
|
||||
font-family: 'Courier New', monospace;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.table-cell--email {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.table-cell--date {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
padding: 0 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-badge--active {
|
||||
background: rgba(32, 92, 64, 0.12);
|
||||
color: #205c40;
|
||||
}
|
||||
|
||||
.status-badge--leave {
|
||||
background: rgba(180, 128, 64, 0.12);
|
||||
color: #b48040;
|
||||
}
|
||||
|
||||
.status-badge--resigned {
|
||||
background: rgba(107, 114, 128, 0.12);
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
border-color: rgba(180, 128, 64, 0.4);
|
||||
background: rgba(180, 128, 64, 0.08);
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.pagination__info {
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.pagination__controls {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pagination-btn {
|
||||
min-width: 36px;
|
||||
height: 36px;
|
||||
padding: 0 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.pagination-btn:hover:not(:disabled) {
|
||||
border-color: rgba(180, 128, 64, 0.4);
|
||||
background: rgba(180, 128, 64, 0.08);
|
||||
}
|
||||
|
||||
.pagination-btn--active {
|
||||
background: linear-gradient(135deg, #b48040, #205c40);
|
||||
color: #fff;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.pagination-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.filter-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.person-list {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pagination__controls {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.person-table th,
|
||||
.person-table td {
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,638 @@
|
|||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const project = ref({
|
||||
id: 'PRJ001',
|
||||
name: '大殿屋顶修缮工程',
|
||||
type: '修缮工程',
|
||||
status: 'ongoing',
|
||||
manager: '张明远',
|
||||
organization: '文物保护局',
|
||||
budget: '850,000',
|
||||
actualCost: '520,000',
|
||||
startDate: '2024-01-15',
|
||||
endDate: '2024-06-30',
|
||||
progress: 65,
|
||||
description: '对大殿屋顶进行全面修缮,包括更换破损瓦片、加固木结构、修复彩绘等工作,确保古建筑的结构安全和历史风貌。'
|
||||
})
|
||||
|
||||
const milestones = ref([
|
||||
{ name: '项目立项', status: 'completed', date: '2024-01-15', description: '完成项目立项审批和资金拨付' },
|
||||
{ name: '勘察设计', status: 'completed', date: '2024-02-28', description: '完成现场勘察和施工图设计' },
|
||||
{ name: '材料采购', status: 'completed', date: '2024-03-15', description: '完成修缮材料采购和验收' },
|
||||
{ name: '施工准备', status: 'completed', date: '2024-03-30', description: '完成施工队伍组织和现场准备' },
|
||||
{ name: '主体施工', status: 'ongoing', date: '2024-05-15', description: '正在进行主体结构修缮施工' },
|
||||
{ name: '质量验收', status: 'pending', date: '2024-06-15', description: '完成施工质量验收' },
|
||||
{ name: '项目竣工', status: 'pending', date: '2024-06-30', description: '完成项目竣工验收和资料归档' }
|
||||
])
|
||||
|
||||
const documents = ref([
|
||||
{ name: '项目立项报告.pdf', size: '2.3 MB', date: '2024-01-15', type: 'pdf' },
|
||||
{ name: '施工图纸.dwg', size: '5.8 MB', date: '2024-02-28', type: 'dwg' },
|
||||
{ name: '材料清单.xlsx', size: '1.2 MB', date: '2024-03-15', type: 'excel' },
|
||||
{ name: '施工方案.docx', size: '3.5 MB', date: '2024-03-30', type: 'word' },
|
||||
{ name: '进度报告.pdf', size: '1.8 MB', date: '2024-04-15', type: 'pdf' }
|
||||
])
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
ongoing: '进行中',
|
||||
completed: '已完成',
|
||||
pending: '未开始',
|
||||
planning: '规划中',
|
||||
suspended: '已暂停'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
const getMilestoneStatusText = (status) => {
|
||||
const statusMap = {
|
||||
completed: '已完成',
|
||||
ongoing: '进行中',
|
||||
pending: '未开始'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
const getProgressColor = (progress) => {
|
||||
if (progress >= 80) return '#205c40'
|
||||
if (progress >= 50) return '#b48040'
|
||||
return '#f59e0b'
|
||||
}
|
||||
|
||||
const getDocumentIcon = (type) => {
|
||||
const icons = {
|
||||
pdf: '📄',
|
||||
dwg: '📐',
|
||||
excel: '📊',
|
||||
word: '📝'
|
||||
}
|
||||
return icons[type] || '📄'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="project-detail">
|
||||
<div class="page-header">
|
||||
<div class="header-left">
|
||||
<h1 class="page-title">{{ project.name }}</h1>
|
||||
<p class="page-subtitle">项目编号:{{ project.id }}</p>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span class="status-badge" :class="`status-badge--${project.status}`">
|
||||
{{ getStatusText(project.status) }}
|
||||
</span>
|
||||
<button class="btn btn-primary">编辑项目</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="progress-section">
|
||||
<div class="progress-header">
|
||||
<h2 class="section-title">项目进度</h2>
|
||||
<div class="progress-info">
|
||||
<span class="progress-value">{{ project.progress }}%</span>
|
||||
<span class="progress-label">已完成</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-bar-large">
|
||||
<div
|
||||
class="progress-bar-large__fill"
|
||||
:style="{ width: project.progress + '%', background: getProgressColor(project.progress) }"
|
||||
></div>
|
||||
</div>
|
||||
<div class="progress-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat__label">预算总额</div>
|
||||
<div class="stat__value">¥{{ project.budget }}</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat__label">已使用</div>
|
||||
<div class="stat__value">¥{{ project.actualCost }}</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat__label">剩余预算</div>
|
||||
<div class="stat__value">¥{{ (parseInt(project.budget.replace(/,/g, '')) - parseInt(project.actualCost.replace(/,/g, ''))).toLocaleString() }}</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat__label">预算使用率</div>
|
||||
<div class="stat__value">{{ Math.round((parseInt(project.actualCost.replace(/,/g, '')) / parseInt(project.budget.replace(/,/g, ''))) * 100) }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="content-grid">
|
||||
<section class="info-section">
|
||||
<h2 class="section-title">项目信息</h2>
|
||||
<div class="info-list">
|
||||
<div class="info-item">
|
||||
<div class="info__label">项目类型</div>
|
||||
<div class="info__value">{{ project.type }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info__label">负责人</div>
|
||||
<div class="info__value">{{ project.manager }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info__label">所属机构</div>
|
||||
<div class="info__value">{{ project.organization }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info__label">开始日期</div>
|
||||
<div class="info__value">{{ project.startDate }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info__label">结束日期</div>
|
||||
<div class="info__value">{{ project.endDate }}</div>
|
||||
</div>
|
||||
<div class="info-item info-item--full">
|
||||
<div class="info__label">项目描述</div>
|
||||
<div class="info__value info__value--text">{{ project.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="milestone-section">
|
||||
<h2 class="section-title">阶段里程碑</h2>
|
||||
<div class="milestone-list">
|
||||
<div v-for="(milestone, index) in milestones" :key="index" class="milestone-item" :class="`milestone-item--${milestone.status}`">
|
||||
<div class="milestone__dot"></div>
|
||||
<div class="milestone__content">
|
||||
<div class="milestone__header">
|
||||
<div class="milestone__name">{{ milestone.name }}</div>
|
||||
<span class="milestone__status" :class="`milestone__status--${milestone.status}`">
|
||||
{{ getMilestoneStatusText(milestone.status) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="milestone__date">{{ milestone.date }}</div>
|
||||
<div class="milestone__description">{{ milestone.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="documents-section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">项目文档</h2>
|
||||
<button class="btn btn-secondary">上传文档</button>
|
||||
</div>
|
||||
<div class="documents-grid">
|
||||
<div v-for="(doc, index) in documents" :key="index" class="document-card">
|
||||
<div class="document__icon">{{ getDocumentIcon(doc.type) }}</div>
|
||||
<div class="document__info">
|
||||
<div class="document__name">{{ doc.name }}</div>
|
||||
<div class="document__meta">
|
||||
<span>{{ doc.size }}</span>
|
||||
<span>{{ doc.date }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="document__action">下载</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.project-detail {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 24px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
padding: 0 14px;
|
||||
border-radius: 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-badge--ongoing {
|
||||
background: rgba(32, 92, 64, 0.12);
|
||||
color: #205c40;
|
||||
}
|
||||
|
||||
.status-badge--completed {
|
||||
background: rgba(16, 185, 129, 0.12);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.status-badge--pending {
|
||||
background: rgba(107, 114, 128, 0.12);
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.btn {
|
||||
height: 40px;
|
||||
padding: 0 18px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #b48040, #205c40);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
border-color: rgba(180, 128, 64, 0.4);
|
||||
background: rgba(180, 128, 64, 0.08);
|
||||
}
|
||||
|
||||
.progress-section {
|
||||
margin-bottom: 18px;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.progress-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.progress-value {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.progress-bar-large {
|
||||
height: 12px;
|
||||
background: rgba(31, 35, 40, 0.1);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.progress-bar-large__fill {
|
||||
height: 100%;
|
||||
border-radius: 6px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
background: rgba(31, 35, 40, 0.04);
|
||||
}
|
||||
|
||||
.stat__label {
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat__value {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.info-section,
|
||||
.milestone-section {
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.info-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.info-item--full {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info__label {
|
||||
width: 100px;
|
||||
flex-shrink: 0;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info__value {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info__value--text {
|
||||
line-height: 1.6;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.milestone-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.milestone-item {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 16px 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.milestone-item:not(:last-child)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 7px;
|
||||
top: 32px;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
.milestone__dot {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
background: var(--border);
|
||||
border: 3px solid var(--surface);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.milestone-item--completed .milestone__dot {
|
||||
background: #205c40;
|
||||
}
|
||||
|
||||
.milestone-item--ongoing .milestone__dot {
|
||||
background: #b48040;
|
||||
}
|
||||
|
||||
.milestone__content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.milestone__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.milestone__name {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.milestone__status {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.milestone__status--completed {
|
||||
background: rgba(32, 92, 64, 0.12);
|
||||
color: #205c40;
|
||||
}
|
||||
|
||||
.milestone__status--ongoing {
|
||||
background: rgba(180, 128, 64, 0.12);
|
||||
color: #b48040;
|
||||
}
|
||||
|
||||
.milestone__status--pending {
|
||||
background: rgba(107, 114, 128, 0.12);
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.milestone__date {
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.milestone__description {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.documents-section {
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.documents-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.document-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
background: rgba(31, 35, 40, 0.02);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.document-card:hover {
|
||||
border-color: rgba(180, 128, 64, 0.4);
|
||||
background: rgba(180, 128, 64, 0.04);
|
||||
}
|
||||
|
||||
.document__icon {
|
||||
font-size: 32px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.document__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.document__name {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.document__meta {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.document__action {
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.document__action:hover {
|
||||
border-color: rgba(180, 128, 64, 0.4);
|
||||
background: rgba(180, 128, 64, 0.08);
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.content-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.progress-stats {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.project-detail {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.progress-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.documents-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.section-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.document-card {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.document__action {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,499 @@
|
|||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const searchQuery = ref('')
|
||||
const selectedType = ref('all')
|
||||
const selectedStatus = ref('all')
|
||||
|
||||
const projectTypes = [
|
||||
{ value: 'all', label: '全部类型' },
|
||||
{ value: 'repair', label: '修缮工程' },
|
||||
{ value: 'protection', label: '保护工程' },
|
||||
{ value: 'renovation', label: '改造工程' },
|
||||
{ value: 'maintenance', label: '维护工程' }
|
||||
]
|
||||
|
||||
const statuses = [
|
||||
{ value: 'all', label: '全部状态' },
|
||||
{ value: 'planning', label: '规划中' },
|
||||
{ value: 'ongoing', label: '进行中' },
|
||||
{ value: 'completed', label: '已完成' },
|
||||
{ value: 'suspended', label: '已暂停' }
|
||||
]
|
||||
|
||||
const projects = ref([
|
||||
{ id: 'PRJ001', name: '大殿屋顶修缮工程', type: '修缮工程', status: 'ongoing', manager: '张明远', budget: '850,000', startDate: '2024-01-15', endDate: '2024-06-30', progress: 65 },
|
||||
{ id: 'PRJ002', name: '山门保护工程', type: '保护工程', status: 'ongoing', manager: '李华', budget: '1,200,000', startDate: '2024-02-01', endDate: '2024-08-15', progress: 45 },
|
||||
{ id: 'PRJ003', name: '钟楼结构加固', type: '修缮工程', status: 'planning', manager: '王建国', budget: '680,000', startDate: '2024-04-01', endDate: '2024-09-30', progress: 0 },
|
||||
{ id: 'PRJ004', name: '藏经阁环境改造', type: '改造工程', status: 'completed', manager: '陈志强', budget: '950,000', startDate: '2023-08-01', endDate: '2023-12-31', progress: 100 },
|
||||
{ id: 'PRJ005', name: '鼓楼日常维护', type: '维护工程', status: 'ongoing', manager: '刘芳', budget: '320,000', startDate: '2024-01-01', endDate: '2024-12-31', progress: 25 },
|
||||
{ id: 'PRJ006', name: '东厢房修缮', type: '修缮工程', status: 'suspended', manager: '孙伟', budget: '560,000', startDate: '2023-11-01', endDate: '2024-05-31', progress: 35 },
|
||||
{ id: 'PRJ007', name: '西厢房保护', type: '保护工程', status: 'ongoing', manager: '周敏', budget: '780,000', startDate: '2024-03-01', endDate: '2024-10-31', progress: 30 },
|
||||
{ id: 'PRJ008', name: '围墙修复工程', type: '修缮工程', status: 'completed', manager: '张明远', budget: '420,000', startDate: '2023-09-01', endDate: '2024-01-31', progress: 100 }
|
||||
])
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
planning: '规划中',
|
||||
ongoing: '进行中',
|
||||
completed: '已完成',
|
||||
suspended: '已暂停'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
const getProgressColor = (progress) => {
|
||||
if (progress >= 80) return '#205c40'
|
||||
if (progress >= 50) return '#b48040'
|
||||
return '#f59e0b'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="project-list">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">项目管理</h1>
|
||||
<p class="page-subtitle">管理修缮项目全生命周期与进度跟踪</p>
|
||||
</div>
|
||||
|
||||
<section class="filter-section">
|
||||
<div class="filter-row">
|
||||
<div class="search-box">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
class="search-input"
|
||||
placeholder="搜索项目名称、编号或负责人..."
|
||||
>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<select v-model="selectedType" class="filter-select">
|
||||
<option v-for="type in projectTypes" :key="type.value" :value="type.value">
|
||||
{{ type.label }}
|
||||
</option>
|
||||
</select>
|
||||
<select v-model="selectedStatus" class="filter-select">
|
||||
<option v-for="stat in statuses" :key="stat.value" :value="stat.value">
|
||||
{{ stat.label }}
|
||||
</option>
|
||||
</select>
|
||||
<button class="btn btn-primary">新建项目</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="table-section">
|
||||
<div class="table-container">
|
||||
<table class="project-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>项目编号</th>
|
||||
<th>项目名称</th>
|
||||
<th>项目类型</th>
|
||||
<th>状态</th>
|
||||
<th>负责人</th>
|
||||
<th>预算</th>
|
||||
<th>开始日期</th>
|
||||
<th>结束日期</th>
|
||||
<th>进度</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="project in projects" :key="project.id">
|
||||
<td class="table-cell--code">{{ project.id }}</td>
|
||||
<td class="table-cell--name">{{ project.name }}</td>
|
||||
<td>{{ project.type }}</td>
|
||||
<td>
|
||||
<span class="status-badge" :class="`status-badge--${project.status}`">
|
||||
{{ getStatusText(project.status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ project.manager }}</td>
|
||||
<td class="table-cell--budget">¥{{ project.budget }}</td>
|
||||
<td class="table-cell--date">{{ project.startDate }}</td>
|
||||
<td class="table-cell--date">{{ project.endDate }}</td>
|
||||
<td class="table-cell--progress">
|
||||
<div class="progress-bar">
|
||||
<div
|
||||
class="progress-bar__fill"
|
||||
:style="{ width: project.progress + '%', background: getProgressColor(project.progress) }"
|
||||
></div>
|
||||
</div>
|
||||
<span class="progress-text">{{ project.progress }}%</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="btn-icon" title="查看详情">详情</button>
|
||||
<button class="btn-icon" title="编辑">编辑</button>
|
||||
<button class="btn-icon" title="删除">删除</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="pagination">
|
||||
<div class="pagination__info">显示 1-8 共 128 条</div>
|
||||
<div class="pagination__controls">
|
||||
<button class="pagination-btn" disabled>上一页</button>
|
||||
<button class="pagination-btn pagination-btn--active">1</button>
|
||||
<button class="pagination-btn">2</button>
|
||||
<button class="pagination-btn">3</button>
|
||||
<button class="pagination-btn">...</button>
|
||||
<button class="pagination-btn">16</button>
|
||||
<button class="pagination-btn">下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.project-list {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 18px;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
flex: 1;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
border-color: rgba(180, 128, 64, 0.5);
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.btn {
|
||||
height: 40px;
|
||||
padding: 0 18px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #b48040, #205c40);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.project-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.project-table thead {
|
||||
background: rgba(31, 35, 40, 0.04);
|
||||
}
|
||||
|
||||
.project-table th {
|
||||
padding: 14px 16px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
border-bottom: 1px solid var(--border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.project-table td {
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.project-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.project-table tbody tr:hover {
|
||||
background: rgba(31, 35, 40, 0.02);
|
||||
}
|
||||
|
||||
.table-cell--code {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.table-cell--name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.table-cell--budget {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.table-cell--date {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.table-cell--progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 60px;
|
||||
height: 6px;
|
||||
background: rgba(31, 35, 40, 0.1);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar__fill {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
padding: 0 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-badge--planning {
|
||||
background: rgba(107, 114, 128, 0.12);
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.status-badge--ongoing {
|
||||
background: rgba(32, 92, 64, 0.12);
|
||||
color: #205c40;
|
||||
}
|
||||
|
||||
.status-badge--completed {
|
||||
background: rgba(16, 185, 129, 0.12);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.status-badge--suspended {
|
||||
background: rgba(180, 128, 64, 0.12);
|
||||
color: #b48040;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
border-color: rgba(180, 128, 64, 0.4);
|
||||
background: rgba(180, 128, 64, 0.08);
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.pagination__info {
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.pagination__controls {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pagination-btn {
|
||||
min-width: 36px;
|
||||
height: 36px;
|
||||
padding: 0 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.pagination-btn:hover:not(:disabled) {
|
||||
border-color: rgba(180, 128, 64, 0.4);
|
||||
background: rgba(180, 128, 64, 0.08);
|
||||
}
|
||||
|
||||
.pagination-btn--active {
|
||||
background: linear-gradient(135deg, #b48040, #205c40);
|
||||
color: #fff;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.pagination-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.filter-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.project-list {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pagination__controls {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.project-table th,
|
||||
.project-table td {
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,533 @@
|
|||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const systemSettings = ref([
|
||||
{ id: 1, category: '系统设置', key: 'system_name', name: '系统名称', value: '古建筑保护信息化平台', type: 'text', description: '系统显示的名称' },
|
||||
{ id: 2, category: '系统设置', key: 'system_version', name: '系统版本', value: 'v1.0.0', type: 'text', description: '当前系统版本号' },
|
||||
{ id: 3, category: '系统设置', key: 'max_upload_size', name: '最大上传大小', value: '10', type: 'number', unit: 'MB', description: '文件上传的最大限制' },
|
||||
{ id: 4, category: '系统设置', key: 'session_timeout', name: '会话超时时间', value: '30', type: 'number', unit: '分钟', description: '用户会话自动超时时间' }
|
||||
])
|
||||
|
||||
const featureSettings = ref([
|
||||
{ id: 1, name: '设备实时监控', key: 'device_monitoring', enabled: true, description: '启用设备实时状态监控功能' },
|
||||
{ id: 2, name: '自动预警通知', key: 'auto_alert', enabled: true, description: '设备异常时自动发送预警通知' },
|
||||
{ id: 3, name: '数据自动备份', key: 'auto_backup', enabled: true, description: '定期自动备份系统数据' },
|
||||
{ id: 4, name: '巡查任务提醒', key: 'inspection_reminder', enabled: true, description: '巡查任务到期前发送提醒' },
|
||||
{ id: 5, name: '项目进度跟踪', key: 'project_tracking', enabled: true, description: '自动跟踪和更新项目进度' },
|
||||
{ id: 6, name: '统计分析报表', key: 'analytics_report', enabled: true, description: '生成统计分析报表' },
|
||||
{ id: 7, name: '移动端访问', key: 'mobile_access', enabled: false, description: '启用移动端访问功能' },
|
||||
{ id: 8, name: 'API接口访问', key: 'api_access', enabled: true, description: '启用外部API接口访问' }
|
||||
])
|
||||
|
||||
const notificationSettings = ref([
|
||||
{ id: 1, name: '设备离线通知', key: 'device_offline', enabled: true, channels: ['email', 'sms'], description: '设备离线时发送通知' },
|
||||
{ id: 2, name: '设备故障通知', key: 'device_error', enabled: true, channels: ['email', 'sms', 'push'], description: '设备故障时发送通知' },
|
||||
{ id: 3, name: '任务到期提醒', key: 'task_due', enabled: true, channels: ['email', 'push'], description: '任务到期前发送提醒' },
|
||||
{ id: 4, name: '项目状态变更', key: 'project_status', enabled: true, channels: ['email'], description: '项目状态变更时发送通知' },
|
||||
{ id: 5, name: '系统维护通知', key: 'system_maintenance', enabled: true, channels: ['email', 'push'], description: '系统维护时发送通知' },
|
||||
{ id: 6, name: '数据异常警报', key: 'data_anomaly', enabled: true, channels: ['email', 'sms', 'push'], description: '数据异常时发送警报' }
|
||||
])
|
||||
|
||||
const activeTab = ref('system')
|
||||
|
||||
const tabs = [
|
||||
{ key: 'system', label: '系统参数' },
|
||||
{ key: 'feature', label: '功能开关' },
|
||||
{ key: 'notification', label: '通知设置' }
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="option-config">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">系统配置</h1>
|
||||
<p class="page-subtitle">管理系统参数、功能开关和通知设置</p>
|
||||
</div>
|
||||
|
||||
<section class="tab-section">
|
||||
<div class="tab-header">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="tab-btn"
|
||||
:class="{ 'tab-btn--active': activeTab === tab.key }"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-content">
|
||||
<div v-if="activeTab === 'system'" class="tab-panel">
|
||||
<div class="config-list">
|
||||
<div v-for="setting in systemSettings" :key="setting.id" class="config-item">
|
||||
<div class="config-item__header">
|
||||
<div class="config-item__name">{{ setting.name }}</div>
|
||||
<div class="config-item__key">{{ setting.key }}</div>
|
||||
</div>
|
||||
<div class="config-item__description">{{ setting.description }}</div>
|
||||
<div class="config-item__control">
|
||||
<input
|
||||
v-if="setting.type === 'text'"
|
||||
type="text"
|
||||
class="config-input"
|
||||
:value="setting.value"
|
||||
>
|
||||
<div v-else-if="setting.type === 'number'" class="number-input-group">
|
||||
<input
|
||||
type="number"
|
||||
class="config-input config-input--number"
|
||||
:value="setting.value"
|
||||
>
|
||||
<span class="number-unit">{{ setting.unit }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-actions">
|
||||
<button class="btn btn-primary">保存配置</button>
|
||||
<button class="btn btn-secondary">重置默认</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'feature'" class="tab-panel">
|
||||
<div class="feature-list">
|
||||
<div v-for="feature in featureSettings" :key="feature.id" class="feature-item">
|
||||
<div class="feature-item__info">
|
||||
<div class="feature-item__name">{{ feature.name }}</div>
|
||||
<div class="feature-item__description">{{ feature.description }}</div>
|
||||
</div>
|
||||
<div class="feature-item__toggle">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" :checked="feature.enabled">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-actions">
|
||||
<button class="btn btn-primary">保存配置</button>
|
||||
<button class="btn btn-secondary">重置默认</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'notification'" class="tab-panel">
|
||||
<div class="notification-list">
|
||||
<div v-for="notif in notificationSettings" :key="notif.id" class="notification-item">
|
||||
<div class="notification-item__header">
|
||||
<div class="notification-item__name">{{ notif.name }}</div>
|
||||
<div class="notification-item__toggle">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" :checked="notif.enabled">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="notification-item__description">{{ notif.description }}</div>
|
||||
<div class="notification-item__channels">
|
||||
<span class="channel-label">通知渠道:</span>
|
||||
<label class="channel-checkbox">
|
||||
<input type="checkbox" :checked="notif.channels.includes('email')">
|
||||
<span>邮件</span>
|
||||
</label>
|
||||
<label class="channel-checkbox">
|
||||
<input type="checkbox" :checked="notif.channels.includes('sms')">
|
||||
<span>短信</span>
|
||||
</label>
|
||||
<label class="channel-checkbox">
|
||||
<input type="checkbox" :checked="notif.channels.includes('push')">
|
||||
<span>推送</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-actions">
|
||||
<button class="btn btn-primary">保存配置</button>
|
||||
<button class="btn btn-secondary">重置默认</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.option-config {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tab-section {
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tab-header {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: rgba(31, 35, 40, 0.04);
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
flex: 1;
|
||||
padding: 16px 20px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
color: var(--text);
|
||||
background: rgba(31, 35, 40, 0.02);
|
||||
}
|
||||
|
||||
.tab-btn--active {
|
||||
color: var(--text);
|
||||
border-bottom-color: #b48040;
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.tab-panel {
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.config-list,
|
||||
.feature-list,
|
||||
.notification-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.config-item {
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
background: rgba(31, 35, 40, 0.02);
|
||||
}
|
||||
|
||||
.config-item__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.config-item__name {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.config-item__key {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
background: rgba(31, 35, 40, 0.08);
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.config-item__description {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.config-item__control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.config-input {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
height: 40px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.config-input:focus {
|
||||
border-color: rgba(180, 128, 64, 0.5);
|
||||
}
|
||||
|
||||
.config-input--number {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.number-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.number-unit {
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
background: rgba(31, 35, 40, 0.02);
|
||||
}
|
||||
|
||||
.feature-item__info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.feature-item__name {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.feature-item__description {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.feature-item__toggle {
|
||||
flex-shrink: 0;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.notification-item {
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
background: rgba(31, 35, 40, 0.02);
|
||||
}
|
||||
|
||||
.notification-item__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.notification-item__name {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.notification-item__description {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.notification-item__channels {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.channel-label {
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.channel-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.channel-checkbox input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 48px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.toggle-switch input[type="checkbox"] {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
transition: 0.3s;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.toggle-slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
transition: 0.3s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.toggle-switch input:checked + .toggle-slider {
|
||||
background: linear-gradient(135deg, #b48040, #205c40);
|
||||
}
|
||||
|
||||
.toggle-switch input:checked + .toggle-slider:before {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
|
||||
.config-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn {
|
||||
height: 40px;
|
||||
padding: 0 18px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #b48040, #205c40);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
border-color: rgba(180, 128, 64, 0.4);
|
||||
background: rgba(180, 128, 64, 0.08);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.option-config {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.tab-header {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
flex: 1 1 auto;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.feature-item__toggle {
|
||||
margin-left: 0;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.config-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.tab-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.config-item,
|
||||
.feature-item,
|
||||
.notification-item {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.notification-item__channels {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,431 @@
|
|||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const searchQuery = ref('')
|
||||
const selectedStatus = ref('all')
|
||||
|
||||
const statuses = [
|
||||
{ value: 'all', label: '全部状态' },
|
||||
{ value: 'active', label: '启用' },
|
||||
{ value: 'disabled', label: '禁用' }
|
||||
]
|
||||
|
||||
const roles = ref([
|
||||
{ id: 'R001', name: '系统管理员', code: 'admin', description: '拥有系统所有权限,可管理所有功能和数据', userCount: 3, status: 'active', createTime: '2024-01-01' },
|
||||
{ id: 'R002', name: '文物保护员', code: 'heritage', description: '负责文物保护相关业务操作和数据管理', userCount: 15, status: 'active', createTime: '2024-01-05' },
|
||||
{ id: 'R003', name: '巡查员', code: 'inspector', description: '负责日常巡查任务执行和隐患上报', userCount: 28, status: 'active', createTime: '2024-01-10' },
|
||||
{ id: 'R004', name: '维护工程师', code: 'maintenance', description: '负责设备维护和故障处理', userCount: 8, status: 'active', createTime: '2024-01-15' },
|
||||
{ id: 'R005', name: '项目经理', code: 'manager', description: '负责修缮项目的管理和协调', userCount: 5, status: 'active', createTime: '2024-01-20' },
|
||||
{ id: 'R006', name: '数据分析师', code: 'analyst', description: '负责数据统计分析和报表生成', userCount: 4, status: 'active', createTime: '2024-01-25' },
|
||||
{ id: 'R007', name: '临时访客', code: 'guest', description: '仅拥有查看权限,无法进行任何操作', userCount: 12, status: 'disabled', createTime: '2024-02-01' },
|
||||
{ id: 'R008', name: '审核员', code: 'reviewer', description: '负责项目审核和质量验收', userCount: 6, status: 'active', createTime: '2024-02-05' }
|
||||
])
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
active: '启用',
|
||||
disabled: '禁用'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="role-list">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">角色管理</h1>
|
||||
<p class="page-subtitle">管理系统角色与权限分配</p>
|
||||
</div>
|
||||
|
||||
<section class="filter-section">
|
||||
<div class="filter-row">
|
||||
<div class="search-box">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
class="search-input"
|
||||
placeholder="搜索角色名称或编码..."
|
||||
>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<select v-model="selectedStatus" class="filter-select">
|
||||
<option v-for="stat in statuses" :key="stat.value" :value="stat.value">
|
||||
{{ stat.label }}
|
||||
</option>
|
||||
</select>
|
||||
<button class="btn btn-primary">添加角色</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="table-section">
|
||||
<div class="table-container">
|
||||
<table class="role-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>角色编号</th>
|
||||
<th>角色名称</th>
|
||||
<th>角色编码</th>
|
||||
<th>描述</th>
|
||||
<th>用户数量</th>
|
||||
<th>状态</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="role in roles" :key="role.id">
|
||||
<td class="table-cell--code">{{ role.id }}</td>
|
||||
<td class="table-cell--name">{{ role.name }}</td>
|
||||
<td class="table-cell--code">{{ role.code }}</td>
|
||||
<td class="table-cell--desc">{{ role.description }}</td>
|
||||
<td class="table-cell--count">{{ role.userCount }}</td>
|
||||
<td>
|
||||
<span class="status-badge" :class="`status-badge--${role.status}`">
|
||||
{{ getStatusText(role.status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="table-cell--date">{{ role.createTime }}</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="btn-icon" title="查看详情">详情</button>
|
||||
<button class="btn-icon" title="编辑">编辑</button>
|
||||
<button class="btn-icon" title="权限配置">权限</button>
|
||||
<button class="btn-icon" title="删除">删除</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="pagination">
|
||||
<div class="pagination__info">显示 1-8 共 8 条</div>
|
||||
<div class="pagination__controls">
|
||||
<button class="pagination-btn" disabled>上一页</button>
|
||||
<button class="pagination-btn pagination-btn--active">1</button>
|
||||
<button class="pagination-btn" disabled>下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.role-list {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 18px;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
flex: 1;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
border-color: rgba(180, 128, 64, 0.5);
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.btn {
|
||||
height: 40px;
|
||||
padding: 0 18px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #b48040, #205c40);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.role-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.role-table thead {
|
||||
background: rgba(31, 35, 40, 0.04);
|
||||
}
|
||||
|
||||
.role-table th {
|
||||
padding: 14px 16px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
border-bottom: 1px solid var(--border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.role-table td {
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.role-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.role-table tbody tr:hover {
|
||||
background: rgba(31, 35, 40, 0.02);
|
||||
}
|
||||
|
||||
.table-cell--code {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.table-cell--name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.table-cell--desc {
|
||||
max-width: 300px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.table-cell--count {
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.table-cell--date {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
padding: 0 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-badge--active {
|
||||
background: rgba(32, 92, 64, 0.12);
|
||||
color: #205c40;
|
||||
}
|
||||
|
||||
.status-badge--disabled {
|
||||
background: rgba(107, 114, 128, 0.12);
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
border-color: rgba(180, 128, 64, 0.4);
|
||||
background: rgba(180, 128, 64, 0.08);
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.pagination__info {
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.pagination__controls {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pagination-btn {
|
||||
min-width: 36px;
|
||||
height: 36px;
|
||||
padding: 0 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.pagination-btn:hover:not(:disabled) {
|
||||
border-color: rgba(180, 128, 64, 0.4);
|
||||
background: rgba(180, 128, 64, 0.08);
|
||||
}
|
||||
|
||||
.pagination-btn--active {
|
||||
background: linear-gradient(135deg, #b48040, #205c40);
|
||||
color: #fff;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.pagination-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.role-list {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pagination__controls {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.role-table th,
|
||||
.role-table td {
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.table-cell--desc {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,509 @@
|
|||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const searchQuery = ref('')
|
||||
const selectedType = ref('all')
|
||||
const selectedPriority = ref('all')
|
||||
const selectedStatus = ref('all')
|
||||
|
||||
const taskTypes = [
|
||||
{ value: 'all', label: '全部类型' },
|
||||
{ value: 'inspection', label: '巡查任务' },
|
||||
{ value: 'maintenance', label: '维护任务' },
|
||||
{ value: 'repair', label: '修缮任务' },
|
||||
{ value: 'monitoring', label: '监测任务' }
|
||||
]
|
||||
|
||||
const priorities = [
|
||||
{ value: 'all', label: '全部优先级' },
|
||||
{ value: 'high', label: '高' },
|
||||
{ value: 'medium', label: '中' },
|
||||
{ value: 'low', label: '低' }
|
||||
]
|
||||
|
||||
const statuses = [
|
||||
{ value: 'all', label: '全部状态' },
|
||||
{ value: 'pending', label: '待处理' },
|
||||
{ value: 'ongoing', label: '进行中' },
|
||||
{ value: 'completed', label: '已完成' },
|
||||
{ value: 'overdue', label: '已逾期' }
|
||||
]
|
||||
|
||||
const tasks = ref([
|
||||
{ id: 'TSK001', name: '大殿屋顶巡查', type: '巡查任务', priority: 'high', status: 'ongoing', assignee: '张明远', startDate: '2024-03-15', endDate: '2024-03-20', location: '大殿-屋顶' },
|
||||
{ id: 'TSK002', name: '山门设备维护', type: '维护任务', priority: 'medium', status: 'pending', assignee: '李华', startDate: '2024-03-18', endDate: '2024-03-22', location: '山门-入口' },
|
||||
{ id: 'TSK003', name: '钟楼结构监测', type: '监测任务', priority: 'high', status: 'completed', assignee: '王建国', startDate: '2024-03-10', endDate: '2024-03-15', location: '钟楼-二楼' },
|
||||
{ id: 'TSK004', name: '藏经阁修缮', type: '修缮任务', priority: 'high', status: 'overdue', assignee: '陈志强', startDate: '2024-03-05', endDate: '2024-03-12', location: '藏经阁-地下室' },
|
||||
{ id: 'TSK005', name: '鼓楼日常巡查', type: '巡查任务', priority: 'low', status: 'ongoing', assignee: '刘芳', startDate: '2024-03-16', endDate: '2024-03-25', location: '鼓楼-一楼' },
|
||||
{ id: 'TSK006', name: '东厢房设备检修', type: '维护任务', priority: 'medium', status: 'pending', assignee: '孙伟', startDate: '2024-03-19', endDate: '2024-03-24', location: '东厢房-正厅' },
|
||||
{ id: 'TSK007', name: '西厢房环境监测', type: '监测任务', priority: 'low', status: 'completed', assignee: '周敏', startDate: '2024-03-08', endDate: '2024-03-14', location: '西厢房-后院' },
|
||||
{ id: 'TSK008', name: '围墙紧急修缮', type: '修缮任务', priority: 'high', status: 'ongoing', assignee: '张明远', startDate: '2024-03-17', endDate: '2024-03-21', location: '围墙-北段' }
|
||||
])
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
pending: '待处理',
|
||||
ongoing: '进行中',
|
||||
completed: '已完成',
|
||||
overdue: '已逾期'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
const getPriorityText = (priority) => {
|
||||
const priorityMap = {
|
||||
high: '高',
|
||||
medium: '中',
|
||||
low: '低'
|
||||
}
|
||||
return priorityMap[priority] || priority
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="task-list">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">任务管理</h1>
|
||||
<p class="page-subtitle">管理巡查、维护、修缮等各类任务</p>
|
||||
</div>
|
||||
|
||||
<section class="filter-section">
|
||||
<div class="filter-row">
|
||||
<div class="search-box">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
class="search-input"
|
||||
placeholder="搜索任务名称、编号或负责人..."
|
||||
>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<select v-model="selectedType" class="filter-select">
|
||||
<option v-for="type in taskTypes" :key="type.value" :value="type.value">
|
||||
{{ type.label }}
|
||||
</option>
|
||||
</select>
|
||||
<select v-model="selectedPriority" class="filter-select">
|
||||
<option v-for="prio in priorities" :key="prio.value" :value="prio.value">
|
||||
{{ prio.label }}
|
||||
</option>
|
||||
</select>
|
||||
<select v-model="selectedStatus" class="filter-select">
|
||||
<option v-for="stat in statuses" :key="stat.value" :value="stat.value">
|
||||
{{ stat.label }}
|
||||
</option>
|
||||
</select>
|
||||
<button class="btn btn-primary">新建任务</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="table-section">
|
||||
<div class="table-container">
|
||||
<table class="task-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>任务编号</th>
|
||||
<th>任务名称</th>
|
||||
<th>任务类型</th>
|
||||
<th>优先级</th>
|
||||
<th>状态</th>
|
||||
<th>负责人</th>
|
||||
<th>位置</th>
|
||||
<th>开始日期</th>
|
||||
<th>结束日期</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="task in tasks" :key="task.id">
|
||||
<td class="table-cell--code">{{ task.id }}</td>
|
||||
<td class="table-cell--name">{{ task.name }}</td>
|
||||
<td>{{ task.type }}</td>
|
||||
<td>
|
||||
<span class="priority-badge" :class="`priority-badge--${task.priority}`">
|
||||
{{ getPriorityText(task.priority) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-badge" :class="`status-badge--${task.status}`">
|
||||
{{ getStatusText(task.status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ task.assignee }}</td>
|
||||
<td class="table-cell--location">{{ task.location }}</td>
|
||||
<td class="table-cell--date">{{ task.startDate }}</td>
|
||||
<td class="table-cell--date">{{ task.endDate }}</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="btn-icon" title="查看详情">详情</button>
|
||||
<button class="btn-icon" title="编辑">编辑</button>
|
||||
<button class="btn-icon" title="删除">删除</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="pagination">
|
||||
<div class="pagination__info">显示 1-8 共 256 条</div>
|
||||
<div class="pagination__controls">
|
||||
<button class="pagination-btn" disabled>上一页</button>
|
||||
<button class="pagination-btn pagination-btn--active">1</button>
|
||||
<button class="pagination-btn">2</button>
|
||||
<button class="pagination-btn">3</button>
|
||||
<button class="pagination-btn">...</button>
|
||||
<button class="pagination-btn">32</button>
|
||||
<button class="pagination-btn">下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.task-list {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 18px;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
flex: 1;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
border-color: rgba(180, 128, 64, 0.5);
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.btn {
|
||||
height: 40px;
|
||||
padding: 0 18px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #b48040, #205c40);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.task-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.task-table thead {
|
||||
background: rgba(31, 35, 40, 0.04);
|
||||
}
|
||||
|
||||
.task-table th {
|
||||
padding: 14px 16px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
border-bottom: 1px solid var(--border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-table td {
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.task-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.task-table tbody tr:hover {
|
||||
background: rgba(31, 35, 40, 0.02);
|
||||
}
|
||||
|
||||
.table-cell--code {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.table-cell--name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.table-cell--location {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.table-cell--date {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.priority-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
padding: 0 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.priority-badge--high {
|
||||
background: rgba(220, 38, 38, 0.12);
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.priority-badge--medium {
|
||||
background: rgba(180, 128, 64, 0.12);
|
||||
color: #b48040;
|
||||
}
|
||||
|
||||
.priority-badge--low {
|
||||
background: rgba(107, 114, 128, 0.12);
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
padding: 0 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-badge--pending {
|
||||
background: rgba(107, 114, 128, 0.12);
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.status-badge--ongoing {
|
||||
background: rgba(32, 92, 64, 0.12);
|
||||
color: #205c40;
|
||||
}
|
||||
|
||||
.status-badge--completed {
|
||||
background: rgba(16, 185, 129, 0.12);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.status-badge--overdue {
|
||||
background: rgba(220, 38, 38, 0.12);
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
border-color: rgba(180, 128, 64, 0.4);
|
||||
background: rgba(180, 128, 64, 0.08);
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.pagination__info {
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.pagination__controls {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pagination-btn {
|
||||
min-width: 36px;
|
||||
height: 36px;
|
||||
padding: 0 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.pagination-btn:hover:not(:disabled) {
|
||||
border-color: rgba(180, 128, 64, 0.4);
|
||||
background: rgba(180, 128, 64, 0.08);
|
||||
}
|
||||
|
||||
.pagination-btn--active {
|
||||
background: linear-gradient(135deg, #b48040, #205c40);
|
||||
color: #fff;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.pagination-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.filter-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.task-list {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pagination__controls {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.task-table th,
|
||||
.task-table td {
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue