| | |
| | | <template>
|
| | | <router-view />
|
| | | </template>
|
| | |
|
| | | <script setup>
|
| | | import useSettingsStore from '@/store/modules/settings'
|
| | | import { handleThemeStyle } from '@/utils/theme'
|
| | |
|
| | | onMounted(() => {
|
| | | nextTick(() => {
|
| | | // 初始化主题样式
|
| | | handleThemeStyle(useSettingsStore().theme)
|
| | | })
|
| | | })
|
| | | </script>
|
| | | <template> |
| | | <router-view /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import useSettingsStore from '@/store/modules/settings' |
| | | import { handleThemeStyle } from '@/utils/theme' |
| | | |
| | | onMounted(() => { |
| | | nextTick(() => { |
| | | // 初始化主题样式 |
| | | handleThemeStyle(useSettingsStore().theme) |
| | | }) |
| | | }) |
| | | </script> |
| | |
| | | #app {
|
| | | .main-container {
|
| | | min-height: 100%;
|
| | | transition: margin-left 0.28s;
|
| | | margin-left: $base-sidebar-width;
|
| | | position: relative;
|
| | | background: transparent;
|
| | | }
|
| | |
|
| | | .sidebarHide {
|
| | | margin-left: 0 !important;
|
| | | }
|
| | |
|
| | | .sidebar-container {
|
| | | transition: width 0.28s;
|
| | | width: $base-sidebar-width !important;
|
| | | height: 100%;
|
| | | position: fixed;
|
| | | font-size: 0px;
|
| | | top: 0;
|
| | | bottom: 0;
|
| | | left: 0;
|
| | | z-index: 1001;
|
| | | overflow: hidden;
|
| | | padding: 12px 0 16px 16px;
|
| | | background: transparent;
|
| | | box-shadow: none;
|
| | |
|
| | | // reset element-ui css
|
| | | .horizontal-collapse-transition {
|
| | | transition: 0s width ease-in-out, 0s padding-left ease-in-out,
|
| | | 0s padding-right ease-in-out;
|
| | | }
|
| | |
|
| | | .scrollbar-wrapper {
|
| | | overflow-x: hidden !important;
|
| | | }
|
| | |
|
| | | .el-scrollbar__bar.is-vertical {
|
| | | right: 0px;
|
| | | }
|
| | |
|
| | | .el-scrollbar {
|
| | | height: 100%;
|
| | | }
|
| | |
|
| | | &.has-logo {
|
| | | .el-scrollbar {
|
| | | height: calc(100% - 72px);
|
| | | margin-top: 10px;
|
| | | }
|
| | | }
|
| | |
|
| | | .is-horizontal {
|
| | | display: none;
|
| | | }
|
| | |
|
| | | a {
|
| | | display: inline-block;
|
| | | width: 100%;
|
| | | overflow: hidden;
|
| | | }
|
| | |
|
| | | .svg-icon {
|
| | | margin-right: 16px;
|
| | | }
|
| | |
|
| | | #app { |
| | | .main-container { |
| | | min-height: 100%; |
| | | transition: margin-left 0.28s; |
| | | margin-left: $base-sidebar-width; |
| | | position: relative; |
| | | background: transparent; |
| | | } |
| | | |
| | | .sidebarHide { |
| | | margin-left: 0 !important; |
| | | } |
| | | |
| | | .sidebar-container { |
| | | transition: width 0.28s; |
| | | width: $base-sidebar-width !important; |
| | | height: 100%; |
| | | position: fixed; |
| | | font-size: 0px; |
| | | top: 0; |
| | | bottom: 0; |
| | | left: 0; |
| | | z-index: 1001; |
| | | overflow: hidden; |
| | | padding: 12px 0 16px 16px; |
| | | background: transparent; |
| | | box-shadow: none; |
| | | |
| | | // reset element-ui css |
| | | .horizontal-collapse-transition { |
| | | transition: 0s width ease-in-out, 0s padding-left ease-in-out, |
| | | 0s padding-right ease-in-out; |
| | | } |
| | | |
| | | .scrollbar-wrapper { |
| | | overflow-x: hidden !important; |
| | | } |
| | | |
| | | .el-scrollbar__bar.is-vertical { |
| | | right: 0px; |
| | | } |
| | | |
| | | .el-scrollbar { |
| | | height: 100%; |
| | | } |
| | | |
| | | &.has-logo { |
| | | .el-scrollbar { |
| | | height: calc(100% - 72px); |
| | | margin-top: 10px; |
| | | } |
| | | } |
| | | |
| | | .is-horizontal { |
| | | display: none; |
| | | } |
| | | |
| | | a { |
| | | display: inline-block; |
| | | width: 100%; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .svg-icon { |
| | | margin-right: 16px; |
| | | } |
| | | |
| | | .el-menu { |
| | | border: none; |
| | | height: 100%; |
| | | width: 100% !important; |
| | | padding: 10px 8px 18px; |
| | | border-radius: 22px; |
| | | background: var(--menu-surface); |
| | | background-color: var(--el-menu-bg-color, var(--sidebar-bg)) !important; |
| | | backdrop-filter: blur(18px); |
| | | box-shadow: var(--shadow-sm); |
| | | } |
| | |
|
| | | .el-menu-item,
|
| | | .menu-title {
|
| | | overflow: hidden !important;
|
| | | text-overflow: ellipsis !important;
|
| | | white-space: nowrap !important;
|
| | | }
|
| | |
|
| | | .el-menu-item .el-menu-tooltip__trigger {
|
| | | display: inline-block !important;
|
| | | }
|
| | |
|
| | | |
| | | .el-menu-item, |
| | | .menu-title { |
| | | overflow: hidden !important; |
| | | text-overflow: ellipsis !important; |
| | | white-space: nowrap !important; |
| | | color: var(--sidebar-text) !important; |
| | | } |
| | | |
| | | .el-menu-item .el-menu-tooltip__trigger { |
| | | display: inline-block !important; |
| | | } |
| | | |
| | | // menu hover |
| | | .submenu-title-noDropdown, |
| | | .el-sub-menu__title { |
| | | color: var(--sidebar-text) !important; |
| | | &:hover { |
| | | background-color: var(--menu-hover) !important; |
| | | border-radius: 14px; |
| | | } |
| | | } |
| | | & .theme-light .is-active > .el-sub-menu__title {
|
| | | color: var(--current-color) !important;
|
| | | }
|
| | |
|
| | | & .is-active > .el-sub-menu__title { |
| | | color: var(--sidebar-text) !important; |
| | | } |
| | | |
| | | & .nest-menu .el-sub-menu > .el-sub-menu__title, |
| | | & .el-sub-menu .el-menu-item { |
| | | min-width: 0 !important; |
| | |
| | | border-radius: 14px; |
| | | } |
| | | } |
| | |
|
| | | & .theme-light .nest-menu .el-sub-menu > .el-sub-menu__title, |
| | | & .theme-light .el-sub-menu .el-menu-item { |
| | | //background-color: transparent; |
| | | |
| | | & .nest-menu .el-sub-menu > .el-sub-menu__title, |
| | | & .el-sub-menu .el-menu-item { |
| | | &:hover { |
| | | background-color: var(--menu-hover) !important; |
| | | border-radius: 14px; |
| | | } |
| | | } |
| | | } |
| | |
|
| | | |
| | | .hideSidebar { |
| | | .sidebar-container { |
| | | width: 68px !important; |
| | |
| | | .main-container { |
| | | margin-left: 84px; |
| | | } |
| | |
|
| | | |
| | | .submenu-title-noDropdown { |
| | | padding: 0 !important; |
| | | position: relative; |
| | |
| | | width: 0; |
| | | overflow: hidden; |
| | | visibility: hidden; |
| | | display: inline-block;
|
| | | }
|
| | | & > i {
|
| | | height: 0;
|
| | | width: 0;
|
| | | overflow: hidden;
|
| | | visibility: hidden;
|
| | | display: inline-block;
|
| | | display: inline-block; |
| | | } |
| | | & > i { |
| | | height: 0; |
| | | width: 0; |
| | | overflow: hidden; |
| | | visibility: hidden; |
| | | display: inline-block; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
|
| | | .el-menu--collapse .el-menu .el-sub-menu {
|
| | | min-width: $base-sidebar-width !important;
|
| | | }
|
| | |
|
| | | // mobile responsive
|
| | | .mobile {
|
| | | .main-container {
|
| | | margin-left: 0px;
|
| | | }
|
| | |
|
| | | .sidebar-container {
|
| | | transition: transform 0.28s;
|
| | | width: $base-sidebar-width !important;
|
| | | }
|
| | |
|
| | | &.hideSidebar {
|
| | | .sidebar-container {
|
| | | pointer-events: none;
|
| | | transition-duration: 0.3s;
|
| | | transform: translate3d(-$base-sidebar-width, 0, 0);
|
| | | }
|
| | | }
|
| | | }
|
| | |
|
| | | .withoutAnimation {
|
| | | .main-container,
|
| | | .sidebar-container {
|
| | | transition: none;
|
| | | }
|
| | | }
|
| | | }
|
| | |
|
| | | // when menu collapsed
|
| | | .el-menu--vertical {
|
| | | & > .el-menu {
|
| | | .svg-icon {
|
| | | margin-right: 16px;
|
| | | }
|
| | | }
|
| | |
|
| | | |
| | | .el-menu--collapse .el-menu .el-sub-menu { |
| | | min-width: $base-sidebar-width !important; |
| | | } |
| | | |
| | | // mobile responsive |
| | | .mobile { |
| | | .main-container { |
| | | margin-left: 0px; |
| | | } |
| | | |
| | | .sidebar-container { |
| | | transition: transform 0.28s; |
| | | width: $base-sidebar-width !important; |
| | | } |
| | | |
| | | &.hideSidebar { |
| | | .sidebar-container { |
| | | pointer-events: none; |
| | | transition-duration: 0.3s; |
| | | transform: translate3d(-$base-sidebar-width, 0, 0); |
| | | } |
| | | } |
| | | } |
| | | |
| | | .withoutAnimation { |
| | | .main-container, |
| | | .sidebar-container { |
| | | transition: none; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // when menu collapsed |
| | | .el-menu--vertical { |
| | | & > .el-menu { |
| | | .svg-icon { |
| | | margin-right: 16px; |
| | | } |
| | | } |
| | | |
| | | .nest-menu .el-sub-menu > .el-sub-menu__title, |
| | | .el-menu-item { |
| | | min-width: 0 !important; |
| | |
| | | border-radius: 14px; |
| | | } |
| | | } |
| | |
|
| | | // the scroll bar appears when the sub-menu is too long
|
| | | > .el-menu--popup {
|
| | | max-height: 100vh;
|
| | | overflow-y: auto;
|
| | | padding: 8px;
|
| | | border-radius: 18px;
|
| | | border: 1px solid var(--surface-border);
|
| | | box-shadow: var(--shadow-md);
|
| | |
|
| | | &::-webkit-scrollbar-track-piece {
|
| | | background: #dfe7e1;
|
| | | }
|
| | |
|
| | | &::-webkit-scrollbar {
|
| | | width: 6px;
|
| | | }
|
| | |
|
| | | &::-webkit-scrollbar-thumb {
|
| | | background: #9aa79e;
|
| | | border-radius: 20px;
|
| | | }
|
| | | }
|
| | | }
|
| | | |
| | | // the scroll bar appears when the sub-menu is too long |
| | | > .el-menu--popup { |
| | | max-height: 100vh; |
| | | overflow-y: auto; |
| | | padding: 8px; |
| | | border-radius: 18px; |
| | | border: 1px solid var(--surface-border); |
| | | box-shadow: var(--shadow-md); |
| | | |
| | | &::-webkit-scrollbar-track-piece { |
| | | background: #dfe7e1; |
| | | } |
| | | |
| | | &::-webkit-scrollbar { |
| | | width: 6px; |
| | | } |
| | | |
| | | &::-webkit-scrollbar-thumb { |
| | | background: #9aa79e; |
| | | border-radius: 20px; |
| | | } |
| | | } |
| | | } |
| | |
| | | } |
| | | |
| | | :root { |
| | | --el-menu-bg-color: var(--sidebar-bg, #{$menuBg}); |
| | | --sidebar-bg: #{$menuBg}; |
| | | --sidebar-text: #{$menuText}; |
| | | --sidebar-muted: #93a0b1; |
| | |
| | | --el-text-color-regular: #d0d0d0; |
| | | --el-border-color: #434343; |
| | | --el-border-color-light: #434343; |
| | | |
| | | --sidebar-bg: #141414; |
| | | --sidebar-text: #ffffff; |
| | | --menu-hover: #2d2d2d; |
| | | --menu-active-text: #{$menuActiveText}; |
| | | /* 菜单栏背景色由 JS 动态设置,跟随主题色,不在这里定义 */ |
| | | |
| | | --navbar-bg: #141414; |
| | | --navbar-text: #ffffff; |
| | |
| | | .sidebar-container { |
| | | .el-menu-item, |
| | | .menu-title { |
| | | color: var(--el-text-color-regular); |
| | | color: var(--sidebar-text, #{$menuText}); |
| | | } |
| | | |
| | | & .theme-dark .nest-menu .el-sub-menu > .el-sub-menu__title, |
| | | & .theme-dark .el-sub-menu .el-menu-item { |
| | | background-color: var(--el-bg-color) !important; |
| | | & .nest-menu .el-sub-menu > .el-sub-menu__title, |
| | | & .el-sub-menu .el-menu-item { |
| | | background-color: var(--sidebar-bg, #{$menuBg}) !important; |
| | | } |
| | | } |
| | | |
| | |
| | | const permissionStore = usePermissionStore();
|
| | | const showSettings = ref(false);
|
| | | const theme = ref(settingsStore.theme);
|
| | | const sideTheme = ref(settingsStore.sideTheme);
|
| | | const storeSettings = computed(() => settingsStore);
|
| | | const predefineColors = ref([
|
| | | "#002fa7",
|
| | |
| | | settingsStore.setDarkMode(val);
|
| | | }
|
| | |
|
| | | function handleTheme(val) {
|
| | | settingsStore.sideTheme = val;
|
| | | sideTheme.value = val;
|
| | | }
|
| | |
|
| | |
|
| | | function saveSetting() {
|
| | | proxy.$modal.loading("正在保存到本地,请稍候...");
|
| | |
| | | fixedHeader: storeSettings.value.fixedHeader,
|
| | | sidebarLogo: storeSettings.value.sidebarLogo,
|
| | | dynamicTitle: storeSettings.value.dynamicTitle,
|
| | | sideTheme: storeSettings.value.sideTheme,
|
| | | theme: storeSettings.value.theme,
|
| | | darkMode: storeSettings.value.darkMode,
|
| | | };
|
| | |
| | | <template> |
| | | <div :class="{ 'has-logo': showLogo }" |
| | | class="sidebar-container"> |
| | | <logo v-if="showLogo" |
| | | :collapse="isCollapse" /> |
| | | <div class="sidebar-container"> |
| | | <el-scrollbar wrap-class="scrollbar-wrapper"> |
| | | <el-menu :default-active="activeMenu" |
| | | :collapse="isCollapse" |
| | | :background-color="getMenuBackground" |
| | | :text-color="getMenuTextColor" |
| | | :unique-opened="true" |
| | | :active-text-color="theme" |
| | | :collapse-transition="false" |
| | | mode="vertical" |
| | | :class="sideTheme"> |
| | | mode="vertical"> |
| | | <sidebar-item v-for="(route, index) in sidebarRouters" |
| | | :key="route.path + index" |
| | | :item="route" |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import Logo from "./Logo"; |
| | | import SidebarItem from "./SidebarItem"; |
| | | import variables from "@/assets/styles/variables.module.scss"; |
| | | import useAppStore from "@/store/modules/app"; |
| | | import useSettingsStore from "@/store/modules/settings"; |
| | | import usePermissionStore from "@/store/modules/permission"; |
| | |
| | | const permissionStore = usePermissionStore(); |
| | | |
| | | const sidebarRouters = computed(() => permissionStore.sidebarRouters); |
| | | const showLogo = computed(() => settingsStore.sidebarLogo); |
| | | const sideTheme = computed(() => settingsStore.sideTheme); |
| | | const theme = computed(() => settingsStore.theme); |
| | | const isCollapse = computed(() => !appStore.sidebar.opened); |
| | | |
| | | const getMenuBackground = computed(() => "var(--sidebar-bg)"); |
| | | |
| | | const getMenuTextColor = computed(() => { |
| | | if (settingsStore.isDark) { |
| | | return "var(--sidebar-text)"; |
| | | } |
| | | return sideTheme.value === "theme-dark" |
| | | ? variables.menuText |
| | | : variables.menuLightText; |
| | | }); |
| | | |
| | | const activeMenu = computed(() => { |
| | | const { meta, path } = route; |
| | |
| | | |
| | | <style lang="scss" scoped> |
| | | .sidebar-container { |
| | | background-color: v-bind(getMenuBackground); |
| | | background-color: var(--sidebar-bg); |
| | | border-radius: 22px; |
| | | overflow: hidden; |
| | | transition: background-color 0.3s ease; |
| | | |
| | | .scrollbar-wrapper { |
| | | background-color: v-bind(getMenuBackground); |
| | | background-color: var(--sidebar-bg); |
| | | transition: background-color 0.3s ease; |
| | | } |
| | | |
| | | .el-menu { |
| | |
| | | height: 100%; |
| | | width: 100% !important; |
| | | border-radius: 22px; |
| | | background-color: var(--el-menu-bg-color, var(--sidebar-bg)) !important; |
| | | transition: background-color 0.3s ease; |
| | | |
| | | .el-menu-item, |
| | | .el-sub-menu__title { |
| | | margin-bottom: 6px; |
| | | border-radius: 14px; |
| | | color: v-bind(getMenuTextColor); |
| | | color: var(--sidebar-text); |
| | | transition: all 0.3s ease; |
| | | |
| | | &:hover { |
| | | background-color: var(--menu-hover, rgba(0, 0, 0, 0.06)) !important; |
| | |
| | | } |
| | | |
| | | .el-menu-item { |
| | | color: var(--sidebar-text); |
| | | &.is-active { |
| | | color: v-bind(theme); |
| | | color: var(--sidebar-text); |
| | | background-color: var(--menu-active-bg, rgba(0, 0, 0, 0.06)) !important; |
| | | font-weight: 600; |
| | | } |
| | | } |
| | | |
| | | .el-sub-menu__title { |
| | | color: v-bind(getMenuTextColor); |
| | | color: var(--sidebar-text); |
| | | } |
| | | |
| | | :deep(.el-sub-menu.is-active > .el-sub-menu__title) { |
| | | color: v-bind(theme) !important; |
| | | color: var(--sidebar-text) !important; |
| | | font-weight: 600; |
| | | background-color: var(--menu-active-bg, rgba(0, 0, 0, 0.06)) !important; |
| | | border-radius: 14px; |
| | | margin: 0 10px 6px !important; |
| | | // width: calc(100% - 20px) !important; |
| | | padding-left: 10px !important; |
| | | padding-right: 10px !important; |
| | | box-sizing: border-box; |
| | |
| | | :deep(.el-sub-menu.is-active > .el-sub-menu__title .svg-icon), |
| | | :deep(.el-menu-item.is-active .menu-title), |
| | | :deep(.el-menu-item.is-active .svg-icon) { |
| | | color: v-bind(theme) !important; |
| | | color: var(--sidebar-text) !important; |
| | | } |
| | | |
| | | :deep(.el-sub-menu__title:hover), |
| | |
| | | const userStore = useUserStore(); |
| | | const route = useRoute(); |
| | | const theme = computed(() => settingsStore.theme); |
| | | const sideTheme = computed(() => settingsStore.sideTheme); |
| | | const sidebar = computed(() => useAppStore().sidebar); |
| | | const device = computed(() => useAppStore().device); |
| | | const needTagsView = computed(() => settingsStore.tagsView); |
| | |
| | | */
|
| | | title: import.meta.env.VITE_APP_TITLE,
|
| | | /**
|
| | | * 侧边栏主题 深色主题theme-dark,浅色主题theme-light
|
| | | */
|
| | | sideTheme: 'theme-light',
|
| | | /**
|
| | | * 是否系统布局配置
|
| | | */
|
| | | showSettings: true,
|
| | |
| | | |
| | | const preferredDark = usePreferredDark(); |
| | | const colorMode = useColorMode({ |
| | | emitAuto: true, |
| | | attribute: 'class', |
| | | selector: 'html', |
| | | modes: { |
| | | dark: 'dark', |
| | | light: '', |
| | | } |
| | | }); |
| | | |
| | | const { |
| | | sideTheme, |
| | | showSettings, |
| | | topNav, |
| | | tagsView, |
| | |
| | | const storageSetting = JSON.parse(localStorage.getItem("layout-setting") || "{}"); |
| | | const defaultDarkMode = darkMode || "auto"; |
| | | const initialDarkMode = storageSetting.darkMode || defaultDarkMode; |
| | | |
| | | // 设置初始值 |
| | | colorMode.value = initialDarkMode; |
| | | |
| | | const getIsDark = (mode) => mode === "dark" || (mode === "auto" && preferredDark.value); |
| | | |
| | | const useSettingsStore = defineStore("settings", () => { |
| | | const title = ref(""); |
| | | const theme = ref(storageSetting.theme || "#002fa7"); |
| | | const sideThemeValue = ref(storageSetting.sideTheme || sideTheme); |
| | | const showSettingsValue = ref(showSettings); |
| | | const topNavValue = ref( |
| | | storageSetting.topNav === undefined ? topNav : storageSetting.topNav |
| | |
| | | const darkModeValue = ref(initialDarkMode); |
| | | const isDark = computed(() => getIsDark(darkModeValue.value)); |
| | | |
| | | // 监听系统主题变化 |
| | | watch(preferredDark, (newVal) => { |
| | | if (darkModeValue.value === 'auto') { |
| | | colorMode.value = 'auto'; |
| | | } |
| | | }); |
| | | |
| | | function changeSetting(data) { |
| | | const { key, value } = data; |
| | | const settingMap = { |
| | | title, |
| | | theme, |
| | | sideTheme: sideThemeValue, |
| | | showSettings: showSettingsValue, |
| | | topNav: topNavValue, |
| | | tagsView: tagsViewValue, |
| | |
| | | return { |
| | | title, |
| | | theme, |
| | | sideTheme: sideThemeValue, |
| | | showSettings: showSettingsValue, |
| | | topNav: topNavValue, |
| | | tagsView: tagsViewValue, |
| | |
| | | } else {
|
| | | this.roles = ['ROLE_DEFAULT']
|
| | | }
|
| | | this.id = user.userId
|
| | | this.name = user.userName
|
| | | this.avatar = avatar
|
| | | this.id = user.userId |
| | | this.name = user.userName |
| | | this.avatar = avatar |
| | | this.currentFactoryName = user.currentFactoryName |
| | | this.nickName = user.nickName |
| | | this.roleName = user.roles[0].roleName |
| | | this.currentDeptId = user.tenantId |
| | | this.currentLoginTime = this.getCurrentTime() |
| | | this.aiEnabled = Number(res.aiEnabled) === 1 ? 1 : 0 |
| | | this.phonenumber = user.phonenumber |
| | | this.remark = user.remark |
| | | resolve(res) |
| | | }).catch(error => { |
| | | reject(error) |
| | |
| | | for (let i = 1; i <= 9; i++) { |
| | | document.documentElement.style.setProperty(`--el-color-primary-dark-${i}`, `${getDarkColor(theme, i / 10)}`) |
| | | } |
| | | // 设置菜单栏背景色为主题色的浅色版本(50%浅度),文字用白色 |
| | | document.documentElement.style.setProperty('--sidebar-bg', getLightColor(theme, 0.5)) |
| | | document.documentElement.style.setProperty('--sidebar-text', '#ffffff') |
| | | document.documentElement.style.setProperty('--menu-hover', getLightColor(theme, 0.4)) |
| | | document.documentElement.style.setProperty('--menu-active-bg', getLightColor(theme, 0.35)) |
| | | } |
| | | |
| | | // hex颜色转rgb颜色 |
| | |
| | | <div class="user-name">{{ userStore.name }}</div> |
| | | <div class="user-role">{{ userStore.roleName }}</div> |
| | | <div class="user-meta"> |
| | | <span>{{ userStore.phoneNumber || '123456789' }}</span> |
| | | <span>{{ userStore.phonenumber || '123456789' }}</span> |
| | | <span class="sep">|</span> |
| | | <span>{{ userStore.deptName || '组织架构' }}</span> |
| | | <span>{{ userStore.currentFactoryName || '组织架构' }}</span> |
| | | <span class="sep">|</span> |
| | | <span>{{ userStore.postName || '岗位名' }}</span> |
| | | <span>{{ userStore.remark || '岗位名' }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <!-- |
| | | <div class="data-cards"> |
| | | <div class="data-card sales"> |
| | | <div class="data-title">销售数据</div> |
| | |
| | | </div> |
| | | </div> |
| | | </div> |
| | | --> |
| | | <!-- 右:待办事项 --> |
| | | <!-- |
| | | <div class="todo-panel"> |
| | | <div class="section-title">待办事项</div> |
| | | <ul class="todo-list" v-if="todoList.length > 0"> |
| | |
| | | 暂无数据 |
| | | </div> |
| | | </div> |
| | | --> |
| | | </div> |
| | | <!-- |
| | | <div class="dashboard-row"> |
| | | <div class="main-panel process-panel"> |
| | | <div class="process-panel__header"> |
| | |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 工序选择弹窗 --> |
| | | <el-dialog v-model="processDialogVisible" title="选择工序" width="500px" append-to-body> |
| | | <div class="process-selection-wrapper"> |
| | | <el-checkbox-group v-model="tempProcessIds"> |
| | |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- 中部横向两栏 --> |
| | | --> |
| | | <!-- 中部:客户合同金额分析 --> |
| | | <div class="dashboard-row"> |
| | | <div class="main-panel"> |
| | | <div class="main-panel contract-panel"> |
| | | <div class="section-title">客户合同金额分析</div> |
| | | <div class="contract-summary"> |
| | | <div class="contract-info"> |
| | |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div |
| | | style="display: flex;align-items: center;gap: 20px;justify-content: space-evenly;height: 180px;margin-top: 20px"> |
| | | <div> |
| | | <Echarts ref="chart" :legend="pieLegend" :chartStyle="chartStylePie" :series="materialPieSeries" |
| | | <div class="contract-chart-wrapper"> |
| | | <div class="chart-container"> |
| | | <Echarts ref="chart" :legend="pieLegend" :chartStyle="{ width: '100%', height: '280px' }" :series="materialPieSeries" |
| | | :tooltip="pieTooltip"></Echarts> |
| | | </div> |
| | | <ul class="contract-list"> |
| | | <li v-for="item in materialPieSeries[0].data" :key="item.name"> |
| | | <div style="display: flex;align-items: center;justify-content: space-between;width: 100%"> |
| | | <div class="line" :style="{ color: item.itemStyle.color }">●{{ item.name }}</div> |
| | | <div style="width: 70px">{{ item.rate }}%</div> |
| | | <div>¥{{ item.value }}</div> |
| | | <div class="contract-item"> |
| | | <span class="contract-dot" :style="{ background: item.itemStyle?.color || '#999' }"></span> |
| | | <span class="contract-name">{{ item.name }}</span> |
| | | <span class="contract-rate">{{ item.rate }}%</span> |
| | | <span class="contract-value">¥{{ item.value }}</span> |
| | | </div> |
| | | </li> |
| | | </ul> |
| | | </div> |
| | | </div> |
| | | <div class="main-panel"> |
| | | <div style="display: flex;justify-content: space-between;"> |
| | | <div class="section-title">应收应付统计</div> |
| | | <!-- <el-radio-group v-model="radio1" size="large" @change="statisticsReceivable">--> |
| | | <!-- <el-radio-button label="按周" :value="1" />--> |
| | | <!-- <el-radio-button label="按月" :value="2" />--> |
| | | <!-- <el-radio-button label="按季度" :value="3" />--> |
| | | <!-- </el-radio-group>--> |
| | | </div> |
| | | <Echarts ref="chart" :color="barColors2" :chartStyle="chartStyle" :grid="grid" :series="barSeries" |
| | | :tooltip="tooltip" :xAxis="xAxis" :yAxis="yAxis" style="height: 260px"></Echarts> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 底部横向两栏 --> |
| | | <!-- |
| | | <div class="dashboard-row"> |
| | | <div class="main-panel"> |
| | | <div style="display: flex;justify-content: space-between;align-items: center;margin-bottom: 10px;"> |
| | |
| | | :tooltip="tooltipLine" :xAxis="xAxis2" :yAxis="yAxis2" style="height: 270px;" /> |
| | | </div> |
| | | </div> |
| | | --> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | ]) |
| | | const chartStyle = { |
| | | width: '100%', |
| | | height: '100%' // 设置图表容器的高度 |
| | | height: '100%' |
| | | } |
| | | const chartStylePie = { |
| | | width: '140%', |
| | | height: '140%' // 设置图表容器的高度 |
| | | height: '140%' |
| | | } |
| | | const grid = { |
| | | left: '3%', |
| | |
| | | const pieTooltip = reactive({ |
| | | trigger: 'item', |
| | | formatter: function (params) { |
| | | // 动态生成提示信息,基于数据项的 name 属性 |
| | | const description = params.name === '本月回款金额' ? '本月回款金额' : '应收款金额'; |
| | | return `${description} ${formatNumber(params.value)}元 ${params.percent}%`; |
| | | }, |
| | |
| | | label: { |
| | | show: true |
| | | }, |
| | | showSymbol: true, // 显示圆点 |
| | | showSymbol: true, |
| | | }, |
| | | ]) |
| | | const tooltipLine = { |
| | |
| | | } |
| | | ]) |
| | | |
| | | // 待办事项 |
| | | const todoList = ref([]) |
| | | const radio1 = ref(1) |
| | | const qualityRange = ref(1) |
| | | |
| | | // 图表引用 |
| | | const barChart = ref(null) |
| | | const lineChart = ref(null) |
| | | const barColors2 = ['#5181DB', '#D369E0', '#F2CA6D', '#60CCA8'] |
| | | |
| | | // 随机颜色生成函数 |
| | | const getRandomColor = () => { |
| | | return '#' + Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, '0'); |
| | | } |
| | |
| | | getAmountHalfYearNum() |
| | | getProcessList() |
| | | }) |
| | | // 数据统计 |
| | | |
| | | const getBusinessData = () => { |
| | | getBusiness().then((res) => { |
| | | businessInfo.value = { ...res.data } |
| | | }) |
| | | } |
| | | // 合同金额 |
| | | |
| | | const analysisCustomer = () => { |
| | | analysisCustomerContractAmounts().then((res) => { |
| | | sum.value = res.data.sum |
| | | yny.value = res.data.yny |
| | | chain.value = res.data.chain |
| | | // 为每个数据项分配随机颜色 |
| | | materialPieSeries.value[0].data = res.data.item.map(item => ({ |
| | | ...item, |
| | | itemStyle: { color: getRandomColor() } |
| | | })) |
| | | }) |
| | | } |
| | | // 待办事项 |
| | | |
| | | const todoInfoS = () => { |
| | | homeTodos().then((res) => { |
| | | todoList.value = res.data |
| | | }) |
| | | } |
| | | // 获取工序列表 |
| | | |
| | | const getProcessList = () => { |
| | | list().then(res => { |
| | | processOptions.value = res.data.records |
| | |
| | | activeProcessIndex.value = params.dataIndex |
| | | } |
| | | } |
| | | // 应付应收统计 |
| | | |
| | | const statisticsReceivable = () => { |
| | | statisticsReceivablePayable({ type: radio1.value }).then((res) => { |
| | | barSeries.value[0].data = [ |
| | | // { value: res.data.prepayMoney, itemStyle: { color: barColors2[0] } }, |
| | | { value: res.data.payableMoney, itemStyle: { color: barColors2[0] } }, |
| | | // { value: res.data.advanceMoney, itemStyle: { color: barColors2[2] } }, |
| | | { value: res.data.receivableMoney, itemStyle: { color: barColors2[1] } } |
| | | ] |
| | | }) |
| | | } |
| | | // 质检统计 |
| | | |
| | | const qualityStatisticsInfo = () => { |
| | | qualityInspectionStatistics({ type: qualityRange.value }).then((res) => { |
| | | xAxis1.value[0].data = [] |
| | |
| | | qualityStatisticsObject.value.factoryNum = res.data.factoryNum |
| | | }) |
| | | } |
| | | |
| | | const getAmountHalfYearNum = async () => { |
| | | const res = await getAmountHalfYear() |
| | | console.log(res) |
| | | const monthName = [] |
| | | const receiptAmount = [] |
| | | const invoiceAmount = [] |
| | |
| | | receiptAmount.push(item.receiptAmount) |
| | | invoiceAmount.push(item.invoiceAmount) |
| | | }) |
| | | // 正确响应式赋值:创建新的 xAxis 和 series 对象 |
| | | xAxis2.value[0].data = monthName |
| | | xAxis2.value[0].data = monthName.map(item => item.replace(/~/g, '\n~')); |
| | | lineSeries.value = [ |
| | |
| | | ] |
| | | } |
| | | |
| | | // 工序数据生产统计明细(假数据 + 图表) |
| | | const processRange = ref(1) |
| | | const processChartData = ref([]) |
| | | |
| | |
| | | const processYAxis = ref([ |
| | | { |
| | | type: 'category', |
| | | axisTick: { show: false }, |
| | | axisLabel: { color: 'rgba(0,0,0,0.35)', fontSize: 12 }, |
| | | axisLine: { show: false }, |
| | | axisLabel: { color: 'rgba(0,0,0,0.45)' }, |
| | | axisTick: { show: false }, |
| | | data: [], |
| | | }, |
| | | ]) |
| | | |
| | | const processGrid = reactive({ left: 0, right: 100, top: 30, bottom: 20, containLabel: true }) |
| | | |
| | | const processTooltip = reactive({ |
| | | const processTooltip = ref({ |
| | | trigger: 'axis', |
| | | axisPointer: { type: 'shadow' }, |
| | | formatter: (params) => { |
| | | const name = params?.[0]?.name ?? '' |
| | | const list = Array.isArray(params) ? params : [] |
| | | const lines = list |
| | | .map((p) => { |
| | | const colorBox = `<span style="display:inline-block;margin-right:6px;border-radius:2px;width:10px;height:10px;background:${p.color}"></span>` |
| | | return `${colorBox}${p.seriesName} <b style="float:right;">${Number(p.value || 0).toFixed(2)}</b>` |
| | | }) |
| | | .join('<br/>') |
| | | return `<div style="min-width:140px;"><div style="font-weight:700;margin-bottom:6px;">${name}</div>${lines}</div>` |
| | | }, |
| | | }) |
| | | |
| | | const processSeries = computed(() => { |
| | | const input = processChartData.value.map((i) => i.input) |
| | | const scrap = processChartData.value.map((i) => i.scrap) |
| | | const output = processChartData.value.map((i) => i.output) |
| | | const processSeries = ref([ |
| | | { |
| | | name: '投入量', |
| | | type: 'bar', |
| | | stack: 'total', |
| | | barWidth: 16, |
| | | itemStyle: { color: '#2D99FF', borderRadius: [0, 4, 4, 0] }, |
| | | data: [], |
| | | }, |
| | | { |
| | | name: '报废量', |
| | | type: 'bar', |
| | | stack: 'total', |
| | | barWidth: 16, |
| | | itemStyle: { color: '#F2CA6D', borderRadius: [0, 4, 4, 0] }, |
| | | data: [], |
| | | }, |
| | | { |
| | | name: '产出量', |
| | | type: 'bar', |
| | | stack: 'total', |
| | | barWidth: 16, |
| | | itemStyle: { color: '#5EE9C0', borderRadius: [0, 4, 4, 0] }, |
| | | data: [], |
| | | }, |
| | | ]) |
| | | |
| | | return [ |
| | | { |
| | | name: '投入量', |
| | | type: 'bar', |
| | | stack: 'total', |
| | | barWidth: 22, |
| | | itemStyle: { color: '#1E5BFF', borderRadius: [6, 0, 0, 6] }, |
| | | data: input, |
| | | }, |
| | | { |
| | | name: '报废量', |
| | | type: 'bar', |
| | | stack: 'total', |
| | | barWidth: 22, |
| | | itemStyle: { color: '#F7B500' }, |
| | | data: scrap, |
| | | }, |
| | | { |
| | | name: '产出量', |
| | | type: 'bar', |
| | | stack: 'total', |
| | | barWidth: 22, |
| | | itemStyle: { color: '#19C6C6', borderRadius: [0, 6, 6, 0] }, |
| | | data: output, |
| | | }, |
| | | ] |
| | | const processGrid = ref({ |
| | | left: '3%', |
| | | right: '4%', |
| | | bottom: '3%', |
| | | top: '3%', |
| | | containLabel: true, |
| | | }) |
| | | |
| | | const processAside = computed(() => { |
| | | const list = processChartData.value |
| | | const item = list[activeProcessIndex.value] || {} |
| | | const idx = activeProcessIndex.value |
| | | const item = processChartData.value[idx] || {} |
| | | return { |
| | | processName: item.name || '暂无数据', |
| | | totalInput: item.input || 0, |
| | | totalScrap: item.scrap || 0, |
| | | totalOutput: item.output || 0, |
| | | processName: item.processName || '-', |
| | | totalInput: item.inputNum || 0, |
| | | totalScrap: item.scrapNum || 0, |
| | | totalOutput: item.outputNum || 0, |
| | | } |
| | | }) |
| | | |
| | | const formatAmount = (n) => { |
| | | const num = Number(n || 0) |
| | | return num.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) |
| | | const refreshProcessStats = async () => { |
| | | const params = { type: processRange.value } |
| | | if (selectedProcessIds.value.length > 0) { |
| | | params.processIds = selectedProcessIds.value.join(',') |
| | | } |
| | | const res = await processDataProductionStatistics(params) |
| | | const list = res.data || [] |
| | | processChartData.value = list |
| | | processYAxis.value[0].data = list.map(i => i.processName) |
| | | processSeries.value[0].data = list.map(i => i.inputNum) |
| | | processSeries.value[1].data = list.map(i => i.scrapNum) |
| | | processSeries.value[2].data = list.map(i => i.outputNum) |
| | | activeProcessIndex.value = 0 |
| | | } |
| | | |
| | | const refreshProcessStats = () => { |
| | | processDataProductionStatistics({ |
| | | type: processRange.value, |
| | | processIds: selectedProcessIds.value.length > 0 ? selectedProcessIds.value.join(',') : null |
| | | }).then(res => { |
| | | processChartData.value = res.data.map(item => ({ |
| | | name: item.processName, |
| | | input: item.totalInput, |
| | | scrap: item.totalScrap, |
| | | output: item.totalOutput |
| | | })) |
| | | processYAxis.value[0].data = processChartData.value.map((i) => i.name) |
| | | activeProcessIndex.value = 0 |
| | | }) |
| | | const formatAmount = (num) => { |
| | | if (!num && num !== 0) return '-' |
| | | return Number(num).toLocaleString() |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getBusinessData() |
| | | analysisCustomer() |
| | | todoInfoS() |
| | | statisticsReceivable() |
| | | qualityStatisticsInfo() |
| | | getAmountHalfYearNum() |
| | | refreshProcessStats() |
| | | }) |
| | | const formatNumber = (num) => { |
| | | if (!num) return '0' |
| | | return num.toLocaleString() |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | <style lang="scss" scoped> |
| | | .dashboard { |
| | | min-height: 100vh; |
| | | padding: 20px; |
| | | box-sizing: border-box; |
| | | background: #f5f7fa; |
| | | } |
| | | |
| | | .dashboard-top { |
| | | display: flex; |
| | | display: grid; |
| | | grid-template-columns: 2fr 1fr; |
| | | gap: 20px; |
| | | margin-bottom: 20px; |
| | | align-items: flex-start; |
| | | justify-content: space-evenly; |
| | | } |
| | | |
| | | .company-info { |
| | | padding: 0; |
| | | overflow: hidden; |
| | | border-radius: 12px; |
| | | background: #fff; |
| | | height: 100%; |
| | | } |
| | | |
| | | .welcome-banner { |
| | | padding: 10px 10px; |
| | | background: linear-gradient(135deg, rgba(229, 240, 255, 0.9), rgba(214, 232, 255, 0.7), rgba(207, 236, 255, 0.9)); |
| | | } |
| | | |
| | | .welcome-title { |
| | | font-size: 18px; |
| | | font-weight: 700; |
| | | color: #222; |
| | | line-height: 1.3; |
| | | } |
| | | |
| | | .welcome-user { |
| | | margin-right: 6px; |
| | | } |
| | | |
| | | .welcome-time { |
| | | margin-top: 10px; |
| | | font-size: 16px; |
| | | color: rgba(0, 0, 0, 0.55); |
| | | } |
| | | |
| | | .user-card { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | padding: 18px 22px; |
| | | } |
| | | |
| | | .user-card-main { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 5px; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .user-name { |
| | | font-size: 16px; |
| | | font-weight: bold; |
| | | color: #111; |
| | | letter-spacing: 1px; |
| | | } |
| | | |
| | | .user-role { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | height: 20px; |
| | | padding: 5px 10px; |
| | | background: rgba(245, 246, 248, 1); |
| | | color: #333; |
| | | width: fit-content; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .user-meta { |
| | | font-size: 12px; |
| | | color: rgba(0, 0, 0, 0.55); |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | } |
| | | |
| | | .user-meta .sep { |
| | | margin: 0 10px; |
| | | color: rgba(0, 0, 0, 0.25); |
| | | } |
| | | |
| | | .avatar { |
| | | width: 90px; |
| | | height: 90px; |
| | | border-radius: 50%; |
| | | object-fit: cover; |
| | | flex: 0 0 auto; |
| | | } |
| | | |
| | | .data-cards { |
| | | width: 50%; |
| | | display: flex; |
| | | gap: 16px; |
| | | justify-content: flex-start; |
| | | background: #ffffff; |
| | | border-radius: 12px; |
| | | padding: 20px; |
| | | } |
| | | |
| | | .data-title { |
| | | font-weight: 700; |
| | | font-size: 26px; |
| | | color: #FFFFFF; |
| | | } |
| | | |
| | | .data-num { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | margin-top: 20px; |
| | | } |
| | | |
| | | .data-card { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | padding: 14px 10px 10px 10px; |
| | | min-width: 160px; |
| | | box-shadow: 0 2px 8px #eee; |
| | | display: flex; |
| | | flex-direction: column; |
| | | width: 32%; |
| | | height: 140px; |
| | | } |
| | | |
| | | .data-card.sales { |
| | | background-image: url("../assets/images/xioashoushuju.png"); |
| | | background-size: cover; |
| | | background-position: center; |
| | | background-repeat: no-repeat; |
| | | } |
| | | |
| | | .data-card.purchase { |
| | | background-image: url("../assets/images/caigou.png"); |
| | | background-size: cover; |
| | | background-position: center; |
| | | background-repeat: no-repeat; |
| | | } |
| | | |
| | | .data-card.inventory { |
| | | background-image: url("../assets/images/kucun.png"); |
| | | background-size: cover; |
| | | background-position: center; |
| | | background-repeat: no-repeat; |
| | | } |
| | | |
| | | .data-desc { |
| | | font-weight: 500; |
| | | font-size: 13px; |
| | | color: #FFFFFF; |
| | | } |
| | | |
| | | .data-value { |
| | | font-size: 18px; |
| | | font-weight: 500; |
| | | margin: 10px 0; |
| | | color: #FFFFFF; |
| | | } |
| | | |
| | | .top-left { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 20px; |
| | | height: 180px; |
| | | width: 20%; |
| | | } |
| | | |
| | | .company-info { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | padding: 20px; |
| | | } |
| | | |
| | | .welcome-banner { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 20px; |
| | | padding-bottom: 16px; |
| | | border-bottom: 1px solid #ebeef5; |
| | | } |
| | | |
| | | .welcome-title { |
| | | font-size: 18px; |
| | | color: #303133; |
| | | |
| | | .welcome-user { |
| | | font-weight: 600; |
| | | color: var(--el-color-primary); |
| | | } |
| | | } |
| | | |
| | | .welcome-time { |
| | | font-size: 13px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .user-card { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 16px; |
| | | } |
| | | |
| | | .avatar { |
| | | width: 64px; |
| | | height: 64px; |
| | | border-radius: 50%; |
| | | object-fit: cover; |
| | | border: 3px solid var(--el-color-primary-light-8); |
| | | } |
| | | |
| | | .user-card-main { |
| | | .user-name { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .user-role { |
| | | font-size: 13px; |
| | | color: var(--el-color-primary); |
| | | background: var(--el-color-primary-light-9); |
| | | padding: 2px 10px; |
| | | border-radius: 4px; |
| | | display: inline-block; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .user-meta { |
| | | font-size: 13px; |
| | | color: #606266; |
| | | |
| | | .sep { |
| | | margin: 0 8px; |
| | | color: #dcdfe6; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .data-cards { |
| | | display: grid; |
| | | grid-template-columns: repeat(3, 1fr); |
| | | gap: 16px; |
| | | } |
| | | |
| | | .data-card { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | padding: 16px; |
| | | |
| | | .data-title { |
| | | font-size: 14px; |
| | | color: #606266; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .data-num { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .data-desc { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .data-value { |
| | | font-size: 20px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | } |
| | | |
| | | .todo-panel { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | padding: 20px; |
| | | height: 180px; |
| | | width: 30%; |
| | | } |
| | | |
| | | .section-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | margin-bottom: 16px; |
| | | position: relative; |
| | | padding-left: 12px; |
| | | |
| | | &::before { |
| | | content: ''; |
| | | position: absolute; |
| | | left: 0; |
| | | top: 50%; |
| | | transform: translateY(-50%); |
| | | width: 4px; |
| | | height: 16px; |
| | | background: var(--el-color-primary); |
| | | border-radius: 2px; |
| | | } |
| | | } |
| | | |
| | | .todo-list { |
| | | height: 100px; |
| | | list-style: none; |
| | | padding: 0; |
| | | margin: 0; |
| | | font-size: 15px; |
| | | overflow-y: auto; |
| | | } |
| | | |
| | | .todo-list li { |
| | | border-radius: 8px; |
| | | margin-bottom: 12px; |
| | | padding: 8px 20px; |
| | | height: 74px; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | background: rgba(225, 227, 250, 0.62); |
| | | } |
| | | li { |
| | | padding: 12px 0; |
| | | border-bottom: 1px solid #ebeef5; |
| | | |
| | | .todo-title { |
| | | font-weight: 400; |
| | | font-size: 12px; |
| | | color: #000000; |
| | | position: relative; |
| | | } |
| | | |
| | | .todo-title::before { |
| | | content: ''; |
| | | /* 必需,表示这里有一个内容 */ |
| | | position: absolute; |
| | | left: -10px; |
| | | /* 定位到左侧 */ |
| | | top: 50%; |
| | | /* 垂直居中 */ |
| | | transform: translateY(-50%); |
| | | /* 微调垂直居中 */ |
| | | width: 6px; |
| | | /* 圆的直径 */ |
| | | height: 6px; |
| | | /* 圆的直径 */ |
| | | background: #498CEB; |
| | | border-radius: 50%; |
| | | /* 让其变成圆形 */ |
| | | } |
| | | |
| | | .todo-division { |
| | | font-weight: 400; |
| | | font-size: 12px; |
| | | color: #000000; |
| | | } |
| | | |
| | | .todo-time { |
| | | font-weight: 400; |
| | | font-size: 12px; |
| | | color: #000000; |
| | | } |
| | | |
| | | .todo-meta { |
| | | color: #888; |
| | | font-size: 13px; |
| | | &:last-child { |
| | | border-bottom: none; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .dashboard-row { |
| | | display: flex; |
| | | display: grid; |
| | | grid-template-columns: 1fr 1fr; |
| | | gap: 20px; |
| | | margin-bottom: 20px; |
| | | } |
| | |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | padding: 20px; |
| | | flex: 1; |
| | | min-width: 0; |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .section-title { |
| | | position: relative; |
| | | font-size: 18px; |
| | | color: #333; |
| | | padding-left: 10px; |
| | | margin-bottom: 10px; |
| | | font-weight: 700; |
| | | .contract-panel { |
| | | grid-column: 1 / -1; |
| | | } |
| | | |
| | | .section-title::before { |
| | | position: absolute; |
| | | left: 0; |
| | | top: 4px; |
| | | content: ''; |
| | | width: 4px; |
| | | height: 18px; |
| | | background-color: #002FA7; |
| | | border-radius: 2px; |
| | | .contract-summary { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .contract-info { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 20px; |
| | | height: 90px; |
| | | background: rgba(245, 245, 245, 0.59); |
| | | width: 100%; |
| | | border-radius: 10px; |
| | | padding: 10px 30px; |
| | | } |
| | | |
| | | .contract-summary { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 30px; |
| | | gap: 16px; |
| | | } |
| | | |
| | | .contract-card { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | .contract-name { |
| | | font-size: 14px; |
| | | color: #909399; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .contract-meta { |
| | | .main-amount { |
| | | font-size: 28px; |
| | | font-weight: 700; |
| | | color: #303133; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .up { |
| | | color: #67c23a; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .contract-name { |
| | | font-weight: 400; |
| | | font-size: 14px; |
| | | color: #050505; |
| | | } |
| | | |
| | | .contract-meta { |
| | | .contract-chart-wrapper { |
| | | display: flex; |
| | | align-items: center; |
| | | width: 100%; |
| | | gap: 80px; |
| | | gap: 40px; |
| | | justify-content: center; |
| | | margin-top: 20px; |
| | | } |
| | | |
| | | .main-amount { |
| | | font-size: 24px; |
| | | color: rgba(51, 50, 50, 0.85); |
| | | } |
| | | |
| | | .up { |
| | | color: #e57373; |
| | | .chart-container { |
| | | width: 320px; |
| | | height: 280px; |
| | | } |
| | | |
| | | .contract-list { |
| | | margin-top: 16px; |
| | | font-size: 14px; |
| | | color: #666; |
| | | list-style: none; |
| | | padding: 0; |
| | | height: 190px; |
| | | overflow-y: auto; |
| | | width: 460px; |
| | | } |
| | | margin: 0; |
| | | min-width: 300px; |
| | | |
| | | .line { |
| | | position: relative; |
| | | width: 230px; |
| | | } |
| | | li { |
| | | margin-bottom: 12px; |
| | | |
| | | .line::after { |
| | | content: ''; |
| | | position: absolute; |
| | | right: 2px; |
| | | top: 0; |
| | | bottom: 0; |
| | | width: 1px; |
| | | background-color: #C9C5C5; |
| | | border-radius: 2px; |
| | | } |
| | | &:last-child { |
| | | margin-bottom: 0; |
| | | } |
| | | } |
| | | |
| | | .contract-list li { |
| | | margin-top: 10px; |
| | | } |
| | | .contract-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | padding: 10px 16px; |
| | | background: #f5f7fa; |
| | | border-radius: 8px; |
| | | |
| | | .quality-cards { |
| | | display: flex; |
| | | gap: 12px; |
| | | margin-bottom: 12px; |
| | | } |
| | | .contract-dot { |
| | | width: 10px; |
| | | height: 10px; |
| | | border-radius: 50%; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .quality-card { |
| | | border-radius: 8px; |
| | | padding: 15px 10px 10px 50px; |
| | | font-weight: 400; |
| | | font-size: 12px; |
| | | color: rgba(0, 0, 0, 0.67); |
| | | width: 236px; |
| | | height: 49px; |
| | | background-size: cover; |
| | | background-position: center; |
| | | background-repeat: no-repeat; |
| | | } |
| | | .contract-name { |
| | | flex: 1; |
| | | font-size: 14px; |
| | | color: #303133; |
| | | } |
| | | |
| | | .quality-card.one { |
| | | background-image: url("../assets/images/yuancailiao.png"); |
| | | } |
| | | .contract-rate { |
| | | font-size: 13px; |
| | | color: #909399; |
| | | min-width: 50px; |
| | | text-align: right; |
| | | } |
| | | |
| | | .quality-card.two { |
| | | background-image: url("../assets/images/guocheng.png"); |
| | | } |
| | | |
| | | .quality-card.three { |
| | | background-image: url("../assets/images/chuchang.png"); |
| | | |
| | | } |
| | | |
| | | .quality-card span { |
| | | color: #4fc3f7; |
| | | font-weight: bold; |
| | | margin-left: 6px; |
| | | } |
| | | |
| | | .chart { |
| | | width: 100%; |
| | | height: 220px; |
| | | margin-top: 10px; |
| | | .contract-value { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | min-width: 100px; |
| | | text-align: right; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .process-panel { |
| | | padding-bottom: 10px; |
| | | grid-column: 1 / -1; |
| | | } |
| | | |
| | | .process-panel__header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .process-panel__body { |
| | | display: flex; |
| | | gap: 24px; |
| | | align-items: stretch; |
| | | margin-top: 10px; |
| | | display: grid; |
| | | grid-template-columns: 1fr 280px; |
| | | gap: 20px; |
| | | height: 300px; |
| | | } |
| | | |
| | | .process-panel__chart { |
| | | flex: 1; |
| | | min-width: 0; |
| | | padding: 6px 0; |
| | | height: 100%; |
| | | } |
| | | |
| | | .process-panel__aside { |
| | | width: 260px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 12px; |
| | |
| | | |
| | | .process-legend { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | align-items: flex-start; |
| | | padding: 8px 6px; |
| | | gap: 16px; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .process-legend__item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | gap: 6px; |
| | | font-size: 13px; |
| | | color: rgba(0, 0, 0, 0.55); |
| | | } |
| | | color: #606266; |
| | | |
| | | .dot { |
| | | width: 10px; |
| | | height: 10px; |
| | | border-radius: 2px; |
| | | display: inline-block; |
| | | } |
| | | .dot { |
| | | width: 8px; |
| | | height: 8px; |
| | | border-radius: 50%; |
| | | |
| | | .dot-blue { |
| | | background: #1E5BFF; |
| | | } |
| | | &.dot-blue { |
| | | background: #2D99FF; |
| | | } |
| | | |
| | | .dot-yellow { |
| | | background: #F7B500; |
| | | } |
| | | &.dot-yellow { |
| | | background: #F2CA6D; |
| | | } |
| | | |
| | | .dot-teal { |
| | | background: #19C6C6; |
| | | &.dot-teal { |
| | | background: #5EE9C0; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .process-card { |
| | | background: rgba(245, 247, 250, 0.9); |
| | | border-radius: 10px; |
| | | padding: 16px 16px; |
| | | background: #f5f7fa; |
| | | border-radius: 8px; |
| | | padding: 12px 16px; |
| | | |
| | | &--name { |
| | | font-weight: 600; |
| | | color: #303133; |
| | | background: var(--el-color-primary-light-9); |
| | | } |
| | | |
| | | &__label { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | &__value { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | } |
| | | |
| | | .process-card--name { |
| | | background: rgba(235, 242, 255, 1); |
| | | color: #1E5BFF; |
| | | font-weight: 800; |
| | | font-size: 14px; |
| | | .quality-cards { |
| | | display: flex; |
| | | gap: 12px; |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .process-card__label { |
| | | .quality-card { |
| | | flex: 1; |
| | | background: #f5f7fa; |
| | | border-radius: 8px; |
| | | padding: 12px; |
| | | font-size: 13px; |
| | | color: rgba(0, 0, 0, 0.55); |
| | | margin-bottom: 10px; |
| | | } |
| | | color: #606266; |
| | | |
| | | .process-card__value { |
| | | font-size: 24px; |
| | | font-weight: 800; |
| | | color: rgba(0, 0, 0, 0.8); |
| | | } |
| | | |
| | | .process-card__value .unit { |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | color: rgba(0, 0, 0, 0.45); |
| | | margin-left: 6px; |
| | | } |
| | | |
| | | @media (max-width: 1200px) { |
| | | .process-panel__body { |
| | | flex-direction: column; |
| | | span { |
| | | display: block; |
| | | font-size: 20px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | margin-top: 4px; |
| | | } |
| | | |
| | | .process-panel__aside { |
| | | width: 100%; |
| | | flex-direction: row; |
| | | flex-wrap: wrap; |
| | | &.one { |
| | | background: #e6f7ff; |
| | | color: #1890ff; |
| | | |
| | | span { |
| | | color: #1890ff; |
| | | } |
| | | } |
| | | |
| | | .process-card { |
| | | flex: 1; |
| | | min-width: 220px; |
| | | &.two { |
| | | background: #f6ffed; |
| | | color: #52c41a; |
| | | |
| | | span { |
| | | color: #52c41a; |
| | | } |
| | | } |
| | | |
| | | &.three { |
| | | background: #fff7e6; |
| | | color: #fa8c16; |
| | | |
| | | span { |
| | | color: #fa8c16; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .process-selection-wrapper { |
| | | max-height: 400px; |
| | | overflow-y: auto; |
| | | padding: 10px; |
| | | } |
| | | |
| | | .process-grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); |
| | | grid-template-columns: repeat(2, 1fr); |
| | | gap: 12px; |
| | | } |
| | | |
| | | :deep(.el-checkbox.is-bordered) { |
| | | margin-left: 0 !important; |
| | | width: 100%; |
| | | } |
| | | </style> |
| | | </style> |
| | |
| | | <template> |
| | | <div class="login-page"> |
| | | <div class="login-shell"> |
| | | <section class="login-brand"> |
| | | <div class="brand-badge">PRODUCT INVENTORY</div> |
| | | <img :src="brandLogo" alt="brand logo" class="brand-logo" /> |
| | | <h1 class="brand-title">{{ title }}</h1> |
| | | <p class="brand-copy"> |
| | | 统一管理库存、流程与业务数据,让系统入口和后台主界面保持同一套简约视觉语言。 |
| | | </p> |
| | | <div class="brand-points"> |
| | | <div class="brand-point"> |
| | | <span class="point-dot"></span> |
| | | <span>清晰的数据入口</span> |
| | | </div> |
| | | <div class="brand-point"> |
| | | <span class="point-dot"></span> |
| | | <span>更轻的界面层次</span> |
| | | </div> |
| | | <div class="brand-point"> |
| | | <span class="point-dot"></span> |
| | | <span>稳定的业务协同体验</span> |
| | | </div> |
| | | <div class="login-card"> |
| | | <div class="card-header"> |
| | | <div class="logo"> |
| | | <svg-icon icon-class="user" /> |
| | | </div> |
| | | </section> |
| | | <h1>客户关系管理系统</h1> |
| | | <p>高效管理客户资源,驱动业务增长</p> |
| | | </div> |
| | | |
| | | <section class="login-panel"> |
| | | <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form"> |
| | | <div class="panel-head"> |
| | | <p class="panel-kicker">WELCOME BACK</p> |
| | | <h2 class="panel-title">登录系统</h2> |
| | | <p class="panel-subtitle">输入账号和密码进入工作台。</p> |
| | | </div> |
| | | |
| | | <el-form-item prop="username"> |
| | | <el-input |
| | | v-model="loginForm.username" |
| | | type="text" |
| | | size="large" |
| | | auto-complete="off" |
| | | placeholder="账号" |
| | | > |
| | | <template #prefix><el-icon><User /></el-icon></template> |
| | | </el-input> |
| | | </el-form-item> |
| | | |
| | | <el-form-item prop="password"> |
| | | <el-input |
| | | v-model="loginForm.password" |
| | | type="password" |
| | | size="large" |
| | | auto-complete="off" |
| | | placeholder="密码" |
| | | show-password |
| | | @keyup.enter="handleLogin" |
| | | > |
| | | <template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template> |
| | | </el-input> |
| | | </el-form-item> |
| | | |
| | | <div class="login-options"> |
| | | <el-checkbox v-model="loginForm.rememberMe">记住密码</el-checkbox> |
| | | <router-link v-if="register" class="register-link" :to="'/register'">立即注册</router-link> |
| | | </div> |
| | | |
| | | <el-button |
| | | :loading="loading" |
| | | <el-form ref="loginRef" :model="loginForm" :rules="loginRules"> |
| | | <el-form-item prop="username"> |
| | | <el-input |
| | | v-model="loginForm.username" |
| | | type="text" |
| | | size="large" |
| | | type="primary" |
| | | class="login-submit" |
| | | @click.prevent="handleLogin" |
| | | auto-complete="off" |
| | | placeholder="请输入账号" |
| | | > |
| | | <span v-if="!loading">登录</span> |
| | | <span v-else>登录中...</span> |
| | | </el-button> |
| | | </el-form> |
| | | </section> |
| | | <template #prefix><el-icon><User /></el-icon></template> |
| | | </el-input> |
| | | </el-form-item> |
| | | |
| | | <el-form-item prop="password"> |
| | | <el-input |
| | | v-model="loginForm.password" |
| | | type="password" |
| | | size="large" |
| | | auto-complete="off" |
| | | placeholder="请输入密码" |
| | | show-password |
| | | @keyup.enter="handleLogin" |
| | | > |
| | | <template #prefix><el-icon><Lock /></el-icon></template> |
| | | </el-input> |
| | | </el-form-item> |
| | | |
| | | <div class="form-options"> |
| | | <el-checkbox v-model="loginForm.rememberMe">记住密码</el-checkbox> |
| | | <router-link v-if="register" :to="'/register'">立即注册</router-link> |
| | | </div> |
| | | |
| | | <el-button |
| | | :loading="loading" |
| | | size="large" |
| | | type="primary" |
| | | class="login-btn" |
| | | @click.prevent="handleLogin" |
| | | > |
| | | <span v-if="!loading">登 录</span> |
| | | <span v-else>登录中...</span> |
| | | </el-button> |
| | | </el-form> |
| | | </div> |
| | | |
| | | <div class="bg-pattern"> |
| | | <div class="pattern-item"></div> |
| | | <div class="pattern-item"></div> |
| | | <div class="pattern-item"></div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | |
| | | import Cookies from "js-cookie" |
| | | import { encrypt, decrypt } from "@/utils/jsencrypt" |
| | | import useUserStore from "@/store/modules/user" |
| | | import brandLogo from "@/assets/logo/logo.png" |
| | | |
| | | const title = import.meta.env.VITE_APP_TITLE |
| | | const userStore = useUserStore() |
| | | const route = useRoute() |
| | | const router = useRouter() |
| | |
| | | }) |
| | | |
| | | const loginRules = { |
| | | username: [{ required: true, trigger: "blur", message: "请输入您的账号" }], |
| | | password: [{ required: true, trigger: "blur", message: "请输入您的密码" }], |
| | | username: [{ required: true, trigger: "blur", message: "请输入账号" }], |
| | | password: [{ required: true, trigger: "blur", message: "请输入密码" }], |
| | | } |
| | | |
| | | const codeUrl = ref("") |
| | | const loading = ref(false) |
| | | const captchaEnabled = ref(true) |
| | | const register = ref(false) |
| | |
| | | getCodeImg().then((res) => { |
| | | captchaEnabled.value = res.captchaEnabled === undefined ? true : res.captchaEnabled |
| | | if (captchaEnabled.value) { |
| | | codeUrl.value = "data:image/gif;base64," + res.img |
| | | loginForm.value.uuid = res.uuid |
| | | } |
| | | }) |
| | |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | padding: 32px; |
| | | background: |
| | | radial-gradient(circle at top left, rgba(207, 223, 214, 0.95), transparent 30%), |
| | | radial-gradient(circle at bottom right, rgba(222, 232, 227, 0.9), transparent 28%), |
| | | linear-gradient(180deg, #f7faf8 0%, #eef2ee 100%); |
| | | } |
| | | |
| | | .login-shell { |
| | | width: min(1120px, 100%); |
| | | min-height: 680px; |
| | | display: grid; |
| | | grid-template-columns: 1.1fr 0.9fr; |
| | | border: 1px solid rgba(216, 225, 219, 0.9); |
| | | border-radius: 32px; |
| | | overflow: hidden; |
| | | background: rgba(255, 255, 255, 0.76); |
| | | box-shadow: 0 26px 80px rgba(31, 49, 38, 0.12); |
| | | backdrop-filter: blur(24px); |
| | | } |
| | | |
| | | .login-brand { |
| | | background: linear-gradient(135deg, var(--el-color-primary-light-9) 0%, var(--el-color-primary-light-8) 50%, var(--el-color-primary-light-9) 100%); |
| | | position: relative; |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: center; |
| | | padding: 56px 64px; |
| | | background: |
| | | linear-gradient(180deg, rgba(244, 248, 245, 0.9), rgba(233, 240, 236, 0.9)), |
| | | linear-gradient(135deg, rgba(31, 122, 114, 0.05), rgba(255, 255, 255, 0)); |
| | | overflow: hidden; |
| | | } |
| | | |
| | | &::after { |
| | | content: ""; |
| | | .bg-pattern { |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | pointer-events: none; |
| | | |
| | | .pattern-item { |
| | | position: absolute; |
| | | inset: 28px; |
| | | border: 1px solid rgba(31, 122, 114, 0.08); |
| | | border-radius: 28px; |
| | | pointer-events: none; |
| | | border-radius: 50%; |
| | | background: linear-gradient(135deg, var(--el-color-primary-light-7), var(--el-color-primary-light-9)); |
| | | } |
| | | |
| | | .pattern-item:nth-child(1) { |
| | | width: 500px; |
| | | height: 500px; |
| | | top: -150px; |
| | | right: -100px; |
| | | } |
| | | |
| | | .pattern-item:nth-child(2) { |
| | | width: 350px; |
| | | height: 350px; |
| | | bottom: -100px; |
| | | left: -80px; |
| | | } |
| | | |
| | | .pattern-item:nth-child(3) { |
| | | width: 200px; |
| | | height: 200px; |
| | | top: 40%; |
| | | left: 15%; |
| | | background: linear-gradient(135deg, var(--el-color-primary-light-8), var(--el-color-primary-light-9)); |
| | | } |
| | | } |
| | | |
| | | .brand-badge { |
| | | width: fit-content; |
| | | padding: 8px 14px; |
| | | border-radius: 999px; |
| | | background: rgba(31, 122, 114, 0.1); |
| | | color: #1f7a72; |
| | | font-size: 12px; |
| | | font-weight: 700; |
| | | letter-spacing: 0.14em; |
| | | .login-card { |
| | | width: 420px; |
| | | padding: 48px 40px; |
| | | background: rgba(255, 255, 255, 0.95); |
| | | border-radius: 20px; |
| | | box-shadow: |
| | | 0 4px 6px -1px rgba(0, 0, 0, 0.05), |
| | | 0 10px 15px -3px rgba(0, 0, 0, 0.08), |
| | | 0 20px 25px -5px rgba(0, 0, 0, 0.05); |
| | | backdrop-filter: blur(10px); |
| | | position: relative; |
| | | z-index: 1; |
| | | } |
| | | |
| | | .brand-logo { |
| | | width: 160px; |
| | | height: auto; |
| | | margin: 30px 0 24px; |
| | | object-fit: contain; |
| | | } |
| | | .card-header { |
| | | text-align: center; |
| | | margin-bottom: 36px; |
| | | |
| | | .brand-title { |
| | | margin: 0; |
| | | font-size: 42px; |
| | | line-height: 1.12; |
| | | color: #21313f; |
| | | letter-spacing: -0.03em; |
| | | } |
| | | .logo { |
| | | width: 72px; |
| | | height: 72px; |
| | | margin: 0 auto 20px; |
| | | background: linear-gradient(135deg, var(--el-color-primary) 0%, var(--el-color-primary-light-3) 100%); |
| | | border-radius: 18px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | box-shadow: 0 8px 20px var(--el-color-primary-light-5); |
| | | |
| | | .brand-copy { |
| | | max-width: 460px; |
| | | margin: 18px 0 0; |
| | | font-size: 16px; |
| | | line-height: 1.75; |
| | | color: #5f6d7e; |
| | | } |
| | | :deep(svg) { |
| | | width: 36px; |
| | | height: 36px; |
| | | color: #fff; |
| | | } |
| | | } |
| | | |
| | | .brand-points { |
| | | margin-top: 34px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 14px; |
| | | } |
| | | h1 { |
| | | font-size: 24px; |
| | | font-weight: 600; |
| | | color: #1f2937; |
| | | margin: 0 0 8px; |
| | | } |
| | | |
| | | .brand-point { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | color: #3d4b59; |
| | | font-size: 15px; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .point-dot { |
| | | width: 10px; |
| | | height: 10px; |
| | | border-radius: 50%; |
| | | background: linear-gradient(135deg, #1f7a72, #5ca39c); |
| | | box-shadow: 0 0 0 6px rgba(31, 122, 114, 0.08); |
| | | } |
| | | |
| | | .login-panel { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | padding: 40px; |
| | | background: rgba(255, 255, 255, 0.7); |
| | | } |
| | | |
| | | .login-form { |
| | | width: min(420px, 100%); |
| | | padding: 38px 34px 34px; |
| | | border: 1px solid rgba(216, 225, 219, 0.92); |
| | | border-radius: 28px; |
| | | background: rgba(255, 255, 255, 0.88); |
| | | box-shadow: 0 18px 52px rgba(31, 49, 38, 0.1); |
| | | } |
| | | |
| | | .panel-head { |
| | | margin-bottom: 28px; |
| | | } |
| | | |
| | | .panel-kicker { |
| | | margin: 0 0 10px; |
| | | color: #8a98a8; |
| | | font-size: 12px; |
| | | font-weight: 700; |
| | | letter-spacing: 0.16em; |
| | | } |
| | | |
| | | .panel-title { |
| | | margin: 0; |
| | | color: #21313f; |
| | | font-size: 30px; |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .panel-subtitle { |
| | | margin: 10px 0 0; |
| | | color: #6b7888; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .login-options { |
| | | margin: -4px 0 22px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | color: #5f6d7e; |
| | | } |
| | | |
| | | .register-link { |
| | | color: var(--el-color-primary); |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .login-submit { |
| | | width: 100%; |
| | | height: 48px; |
| | | } |
| | | |
| | | .input-icon { |
| | | width: 14px; |
| | | p { |
| | | font-size: 14px; |
| | | color: #6b7280; |
| | | margin: 0; |
| | | } |
| | | } |
| | | |
| | | :deep(.el-form-item) { |
| | | margin-bottom: 22px; |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | :deep(.el-input__wrapper) { |
| | | min-height: 42px; |
| | | height: 42px; |
| | | padding-top: 0; |
| | | padding-bottom: 0; |
| | | border-radius: 12px; |
| | | box-shadow: 0 0 0 1px #e5e7eb; |
| | | padding: 0 16px; |
| | | height: 48px; |
| | | transition: all 0.2s; |
| | | |
| | | &:hover { |
| | | box-shadow: 0 0 0 1px var(--el-color-primary-light-5); |
| | | } |
| | | |
| | | &:focus-within { |
| | | box-shadow: 0 0 0 2px var(--el-color-primary); |
| | | } |
| | | } |
| | | |
| | | :deep(.el-input__inner) { |
| | | height: 42px; |
| | | line-height: 42px; |
| | | height: 48px; |
| | | font-size: 15px; |
| | | color: #374151; |
| | | |
| | | &::placeholder { |
| | | color: #9ca3af; |
| | | } |
| | | } |
| | | |
| | | :deep(.el-checkbox) { |
| | | color: #5f6d7e; |
| | | :deep(.el-input__prefix) { |
| | | color: #9ca3af; |
| | | font-size: 18px; |
| | | } |
| | | |
| | | @media (max-width: 960px) { |
| | | .login-page { |
| | | padding: 18px; |
| | | } |
| | | .form-options { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | margin: 8px 0 24px; |
| | | |
| | | .login-shell { |
| | | min-height: auto; |
| | | grid-template-columns: 1fr; |
| | | } |
| | | |
| | | .login-brand { |
| | | padding: 40px 28px 22px; |
| | | } |
| | | |
| | | .login-brand::after { |
| | | inset: 16px; |
| | | } |
| | | |
| | | .brand-title { |
| | | font-size: 32px; |
| | | } |
| | | |
| | | .brand-copy { |
| | | :deep(.el-checkbox__label) { |
| | | color: #6b7280; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .brand-points { |
| | | margin-top: 24px; |
| | | :deep(.el-checkbox__input.is-checked .el-checkbox__inner) { |
| | | background-color: var(--el-color-primary); |
| | | border-color: var(--el-color-primary); |
| | | } |
| | | |
| | | .login-panel { |
| | | padding: 12px 18px 24px; |
| | | a { |
| | | color: var(--el-color-primary); |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | text-decoration: none; |
| | | transition: color 0.2s; |
| | | |
| | | &:hover { |
| | | color: var(--el-color-primary-light-3); |
| | | } |
| | | } |
| | | } |
| | | |
| | | .login-btn { |
| | | width: 100%; |
| | | height: 48px; |
| | | border-radius: 12px; |
| | | font-size: 16px; |
| | | font-weight: 500; |
| | | background: linear-gradient(135deg, var(--el-color-primary) 0%, var(--el-color-primary-light-3) 100%); |
| | | border: none; |
| | | box-shadow: 0 4px 14px var(--el-color-primary-light-5); |
| | | transition: all 0.2s; |
| | | |
| | | &:hover { |
| | | transform: translateY(-1px); |
| | | box-shadow: 0 6px 20px var(--el-color-primary-light-4); |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 480px) { |
| | | .login-card { |
| | | width: 90%; |
| | | padding: 36px 24px; |
| | | } |
| | | |
| | | .login-form { |
| | | width: 100%; |
| | | padding: 28px 22px 24px; |
| | | .card-header { |
| | | h1 { |
| | | font-size: 20px; |
| | | } |
| | | } |
| | | } |
| | | </style> |
| | |
| | | const { VITE_APP_ENV } = env; |
| | | const baseUrl = |
| | | env.VITE_APP_ENV === "development" |
| | | ? "http://1.15.17.182:9048" |
| | | ? "http://1.15.17.182:9055" |
| | | : env.VITE_BASE_API; |
| | | const javaUrl = |
| | | env.VITE_APP_ENV === "development" |