HvacAside.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857
  1. <script setup lang="ts">
  2. import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef } from 'vue';
  3. import { useRoute, useRouter } from 'vue-router';
  4. import { message } from 'ant-design-vue';
  5. import Simplebar from 'simplebar-vue';
  6. import ConfirmModal from '@/components/ConfirmModal.vue';
  7. import SvgIcon from '@/components/SvgIcon.vue';
  8. import { useRequest } from '@/hooks/request';
  9. import { useUserInfoStore } from '@/stores/user-info';
  10. import { dataCenterRoutes, opsCenterRoutes } from '@/router';
  11. import { t } from '@/i18n';
  12. import { addRevisePassword, getNoticePageList, getPageList, getUnreadNotifications } from '@/api';
  13. import { translateNavigation } from '@/utils';
  14. import { removeToken } from '@/utils/auth';
  15. import type { RouteRecordRaw } from 'vue-router';
  16. import type { FormInstance, Rule } from 'ant-design-vue/es/form';
  17. import type { MenuInfo } from 'ant-design-vue/es/menu/src/interface';
  18. import type { ChangePasswordForm, DeviceGroupItem, NoticePageItem, PageParams } from '@/types';
  19. const router = useRouter();
  20. const route = useRoute();
  21. const { permission, resetToken } = useUserInfoStore();
  22. const menuRef = useTemplateRef('menu');
  23. const selectedKeys = ref<string[]>([route.path]);
  24. const openKeys = ref<string[]>([]);
  25. const deviceGroupList = ref<DeviceGroupItem[]>([]);
  26. const messageOpen = ref<boolean>(false);
  27. const changePasswordOpen = ref<boolean>(false);
  28. const messageList = ref<NoticePageItem[]>([]);
  29. const formRef = ref<FormInstance>();
  30. const modalComponentRef = useTemplateRef('modalComponent');
  31. const changePasswordForm = ref<ChangePasswordForm>({
  32. rawPassword: '',
  33. oldRawPassword: '',
  34. confirmPassword: '',
  35. });
  36. const messageParam = ref<PageParams>({
  37. pageIndex: 1,
  38. pageSize: 10,
  39. });
  40. const messageTotal = ref<number>();
  41. const { handleRequest } = useRequest();
  42. let timer: number | null = null; // 保存定时器ID
  43. const countNumber = ref<number>();
  44. // 递归过滤函数
  45. const filterRoutes = (routeList: Readonly<RouteRecordRaw[]>) => {
  46. return routeList.filter((route) => {
  47. const routeHasPermission =
  48. route.meta &&
  49. permission
  50. ?.split(',')
  51. .map(Number)
  52. .includes(route.meta.permission as number);
  53. let childrenHavePermission = false;
  54. // 如果有子路由,递归过滤
  55. if (routeHasPermission) {
  56. if (route.children && route.children.length > 0) {
  57. const filteredChildren: RouteRecordRaw[] = filterRoutes(route.children);
  58. childrenHavePermission = filteredChildren.length > 0;
  59. // 更新子路由为过滤后的结果
  60. if (childrenHavePermission) {
  61. route.children = filteredChildren;
  62. }
  63. }
  64. }
  65. // 只有当路由自身有权限或子路由有权限才保留
  66. return routeHasPermission || childrenHavePermission;
  67. });
  68. };
  69. const menuGroupList = computed(() => {
  70. return [
  71. {
  72. category: t('common.dataCenter'),
  73. routes: filterRoutes(dataCenterRoutes),
  74. },
  75. {
  76. category: t('common.opsCenter'),
  77. routes: filterRoutes(opsCenterRoutes),
  78. },
  79. ];
  80. });
  81. const rules: Record<string, Rule[]> = {
  82. rawPassword: [{ required: true, message: t('common.cannotEmpty'), trigger: 'change' }],
  83. oldRawPassword: [{ required: true, message: t('common.cannotEmpty'), trigger: 'change' }],
  84. confirmPassword: [
  85. { required: true, message: t('common.cannotEmpty'), trigger: 'change' },
  86. {
  87. validator: (_rule: unknown, value: string) => {
  88. if (changePasswordForm.value.rawPassword !== value) {
  89. return Promise.reject(t('hcacAside.newPasswordError'));
  90. }
  91. return Promise.resolve();
  92. },
  93. },
  94. ],
  95. };
  96. const messageColumns = [
  97. {
  98. title: t('alarmManage.alarmContent'),
  99. dataIndex: 'content',
  100. key: 'content',
  101. ellipsis: true,
  102. },
  103. {
  104. title: t('hcacAside.deviceEventName'),
  105. dataIndex: 'name',
  106. key: 'name',
  107. ellipsis: true,
  108. },
  109. {
  110. title: t('hcacAside.deviceMonitoringPoint'),
  111. dataIndex: 'dev',
  112. key: 'dev',
  113. ellipsis: true,
  114. },
  115. {
  116. title: t('common.time'),
  117. dataIndex: 'createTime',
  118. key: 'createTime',
  119. ellipsis: true,
  120. },
  121. ];
  122. const getNoticeNumber = () => {
  123. handleRequest(async () => {
  124. countNumber.value = await getUnreadNotifications();
  125. });
  126. };
  127. // 启动定时器
  128. const startTimer = () => {
  129. timer = setInterval(() => {
  130. getNoticeNumber();
  131. }, 10000) as unknown as number;
  132. };
  133. onMounted(async () => {
  134. try {
  135. const data = await getPageList();
  136. deviceGroupList.value = data.filter((item) => item.deviceGroupChilds.length > 0);
  137. } catch (err) {
  138. if (err instanceof Error) {
  139. message.error(err.message);
  140. }
  141. console.error(err);
  142. }
  143. getNoticeNumber();
  144. startTimer();
  145. openSelectedSubMenu();
  146. });
  147. // 组件卸载前清理定时器
  148. onBeforeUnmount(() => {
  149. if (timer) clearInterval(timer);
  150. });
  151. const openSelectedSubMenu = () => {
  152. setTimeout(() => {
  153. const menuEl = menuRef.value?.$el as HTMLElement | undefined;
  154. const selectedSubMenuEl = menuEl?.querySelector<HTMLElement>('.ant-menu-submenu-selected');
  155. const selectedSubMenuId = selectedSubMenuEl?.dataset.submenuId;
  156. if (selectedSubMenuId) {
  157. openKeys.value.push(selectedSubMenuId);
  158. }
  159. }, 0);
  160. };
  161. const DEVICE_GROUP_ROUTE_PATH = '/ai-smart-ctrl/device-group';
  162. const getDeviceGroupMenuKey = (id: number) => {
  163. return `${DEVICE_GROUP_ROUTE_PATH}/${id}`;
  164. };
  165. const handleMenuClick = (menu: MenuInfo) => {
  166. const key = menu.key as string;
  167. if (key.includes(DEVICE_GROUP_ROUTE_PATH)) {
  168. router.push({
  169. path: key,
  170. query: {
  171. ...route.query,
  172. },
  173. });
  174. return;
  175. }
  176. router.push(key);
  177. };
  178. const collapsed = ref<boolean>(false);
  179. const getNoticeList = () => {
  180. handleRequest(async () => {
  181. const { records, total } = await getNoticePageList(messageParam.value);
  182. messageList.value = records;
  183. messageTotal.value = total;
  184. });
  185. };
  186. const toggleCollapsed = () => {
  187. collapsed.value = !collapsed.value;
  188. };
  189. const addInformation = () => {
  190. getNoticeList();
  191. messageOpen.value = true;
  192. };
  193. const messageChange = () => {
  194. getNoticeList();
  195. };
  196. const clickNavigateMonitor = (monitorId: number, parentDevGroupId: number, devGroupId: number) => {
  197. if (monitorId) {
  198. router.push({
  199. path: '/env-monitor/index',
  200. query: { monitorId, parentDevGroupId, devGroupId }, // 查询参数
  201. });
  202. messageOpen.value = false;
  203. }
  204. };
  205. const clickNavigateGroup = (devGroupId: number) => {
  206. if (devGroupId) {
  207. router.push(`/ai-smart-ctrl/device-group/${devGroupId}`);
  208. messageOpen.value = false;
  209. }
  210. };
  211. const clickExit = () => {
  212. modalComponentRef.value?.showView();
  213. };
  214. const confirm = () => {
  215. removeToken();
  216. resetToken();
  217. router.push('/login');
  218. modalComponentRef.value?.hideView();
  219. };
  220. const changePassword = () => {
  221. changePasswordOpen.value = true;
  222. };
  223. const handleClose = () => {
  224. changePasswordForm.value = {
  225. rawPassword: '',
  226. oldRawPassword: '',
  227. confirmPassword: '',
  228. };
  229. formRef.value?.resetFields();
  230. };
  231. const bindingPassword = () => {
  232. formRef.value?.validate().then(() => {
  233. handleRequest(async () => {
  234. const { rawPassword, oldRawPassword } = changePasswordForm.value;
  235. await addRevisePassword({
  236. rawPassword,
  237. oldRawPassword,
  238. });
  239. message.success(t('common.modifySuccess'));
  240. confirm();
  241. changePasswordOpen.value = false;
  242. });
  243. });
  244. };
  245. </script>
  246. <template>
  247. <ALayoutSider
  248. class="aside-container"
  249. v-model:collapsed="collapsed"
  250. collapsible
  251. :collapsed-width="64"
  252. :trigger="null"
  253. :width="246"
  254. >
  255. <div class="aside-header">
  256. <img class="aside-header-logo" src="@/assets/img/logo.png" />
  257. <span v-show="!collapsed" class="aside-header-title">{{ $t('common.unimatIoT') }}</span>
  258. </div>
  259. <Simplebar class="aside-scroll">
  260. <AMenu
  261. ref="menu"
  262. class="aside-menu"
  263. v-model:selected-keys="selectedKeys"
  264. v-model:open-keys="openKeys"
  265. mode="inline"
  266. @click="handleMenuClick"
  267. >
  268. <div class="aside-menu-category">{{ $t('common.aiCtrl') }}</div>
  269. <template v-for="item in deviceGroupList" :key="item.id">
  270. <ASubMenu
  271. v-if="item.deviceGroupChilds?.length"
  272. :key="getDeviceGroupMenuKey(item.id)"
  273. :title="item.groupName"
  274. popup-class-name="aside-menu-submenu-popup"
  275. >
  276. <template #icon>
  277. <SvgIcon name="air-conditioning" />
  278. </template>
  279. <AMenuItem v-for="subItem in item.deviceGroupChilds" :key="getDeviceGroupMenuKey(subItem.id)">
  280. {{ subItem.groupName }}
  281. </AMenuItem>
  282. </ASubMenu>
  283. <template v-else>
  284. <AMenuItem :key="getDeviceGroupMenuKey(item.id)" :title="item.groupName">
  285. <template #icon>
  286. <SvgIcon name="air-conditioning" />
  287. </template>
  288. {{ item.groupName }}
  289. </AMenuItem>
  290. </template>
  291. </template>
  292. <template v-for="(item, index) in menuGroupList" :key="index">
  293. <div class="aside-menu-category">{{ item.category }}</div>
  294. <template v-for="{ path, meta, children } in item.routes" :key="path">
  295. <template v-if="meta && !meta.hideInMenu">
  296. <ASubMenu
  297. v-if="!meta.hideSubMenu && children"
  298. :key="path"
  299. :title="translateNavigation(meta.title)"
  300. popup-class-name="aside-menu-submenu-popup"
  301. >
  302. <template #icon>
  303. <SvgIcon v-if="meta.icon" :name="meta.icon" />
  304. </template>
  305. <AMenuItem v-for="{ path: subPath, meta } in children" :key="`${path}/${subPath}`">
  306. {{ translateNavigation(meta?.title) }}
  307. </AMenuItem>
  308. </ASubMenu>
  309. <AMenuItem v-else :key="`${path}/index`" :title="translateNavigation(meta.title)">
  310. <template #icon>
  311. <SvgIcon v-if="meta.icon" :name="meta.icon" />
  312. </template>
  313. {{ translateNavigation(meta.title) }}
  314. </AMenuItem>
  315. </template>
  316. </template>
  317. </template>
  318. </AMenu>
  319. </Simplebar>
  320. <div v-show="collapsed" class="aside-collapsed-icons">
  321. <ABadge :count="countNumber">
  322. <SvgIcon name="information" @click="addInformation" />
  323. </ABadge>
  324. <SvgIcon name="setting" />
  325. <SvgIcon name="unfold" @click="toggleCollapsed" />
  326. </div>
  327. <div class="aside-footer">
  328. <ADropdown
  329. overlay-class-name="hvac-dropdown"
  330. placement="topLeft"
  331. :align="{ offset: [0, -30] }"
  332. :trigger="['click']"
  333. >
  334. <div class="aside-footer-avatar" @click.prevent></div>
  335. <template #overlay>
  336. <AMenu>
  337. <AMenuItem>
  338. <div @click="changePassword">
  339. <AFlex class="hvac-item" justify="space-between" align="center">
  340. <AFlex align="center"
  341. ><SvgIcon class="icon-style" name="edit-o" /> {{ t('hcacAside.changePassword') }}</AFlex
  342. >
  343. <SvgIcon name="right"
  344. /></AFlex>
  345. </div>
  346. </AMenuItem>
  347. <AMenuDivider />
  348. <AMenuItem>
  349. <div @click="clickExit">
  350. <AFlex class="hvac-item" align="center">
  351. <SvgIcon name="a-log-out" class="icon-style" /> {{ t('hcacAside.logout') }}
  352. </AFlex>
  353. </div>
  354. </AMenuItem>
  355. </AMenu>
  356. </template>
  357. </ADropdown>
  358. <div v-show="!collapsed">
  359. <SvgIcon name="setting" />
  360. <span @click="addInformation">
  361. <ABadge :count="countNumber">
  362. <SvgIcon name="information" />
  363. </ABadge>
  364. </span>
  365. <SvgIcon name="fold" @click="toggleCollapsed" />
  366. </div>
  367. </div>
  368. </ALayoutSider>
  369. <AModal
  370. v-model:open="messageOpen"
  371. :title="t('hcacAside.messageCenter')"
  372. width="926px"
  373. :footer="null"
  374. :keyboard="false"
  375. >
  376. <ATable class="table-margin" :columns="messageColumns" :data-source="messageList" :pagination="false">
  377. <template #bodyCell="{ column, record }">
  378. <template v-if="column.key === 'name'">
  379. <span class="text-style" v-if="record.type === 1" @click="clickNavigateGroup(record.devGroupId)">{{
  380. record.groupName
  381. }}</span>
  382. </template>
  383. <template v-if="column.key === 'dev'">
  384. <span
  385. class="text-style"
  386. v-if="record.type === 1"
  387. @click="clickNavigateMonitor(record.monitorId, record.parentDevGroupId, record.devGroupId)"
  388. >{{ record.monitorName }}</span
  389. >
  390. </template>
  391. </template>
  392. </ATable>
  393. <AFlex justify="flex-end" class="footer">
  394. <APagination
  395. v-model:current="messageParam.pageIndex"
  396. v-model:page-size="messageParam.pageSize"
  397. :total="messageTotal"
  398. :show-size-changer="true"
  399. @change="messageChange"
  400. show-quick-jumper
  401. :show-total="(total) => $t('common.pageTotal', { total })"
  402. />
  403. </AFlex>
  404. </AModal>
  405. <AModal
  406. v-model:open="changePasswordOpen"
  407. :title="t('hcacAside.changePassword')"
  408. width="460px"
  409. :footer="null"
  410. :mask-closable="false"
  411. :keyboard="false"
  412. :after-close="handleClose"
  413. >
  414. <AForm
  415. ref="formRef"
  416. :colon="false"
  417. label-align="left"
  418. :model="changePasswordForm"
  419. :rules="rules"
  420. :label-col="{ span: 6 }"
  421. class="form-style"
  422. >
  423. <AFlex :vertical="true">
  424. <AFormItem :label="t('hcacAside.originalPassword')" name="oldRawPassword">
  425. <AInput
  426. class="input-width"
  427. v-model:value="changePasswordForm.oldRawPassword"
  428. :placeholder="t('hcacAside.pleaseEnterOriginalPassword')"
  429. />
  430. </AFormItem>
  431. <AFormItem :label="t('hcacAside.newPassword')" name="rawPassword">
  432. <AInput
  433. class="input-width"
  434. v-model:value="changePasswordForm.rawPassword"
  435. :placeholder="t('hcacAside.pleaseEnterNewPassword')"
  436. />
  437. </AFormItem>
  438. <AFormItem :label="t('hcacAside.confirmNewPassword')" name="confirmPassword">
  439. <AInput
  440. class="input-width"
  441. v-model:value="changePasswordForm.confirmPassword"
  442. :placeholder="t('hcacAside.pleaseEnterNewPassword')"
  443. />
  444. </AFormItem>
  445. </AFlex>
  446. </AForm>
  447. <AFlex justify="flex-end">
  448. <AButton class="button-style" type="primary" ghost @click="changePasswordOpen = false">{{
  449. $t('common.cancel')
  450. }}</AButton>
  451. <AButton class="button-style" type="primary" @click="bindingPassword">{{ t('common.certain') }}</AButton>
  452. </AFlex>
  453. </AModal>
  454. <ConfirmModal
  455. ref="modalComponent"
  456. :title="t('common.exitConfirmation')"
  457. :description-text="t('common.confirmExitPrompt')"
  458. :icon="{ name: 'a-log-out' }"
  459. :icon-bg-color="'#F56C6C'"
  460. @confirm="confirm"
  461. />
  462. </template>
  463. <style lang="scss">
  464. .aside-menu-submenu-popup.ant-menu-submenu-popup {
  465. .ant-menu-item:not(.ant-menu-item-selected):hover {
  466. color: var(--antd-color-primary);
  467. background-color: initial;
  468. }
  469. .ant-menu-item-selected {
  470. background-color: initial;
  471. }
  472. }
  473. </style>
  474. <style lang="scss" scoped>
  475. .form-style {
  476. margin-top: 24px;
  477. }
  478. .button-style {
  479. width: 76px;
  480. margin-left: 16px;
  481. }
  482. .input-width {
  483. width: 256px;
  484. }
  485. .hvac-item {
  486. width: 190px;
  487. height: 50px;
  488. }
  489. .icon-style {
  490. margin-right: 12px;
  491. }
  492. .hvac-dropdown > ul > li {
  493. width: 190px;
  494. margin-left: 10px !important;
  495. }
  496. .hvac-dropdown {
  497. bottom: 20px;
  498. }
  499. .text-style {
  500. font-size: 14px;
  501. font-style: normal;
  502. font-weight: 400;
  503. line-height: 22px;
  504. color: var(--antd-color-primary);
  505. text-align: left;
  506. text-decoration-line: underline;
  507. cursor: pointer;
  508. }
  509. :deep(.aside-collapsed-icons) {
  510. .ant-badge .ant-badge-count {
  511. min-width: 15px;
  512. height: 15px;
  513. margin-top: 10px;
  514. margin-right: 14px;
  515. font-size: 10px;
  516. line-height: 15px;
  517. cursor: pointer;
  518. }
  519. }
  520. :deep(.aside-footer) {
  521. .ant-badge .ant-badge-count {
  522. min-width: 15px;
  523. height: 15px;
  524. font-size: 10px;
  525. line-height: 15px;
  526. cursor: pointer;
  527. }
  528. .ant-badge {
  529. margin: 0 15px;
  530. }
  531. }
  532. .table-margin {
  533. margin: 24px 0;
  534. }
  535. .aside-container {
  536. --aside-border-radius: 18px;
  537. --aside-padding: 12px;
  538. background-color: var(--hvac-layout-bg);
  539. :deep(.ant-layout-sider-children) {
  540. display: flex;
  541. flex-direction: column;
  542. }
  543. :deep(.aside-menu) {
  544. flex: 1;
  545. border-inline-end: none;
  546. & > .ant-menu-item,
  547. .ant-menu-submenu-title {
  548. padding-left: 12px !important;
  549. }
  550. .ant-menu-item,
  551. .ant-menu-submenu-title {
  552. width: calc(100%);
  553. height: 40px;
  554. padding-inline: 12px;
  555. margin-block: 0;
  556. margin-inline: 0;
  557. overflow: hidden;
  558. line-height: 40px;
  559. text-overflow: ellipsis;
  560. .ant-menu-item-icon {
  561. font-size: 16px;
  562. + span {
  563. margin-inline-start: 8px;
  564. }
  565. }
  566. }
  567. & > .ant-menu-submenu,
  568. & > .ant-menu-item {
  569. & + .ant-menu-submenu,
  570. & + .ant-menu-item {
  571. margin-top: 2px;
  572. }
  573. }
  574. .ant-menu-submenu-title + .ant-menu-sub > li:first-child {
  575. margin-top: 8px;
  576. }
  577. .ant-menu-submenu-arrow {
  578. right: var(--aside-padding);
  579. &::before,
  580. &::after {
  581. height: 1px;
  582. }
  583. }
  584. .ant-menu-sub.ant-menu-inline {
  585. background: var(--antd-color-bg-base);
  586. }
  587. .ant-menu-sub .ant-menu-item {
  588. padding-left: 34px !important;
  589. &::before {
  590. position: absolute;
  591. left: 16px;
  592. height: 100%;
  593. content: '';
  594. border: 1px solid var(--antd-color-primary-opacity-15);
  595. }
  596. &.ant-menu-item-selected::before {
  597. border-color: var(--antd-color-primary);
  598. }
  599. }
  600. .ant-menu-item-selected {
  601. background-color: var(--antd-color-bg-base);
  602. }
  603. .ant-menu-submenu-selected > .ant-menu-submenu-title {
  604. background-color: var(--antd-color-primary-opacity-15);
  605. }
  606. .ant-menu-item:not(.ant-menu-item-selected):hover,
  607. .ant-menu-submenu-title:hover {
  608. color: var(--antd-color-primary);
  609. background-color: initial;
  610. }
  611. & > .ant-menu-item.ant-menu-item-selected {
  612. background-color: var(--antd-color-primary-opacity-15);
  613. }
  614. .ant-menu-title-content {
  615. display: flex;
  616. align-items: center;
  617. font-weight: 500;
  618. }
  619. }
  620. }
  621. .aside-header {
  622. display: flex;
  623. align-items: center;
  624. padding: 24px var(--aside-padding);
  625. background-color: var(--antd-color-bg-base);
  626. border-top-left-radius: var(--aside-border-radius);
  627. border-top-right-radius: var(--aside-border-radius);
  628. }
  629. .aside-header-logo {
  630. width: 40px;
  631. height: 40px;
  632. }
  633. .aside-header-title {
  634. margin-left: 12px;
  635. overflow: hidden;
  636. font-size: 16px;
  637. font-style: normal;
  638. font-weight: 600;
  639. line-height: 24px;
  640. color: var(--antd-color-text);
  641. text-overflow: clip;
  642. white-space: nowrap;
  643. }
  644. .aside-scroll {
  645. height: calc(100% - 155px);
  646. background-color: #fff;
  647. }
  648. .aside-menu {
  649. padding: 0 var(--aside-padding);
  650. color: var(--antd-color-text);
  651. }
  652. .aside-menu-category {
  653. width: 56px;
  654. height: 24px;
  655. margin-top: 24px;
  656. margin-bottom: 8px;
  657. font-size: 12px;
  658. font-weight: 500;
  659. line-height: 24px;
  660. color: var(--antd-color-text-secondary);
  661. text-align: center;
  662. background-color: #f5f7fa;
  663. border-radius: 4px;
  664. &:first-child {
  665. margin-top: 0;
  666. }
  667. }
  668. .aside-footer {
  669. display: flex;
  670. align-items: center;
  671. justify-content: space-between;
  672. height: 65px;
  673. padding: var(--aside-padding);
  674. margin-top: 2px;
  675. background-color: var(--antd-color-bg-base);
  676. border-bottom-right-radius: var(--aside-border-radius);
  677. border-bottom-left-radius: var(--aside-border-radius);
  678. i {
  679. font-size: 16px;
  680. cursor: pointer;
  681. + i {
  682. margin-left: 12px;
  683. }
  684. }
  685. }
  686. .aside-footer-avatar {
  687. display: flex;
  688. align-items: center;
  689. justify-content: center;
  690. width: 40px;
  691. height: 40px;
  692. cursor: pointer;
  693. background: var(--antd-color-primary);
  694. border-radius: 50%;
  695. // 临时使用
  696. &::before {
  697. font-size: 14px;
  698. font-weight: 500;
  699. line-height: 22px;
  700. color: #fff;
  701. content: '贾';
  702. }
  703. }
  704. .aside-container.ant-layout-sider-collapsed {
  705. .aside-menu-category {
  706. position: relative;
  707. left: -8px;
  708. }
  709. .aside-scroll {
  710. height: calc(100% - 304px);
  711. }
  712. :deep(.aside-menu) {
  713. .ant-menu-submenu,
  714. .ant-menu-submenu-title {
  715. border-radius: 4px;
  716. }
  717. .ant-menu-item,
  718. .ant-menu-submenu-title {
  719. text-overflow: initial;
  720. }
  721. .ant-menu-submenu-title {
  722. &::before {
  723. position: absolute;
  724. right: 6px;
  725. bottom: 6px;
  726. display: block;
  727. width: 4px;
  728. height: 4px;
  729. content: '';
  730. border: 1px solid var(--antd-color-text);
  731. border-top-width: 0;
  732. border-left-width: 0;
  733. }
  734. &:hover::before {
  735. border-color: var(--antd-color-primary);
  736. }
  737. }
  738. .ant-menu-submenu-selected {
  739. .ant-menu-submenu-title {
  740. &::before {
  741. border-color: var(--antd-color-primary);
  742. }
  743. }
  744. }
  745. }
  746. }
  747. .aside-collapsed-icons {
  748. display: flex;
  749. flex-direction: column;
  750. align-items: center;
  751. padding-top: 25px;
  752. background-color: #fff;
  753. i {
  754. width: 40px;
  755. height: 40px;
  756. font-size: 16px;
  757. line-height: 40px;
  758. text-align: center;
  759. cursor: pointer;
  760. transition: color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
  761. + i {
  762. margin-bottom: 2px;
  763. }
  764. &:hover {
  765. color: var(--antd-color-primary);
  766. }
  767. }
  768. }
  769. </style>