Sidebar.vue 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985
  1. <template>
  2. <aside class="sidebar">
  3. <!-- Logo -->
  4. <div class="sidebar-header">
  5. <div class="logo">
  6. <div class="logo-icon">🧠</div>
  7. <div>
  8. <div class="logo-text">领存智贸</div>
  9. <div class="logo-subtitle">AI原生外贸CRM</div>
  10. </div>
  11. </div>
  12. </div>
  13. <!-- Navigation -->
  14. <nav class="nav-menu">
  15. <!-- AI核心 -->
  16. <div class="nav-section">
  17. <div class="nav-section-title">AI 核心</div>
  18. <!-- <router-link to="/" class="nav-item" :class="{ active: $route.path === '/' }">
  19. <i class="fas fa-brain nav-icon"></i>
  20. <span class="nav-text">企业大脑</span>
  21. </router-link> -->
  22. <!-- <router-link to="/prospect" class="nav-item" :class="{ active: $route.path === '/prospect' }">
  23. <i class="fas fa-crosshairs nav-icon"></i>
  24. <span class="nav-text">自动寻客</span>
  25. <span class="nav-badge">新</span>
  26. </router-link> -->
  27. <router-link to="/marketing" class="nav-item" :class="{ active: $route.path === '/marketing' || $route.path === '/' }">
  28. <i class="fas fa-paper-plane nav-icon"></i>
  29. <span class="nav-text">自动营销</span>
  30. </router-link>
  31. <!-- <router-link to="/strategy" class="nav-item" :class="{ active: $route.path === '/strategy' }">
  32. <i class="fas fa-lightbulb nav-icon"></i>
  33. <span class="nav-text">AI策略中心</span>
  34. </router-link> -->
  35. </div>
  36. <!-- 客户管理 -->
  37. <div class="nav-section">
  38. <div class="nav-section-title">客户管理</div>
  39. <router-link to="/customers" class="nav-item" :class="{ active: $route.path === '/customers' }">
  40. <i class="fas fa-users nav-icon"></i>
  41. <span class="nav-text">客户池</span>
  42. </router-link>
  43. <!-- <router-link to="/opportunities" class="nav-item" :class="{ active: $route.path === '/opportunities' }">
  44. <i class="fas fa-briefcase nav-icon"></i>
  45. <span class="nav-text">商机管理</span>
  46. </router-link>
  47. <router-link to="/orders" class="nav-item" :class="{ active: $route.path === '/orders' }">
  48. <i class="fas fa-box nav-icon"></i>
  49. <span class="nav-text">订单跟进</span>
  50. </router-link> -->
  51. </div>
  52. <!-- 数据洞察 -->
  53. <!-- <div class="nav-section">
  54. <div class="nav-section-title">数据洞察</div>
  55. <router-link to="/analysis" class="nav-item" :class="{ active: $route.path === '/analysis' }">
  56. <i class="fas fa-chart-line nav-icon"></i>
  57. <span class="nav-text">经营分析</span>
  58. </router-link>
  59. <router-link to="/market" class="nav-item" :class="{ active: $route.path === '/market' }">
  60. <i class="fas fa-globe nav-icon"></i>
  61. <span class="nav-text">市场情报</span>
  62. </router-link>
  63. <router-link to="/risk" class="nav-item" :class="{ active: $route.path === '/risk' }">
  64. <i class="fas fa-shield-alt nav-icon"></i>
  65. <span class="nav-text">风险风控</span>
  66. <span class="nav-badge" style="background: var(--warning);">3</span>
  67. </router-link>
  68. </div> -->
  69. <!-- 系统设置 -->
  70. <div class="nav-section">
  71. <div class="nav-section-title">系统</div>
  72. <router-link to="/knowledge" class="nav-item" :class="{ active: $route.path === '/knowledge' }">
  73. <i class="fas fa-book nav-icon"></i>
  74. <span class="nav-text">知识库</span>
  75. </router-link>
  76. <router-link to="/settings" class="nav-item" :class="{ active: $route.path === '/settings' }">
  77. <i class="fas fa-cog nav-icon"></i>
  78. <span class="nav-text">设置</span>
  79. </router-link>
  80. </div>
  81. </nav>
  82. <!-- User Profile -->
  83. <div class="sidebar-footer">
  84. <div class="user-card" @click="toggleUserMenu">
  85. <div class="user-avatar-wrapper">
  86. <div class="user-avatar">{{ userData?.realName?.charAt(0) }}</div>
  87. <div class="online-indicator"></div>
  88. </div>
  89. <div class="user-info">
  90. <div class="user-name">{{ userData?.realName }}</div>
  91. <div class="user-status">
  92. <span class="status-dot"></span>
  93. 在线
  94. </div>
  95. </div>
  96. <i class="fas fa-chevron-up user-arrow" :class="{ rotated: showUserMenu }"></i>
  97. </div>
  98. <!-- User Dropdown Menu -->
  99. <transition name="dropdown">
  100. <div v-if="showUserMenu" class="user-menu">
  101. <div class="menu-header">
  102. <div class="menu-avatar">{{ userData?.realName?.charAt(0) }}</div>
  103. <div class="menu-user-info">
  104. <div class="menu-user-name">{{ userData?.realName }}</div>
  105. <div class="menu-user-email">{{ userData?.email }}</div>
  106. </div>
  107. </div>
  108. <div class="menu-divider"></div>
  109. <div class="menu-items">
  110. <div class="menu-item" @click="showProfileModal = true">
  111. <div class="menu-item-icon" style="background: rgba(99, 102, 241, 0.2); color: var(--primary);">
  112. <i class="fas fa-user"></i>
  113. </div>
  114. <div class="menu-item-content">
  115. <span class="menu-item-title">个人资料</span>
  116. <span class="menu-item-desc">编辑头像、姓名等信息</span>
  117. </div>
  118. </div>
  119. </div>
  120. <div class="menu-divider"></div>
  121. <div class="menu-items">
  122. <div class="menu-item" @click="openPwdModal">
  123. <div class="menu-item-icon" style="background: rgba(245, 158, 11, 0.2); color: var(--warning);">
  124. <i :class="isDarkMode ? 'fas fa-sun' : 'fas fa-moon'"></i>
  125. </div>
  126. <div class="menu-item-content">
  127. <span class="menu-item-title">修改密码</span>
  128. </div>
  129. </div>
  130. <div class="menu-item" @click="openHistoryModal">
  131. <div class="menu-item-icon" style="background: rgba(6, 182, 212, 0.2); color: var(--info);">
  132. <i class="fas fa-question-circle"></i>
  133. </div>
  134. <div class="menu-item-content">
  135. <span class="menu-item-title">登录历史</span>
  136. </div>
  137. </div>
  138. </div>
  139. <div class="menu-divider"></div>
  140. <div class="menu-footer">
  141. <div class="menu-item danger" @click="handleLogout">
  142. <div class="menu-item-icon" style="background: rgba(239, 68, 68, 0.2); color: var(--danger);">
  143. <i class="fas fa-sign-out-alt"></i>
  144. </div>
  145. <div class="menu-item-content">
  146. <span class="menu-item-title">退出登录</span>
  147. </div>
  148. </div>
  149. </div>
  150. </div>
  151. </transition>
  152. </div>
  153. <!-- Help Modal -->
  154. <div v-if="showHelpModal" class="modal-overlay" @click.self="showHelpModal = false">
  155. <div class="modal">
  156. <div class="modal-header">
  157. <div class="modal-title"><i class="fas fa-question-circle"></i> 帮助中心</div>
  158. <button class="modal-close" @click="showHelpModal = false">
  159. <i class="fas fa-times"></i>
  160. </button>
  161. </div>
  162. <div class="modal-body">
  163. <div class="help-section">
  164. <h4>🚀 快速入门</h4>
  165. <div class="help-list">
  166. <div class="help-item">
  167. <i class="fas fa-brain"></i>
  168. <span>企业大脑 - 与AI对话,快速执行任务</span>
  169. </div>
  170. <div class="help-item">
  171. <i class="fas fa-crosshairs"></i>
  172. <span>自动寻客 - AI全网挖掘高匹配潜客</span>
  173. </div>
  174. <div class="help-item">
  175. <i class="fas fa-paper-plane"></i>
  176. <span>自动营销 - 多渠道自动化客户触达</span>
  177. </div>
  178. </div>
  179. </div>
  180. <div class="help-section">
  181. <h4>📞 联系支持</h4>
  182. <div class="contact-cards">
  183. <div class="contact-card">
  184. <i class="fas fa-envelope"></i>
  185. <div>
  186. <div class="contact-label">邮箱</div>
  187. <div class="contact-value">support@lingcunai.com</div>
  188. </div>
  189. </div>
  190. <div class="contact-card">
  191. <i class="fab fa-weixin"></i>
  192. <div>
  193. <div class="contact-label">微信</div>
  194. <div class="contact-value">领存智贸</div>
  195. </div>
  196. </div>
  197. </div>
  198. </div>
  199. </div>
  200. <div class="modal-footer">
  201. <button class="btn btn-secondary" @click="showHelpModal = false">关闭</button>
  202. <button class="btn btn-primary">
  203. <i class="fas fa-book"></i> 查看完整文档
  204. </button>
  205. </div>
  206. </div>
  207. </div>
  208. <!-- 个人信息弹窗 -->
  209. <div v-if="showProfileModal" class="modal-overlay" @click.self="showProfileModal = false">
  210. <div class="modal settings-modal">
  211. <div class="modal-header">
  212. <div class="modal-title"><i class="fas fa-user-edit"></i> 编辑个人信息</div>
  213. <button class="modal-close" @click="showProfileModal = false"><i class="fas fa-times"></i></button>
  214. </div>
  215. <div class="modal-body">
  216. <el-form ref="settingsFormRef" label-width="100px">
  217. <el-form-item label="姓名">
  218. <el-input readonly v-model="userData.realName" autocomplete="off" />
  219. </el-form-item>
  220. <el-form-item label="职位">
  221. <el-input readonly v-model="userData.jobName" autocomplete="off" />
  222. </el-form-item>
  223. <el-form-item label="邮箱">
  224. <el-input readonly v-model="userData.email" autocomplete="off" />
  225. </el-form-item>
  226. <el-form-item label="电话">
  227. <el-input readonly v-model="userData.userName" autocomplete="off" />
  228. </el-form-item>
  229. <el-form-item label="所属部门">
  230. <el-input readonly v-model="userData.deptName" autocomplete="off" />
  231. </el-form-item>
  232. <el-form-item label="个人简介">
  233. <el-input type="textarea" v-model="userData.remark" :rows="3" autocomplete="off" placeholder="请输入个人简介" />
  234. </el-form-item>
  235. </el-form>
  236. </div>
  237. <div class="modal-footer">
  238. <button class="btn btn-secondary" @click="showProfileModal = false">取消</button>
  239. <button class="btn btn-primary" @click="saveRemark">保存</button>
  240. </div>
  241. </div>
  242. </div>
  243. <!-- 修改密码弹窗 -->
  244. <div v-if="showPasswordModal" class="modal-overlay" @click.self="showPasswordModal = false">
  245. <div class="modal settings-modal">
  246. <div class="modal-header">
  247. <div class="modal-title"><i class="fas fa-key"></i> 修改密码</div>
  248. <button class="modal-close" @click="showPasswordModal = false"><i class="fas fa-times"></i></button>
  249. </div>
  250. <div class="modal-body">
  251. <el-form ref="resetPwdformRef" :rules="rules" :model="pwdForm" label-width="120px">
  252. <el-form-item label="当前密码" prop="password">
  253. <el-input type='password' v-model="pwdForm.password" placeholder="请输入当前密码" @input="getPassword"></el-input>
  254. </el-form-item>
  255. <el-form-item label="新密码" prop="newPass">
  256. <el-input type='password' v-model="pwdForm.newPass" placeholder="请输入新密码"
  257. @input="getNPassword"></el-input>
  258. </el-form-item>
  259. <el-form-item label="确认新密码" prop="newPassAgain">
  260. <el-input type='password' v-model="pwdForm.newPassAgain" placeholder="请再次输入新密码"
  261. @input="getNPasswordA"></el-input>
  262. </el-form-item>
  263. <div class="password-hint">
  264. <i class="fas fa-info-circle"></i> 密码要求:至少8位,包含字母和数字
  265. </div>
  266. </el-form>
  267. </div>
  268. <div class="modal-footer">
  269. <button class="btn btn-secondary" @click="showPasswordModal = false">取消</button>
  270. <button class="btn btn-primary" @click="submitForm">确认修改</button>
  271. </div>
  272. </div>
  273. </div>
  274. <!-- 登录历史弹窗 -->
  275. <div v-if="showLoginHistoryModal" class="modal-overlay" @click.self="showLoginHistoryModal = false">
  276. <div class="modal settings-modal wide" style="max-width: 520px;">
  277. <div class="modal-header">
  278. <div class="modal-title"><i class="fas fa-history"></i> 登录历史记录</div>
  279. <button class="modal-close" @click="showLoginHistoryModal = false"><i class="fas fa-times"></i></button>
  280. </div>
  281. <div class="modal-body">
  282. <div class="login-history-list">
  283. <div v-for="login in loginHistory" :key="login.id" class="login-history-item">
  284. <div class="login-icon" :class="login.status == 1 ? 'success' : 'error'">
  285. <i :class="login.status === 1 ? 'fas fa-check-circle' : 'fas fa-times-circle'"></i>
  286. </div>
  287. <div class="login-info">
  288. <div class="login-time">{{ login.createTime }}</div>
  289. <div class="login-details">
  290. <span><i class="fas fa-map-marker-alt"></i> {{ login.city }}</span>
  291. <span><i class="fas fa-desktop"></i> {{ login.browser }} / {{ login.os.split(' ')[0] }}</span>
  292. <span><i class="fas fa-network-wired"></i> {{ login.ip }}</span>
  293. </div>
  294. </div>
  295. <div class="login-status" :class="login.status == 1 ? 'success' : 'error'">{{ login.status == 1 ? '成功' : '失败' }}</div>
  296. </div>
  297. </div>
  298. </div>
  299. <div class="modal-footer">
  300. <button class="btn btn-secondary" @click="showLoginHistoryModal = false">关闭</button>
  301. </div>
  302. </div>
  303. </div>
  304. <!-- Click outside to close menu -->
  305. <div v-if="showUserMenu" class="menu-backdrop" @click="showUserMenu = false"></div>
  306. </aside>
  307. </template>
  308. <script setup>
  309. import { ref, onMounted } from 'vue'
  310. import { useRoute, useRouter } from 'vue-router'
  311. import { getUserInfo, updateUserPwd, getLoginData, updateRemark } from '@/api/layout'
  312. import { ElMessageBox } from 'element-plus'
  313. import { encrypt } from "@/utils/jsencrypt";
  314. const route = useRoute()
  315. const router = useRouter()
  316. const showProfileModal = ref(false)
  317. const showPasswordModal = ref(false)
  318. const showLoginHistoryModal = ref(false)
  319. // 登录历史数据
  320. const loginHistory = ref([])
  321. const openHistoryModal = () => {
  322. getHistoryData()
  323. showLoginHistoryModal.value = true
  324. }
  325. const getHistoryData = () => {
  326. getLoginData({ pageIndex: 1, pageSize: 10000 }).then(res => {
  327. if(res.code === 200){
  328. loginHistory.value = res.result.records
  329. }
  330. })
  331. }
  332. const saveRemark = () => {
  333. updateRemark({ remark: userData.value.remark, id: userData.value.id }).then(res => {
  334. ElMessage.success("个人信息已更新");
  335. showProfileModal.value = false
  336. }).catch(err => {
  337. ElMessage.error(err.message);
  338. })
  339. }
  340. const showUserMenu = ref(false)
  341. const showHelpModal = ref(false)
  342. const isDarkMode = ref(true)
  343. const toggleUserMenu = () => {
  344. showUserMenu.value = !showUserMenu.value
  345. }
  346. const goToSettings = (section) => {
  347. showUserMenu.value = false
  348. router.push('/settings')
  349. }
  350. const toggleTheme = () => {
  351. isDarkMode.value = !isDarkMode.value
  352. showUserMenu.value = false
  353. }
  354. const handleLogout = () => {
  355. ElMessageBox.confirm(
  356. '确定要退出登录吗?',
  357. '提示',
  358. {
  359. confirmButtonText: '确认',
  360. cancelButtonText: '取消',
  361. type: 'warning',
  362. }
  363. ).then(() => {
  364. localStorage.clear()
  365. router.push({ path: '/login' })
  366. })
  367. }
  368. const pwdForm = ref({
  369. password: '',
  370. newPass: '',
  371. newPassAgain: ''
  372. });
  373. const resetPwdformRef = ref();
  374. const getPassword = (data) => {
  375. pwdForm.value.password = data.replace(/\s+/g, '')
  376. }
  377. const getNPassword = (data) => {
  378. pwdForm.value.newPass = data.replace(/\s+/g, '')
  379. }
  380. const getNPasswordA = (data) => {
  381. pwdForm.value.newPassAgain = data.replace(/\s+/g, '')
  382. }
  383. const openPwdModal = () => {
  384. showPasswordModal.value = true
  385. resetPwdformRef.value?.resetFields();
  386. pwdForm.value.password = ''
  387. pwdForm.value.newPass = ''
  388. pwdForm.value.newPassAgain = ''
  389. }
  390. var validatePass1 = (rule, value, callback) => {
  391. const reg = /^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z\W]{8,}$/;
  392. if (value === '') {
  393. callback(new Error('请输入新密码'));
  394. } else if (!reg.test(value)) {
  395. callback(new Error('密码须包含字母+数字且长度大于8位!'));
  396. } else {
  397. callback();
  398. }
  399. };
  400. var validatePass2 = (rule, value, callback) => {
  401. if (value === '') {
  402. callback(new Error('请再次输入新密码'));
  403. } else if (value !== pwdForm.value.newPass) {
  404. callback(new Error('两次输入密码不一致!'));
  405. } else {
  406. callback();
  407. }
  408. };
  409. const rules = ref({
  410. password: [
  411. { required: true, message: '请输入现在使用的密码', trigger: 'blur' },
  412. { min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' }
  413. ],
  414. newPass: [
  415. { required: true, validator: validatePass1, trigger: 'blur' },
  416. ],
  417. newPassAgain: [
  418. { required: true, validator: validatePass2, trigger: 'blur' },
  419. ],
  420. });
  421. const submitForm = () => {
  422. resetPwdformRef.value?.validate((valid) => {
  423. if (valid) {
  424. let params = {
  425. password: encrypt(pwdForm.value.password),
  426. newPass: encrypt(pwdForm.value.newPass),
  427. newPassAgain: encrypt(pwdForm.value.newPassAgain)
  428. }
  429. updateUserPwd(params).then(res => {
  430. localStorage.clear()
  431. ElMessage.success("修改成功");
  432. router.push({ path: '/login' })
  433. }).catch(err => {
  434. ElMessage.error(err.message);
  435. })
  436. } else {
  437. }
  438. });
  439. }
  440. const userData = ref()
  441. onMounted(() => {
  442. getUserInfo().then((res) => {
  443. if(res.code == 200) {
  444. userData.value = res.result
  445. localStorage.setItem('userInfo', JSON.stringify(res.result))
  446. }
  447. })
  448. })
  449. </script>
  450. <style scoped>
  451. .sidebar {
  452. width: 260px;
  453. background: var(--bg-card);
  454. border-right: 1px solid var(--border);
  455. display: flex;
  456. flex-direction: column;
  457. position: fixed;
  458. top: 0;
  459. left: 0;
  460. bottom: 0;
  461. z-index: 100;
  462. }
  463. .sidebar-header {
  464. padding: 20px;
  465. border-bottom: 1px solid var(--border);
  466. }
  467. .logo {
  468. display: flex;
  469. align-items: center;
  470. gap: 12px;
  471. }
  472. .logo-icon {
  473. width: 40px;
  474. height: 40px;
  475. background: var(--gradient-1);
  476. border-radius: 10px;
  477. display: flex;
  478. align-items: center;
  479. justify-content: center;
  480. font-size: 20px;
  481. }
  482. .logo-text {
  483. font-size: 18px;
  484. font-weight: 700;
  485. color: var(--text-primary);
  486. }
  487. .logo-subtitle {
  488. font-size: 11px;
  489. color: var(--text-secondary);
  490. }
  491. /* Navigation Menu */
  492. .nav-menu {
  493. flex: 1;
  494. overflow-y: auto;
  495. padding: 16px 12px;
  496. }
  497. .nav-section {
  498. margin-bottom: 20px;
  499. }
  500. .nav-section-title {
  501. font-size: 11px;
  502. font-weight: 600;
  503. color: var(--text-muted);
  504. text-transform: uppercase;
  505. letter-spacing: 0.5px;
  506. padding: 0 12px;
  507. margin-bottom: 8px;
  508. }
  509. .nav-item {
  510. display: flex;
  511. align-items: center;
  512. gap: 12px;
  513. padding: 10px 12px;
  514. border-radius: 8px;
  515. cursor: pointer;
  516. transition: all 0.2s;
  517. margin-bottom: 4px;
  518. color: var(--text-secondary);
  519. text-decoration: none;
  520. }
  521. .nav-item:hover {
  522. background: var(--bg-hover);
  523. color: var(--text-primary);
  524. }
  525. .nav-item.active {
  526. background: rgba(99, 102, 241, 0.2);
  527. color: var(--primary);
  528. }
  529. .nav-icon {
  530. width: 20px;
  531. font-size: 16px;
  532. }
  533. .nav-text {
  534. flex: 1;
  535. font-size: 13px;
  536. }
  537. .nav-badge {
  538. background: var(--danger);
  539. color: white;
  540. font-size: 10px;
  541. padding: 2px 6px;
  542. border-radius: 10px;
  543. font-weight: 600;
  544. }
  545. /* User Card */
  546. .sidebar-footer {
  547. padding: 12px;
  548. border-top: 1px solid var(--border);
  549. position: relative;
  550. }
  551. .user-card {
  552. display: flex;
  553. align-items: center;
  554. gap: 12px;
  555. padding: 12px;
  556. background: var(--bg-hover);
  557. border-radius: 12px;
  558. cursor: pointer;
  559. transition: all 0.2s;
  560. }
  561. .user-card:hover {
  562. background: var(--border);
  563. }
  564. .user-avatar-wrapper {
  565. position: relative;
  566. }
  567. .user-avatar {
  568. width: 40px;
  569. height: 40px;
  570. background: var(--gradient-1);
  571. border-radius: 50%;
  572. display: flex;
  573. align-items: center;
  574. justify-content: center;
  575. font-size: 16px;
  576. font-weight: 600;
  577. color: white;
  578. }
  579. .online-indicator {
  580. position: absolute;
  581. bottom: 0;
  582. right: 0;
  583. width: 12px;
  584. height: 12px;
  585. background: var(--success);
  586. border: 2px solid var(--bg-card);
  587. border-radius: 50%;
  588. }
  589. .user-info {
  590. flex: 1;
  591. }
  592. .user-name {
  593. font-size: 14px;
  594. font-weight: 600;
  595. }
  596. .user-status {
  597. display: flex;
  598. align-items: center;
  599. gap: 6px;
  600. font-size: 12px;
  601. color: var(--text-secondary);
  602. }
  603. .status-dot {
  604. width: 6px;
  605. height: 6px;
  606. background: var(--success);
  607. border-radius: 50%;
  608. }
  609. .user-arrow {
  610. color: var(--text-muted);
  611. font-size: 12px;
  612. transition: transform 0.2s;
  613. }
  614. .user-arrow.rotated {
  615. transform: rotate(180deg);
  616. }
  617. /* User Menu Dropdown */
  618. .menu-backdrop {
  619. position: fixed;
  620. top: 0;
  621. left: 0;
  622. right: 0;
  623. bottom: 0;
  624. z-index: 98;
  625. }
  626. .user-menu {
  627. position: absolute;
  628. bottom: 100%;
  629. left: 12px;
  630. right: 12px;
  631. margin-bottom: 8px;
  632. background: var(--bg-card);
  633. border: 1px solid var(--border);
  634. border-radius: 12px;
  635. box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.3);
  636. z-index: 99;
  637. overflow: hidden;
  638. }
  639. .dropdown-enter-active,
  640. .dropdown-leave-active {
  641. transition: all 0.2s ease;
  642. }
  643. .dropdown-enter-from,
  644. .dropdown-leave-to {
  645. opacity: 0;
  646. transform: translateY(10px);
  647. }
  648. .menu-header {
  649. display: flex;
  650. align-items: center;
  651. gap: 12px;
  652. padding: 16px;
  653. background: var(--bg-hover);
  654. }
  655. .menu-avatar {
  656. width: 44px;
  657. height: 44px;
  658. background: var(--gradient-1);
  659. border-radius: 50%;
  660. display: flex;
  661. align-items: center;
  662. justify-content: center;
  663. font-size: 18px;
  664. font-weight: 600;
  665. color: white;
  666. }
  667. .menu-user-info {
  668. flex: 1;
  669. }
  670. .menu-user-name {
  671. font-size: 14px;
  672. font-weight: 600;
  673. }
  674. .menu-user-email {
  675. font-size: 12px;
  676. color: var(--text-secondary);
  677. }
  678. .menu-divider {
  679. height: 1px;
  680. background: var(--border);
  681. margin: 4px 0;
  682. }
  683. .menu-items {
  684. padding: 8px;
  685. }
  686. .menu-item {
  687. display: flex;
  688. align-items: center;
  689. gap: 12px;
  690. padding: 10px;
  691. border-radius: 8px;
  692. cursor: pointer;
  693. transition: background 0.2s;
  694. text-decoration: none;
  695. color: inherit;
  696. }
  697. .menu-item:hover {
  698. background: var(--bg-hover);
  699. }
  700. .menu-item.danger:hover {
  701. background: rgba(239, 68, 68, 0.1);
  702. }
  703. .menu-item.danger .menu-item-title {
  704. color: var(--danger);
  705. }
  706. .menu-item-icon {
  707. width: 36px;
  708. height: 36px;
  709. border-radius: 8px;
  710. display: flex;
  711. align-items: center;
  712. justify-content: center;
  713. font-size: 14px;
  714. }
  715. .menu-item-content {
  716. flex: 1;
  717. }
  718. .menu-item-title {
  719. display: block;
  720. font-size: 13px;
  721. font-weight: 500;
  722. }
  723. .menu-item-desc {
  724. display: block;
  725. font-size: 11px;
  726. color: var(--text-secondary);
  727. margin-top: 2px;
  728. }
  729. .menu-footer {
  730. padding: 8px;
  731. padding-top: 0;
  732. }
  733. /* Modal */
  734. .modal-overlay {
  735. position: fixed;
  736. top: 0;
  737. left: 0;
  738. right: 0;
  739. bottom: 0;
  740. background: rgba(0, 0, 0, 0.7);
  741. display: flex;
  742. align-items: center;
  743. justify-content: center;
  744. z-index: 1000;
  745. padding: 20px;
  746. }
  747. .modal {
  748. background: var(--bg-card);
  749. border-radius: 16px;
  750. width: 100%;
  751. max-width: 500px;
  752. overflow: hidden;
  753. }
  754. .modal-header {
  755. display: flex;
  756. justify-content: space-between;
  757. align-items: center;
  758. padding: 20px;
  759. border-bottom: 1px solid var(--border);
  760. }
  761. .modal-title {
  762. font-size: 16px;
  763. font-weight: 600;
  764. display: flex;
  765. align-items: center;
  766. gap: 10px;
  767. }
  768. .modal-title i {
  769. color: var(--primary);
  770. }
  771. .modal-close {
  772. width: 32px;
  773. height: 32px;
  774. background: var(--bg-hover);
  775. border: none;
  776. border-radius: 8px;
  777. color: var(--text-secondary);
  778. cursor: pointer;
  779. display: flex;
  780. align-items: center;
  781. justify-content: center;
  782. }
  783. .modal-close:hover {
  784. background: var(--border);
  785. color: var(--text-primary);
  786. }
  787. .modal-body {
  788. padding: 20px;
  789. }
  790. .modal-footer {
  791. display: flex;
  792. justify-content: flex-end;
  793. gap: 12px;
  794. padding: 20px;
  795. border-top: 1px solid var(--border);
  796. }
  797. /* Help Section */
  798. .help-section {
  799. margin-bottom: 20px;
  800. }
  801. .help-section:last-child {
  802. margin-bottom: 0;
  803. }
  804. .help-section h4 {
  805. font-size: 14px;
  806. font-weight: 600;
  807. margin-bottom: 12px;
  808. }
  809. .help-list {
  810. display: flex;
  811. flex-direction: column;
  812. gap: 8px;
  813. }
  814. .help-item {
  815. display: flex;
  816. align-items: center;
  817. gap: 10px;
  818. padding: 12px;
  819. background: var(--bg-hover);
  820. border-radius: 8px;
  821. font-size: 13px;
  822. }
  823. .help-item i {
  824. color: var(--primary);
  825. width: 20px;
  826. }
  827. .contact-cards {
  828. display: grid;
  829. grid-template-columns: repeat(2, 1fr);
  830. gap: 12px;
  831. }
  832. .contact-card {
  833. display: flex;
  834. align-items: center;
  835. gap: 12px;
  836. padding: 14px;
  837. background: var(--bg-hover);
  838. border-radius: 10px;
  839. }
  840. .contact-card i {
  841. font-size: 20px;
  842. color: var(--primary);
  843. }
  844. .contact-label {
  845. font-size: 11px;
  846. color: var(--text-muted);
  847. }
  848. .contact-value {
  849. font-size: 13px;
  850. font-weight: 500;
  851. }
  852. /* Login History Modal */
  853. .login-history-list { display: flex; flex-direction: column; gap: 12px; }
  854. .login-history-item { display: flex; align-items: center; gap: 14px; padding: 14px; background: var(--bg-hover); border-radius: 10px; }
  855. .login-icon { width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 16px; }
  856. .login-icon.success { background: rgba(16, 185, 129, 0.2); color: var(--success); }
  857. .login-icon.error { background: rgba(239, 68, 68, 0.2); color: var(--danger); }
  858. .login-info { flex: 1; }
  859. .login-time { font-size: 14px; font-weight: 500; margin-bottom: 4px; }
  860. .login-details { display: flex; gap: 16px; font-size: 12px; color: var(--text-muted); }
  861. .login-details span { display: flex; align-items: center; gap: 4px; }
  862. .login-status { font-size: 12px; padding: 4px 10px; border-radius: 10px; }
  863. .login-status.success { background: rgba(16, 185, 129, 0.2); color: var(--success); }
  864. .login-status.error { background: rgba(239, 68, 68, 0.2); color: var(--danger); }
  865. :deep(.el-form-item){
  866. display: flex;
  867. flex-direction: column;
  868. }
  869. :deep(.el-form-item .el-form-item__label){
  870. justify-content: flex-start;
  871. }
  872. :deep(.el-form-item__label), :deep(.el-radio__label), :deep(.el-checkbox__label) {
  873. color: var(--text-secondary);
  874. }
  875. :deep(.el-input__inner) {
  876. color: var(--text-primary);
  877. }
  878. :deep(.el-input__wrapper), :deep(.el-textarea__inner), :deep(.el-select__wrapper){
  879. background: var(--bg-dark);
  880. border: 1px solid var(--border);
  881. box-shadow: none;
  882. border-radius: 8px;
  883. color: var(--text-primary);
  884. }
  885. </style>