2037 lines
71 KiB
HTML
2037 lines
71 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>空运宝 - 无人机物流供需匹配平台</title>
|
||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
:root {
|
||
--primary: #0066ff;
|
||
--primary-dark: #0052cc;
|
||
--success: #00a854;
|
||
--warning: #faad14;
|
||
--danger: #ff4d4f;
|
||
--info: #1890ff;
|
||
--dark: #1f2937;
|
||
--gray: #8c8c8c;
|
||
--light: #f5f5f5;
|
||
--white: #ffffff;
|
||
--gradient: linear-gradient(135deg, #0066ff 0%, #00d4ff 100%);
|
||
}
|
||
|
||
body {
|
||
font-family: 'Noto Sans SC', -apple-system, sans-serif;
|
||
background: #0a0a0f;
|
||
color: var(--white);
|
||
min-height: 100vh;
|
||
}
|
||
|
||
/* Login/Landing Page */
|
||
.landing-page {
|
||
min-height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 40px 20px;
|
||
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="%230066ff20"/><stop offset="100%" stop-color="%2300d4ff10"/></linearGradient></defs><circle cx="50" cy="50" r="40" fill="url(%23g)"/></svg>') no-repeat center center;
|
||
background-size: cover;
|
||
}
|
||
|
||
.landing-logo {
|
||
font-size: 48px;
|
||
margin-bottom: 20px;
|
||
animation: float 3s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes float {
|
||
0%, 100% { transform: translateY(0); }
|
||
50% { transform: translateY(-10px); }
|
||
}
|
||
|
||
.landing-title {
|
||
font-size: 42px;
|
||
font-weight: 700;
|
||
margin-bottom: 10px;
|
||
background: var(--gradient);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
}
|
||
|
||
.landing-subtitle {
|
||
font-size: 18px;
|
||
color: #666;
|
||
margin-bottom: 50px;
|
||
}
|
||
|
||
.role-selection {
|
||
display: flex;
|
||
gap: 40px;
|
||
flex-wrap: wrap;
|
||
justify-content: center;
|
||
}
|
||
|
||
.role-card {
|
||
width: 280px;
|
||
padding: 40px 30px;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border: 2px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: 24px;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
text-align: center;
|
||
}
|
||
|
||
.role-card:hover {
|
||
transform: translateY(-10px);
|
||
border-color: var(--primary);
|
||
box-shadow: 0 20px 60px rgba(0, 102, 255, 0.3);
|
||
}
|
||
|
||
.role-icon {
|
||
font-size: 64px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.role-title {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.role-desc {
|
||
font-size: 14px;
|
||
color: #888;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.role-btn {
|
||
margin-top: 20px;
|
||
padding: 12px 30px;
|
||
background: var(--gradient);
|
||
border: none;
|
||
border-radius: 30px;
|
||
color: white;
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: transform 0.2s;
|
||
}
|
||
|
||
.role-btn:hover {
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
/* App Container */
|
||
.app-container {
|
||
display: none;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
.app-container.active {
|
||
display: flex;
|
||
}
|
||
|
||
/* Sidebar */
|
||
.sidebar {
|
||
width: 260px;
|
||
background: #111118;
|
||
border-right: 1px solid rgba(255, 255, 255, 0.06);
|
||
display: flex;
|
||
flex-direction: column;
|
||
position: fixed;
|
||
height: 100vh;
|
||
z-index: 100;
|
||
}
|
||
|
||
.sidebar-logo {
|
||
padding: 24px;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: var(--primary);
|
||
}
|
||
|
||
.sidebar-logo span {
|
||
font-size: 28px;
|
||
}
|
||
|
||
.sidebar-nav {
|
||
flex: 1;
|
||
padding: 16px 12px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.nav-section {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.nav-section-title {
|
||
font-size: 11px;
|
||
color: #555;
|
||
text-transform: uppercase;
|
||
letter-spacing: 1px;
|
||
padding: 8px 12px;
|
||
}
|
||
|
||
.nav-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 12px 16px;
|
||
border-radius: 12px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
color: #888;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.nav-item:hover {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
color: var(--white);
|
||
}
|
||
|
||
.nav-item.active {
|
||
background: var(--gradient);
|
||
color: var(--white);
|
||
}
|
||
|
||
.nav-item .icon {
|
||
font-size: 18px;
|
||
}
|
||
|
||
.nav-badge {
|
||
margin-left: auto;
|
||
background: var(--danger);
|
||
color: white;
|
||
font-size: 11px;
|
||
padding: 2px 8px;
|
||
border-radius: 10px;
|
||
}
|
||
|
||
.sidebar-user {
|
||
padding: 20px;
|
||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.user-avatar {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 50%;
|
||
background: var(--gradient);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.user-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.user-name {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.user-role {
|
||
font-size: 12px;
|
||
color: #666;
|
||
}
|
||
|
||
.logout-btn {
|
||
padding: 8px 16px;
|
||
background: transparent;
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: 8px;
|
||
color: #888;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.logout-btn:hover {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
color: var(--white);
|
||
}
|
||
|
||
/* Main Content */
|
||
.main-content {
|
||
flex: 1;
|
||
margin-left: 260px;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
.page {
|
||
display: none;
|
||
padding: 24px 30px;
|
||
}
|
||
|
||
.page.active {
|
||
display: block;
|
||
}
|
||
|
||
/* Header */
|
||
.page-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.page-title {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.page-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
.btn {
|
||
padding: 10px 20px;
|
||
border-radius: 10px;
|
||
border: none;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
transition: all 0.2s;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: var(--gradient);
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
box-shadow: 0 8px 20px rgba(0, 102, 255, 0.4);
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
color: var(--white);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background: rgba(255, 255, 255, 0.15);
|
||
}
|
||
|
||
/* Stats Cards */
|
||
.stats-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 20px;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.stat-card {
|
||
background: rgba(255, 255, 255, 0.03);
|
||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||
border-radius: 16px;
|
||
padding: 24px;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 13px;
|
||
color: #666;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 32px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.stat-trend {
|
||
font-size: 12px;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.stat-trend.up { color: var(--success); }
|
||
.stat-trend.down { color: var(--danger); }
|
||
|
||
/* Panel */
|
||
.panel {
|
||
background: rgba(255, 255, 255, 0.03);
|
||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||
border-radius: 16px;
|
||
overflow: hidden;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.panel-header {
|
||
padding: 20px 24px;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.panel-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.panel-body {
|
||
padding: 20px 24px;
|
||
}
|
||
|
||
/* Order List */
|
||
.order-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.order-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 16px 20px;
|
||
background: rgba(255, 255, 255, 0.02);
|
||
border-radius: 12px;
|
||
gap: 16px;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.order-item:hover {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
.order-icon {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 12px;
|
||
background: var(--gradient);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 24px;
|
||
}
|
||
|
||
.order-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.order-title {
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.order-route {
|
||
font-size: 13px;
|
||
color: #888;
|
||
}
|
||
|
||
.order-meta {
|
||
display: flex;
|
||
gap: 20px;
|
||
align-items: center;
|
||
}
|
||
|
||
.order-price {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
color: var(--success);
|
||
}
|
||
|
||
.order-status {
|
||
padding: 6px 14px;
|
||
border-radius: 20px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.order-status.pending { background: rgba(136, 136, 136, 0.2); color: #aaa; }
|
||
.order-status.matching { background: rgba(0, 102, 255, 0.2); color: #66b3ff; }
|
||
.order-status.accepted { background: rgba(0, 168, 84, 0.2); color: var(--success); }
|
||
.order-status.in_transit { background: rgba(250, 173, 20, 0.2); color: var(--warning); }
|
||
.order-status.completed { background: rgba(0, 168, 84, 0.3); color: var(--success); }
|
||
.order-status.cancelled { background: rgba(255, 77, 79, 0.2); color: var(--danger); }
|
||
|
||
/* Drone Card */
|
||
.drone-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 20px;
|
||
}
|
||
|
||
.drone-card {
|
||
background: rgba(255, 255, 255, 0.03);
|
||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||
border-radius: 16px;
|
||
padding: 24px;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.drone-card:hover {
|
||
border-color: var(--primary);
|
||
transform: translateY(-4px);
|
||
}
|
||
|
||
.drone-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.drone-icon {
|
||
width: 56px;
|
||
height: 56px;
|
||
border-radius: 14px;
|
||
background: var(--gradient);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 28px;
|
||
}
|
||
|
||
.drone-name {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.drone-model {
|
||
font-size: 13px;
|
||
color: #666;
|
||
}
|
||
|
||
.drone-stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 12px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.drone-stat {
|
||
background: rgba(255, 255, 255, 0.03);
|
||
border-radius: 10px;
|
||
padding: 12px;
|
||
}
|
||
|
||
.drone-stat-label {
|
||
font-size: 11px;
|
||
color: #666;
|
||
}
|
||
|
||
.drone-stat-value {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.drone-status-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 6px 12px;
|
||
border-radius: 20px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.drone-status-badge.available {
|
||
background: rgba(0, 168, 84, 0.2);
|
||
color: var(--success);
|
||
}
|
||
|
||
.drone-status-badge.busy {
|
||
background: rgba(250, 173, 20, 0.2);
|
||
color: var(--warning);
|
||
}
|
||
|
||
.drone-status-badge.offline {
|
||
background: rgba(136, 136, 136, 0.2);
|
||
color: #888;
|
||
}
|
||
|
||
.drone-dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: currentColor;
|
||
}
|
||
|
||
/* Marketplace - Create Order Form */
|
||
.form-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 24px;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.form-label {
|
||
display: block;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
margin-bottom: 8px;
|
||
color: #aaa;
|
||
}
|
||
|
||
.form-input {
|
||
width: 100%;
|
||
padding: 14px 16px;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: 10px;
|
||
color: var(--white);
|
||
font-size: 15px;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.form-input:focus {
|
||
outline: none;
|
||
border-color: var(--primary);
|
||
background: rgba(255, 255, 255, 0.08);
|
||
}
|
||
|
||
.form-input::placeholder {
|
||
color: #555;
|
||
}
|
||
|
||
.form-row {
|
||
display: flex;
|
||
gap: 16px;
|
||
}
|
||
|
||
.form-row .form-group {
|
||
flex: 1;
|
||
}
|
||
|
||
/* Quote Cards */
|
||
.quote-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
.quote-card {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 20px;
|
||
background: rgba(255, 255, 255, 0.03);
|
||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||
border-radius: 16px;
|
||
gap: 20px;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.quote-card:hover {
|
||
border-color: rgba(0, 102, 255, 0.3);
|
||
}
|
||
|
||
.quote-provider {
|
||
width: 60px;
|
||
text-align: center;
|
||
}
|
||
|
||
.provider-avatar {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 50%;
|
||
background: var(--gradient);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 20px;
|
||
margin: 0 auto 8px;
|
||
}
|
||
|
||
.provider-name {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.provider-rating {
|
||
font-size: 12px;
|
||
color: var(--warning);
|
||
}
|
||
|
||
.quote-details {
|
||
flex: 1;
|
||
}
|
||
|
||
.quote-drone {
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.quote-info {
|
||
display: flex;
|
||
gap: 20px;
|
||
font-size: 13px;
|
||
color: #888;
|
||
}
|
||
|
||
.quote-price-section {
|
||
text-align: right;
|
||
}
|
||
|
||
.quote-price {
|
||
font-size: 28px;
|
||
font-weight: 700;
|
||
color: var(--success);
|
||
}
|
||
|
||
.quote-etd {
|
||
font-size: 12px;
|
||
color: #666;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.quote-action {
|
||
padding: 12px 24px;
|
||
background: var(--gradient);
|
||
border: none;
|
||
border-radius: 10px;
|
||
color: white;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.quote-action:hover {
|
||
box-shadow: 0 8px 20px rgba(0, 102, 255, 0.4);
|
||
}
|
||
|
||
/* Map */
|
||
.map-container {
|
||
height: calc(100vh - 200px);
|
||
border-radius: 16px;
|
||
overflow: hidden;
|
||
position: relative;
|
||
}
|
||
|
||
#market-map {
|
||
height: 100%;
|
||
width: 100%;
|
||
}
|
||
|
||
.map-panel {
|
||
position: absolute;
|
||
top: 20px;
|
||
left: 20px;
|
||
width: 360px;
|
||
background: rgba(17, 17, 24, 0.95);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: 16px;
|
||
z-index: 1000;
|
||
max-height: calc(100% - 40px);
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.map-panel-header {
|
||
padding: 20px;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||
}
|
||
|
||
.map-panel-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.map-filters {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.filter-chip {
|
||
padding: 6px 12px;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: 20px;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.filter-chip:hover, .filter-chip.active {
|
||
background: var(--primary);
|
||
border-color: var(--primary);
|
||
}
|
||
|
||
.map-panel-body {
|
||
padding: 16px 20px;
|
||
}
|
||
|
||
.supply-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 12px;
|
||
background: rgba(255, 255, 255, 0.03);
|
||
border-radius: 10px;
|
||
margin-bottom: 10px;
|
||
gap: 12px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.supply-item:hover {
|
||
background: rgba(255, 255, 255, 0.08);
|
||
}
|
||
|
||
.supply-icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 10px;
|
||
background: var(--gradient);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.supply-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.supply-name {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.supply-meta {
|
||
font-size: 12px;
|
||
color: #666;
|
||
}
|
||
|
||
.supply-price {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: var(--success);
|
||
}
|
||
|
||
/* Matching Animation */
|
||
.matching-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.9);
|
||
display: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 2000;
|
||
}
|
||
|
||
.matching-overlay.active {
|
||
display: flex;
|
||
}
|
||
|
||
.matching-content {
|
||
text-align: center;
|
||
}
|
||
|
||
.matching-spinner {
|
||
width: 100px;
|
||
height: 100px;
|
||
border: 4px solid rgba(0, 102, 255, 0.2);
|
||
border-top-color: var(--primary);
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
margin: 0 auto 30px;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
.matching-title {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.matching-subtitle {
|
||
color: #888;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.matching-progress {
|
||
width: 300px;
|
||
height: 6px;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-radius: 3px;
|
||
margin: 0 auto;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.matching-progress-bar {
|
||
height: 100%;
|
||
background: var(--gradient);
|
||
width: 0%;
|
||
transition: width 0.3s;
|
||
}
|
||
|
||
.matching-result {
|
||
display: none;
|
||
}
|
||
|
||
.matching-result.show {
|
||
display: block;
|
||
}
|
||
|
||
.match-success {
|
||
font-size: 60px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
/* Order Detail Modal */
|
||
.modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.8);
|
||
display: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 2000;
|
||
}
|
||
|
||
.modal-overlay.active {
|
||
display: flex;
|
||
}
|
||
|
||
.modal {
|
||
background: #15151d;
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: 20px;
|
||
width: 90%;
|
||
max-width: 600px;
|
||
max-height: 90vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.modal-header {
|
||
padding: 24px;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.modal-title {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.modal-close {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 50%;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border: none;
|
||
color: #888;
|
||
font-size: 20px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 24px;
|
||
}
|
||
|
||
.detail-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 14px 0;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
||
}
|
||
|
||
.detail-label {
|
||
color: #666;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.detail-value {
|
||
font-weight: 500;
|
||
}
|
||
|
||
.modal-footer {
|
||
padding: 20px 24px;
|
||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 12px;
|
||
}
|
||
|
||
/* Toast */
|
||
.toast {
|
||
position: fixed;
|
||
bottom: 30px;
|
||
right: 30px;
|
||
padding: 16px 24px;
|
||
background: #1a1a24;
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
z-index: 3000;
|
||
transform: translateY(100px);
|
||
opacity: 0;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.toast.show {
|
||
transform: translateY(0);
|
||
opacity: 1;
|
||
}
|
||
|
||
.toast-icon {
|
||
font-size: 24px;
|
||
}
|
||
|
||
.toast.success .toast-icon { color: var(--success); }
|
||
.toast.error .toast-icon { color: var(--danger); }
|
||
.toast.info .toast-icon { color: var(--info); }
|
||
|
||
/* Responsive */
|
||
@media (max-width: 1200px) {
|
||
.stats-grid { grid-template-columns: repeat(2, 1fr); }
|
||
.drone-grid { grid-template-columns: repeat(2, 1fr); }
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.sidebar { display: none; }
|
||
.main-content { margin-left: 0; }
|
||
.stats-grid { grid-template-columns: 1fr; }
|
||
.drone-grid { grid-template-columns: 1fr; }
|
||
.form-grid { grid-template-columns: 1fr; }
|
||
}
|
||
|
||
/* Scrollbar */
|
||
::-webkit-scrollbar { width: 6px; }
|
||
::-webkit-scrollbar-track { background: transparent; }
|
||
::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.2); border-radius: 3px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- Landing Page -->
|
||
<div class="landing-page" id="landing-page">
|
||
<div class="landing-logo">🚁</div>
|
||
<h1 class="landing-title">空运宝</h1>
|
||
<p class="landing-subtitle">无人机物流供需匹配平台 | 连接天空与地面</p>
|
||
|
||
<div class="role-selection">
|
||
<div class="role-card" onclick="loginAs('demander')">
|
||
<div class="role-icon">📦</div>
|
||
<div class="role-title">我要发货</div>
|
||
<div class="role-desc">需要使用无人机物流服务<br>发布运货需求,快速匹配无人机</div>
|
||
<button class="role-btn">立即发货</button>
|
||
</div>
|
||
|
||
<div class="role-card" onclick="loginAs('provider')">
|
||
<div class="role-icon">🚁</div>
|
||
<div class="role-title">我要接单</div>
|
||
<div class="role-desc">拥有无人机并提供物流服务<br>接单配送,获取收益</div>
|
||
<button class="role-btn">入驻接单</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- App Container -->
|
||
<div class="app-container" id="app-container">
|
||
<!-- Sidebar -->
|
||
<div class="sidebar">
|
||
<div class="sidebar-logo">
|
||
<span>🚁</span>
|
||
空运宝
|
||
</div>
|
||
|
||
<div class="sidebar-nav" id="sidebar-nav">
|
||
<!-- Nav items will be dynamically inserted -->
|
||
</div>
|
||
|
||
<div class="sidebar-user">
|
||
<div class="user-avatar" id="user-avatar">U</div>
|
||
<div class="user-info">
|
||
<div class="user-name" id="user-name">用户</div>
|
||
<div class="user-role" id="user-role">发货方</div>
|
||
</div>
|
||
<button class="logout-btn" onclick="logout()">退出</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Main Content -->
|
||
<div class="main-content">
|
||
<!-- Provider Dashboard -->
|
||
<div class="page" id="page-provider-dashboard">
|
||
<div class="page-header">
|
||
<h1 class="page-title">运营概览</h1>
|
||
<div class="page-actions">
|
||
<button class="btn btn-primary" onclick="showAddDroneModal()">+ 添加无人机</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stats-grid">
|
||
<div class="stat-card">
|
||
<div class="stat-label">今日订单</div>
|
||
<div class="stat-value">12</div>
|
||
<div class="stat-trend up">↑ 较昨日 +3</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">本月收入</div>
|
||
<div class="stat-value">¥8,640</div>
|
||
<div class="stat-trend up">↑ 12%</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">飞行时长</div>
|
||
<div class="stat-value">48.5h</div>
|
||
<div class="stat-trend up">↑ 8%</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">客户评分</div>
|
||
<div class="stat-value">4.9 ⭐</div>
|
||
<div class="stat-trend">稳定</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<div class="panel-header">
|
||
<span class="panel-title">我的无人机</span>
|
||
<span style="color: #666; font-size: 13px;">共 4 架</span>
|
||
</div>
|
||
<div class="panel-body">
|
||
<div class="drone-grid" id="drone-list">
|
||
<!-- Drone cards -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<div class="panel-header">
|
||
<span class="panel-title">待接订单</span>
|
||
<span style="color: #666; font-size: 13px;">5 单可接</span>
|
||
</div>
|
||
<div class="panel-body">
|
||
<div class="order-list" id="provider-orders">
|
||
<!-- Orders -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Provider Orders -->
|
||
<div class="page" id="page-provider-orders">
|
||
<div class="page-header">
|
||
<h1 class="page-title">订单管理</h1>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<div class="panel-body">
|
||
<div class="order-list" id="provider-all-orders">
|
||
<!-- All orders -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Provider Earnings -->
|
||
<div class="page" id="page-provider-earnings">
|
||
<div class="page-header">
|
||
<h1 class="page-title">收入明细</h1>
|
||
<div class="page-actions">
|
||
<button class="btn btn-secondary">提现</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stats-grid">
|
||
<div class="stat-card">
|
||
<div class="stat-label">账户余额</div>
|
||
<div class="stat-value">¥2,840</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">待结算</div>
|
||
<div class="stat-value">¥1,200</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">本月收入</div>
|
||
<div class="stat-value">¥8,640</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">累计收入</div>
|
||
<div class="stat-value">¥42,800</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<div class="panel-header">
|
||
<span class="panel-title">收入记录</span>
|
||
</div>
|
||
<div class="panel-body">
|
||
<div class="order-list">
|
||
<div class="order-item">
|
||
<div class="order-icon">✓</div>
|
||
<div class="order-info">
|
||
<div class="order-title">订单 #ORDER-0892</div>
|
||
<div class="order-route">望京 → 798园区</div>
|
||
</div>
|
||
<div class="order-meta">
|
||
<div class="order-price">+¥128</div>
|
||
<span class="order-status completed">已完成</span>
|
||
</div>
|
||
</div>
|
||
<div class="order-item">
|
||
<div class="order-icon">✓</div>
|
||
<div class="order-info">
|
||
<div class="order-title">订单 #ORDER-0889</div>
|
||
<div class="order-route">亦庄 → 通州</div>
|
||
</div>
|
||
<div class="order-meta">
|
||
<div class="order-price">+¥256</div>
|
||
<span class="order-status completed">已完成</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Demander Dashboard -->
|
||
<div class="page" id="page-demander-dashboard">
|
||
<div class="page-header">
|
||
<h1 class="page-title">发货大厅</h1>
|
||
<div class="page-actions">
|
||
<button class="btn btn-primary" onclick="showCreateOrderModal()">+ 发布运货需求</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stats-grid">
|
||
<div class="stat-card">
|
||
<div class="stat-label">进行中订单</div>
|
||
<div class="stat-value">3</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">本月发货</div>
|
||
<div class="stat-value">28 单</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">累计运费</div>
|
||
<div class="stat-value">¥4,680</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">平均时效</div>
|
||
<div class="stat-value">42 分钟</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<div class="panel-header">
|
||
<span class="panel-title">实时运力</span>
|
||
<span style="color: #666; font-size: 13px;">附近 8 架可用</span>
|
||
</div>
|
||
<div class="panel-body">
|
||
<div class="drone-grid" id="available-drones">
|
||
<!-- Available drones -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<div class="panel-header">
|
||
<span class="panel-title">我的订单</span>
|
||
</div>
|
||
<div class="panel-body">
|
||
<div class="order-list" id="demander-orders">
|
||
<!-- Orders -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Create Order -->
|
||
<div class="page" id="page-create-order">
|
||
<div class="page-header">
|
||
<h1 class="page-title">发布运货需求</h1>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<div class="panel-body">
|
||
<div class="form-grid">
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">起运地</label>
|
||
<input type="text" class="form-input" placeholder="例如:朝阳区望京SOHO" id="order-from">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">目的地</label>
|
||
<input type="text" class="form-input" placeholder="例如:海淀区中关村" id="order-to">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">货物类型</label>
|
||
<select class="form-input" id="order-cargo-type">
|
||
<option value="">选择类型</option>
|
||
<option value="文件">文件/合同</option>
|
||
<option value="生鲜">生鲜/食品</option>
|
||
<option value="药品">药品/医疗</option>
|
||
<option value="电子产品">电子产品</option>
|
||
<option value="包裹">普通包裹</option>
|
||
<option value="其他">其他</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">货物重量 (kg)</label>
|
||
<input type="number" class="form-input" placeholder="例如:5" id="order-weight">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">期望送达时间</label>
|
||
<select class="form-input" id="order-urgency">
|
||
<option value="normal">普通时效 (2小时内)</option>
|
||
<option value="urgent">加急 (1小时内)</option>
|
||
<option value=" ASAP">立即配送</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">保价金额 (元)</label>
|
||
<input type="number" class="form-input" placeholder="可选" id="order-insurance">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">备注说明</label>
|
||
<input type="text" class="form-input" placeholder="特殊要求、联系方式等" id="order-note">
|
||
</div>
|
||
</div>
|
||
|
||
<div style="text-align: center; margin-top: 30px;">
|
||
<button class="btn btn-primary" style="padding: 16px 60px; font-size: 16px;" onclick="submitOrder()">
|
||
发布需求
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Matching -->
|
||
<div class="page" id="page-matching">
|
||
<div class="page-header">
|
||
<h1 class="page-title">匹配运力</h1>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<div class="panel-body">
|
||
<div class="quote-list" id="quote-list">
|
||
<!-- Quote cards -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Marketplace Map -->
|
||
<div class="page" id="page-marketplace">
|
||
<div class="page-header">
|
||
<h1 class="page-title">运力地图</h1>
|
||
</div>
|
||
|
||
<div class="map-container">
|
||
<div id="market-map"></div>
|
||
<div class="map-panel">
|
||
<div class="map-panel-header">
|
||
<div class="map-panel-title">附近运力</div>
|
||
<div class="map-filters">
|
||
<span class="filter-chip active">全部</span>
|
||
<span class="filter-chip">_available</span>
|
||
<span class="filter-chip">配送中</span>
|
||
</div>
|
||
</div>
|
||
<div class="map-panel-body" id="supply-list">
|
||
<!-- Supply items -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- My Orders -->
|
||
<div class="page" id="page-my-orders">
|
||
<div class="page-header">
|
||
<h1 class="page-title">我的订单</h1>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<div class="panel-body">
|
||
<div class="order-list" id="my-orders-list">
|
||
<!-- Orders -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Matching Overlay -->
|
||
<div class="matching-overlay" id="matching-overlay">
|
||
<div class="matching-content">
|
||
<div class="matching-spinner"></div>
|
||
<div class="matching-title" id="matching-title">正在匹配运力</div>
|
||
<div class="matching-subtitle" id="matching-subtitle">系统正在为您寻找附近最合适的无人机...</div>
|
||
<div class="matching-progress">
|
||
<div class="matching-progress-bar" id="matching-progress"></div>
|
||
</div>
|
||
<div class="matching-result" id="matching-result">
|
||
<div class="match-success">🎉</div>
|
||
<div class="matching-title">匹配成功!</div>
|
||
<div class="matching-subtitle">无人机已接单,预计12分钟后送达</div>
|
||
<button class="btn btn-primary" style="margin-top: 20px;" onclick="closeMatching()">查看订单详情</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Create Order Modal -->
|
||
<div class="modal-overlay" id="create-order-modal">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<span class="modal-title">发布运货需求</span>
|
||
<button class="modal-close" onclick="closeModal('create-order-modal')">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="form-group">
|
||
<label class="form-label">起运地</label>
|
||
<input type="text" class="form-input" placeholder="输入起运地址" id="modal-from">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">目的地</label>
|
||
<input type="text" class="form-input" placeholder="输入目的地地址" id="modal-to">
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">货物类型</label>
|
||
<select class="form-input" id="modal-cargo-type">
|
||
<option value="文件">文件/合同</option>
|
||
<option value="生鲜">生鲜/食品</option>
|
||
<option value="药品">药品/医疗</option>
|
||
<option value="包裹">普通包裹</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">重量 (kg)</label>
|
||
<input type="number" class="form-input" placeholder="例如:5" id="modal-weight">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">期望送达时间</label>
|
||
<select class="form-input" id="modal-urgency">
|
||
<option value="normal">普通时效 (2小时内)</option>
|
||
<option value="urgent">加急 (1小时内)</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">备注</label>
|
||
<input type="text" class="form-input" placeholder="特殊要求" id="modal-note">
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" onclick="closeModal('create-order-modal')">取消</button>
|
||
<button class="btn btn-primary" onclick="confirmCreateOrder()">发布需求</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Order Detail Modal -->
|
||
<div class="modal-overlay" id="order-detail-modal">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<span class="modal-title">订单详情</span>
|
||
<button class="modal-close" onclick="closeModal('order-detail-modal')">×</button>
|
||
</div>
|
||
<div class="modal-body" id="order-detail-content">
|
||
<!-- Order details -->
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" onclick="closeModal('order-detail-modal')">关闭</button>
|
||
<button class="btn btn-primary" id="order-action-btn" onclick="">确认收货</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Toast -->
|
||
<div class="toast" id="toast">
|
||
<span class="toast-icon">✓</span>
|
||
<span class="toast-message">操作成功</span>
|
||
</div>
|
||
|
||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||
<script>
|
||
// ── API Service Layer ──
|
||
const API = {
|
||
base: '',
|
||
enabled: false,
|
||
|
||
async init() {
|
||
try {
|
||
const r = await fetch('/api/health');
|
||
if (r.ok) { this.enabled = true; console.log('[API] 后端已连接'); }
|
||
} catch { console.log('[API] 后端未连接,使用离线数据'); }
|
||
},
|
||
|
||
async request(method, path, body) {
|
||
if (!this.enabled) throw new Error('API不可用');
|
||
const opts = { method, headers: { 'Content-Type': 'application/json' } };
|
||
if (body) opts.body = JSON.stringify(body);
|
||
const r = await fetch(this.base + path, opts);
|
||
const j = await r.json();
|
||
if (j.code !== 0) throw new Error(j.msg || 'API错误');
|
||
return j.data;
|
||
},
|
||
|
||
// Auth
|
||
login(phone, role) { return this.request('POST', '/api/login', { phone, role }); },
|
||
|
||
// Drones
|
||
getDrones(params) { return this.request('GET', '/api/drones?' + new URLSearchParams(params)); },
|
||
getAvailableDrones() { return this.request('GET', '/api/drones/available'); },
|
||
addDrone(data) { return this.request('POST', '/api/drones', data); },
|
||
|
||
// Orders
|
||
getOrders(params) { return this.request('GET', '/api/orders?' + new URLSearchParams(params)); },
|
||
getPendingOrders() { return this.request('GET', '/api/orders/pending'); },
|
||
createOrder(data) { return this.request('POST', '/api/orders', data); },
|
||
getOrder(id) { return this.request('GET', `/api/orders/${id}`); },
|
||
acceptOrder(id, data) { return this.request('POST', `/api/orders/${id}/accept`, data); },
|
||
startTransit(id) { return this.request('POST', `/api/orders/${id}/transit`); },
|
||
completeOrder(id) { return this.request('POST', `/api/orders/${id}/complete`); },
|
||
cancelOrder(id) { return this.request('POST', `/api/orders/${id}/cancel`); },
|
||
rateOrder(id, data) { return this.request('POST', `/api/orders/${id}/rate`, data); },
|
||
|
||
// Quotes
|
||
getQuotes(orderId) { return this.request('GET', '/api/quotes?order_id=' + orderId); },
|
||
acceptQuote(quoteId) { return this.request('POST', `/api/quotes/${quoteId}/accept`); },
|
||
|
||
// Dashboard
|
||
getStats(params) { return this.request('GET', '/api/dashboard/stats?' + new URLSearchParams(params)); },
|
||
|
||
// Agent
|
||
agentAnalyze(data) { return this.request('POST', '/api/agent/analyze', data); },
|
||
getAgentSettings() { return this.request('GET', '/api/agent/settings'); },
|
||
};
|
||
|
||
// ── Data ──
|
||
let currentUser = null;
|
||
let currentRole = null;
|
||
let orders = [
|
||
{ id: 'ORDER-001', from: '望京SOHO', to: '798艺术区', cargo: '文件', weight: 2.5, price: 128, status: 'in_transit', provider: '张师傅', time: '10:32', providerDrone: '大疆M30T' },
|
||
{ id: 'ORDER-002', from: '亦庄物流中心', to: '通州配送站', cargo: '包裹', weight: 8.0, price: 256, status: 'accepted', provider: '李师傅', time: '10:28', providerDrone: '大疆M3E' },
|
||
{ id: 'ORDER-003', from: '顺义机场', to: '平谷物流园', cargo: '生鲜', weight: 15.0, price: 380, status: 'pending', provider: null, time: '10:15', providerDrone: null },
|
||
{ id: 'ORDER-004', from: '海淀中关村', to: '朝阳国贸', cargo: '电子产品', weight: 3.2, price: 188, status: 'completed', provider: '王师傅', time: '09:45', providerDrone: '大疆M30T' },
|
||
{ id: 'ORDER-005', from: '丰台站', to: '石景山', cargo: '药品', weight: 1.0, price: 156, status: 'completed', provider: '赵师傅', time: '09:20', providerDrone: '极飞P100' },
|
||
];
|
||
|
||
let drones = [
|
||
{ id: 'D001', name: '大疆M30T', model: '行业级', maxWeight: 15, range: 15, status: 'available', totalFlights: 856, location: '朝阳区望京' },
|
||
{ id: 'D002', name: '大疆M3E', model: '入门级', maxWeight: 5, range: 10, status: 'busy', totalFlights: 1243, location: '海淀区中关村' },
|
||
{ id: 'D003', name: '极飞P100', model: '农业级', maxWeight: 25, range: 8, status: 'available', totalFlights: 567, location: '通州区' },
|
||
{ id: 'D004', name: '纵横CW-15', model: '测绘级', maxWeight: 20, range: 20, status: 'offline', totalFlights: 234, location: '顺义区' },
|
||
];
|
||
|
||
let quotes = [
|
||
{ id: 'Q1', provider: '张师傅', avatar: '张', drone: '大疆M30T', rating: 4.9, price: 128, distance: 3.2, eta: 12, totalFlights: 856 },
|
||
{ id: 'Q2', provider: '李师傅', avatar: '李', drone: '大疆M3E', rating: 4.8, price: 98, distance: 5.1, eta: 18, totalFlights: 1243 },
|
||
{ id: 'Q3', provider: '王师傅', avatar: '王', drone: '极飞P100', rating: 4.7, price: 156, distance: 2.8, eta: 10, totalFlights: 567 },
|
||
];
|
||
|
||
// Init API on load
|
||
API.init();
|
||
|
||
// Navigation Data
|
||
const providerNav = [
|
||
{ section: '运营', items: [
|
||
{ id: 'dashboard', icon: '📊', label: '运营概览', page: 'page-provider-dashboard' },
|
||
{ id: 'map', icon: '🗺️', label: '运力地图', page: 'page-marketplace' },
|
||
]},
|
||
{ section: '订单', items: [
|
||
{ id: 'orders', icon: '📋', label: '订单管理', page: 'page-provider-orders' },
|
||
]},
|
||
{ section: '财务', items: [
|
||
{ id: 'earnings', icon: '💰', label: '收入明细', page: 'page-provider-earnings' },
|
||
]},
|
||
];
|
||
|
||
const demanderNav = [
|
||
{ section: '发货', items: [
|
||
{ id: 'dashboard', icon: '📦', label: '发货大厅', page: 'page-demander-dashboard' },
|
||
{ id: 'create', icon: '➕', label: '发布需求', page: 'page-create-order' },
|
||
{ id: 'matching', icon: '🔍', label: '匹配运力', page: 'page-matching' },
|
||
]},
|
||
{ section: '订单', items: [
|
||
{ id: 'myorders', icon: '📋', label: '我的订单', page: 'page-my-orders' },
|
||
]},
|
||
{ section: '运力', items: [
|
||
{ id: 'map', icon: '🗺️', label: '运力地图', page: 'page-marketplace' },
|
||
]},
|
||
];
|
||
|
||
// Functions
|
||
function loginAs(role) {
|
||
currentRole = role;
|
||
currentUser = role === 'demander' ? { name: '王先生', type: 'demander' } : { name: '张师傅', type: 'provider' };
|
||
|
||
document.getElementById('landing-page').style.display = 'none';
|
||
document.getElementById('app-container').classList.add('active');
|
||
|
||
document.getElementById('user-name').textContent = currentUser.name;
|
||
document.getElementById('user-role').textContent = role === 'demander' ? '发货方' : '运营方';
|
||
document.getElementById('user-avatar').textContent = currentUser.name.charAt(0);
|
||
|
||
renderNav();
|
||
|
||
const defaultPage = role === 'demander' ? 'page-demander-dashboard' : 'page-provider-dashboard';
|
||
showPage(defaultPage);
|
||
|
||
showToast('登录成功', 'success');
|
||
|
||
if (role === 'provider') {
|
||
renderProviderContent();
|
||
} else {
|
||
renderDemanderContent();
|
||
}
|
||
}
|
||
|
||
function logout() {
|
||
currentUser = null;
|
||
currentRole = null;
|
||
document.getElementById('landing-page').style.display = 'flex';
|
||
document.getElementById('app-container').classList.remove('active');
|
||
}
|
||
|
||
function renderNav() {
|
||
const nav = currentRole === 'provider' ? providerNav : demanderNav;
|
||
const container = document.getElementById('sidebar-nav');
|
||
container.innerHTML = '';
|
||
|
||
nav.forEach(section => {
|
||
const sectionDiv = document.createElement('div');
|
||
sectionDiv.className = 'nav-section';
|
||
sectionDiv.innerHTML = `<div class="nav-section-title">${section.section}</div>`;
|
||
|
||
section.items.forEach(item => {
|
||
const itemDiv = document.createElement('div');
|
||
itemDiv.className = 'nav-item';
|
||
itemDiv.innerHTML = `<span class="icon">${item.icon}</span><span>${item.label}</span>`;
|
||
itemDiv.onclick = () => showPage(item.page);
|
||
sectionDiv.appendChild(itemDiv);
|
||
});
|
||
|
||
container.appendChild(sectionDiv);
|
||
});
|
||
}
|
||
|
||
function showPage(pageId) {
|
||
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
||
document.getElementById(pageId).classList.add('active');
|
||
|
||
document.querySelectorAll('.nav-item').forEach(item => item.classList.remove('active'));
|
||
event?.target?.closest('.nav-item')?.classList.add('active');
|
||
|
||
if (pageId === 'page-marketplace') {
|
||
setTimeout(initMarketMap, 100);
|
||
}
|
||
}
|
||
|
||
function renderProviderContent() {
|
||
// Render drones
|
||
const droneContainer = document.getElementById('drone-list');
|
||
droneContainer.innerHTML = drones.map(drone => `
|
||
<div class="drone-card">
|
||
<div class="drone-header">
|
||
<div class="drone-icon">🚁</div>
|
||
<div>
|
||
<div class="drone-name">${drone.name}</div>
|
||
<div class="drone-model">${drone.model}</div>
|
||
</div>
|
||
</div>
|
||
<div class="drone-stats">
|
||
<div class="drone-stat">
|
||
<div class="drone-stat-label">载重</div>
|
||
<div class="drone-stat-value">${drone.maxWeight}kg</div>
|
||
</div>
|
||
<div class="drone-stat">
|
||
<div class="drone-stat-label">航程</div>
|
||
<div class="drone-stat-value">${drone.range}km</div>
|
||
</div>
|
||
<div class="drone-stat">
|
||
<div class="drone-stat-label">飞行次</div>
|
||
<div class="drone-stat-value">${drone.totalFlights}</div>
|
||
</div>
|
||
<div class="drone-stat">
|
||
<div class="drone-stat-label">位置</div>
|
||
<div class="drone-stat-value">${drone.location}</div>
|
||
</div>
|
||
</div>
|
||
<div class="drone-status-badge ${drone.status}">
|
||
<span class="drone-dot"></span>
|
||
${drone.status === 'available' ? '可接单' : drone.status === 'busy' ? '配送中' : '离线'}
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
|
||
// Render pending orders
|
||
renderProviderOrders();
|
||
renderProviderAllOrders();
|
||
}
|
||
|
||
function renderProviderOrders() {
|
||
const container = document.getElementById('provider-orders');
|
||
const pendingOrders = orders.filter(o => o.status === 'pending');
|
||
|
||
container.innerHTML = pendingOrders.map(order => `
|
||
<div class="order-item">
|
||
<div class="order-icon">📦</div>
|
||
<div class="order-info">
|
||
<div class="order-title">${order.id}</div>
|
||
<div class="order-route">${order.from} → ${order.to}</div>
|
||
</div>
|
||
<div class="order-meta">
|
||
<div class="order-price">¥${order.price}</div>
|
||
<button class="btn btn-primary" style="padding: 8px 16px;" onclick="acceptOrder('${order.id}')">接单</button>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
function renderProviderAllOrders() {
|
||
const container = document.getElementById('provider-all-orders');
|
||
container.innerHTML = orders.map(order => `
|
||
<div class="order-item">
|
||
<div class="order-icon">${order.status === 'completed' ? '✓' : '🚁'}</div>
|
||
<div class="order-info">
|
||
<div class="order-title">${order.id}</div>
|
||
<div class="order-route">${order.from} → ${order.to}</div>
|
||
</div>
|
||
<div class="order-meta">
|
||
<div class="order-price">¥${order.price}</div>
|
||
<span class="order-status ${order.status}">${getStatusText(order.status)}</span>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
function renderDemanderContent() {
|
||
renderAvailableDrones();
|
||
renderDemanderOrders();
|
||
renderQuoteList();
|
||
renderMyOrders();
|
||
renderSupplyList();
|
||
}
|
||
|
||
function renderAvailableDrones() {
|
||
const container = document.getElementById('available-drones');
|
||
const availableDrones = drones.filter(d => d.status === 'available');
|
||
|
||
container.innerHTML = availableDrones.map(drone => `
|
||
<div class="drone-card">
|
||
<div class="drone-header">
|
||
<div class="drone-icon">🚁</div>
|
||
<div>
|
||
<div class="drone-name">${drone.name}</div>
|
||
<div class="drone-model">${drone.model}</div>
|
||
</div>
|
||
</div>
|
||
<div class="drone-stats">
|
||
<div class="drone-stat">
|
||
<div class="drone-stat-label">载重上限</div>
|
||
<div class="drone-stat-value">${drone.maxWeight}kg</div>
|
||
</div>
|
||
<div class="drone-stat">
|
||
<div class="drone-stat-label">服务范围</div>
|
||
<div class="drone-stat-value">${drone.range}km</div>
|
||
</div>
|
||
</div>
|
||
<div class="drone-status-badge available">
|
||
<span class="drone-dot"></span> 可用
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
function renderDemanderOrders() {
|
||
const container = document.getElementById('demander-orders');
|
||
const myOrders = orders.slice(0, 3);
|
||
|
||
container.innerHTML = myOrders.map(order => `
|
||
<div class="order-item" onclick="showOrderDetail('${order.id}')">
|
||
<div class="order-icon">📦</div>
|
||
<div class="order-info">
|
||
<div class="order-title">${order.id}</div>
|
||
<div class="order-route">${order.from} → ${order.to}</div>
|
||
</div>
|
||
<div class="order-meta">
|
||
<div class="order-price">¥${order.price}</div>
|
||
<span class="order-status ${order.status}">${getStatusText(order.status)}</span>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
function renderMyOrders() {
|
||
const container = document.getElementById('my-orders-list');
|
||
container.innerHTML = orders.map(order => `
|
||
<div class="order-item" onclick="showOrderDetail('${order.id}')">
|
||
<div class="order-icon">${order.status === 'completed' ? '✓' : '🚁'}</div>
|
||
<div class="order-info">
|
||
<div class="order-title">${order.id}</div>
|
||
<div class="order-route">${order.from} → ${order.to}</div>
|
||
</div>
|
||
<div class="order-meta">
|
||
<div class="order-price">¥${order.price}</div>
|
||
<span class="order-status ${order.status}">${getStatusText(order.status)}</span>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
function renderQuoteList() {
|
||
const container = document.getElementById('quote-list');
|
||
container.innerHTML = quotes.map(quote => `
|
||
<div class="quote-card">
|
||
<div class="quote-provider">
|
||
<div class="provider-avatar">${quote.avatar}</div>
|
||
<div class="provider-name">${quote.provider}</div>
|
||
<div class="provider-rating">⭐ ${quote.rating}</div>
|
||
</div>
|
||
<div class="quote-details">
|
||
<div class="quote-drone">${quote.drone}</div>
|
||
<div class="quote-info">
|
||
<span>📍 ${quote.distance}km</span>
|
||
<span>🚁 ${quote.totalFlights}单</span>
|
||
</div>
|
||
</div>
|
||
<div class="quote-price-section">
|
||
<div class="quote-price">¥${quote.price}</div>
|
||
<div class="quote-etd">预计${quote.eta}分钟送达</div>
|
||
</div>
|
||
<button class="quote-action" onclick="bookQuote('${quote.id}')">立即下单</button>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
function renderSupplyList() {
|
||
const container = document.getElementById('supply-list');
|
||
container.innerHTML = drones.map(drone => `
|
||
<div class="supply-item">
|
||
<div class="supply-icon">🚁</div>
|
||
<div class="supply-info">
|
||
<div class="supply-name">${drone.name}</div>
|
||
<div class="supply-meta">${drone.location} · ${drone.maxWeight}kg载重</div>
|
||
</div>
|
||
<div class="supply-price">${drone.status === 'available' ? '可接单' : '配送中'}</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
function getStatusText(status) {
|
||
const map = {
|
||
pending: '待接单',
|
||
matching: '匹配中',
|
||
accepted: '已接单',
|
||
in_transit: '配送中',
|
||
completed: '已完成',
|
||
cancelled: '已取消'
|
||
};
|
||
return map[status] || status;
|
||
}
|
||
|
||
function acceptOrder(orderId) {
|
||
const order = orders.find(o => o.id === orderId);
|
||
if (order) {
|
||
order.status = 'accepted';
|
||
order.provider = '张师傅';
|
||
showToast('接单成功', 'success');
|
||
renderProviderContent();
|
||
}
|
||
}
|
||
|
||
function showCreateOrderModal() {
|
||
document.getElementById('create-order-modal').classList.add('active');
|
||
}
|
||
|
||
function confirmCreateOrder() {
|
||
closeModal('create-order-modal');
|
||
showMatching();
|
||
}
|
||
|
||
function showMatching() {
|
||
document.getElementById('matching-overlay').classList.add('active');
|
||
document.getElementById('matching-result').classList.remove('show');
|
||
|
||
let progress = 0;
|
||
const interval = setInterval(() => {
|
||
progress += 10;
|
||
document.getElementById('matching-progress').style.width = progress + '%';
|
||
|
||
if (progress >= 100) {
|
||
clearInterval(interval);
|
||
document.getElementById('matching-spinner').style.display = 'none';
|
||
document.getElementById('matching-result').classList.add('show');
|
||
}
|
||
}, 300);
|
||
}
|
||
|
||
function closeMatching() {
|
||
document.getElementById('matching-overlay').classList.remove('active');
|
||
showToast('订单已确认', 'success');
|
||
}
|
||
|
||
function bookQuote(quoteId) {
|
||
showMatching();
|
||
}
|
||
|
||
function showOrderDetail(orderId) {
|
||
const order = orders.find(o => o.id === orderId);
|
||
if (!order) return;
|
||
|
||
const content = document.getElementById('order-detail-content');
|
||
content.innerHTML = `
|
||
<div class="detail-row">
|
||
<span class="detail-label">订单编号</span>
|
||
<span class="detail-value">${order.id}</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="detail-label">起运地</span>
|
||
<span class="detail-value">${order.from}</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="detail-label">目的地</span>
|
||
<span class="detail-value">${order.to}</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="detail-label">货物类型</span>
|
||
<span class="detail-value">${order.cargo}</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="detail-label">订单金额</span>
|
||
<span class="detail-value" style="color: var(--success);">¥${order.price}</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="detail-label">订单状态</span>
|
||
<span class="detail-value"><span class="order-status ${order.status}">${getStatusText(order.status)}</span></span>
|
||
</div>
|
||
${order.provider ? `
|
||
<div class="detail-row">
|
||
<span class="detail-label">承运司机</span>
|
||
<span class="detail-value">${order.provider}</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="detail-label">使用机型</span>
|
||
<span class="detail-value">${order.providerDrone}</span>
|
||
</div>
|
||
` : ''}
|
||
<div class="detail-row">
|
||
<span class="detail-label">创建时间</span>
|
||
<span class="detail-value">${order.time}</span>
|
||
</div>
|
||
`;
|
||
|
||
const actionBtn = document.getElementById('order-action-btn');
|
||
if (order.status === 'in_transit') {
|
||
actionBtn.style.display = 'block';
|
||
actionBtn.textContent = '确认收货';
|
||
} else {
|
||
actionBtn.style.display = 'none';
|
||
}
|
||
|
||
document.getElementById('order-detail-modal').classList.add('active');
|
||
}
|
||
|
||
function closeModal(modalId) {
|
||
document.getElementById(modalId).classList.remove('active');
|
||
}
|
||
|
||
function submitOrder() {
|
||
const from = document.getElementById('order-from').value;
|
||
const to = document.getElementById('order-to').value;
|
||
const cargo = document.getElementById('order-cargo-type').value;
|
||
const weight = document.getElementById('order-weight').value;
|
||
|
||
if (!from || !to || !cargo || !weight) {
|
||
showToast('请填写完整信息', 'error');
|
||
return;
|
||
}
|
||
|
||
showMatching();
|
||
}
|
||
|
||
function showToast(message, type = 'success') {
|
||
const toast = document.getElementById('toast');
|
||
toast.querySelector('.toast-message').textContent = message;
|
||
toast.className = 'toast ' + type;
|
||
toast.querySelector('.toast-icon').textContent = type === 'success' ? '✓' : type === 'error' ? '✕' : 'ℹ';
|
||
|
||
toast.classList.add('show');
|
||
setTimeout(() => toast.classList.remove('show'), 3000);
|
||
}
|
||
|
||
// Map
|
||
let marketMap;
|
||
|
||
function initMarketMap() {
|
||
if (marketMap) {
|
||
marketMap.remove();
|
||
}
|
||
|
||
marketMap = L.map('market-map').setView([39.904, 116.407], 11);
|
||
|
||
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
||
attribution: '© OpenStreetMap',
|
||
maxZoom: 19
|
||
}).addTo(marketMap);
|
||
|
||
const droneLocations = [
|
||
{ lat: 39.985, lng: 116.470, name: '大疆M30T', status: 'available' },
|
||
{ lat: 39.907, lng: 116.656, name: '大疆M3E', status: 'busy' },
|
||
{ lat: 40.130, lng: 116.635, name: '极飞P100', status: 'available' },
|
||
{ lat: 39.983, lng: 116.312, name: '纵横CW-15', status: 'available' },
|
||
];
|
||
|
||
droneLocations.forEach(drone => {
|
||
const color = drone.status === 'available' ? '#00a854' : '#faad14';
|
||
L.circleMarker([drone.lat, drone.lng], {
|
||
radius: 10,
|
||
fillColor: color,
|
||
color: '#fff',
|
||
weight: 2,
|
||
fillOpacity: 0.9
|
||
}).addTo(marketMap).bindTooltip(`🚁 ${drone.name}`);
|
||
});
|
||
}
|
||
|
||
// Initialize
|
||
document.querySelectorAll('.filter-chip').forEach(chip => {
|
||
chip.addEventListener('click', function() {
|
||
document.querySelectorAll('.filter-chip').forEach(c => c.classList.remove('active'));
|
||
this.classList.add('active');
|
||
});
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |