Sidebar.vue 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. <template>
  2. <aside class="sidebar">
  3. <div class="sidebar-header">
  4. <div class="sidebar-logo">订</div>
  5. <div class="sidebar-brand-wrap">
  6. <div class="sidebar-brand">发货管理</div>
  7. <div class="sidebar-brand-sub">Order Shipping v2.0</div>
  8. </div>
  9. </div>
  10. <nav class="sidebar-nav">
  11. <!-- 金山烟草 -->
  12. <div class="nav-section-label">金山烟草</div>
  13. <div
  14. class="nav-item"
  15. :class="{ active: currentPage === 'js-orders' }"
  16. @click="$emit('navigate', 'js-orders')"
  17. >
  18. <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  19. <path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/>
  20. <line x1="3" y1="6" x2="21" y2="6"/>
  21. <path d="M16 10a4 4 0 0 1-8 0"/>
  22. </svg>
  23. <span>订单管理</span>
  24. <span v-if="pendingCount.js > 0" class="nav-badge">{{ pendingCount.js }}</span>
  25. </div>
  26. <div
  27. class="nav-item"
  28. :class="{ active: currentPage === 'js-import' }"
  29. @click="$emit('navigate', 'js-import')"
  30. >
  31. <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  32. <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
  33. <polyline points="17 8 12 3 7 8"/>
  34. <line x1="12" y1="3" x2="12" y2="15"/>
  35. </svg>
  36. <span>物流导入</span>
  37. </div>
  38. <div
  39. v-if="role === 'admin'"
  40. class="nav-item"
  41. :class="{ active: currentPage === 'js-logs' }"
  42. @click="$emit('navigate', 'js-logs')"
  43. >
  44. <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  45. <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
  46. <polyline points="14 2 14 8 20 8"/>
  47. <line x1="16" y1="13" x2="8" y2="13"/>
  48. <line x1="16" y1="17" x2="8" y2="17"/>
  49. </svg>
  50. <span>操作日志</span>
  51. </div>
  52. <!-- 嘉定烟草 -->
  53. <div class="nav-section-label" style="margin-top: var(--space-3);">嘉定烟草</div>
  54. <div
  55. class="nav-item"
  56. :class="{ active: currentPage === 'jd-orders' }"
  57. @click="$emit('navigate', 'jd-orders')"
  58. >
  59. <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  60. <path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/>
  61. <line x1="3" y1="6" x2="21" y2="6"/>
  62. <path d="M16 10a4 4 0 0 1-8 0"/>
  63. </svg>
  64. <span>订单管理</span>
  65. <span v-if="pendingCount.jd > 0" class="nav-badge">{{ pendingCount.jd }}</span>
  66. </div>
  67. <div
  68. class="nav-item"
  69. :class="{ active: currentPage === 'jd-import' }"
  70. @click="$emit('navigate', 'jd-import')"
  71. >
  72. <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  73. <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
  74. <polyline points="17 8 12 3 7 8"/>
  75. <line x1="12" y1="3" x2="12" y2="15"/>
  76. </svg>
  77. <span>物流导入</span>
  78. </div>
  79. <div
  80. v-if="role === 'admin'"
  81. class="nav-item"
  82. :class="{ active: currentPage === 'jd-logs' }"
  83. @click="$emit('navigate', 'jd-logs')"
  84. >
  85. <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  86. <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
  87. <polyline points="14 2 14 8 20 8"/>
  88. <line x1="16" y1="13" x2="8" y2="13"/>
  89. <line x1="16" y1="17" x2="8" y2="17"/>
  90. </svg>
  91. <span>操作日志</span>
  92. </div>
  93. </nav>
  94. <div class="sidebar-footer">
  95. <div class="sidebar-user" @click="$emit('logout')">
  96. <div class="avatar">{{ username?.charAt(0)?.toUpperCase() || 'U' }}</div>
  97. <div class="sidebar-user-info">
  98. <div class="sidebar-user-name">{{ username }}</div>
  99. <div class="sidebar-user-role">{{ role === 'admin' ? '管理员' : '普通用户' }} · 点击退出</div>
  100. </div>
  101. <svg class="nav-icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: var(--text-tertiary);">
  102. <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
  103. <polyline points="16 17 21 12 16 7"/>
  104. <line x1="21" y1="12" x2="9" y2="12"/>
  105. </svg>
  106. </div>
  107. </div>
  108. </aside>
  109. </template>
  110. <script setup>
  111. defineProps({
  112. currentPage: String,
  113. username: String,
  114. role: String,
  115. pendingCount: { type: Object, default: () => ({ js: 0, jd: 0 }) }
  116. })
  117. defineEmits(['navigate', 'logout'])
  118. </script>
  119. <style scoped>
  120. .sidebar {
  121. width: var(--sidebar-width);
  122. flex-shrink: 0;
  123. background: var(--bg-surface);
  124. border-right: 1px solid var(--border-default);
  125. display: flex;
  126. flex-direction: column;
  127. position: sticky;
  128. top: 0;
  129. height: 100vh;
  130. transition: width var(--dur-normal) var(--ease-out);
  131. z-index: 50;
  132. }
  133. .sidebar-header {
  134. height: var(--header-height);
  135. padding: 0 var(--space-5);
  136. display: flex;
  137. align-items: center;
  138. gap: var(--space-3);
  139. border-bottom: 1px solid var(--border-subtle);
  140. }
  141. .sidebar-logo {
  142. width: 32px; height: 32px;
  143. border-radius: var(--radius-sm);
  144. background: var(--primary-600);
  145. color: white;
  146. display: flex;
  147. align-items: center;
  148. justify-content: center;
  149. font-weight: 800;
  150. font-size: var(--text-md);
  151. flex-shrink: 0;
  152. }
  153. .sidebar-brand { font-weight: 700; font-size: var(--text-md); color: var(--text-primary); }
  154. .sidebar-brand-sub { font-size: var(--text-xs); color: var(--text-tertiary); font-weight: 500; }
  155. .sidebar-nav { flex: 1; padding: var(--space-4); overflow-y: auto; }
  156. .nav-section-label {
  157. font-size: var(--text-xs);
  158. font-weight: 700;
  159. text-transform: uppercase;
  160. letter-spacing: 0.06em;
  161. color: var(--text-tertiary);
  162. padding: var(--space-3) var(--space-3) var(--space-2);
  163. }
  164. .nav-item {
  165. display: flex;
  166. align-items: center;
  167. gap: var(--space-3);
  168. padding: 9px var(--space-3);
  169. border-radius: var(--radius-sm);
  170. color: var(--text-secondary);
  171. font-size: var(--text-base);
  172. font-weight: 500;
  173. cursor: pointer;
  174. margin-bottom: 2px;
  175. position: relative;
  176. transition: all var(--dur-fast) var(--ease-out);
  177. }
  178. .nav-item:hover { background: var(--bg-hover); color: var(--text-primary); }
  179. .nav-item.active {
  180. background: var(--primary-50);
  181. color: var(--primary-700);
  182. font-weight: 600;
  183. }
  184. .nav-item.active::before {
  185. content: '';
  186. position: absolute;
  187. left: -16px;
  188. top: 50%;
  189. transform: translateY(-50%);
  190. width: 3px;
  191. height: 20px;
  192. background: var(--primary-600);
  193. border-radius: var(--radius-full);
  194. }
  195. .nav-icon { width: 18px; height: 18px; flex-shrink: 0; }
  196. .nav-icon-sm { width: 15px; height: 15px; flex-shrink: 0; }
  197. .nav-badge {
  198. margin-left: auto;
  199. font-size: var(--text-xs);
  200. font-weight: 700;
  201. padding: 1px 7px;
  202. border-radius: var(--radius-full);
  203. background: var(--danger-500);
  204. color: white;
  205. }
  206. .sidebar-footer {
  207. padding: var(--space-4);
  208. border-top: 1px solid var(--border-subtle);
  209. }
  210. .sidebar-user {
  211. display: flex;
  212. align-items: center;
  213. gap: var(--space-3);
  214. padding: var(--space-2);
  215. border-radius: var(--radius-md);
  216. cursor: pointer;
  217. transition: background var(--dur-fast) var(--ease-out);
  218. }
  219. .sidebar-user:hover { background: var(--bg-hover); }
  220. .sidebar-user-info { flex: 1; min-width: 0; }
  221. .sidebar-user-name { font-size: var(--text-sm); font-weight: 600; color: var(--text-primary); }
  222. .sidebar-user-role { font-size: var(--text-xs); color: var(--text-tertiary); }
  223. .avatar {
  224. width: 36px; height: 36px;
  225. border-radius: var(--radius-full);
  226. display: inline-flex;
  227. align-items: center;
  228. justify-content: center;
  229. font-size: var(--text-sm);
  230. font-weight: 700;
  231. background: var(--primary-600);
  232. color: white;
  233. flex-shrink: 0;
  234. }
  235. @media (max-width: 768px) {
  236. .sidebar {
  237. position: fixed;
  238. transform: translateX(-100%);
  239. }
  240. }
  241. </style>