diff --git a/.env b/.env index 5919cdb..58cb8dc 100644 --- a/.env +++ b/.env @@ -1,7 +1,24 @@ # 所有环境的环境变量(命名必须以 VITE_ 开头) ## 项目标题 -VITE_APP_TITLE = V3 Admin Vite +VITE_APP_TITLE = 校园物联网管理平台 ## 路由模式 hash 或 html5 VITE_ROUTER_HISTORY = hash + +## serverschool 后端接口前缀(登录 / 用户信息等真实接口,与 apifoxmock 的 VITE_BASE_URL 区分) +## 开发环境由 vite.config.ts 的 /hxyd-api 代理转发;生产环境需由 nginx 反代同名路径 +VITE_SCHOOL_BASE_URL = /hxyd-api + +## kkFileView 文档预览服务(对齐 school-iot-admin 的 VUE_APP_KKFILEVIEW_URL) +VITE_KKFILEVIEW_URL = https://net2.hxzhxy.cn:8012 + +## 跨系统链接(对齐 school-iot-admin,默认生产地址;开发内网地址见 .env.development 注释) +# 问卷系统 +VITE_QUESTION_URL = http://complaints.hxzhxy.cn +# 可视化数据大屏 +VITE_DATA_SCREEN = http://139.159.182.121:1689 +# 新生填报 H5 +VITE_APPLY_URL = http://apply.hxzhxy.cn +# 数据填报表单 +VITE_API_FORM = https://form.hxzhxy.cn diff --git a/.env.development b/.env.development index fbfaaf5..7c16d1b 100644 --- a/.env.development +++ b/.env.development @@ -1,7 +1,24 @@ # 开发环境的环境变量(命名必须以 VITE_ 开头) -## 后端接口地址(如果解决跨域问题采用反向代理就只需写相对路径) +## 后端接口地址(apifoxmock,demo / FastCrud 演示页用) VITE_BASE_URL = /api/v1 ## 开发环境域名和静态资源公共路径(一般 / 或 ./ 都可以) VITE_PUBLIC_PATH = / + +# ===== serverschool 后端代理(默认连生产,与 school-iot-admin 的 dev 一致) ===== + +## /hxyd-api 代理目标;连测试库改成 http://192.168.0.10:8100 +VITE_SCHOOL_TARGET = https://band.hxzhxy.cn + +## WebSocket(/ws-api);老平台 dev 内网值 ws://192.168.0.244:58089 +VITE_WS_TARGET = ws://139.159.182.121:8100 + +## 视频流 wvp(/hxyd-video);老平台 dev 内网值 http://10.19.64.250:8000 +VITE_VIDEO_TARGET = http://11.1.20.107:8000 + +## yolo 抓拍图片 minio(/hxyd-video/yolo);老平台 dev 内网值 http://10.19.64.250:9000 +VITE_VIDEO_MINIO = http://11.1.20.107:9000 + +## 智能体对话(/agent-dialogue-api,本机 score_agent / LangGraph) +VITE_AGENT_DIALOGUE_TARGET = http://127.0.0.1:3030 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..24a72ca --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +allowBuilds: + '@parcel/watcher': true + esbuild: true diff --git a/src/App.vue b/src/App.vue index 22a59a8..64a411a 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,6 +1,5 @@ diff --git a/src/common/apis/users/index.ts b/src/common/apis/users/index.ts index ccd9f72..611fff7 100644 --- a/src/common/apis/users/index.ts +++ b/src/common/apis/users/index.ts @@ -1,10 +1,15 @@ import type * as Users from "./type" import { request } from "@/http/axios" -/** 获取当前登录用户详情 */ +/** + * 获取当前登录用户详情(serverschool /uc/account/current) + * 返回的 data 中包含 pwd 等敏感字段,调用方只取必要字段,不要整体入库/展示 + */ export function getCurrentUserApi() { return request({ - url: "users/me", - method: "get" + baseURL: import.meta.env.VITE_SCHOOL_BASE_URL, + url: "uc/account/current", + method: "get", + timeout: 10000 }) } diff --git a/src/common/apis/users/type.ts b/src/common/apis/users/type.ts index 0b0dfa3..4b0d8e4 100644 --- a/src/common/apis/users/type.ts +++ b/src/common/apis/users/type.ts @@ -1,5 +1,24 @@ -export type CurrentUserResponseData = ApiResponseData<{ - username: string - roles: string[] - permissions?: string[] -}> +/** serverschool 账号信息(/uc/account/current 返回的 AccountDetails,已裁剪无用字段) */ +export interface AccountDetails { + /** 账号 ID */ + id: number + /** 用户名 */ + userName: string + /** 账号类型(3 表示无权限登录后台) */ + type?: number + /** 所属学校/分组 ID */ + teamId?: number + /** 角色 */ + role?: { + id: number + name?: string + [key: string]: unknown + } + /** 是否锁定 */ + locked?: boolean + /** 是否启用 */ + enabled?: boolean + [key: string]: unknown +} + +export type CurrentUserResponseData = ApiResponseData diff --git a/src/common/assets/images/layouts/platform-logo.png b/src/common/assets/images/layouts/platform-logo.png new file mode 100644 index 0000000..4fbea0f Binary files /dev/null and b/src/common/assets/images/layouts/platform-logo.png differ diff --git a/src/common/assets/styles/element-plus.scss b/src/common/assets/styles/element-plus.scss index 790d962..33fb363 100644 --- a/src/common/assets/styles/element-plus.scss +++ b/src/common/assets/styles/element-plus.scss @@ -1,4 +1,16 @@ // 自定义 Element Plus 样式 +// 视觉规范对齐 school-iot-admin(品牌色 #347df9) + +// 品牌主色(对齐 school-iot-admin 的 #347df9,让按钮/链接/开关等统一品牌色) +:root { + --el-color-primary: #347df9; + --el-color-primary-light-3: #71a4fb; + --el-color-primary-light-5: #9abefc; + --el-color-primary-light-7: #c2d8fd; + --el-color-primary-light-8: #d6e5fe; + --el-color-primary-light-9: #ebf2fe; + --el-color-primary-dark-2: #2e68cb; +} // 分页 .el-pagination { @@ -13,3 +25,61 @@ } } } + +// 表格 +.el-table { + --el-table-header-bg-color: #eaf1fe; + --el-table-header-text-color: #3d3e40; + --el-table-text-color: #373535; + --el-table-border-color: #ebeef5; + --el-table-row-hover-bg-color: #f2f6fc; + + th.el-table__cell { + font-size: 15px; + font-weight: 500; + } + + td.el-table__cell { + font-size: 14px; + } + + // 斑马纹 + &.el-table--striped .el-table__body tr.el-table__row--striped td.el-table__cell { + background-color: #f7faff; + } +} + +// 弹窗(标题栏为品牌蓝底白字) +.el-dialog { + border-radius: 10px; + overflow: hidden; + .el-dialog__header { + margin: 0; + padding: 0 20px; + height: 48px; + display: flex; + align-items: center; + background-color: #347df9; + .el-dialog__title { + color: #ffffff; + font-size: 16px; + font-weight: 500; + } + .el-dialog__headerbtn { + top: 50%; + right: 12px; + transform: translateY(-50%); + .el-dialog__close { + color: #ffffff; + } + } + } + .el-dialog__body { + padding: 20px; + } +} + +// 表单 +.el-form-item__label { + font-weight: 500; +} diff --git a/src/common/assets/styles/index.scss b/src/common/assets/styles/index.scss index db2f8b0..438676c 100644 --- a/src/common/assets/styles/index.scss +++ b/src/common/assets/styles/index.scss @@ -16,8 +16,58 @@ @import "./view-transition.scss"; // 业务页面几乎都应该在根元素上挂载 class="app-container",以保持页面美观 +// 视觉规范对齐 school-iot-admin:白卡片 + 10px 圆角 + 淡蓝阴影 .app-container { - padding: 20px; + padding: 16px 20px 20px; + background-color: var(--el-bg-color); + border-radius: 10px; + box-shadow: 0 0 10px rgba(32, 76, 131, 0.12); +} + +// 页面标题(对齐 school-iot-admin 的 .head-title:左侧 6px 蓝色竖条) +.head-title { + position: relative; + display: flex; + align-items: center; + margin: 4px 0 16px; + padding-left: 14px; + font-size: 20px; + font-weight: 500; + color: var(--el-text-color-primary); + &::before { + content: ""; + position: absolute; + top: 50%; + left: 0; + transform: translateY(-50%); + width: 6px; + height: 17px; + border-radius: 10px; + background-color: #347df9; + } + .tle { + margin: 0 5px; + } +} + +// 主按钮(对齐 school-iot-admin 的 .btn-blue) +.btn-blue { + min-width: 68px; + border-radius: 2px; + font-weight: 500; + color: #ffffff; + background-color: #347df9; + border-color: #347df9; +} + +// 次按钮(对齐 school-iot-admin 的 .btn-white) +.btn-white { + min-width: 68px; + border-radius: 2px; + font-weight: 500; + color: #347df9; + background-color: #ffffff; + border: 1px solid #347df9; } html { diff --git a/src/common/assets/styles/theme/dark-blue/variables.scss b/src/common/assets/styles/theme/dark-blue/variables.scss index e65ab92..ef68906 100644 --- a/src/common/assets/styles/theme/dark-blue/variables.scss +++ b/src/common/assets/styles/theme/dark-blue/variables.scss @@ -4,3 +4,30 @@ $theme-name: "dark-blue"; // 主题背景颜色 $theme-bg-color: #001b44; + +// dark-blue 主题下的布局变量覆盖 +html.#{ $theme-name } { + // Element Plus 品牌主色(与亮色主题保持一致) + --el-color-primary: #347df9; + --el-color-primary-light-3: #2460c3; + --el-color-primary-light-5: #1a4c9e; + --el-color-primary-light-7: #10387a; + --el-color-primary-light-8: #0a2f68; + --el-color-primary-light-9: #052556; + --el-color-primary-dark-2: #5d97fa; + + // Body + --v3-body-bg-color: #{$theme-bg-color}; + // Sidebar 组件(深色背景下需要浅色文字) + --v3-sidebar-menu-bg-color: var(--el-bg-color); + --v3-sidebar-menu-hover-bg-color: var(--el-fill-color); + --v3-sidebar-menu-active-bg-color: var(--el-fill-color); + --v3-sidebar-menu-text-color: #cfd3dc; + --v3-sidebar-menu-active-text-color: #ffffff; + // TagsView 组件 + --v3-tagsview-text-color: var(--el-text-color-regular); + --v3-tagsview-tag-bg-color: var(--el-bg-color); + --v3-tagsview-tag-bg: var(--el-bg-color); + --v3-tagsview-tag-active-bg: var(--el-color-primary); + --v3-tagsview-tag-border-color: var(--el-border-color-lighter); +} diff --git a/src/common/assets/styles/theme/dark/variables.scss b/src/common/assets/styles/theme/dark/variables.scss index df0a354..f272874 100644 --- a/src/common/assets/styles/theme/dark/variables.scss +++ b/src/common/assets/styles/theme/dark/variables.scss @@ -4,3 +4,30 @@ $theme-name: "dark"; // 主题背景颜色 $theme-bg-color: #141414; + +// dark 主题下的布局变量覆盖 +html.#{ $theme-name } { + // Element Plus 品牌主色(与亮色主题保持一致) + --el-color-primary: #347df9; + --el-color-primary-light-3: #2a5eb4; + --el-color-primary-light-5: #244886; + --el-color-primary-light-7: #1e3459; + --el-color-primary-light-8: #1a2942; + --el-color-primary-light-9: #171e2b; + --el-color-primary-dark-2: #5d97fa; + + // Body + --v3-body-bg-color: #{$theme-bg-color}; + // Sidebar 组件(深色背景下需要浅色文字) + --v3-sidebar-menu-bg-color: var(--el-bg-color); + --v3-sidebar-menu-hover-bg-color: var(--el-fill-color); + --v3-sidebar-menu-active-bg-color: var(--el-fill-color); + --v3-sidebar-menu-text-color: #cfd3dc; + --v3-sidebar-menu-active-text-color: #ffffff; + // TagsView 组件 + --v3-tagsview-text-color: var(--el-text-color-regular); + --v3-tagsview-tag-bg-color: var(--el-bg-color); + --v3-tagsview-tag-bg: var(--el-bg-color); + --v3-tagsview-tag-active-bg: var(--el-color-primary); + --v3-tagsview-tag-border-color: var(--el-border-color-lighter); +} diff --git a/src/common/assets/styles/variables.css b/src/common/assets/styles/variables.css index 32ec870..19b7b0a 100644 --- a/src/common/assets/styles/variables.css +++ b/src/common/assets/styles/variables.css @@ -3,37 +3,38 @@ :root { /* Body */ --v3-body-text-color: var(--el-text-color-primary); - --v3-body-bg-color: var(--el-bg-color-page); + --v3-body-bg-color: #f5f7fa; /* Header 区域 = NavigationBar 组件 + TagsView 组件 */ - --v3-header-height: calc( - var(--v3-navigationbar-height) + var(--v3-tagsview-height) + var(--v3-header-border-bottom-width) - ); - --v3-header-bg-color: var(--el-bg-color); - --v3-header-box-shadow: var(--el-box-shadow-lighter); - --v3-header-border-bottom-width: 1px; - --v3-header-border-bottom: var(--v3-header-border-bottom-width) solid var(--el-fill-color); + --v3-header-height: calc(var(--v3-navigationbar-height) + var(--v3-tagsview-height)); + --v3-header-bg-color: #347df9; + --v3-header-box-shadow: none; + --v3-header-border-bottom-width: 0px; + --v3-header-border-bottom: none; /* NavigationBar 组件 */ - --v3-navigationbar-height: 50px; - --v3-navigationbar-text-color: var(--el-text-color-regular); + --v3-navigationbar-height: 60px; + --v3-navigationbar-text-color: #ffffff; /* Sidebar 组件(左侧模式全部生效、顶部模式全部不生效、混合模式非颜色部分生效) */ - --v3-sidebar-width: 220px; - --v3-sidebar-hide-width: 58px; - --v3-sidebar-border-right: 1px solid var(--el-fill-color); - --v3-sidebar-menu-item-height: 60px; - --v3-sidebar-menu-tip-line-bg-color: var(--el-color-primary); - --v3-sidebar-menu-bg-color: #001428; - --v3-sidebar-menu-hover-bg-color: #409eff10; - --v3-sidebar-menu-text-color: #cfd3dc; - --v3-sidebar-menu-active-text-color: #ffffff; + --v3-sidebar-width: 240px; + --v3-sidebar-hide-width: 64px; + --v3-sidebar-border-right: none; + --v3-sidebar-menu-item-height: 49px; + --v3-sidebar-menu-tip-line-bg-color: #347df9; + --v3-sidebar-menu-bg-color: #ffffff; + --v3-sidebar-menu-hover-bg-color: #f2f6fc; + --v3-sidebar-menu-active-bg-color: #ebf5ff; + --v3-sidebar-menu-text-color: #303133; + --v3-sidebar-menu-active-text-color: #347df9; /* TagsView 组件 */ - --v3-tagsview-height: 34px; - --v3-tagsview-text-color: var(--el-text-color-regular); + --v3-tagsview-height: 36px; + --v3-tagsview-text-color: #303133; --v3-tagsview-tag-active-text-color: #ffffff; - --v3-tagsview-tag-bg-color: var(--el-bg-color); - --v3-tagsview-tag-active-bg-color: var(--el-color-primary); - --v3-tagsview-tag-border-radius: 2px; - --v3-tagsview-tag-border-color: var(--el-border-color-lighter); - --v3-tagsview-tag-active-border-color: var(--el-color-primary); + --v3-tagsview-tag-bg-color: #ffffff; + --v3-tagsview-tag-bg: linear-gradient(to bottom, #ffffff 20%, #90c8f7); + --v3-tagsview-tag-active-bg-color: #347df9; + --v3-tagsview-tag-active-bg: linear-gradient(to bottom, #56bafb, #0577fa); + --v3-tagsview-tag-border-radius: 4px 4px 0 0; + --v3-tagsview-tag-border-color: #d3d4d6; + --v3-tagsview-tag-active-border-color: #347df9; --v3-tagsview-tag-icon-hover-bg-color: #00000030; --v3-tagsview-tag-icon-hover-color: #ffffff; --v3-tagsview-contextmenu-text-color: var(--el-text-color-regular); @@ -42,9 +43,9 @@ --v3-tagsview-contextmenu-hover-bg-color: var(--el-fill-color); --v3-tagsview-contextmenu-box-shadow: var(--el-box-shadow); /* Hamburger 组件 */ - --v3-hamburger-text-color: var(--el-text-color-primary); + --v3-hamburger-text-color: #ffffff; /* RightPanel 组件 */ - --v3-rightpanel-button-bg-color: #001428; + --v3-rightpanel-button-bg-color: #347df9; } /* 内容区放大时,将不需要的组件隐藏 */ diff --git a/src/common/constants/cache-key.ts b/src/common/constants/cache-key.ts index 8b6fc64..019d9ef 100644 --- a/src/common/constants/cache-key.ts +++ b/src/common/constants/cache-key.ts @@ -8,4 +8,6 @@ export class CacheKey { static readonly ACTIVE_THEME_NAME = `${SYSTEM_NAME}-active-theme-name-key` static readonly VISITED_VIEWS = `${SYSTEM_NAME}-visited-views-key` static readonly CACHED_VIEWS = `${SYSTEM_NAME}-cached-views-key` + static readonly FAVORITE_MENUS = `${SYSTEM_NAME}-favorite-menus-key` + static readonly KEEP_LOGIN = `${SYSTEM_NAME}-keep-login-key` } diff --git a/src/common/constants/platform.ts b/src/common/constants/platform.ts new file mode 100644 index 0000000..c634a8a --- /dev/null +++ b/src/common/constants/platform.ts @@ -0,0 +1,11 @@ +/** 平台名称(中文) */ +export const PLATFORM_NAME = "校园物联网管理平台" + +/** 平台名称(英文) */ +export const PLATFORM_NAME_EN = "Campus IoT Management Platform" + +/** 登录页标题(与 school-iot-admin 的登录页保持一致) */ +export const LOGIN_TITLE = "综合信息化管理平台" + +/** 品牌主色(与 school-iot-admin 保持一致) */ +export const BRAND_COLOR = "#347df9" diff --git a/src/common/types/menu.ts b/src/common/types/menu.ts new file mode 100644 index 0000000..b9ecf7d --- /dev/null +++ b/src/common/types/menu.ts @@ -0,0 +1,7 @@ +/** 常用功能(被收藏的菜单项) */ +export interface FavoriteMenu { + /** 菜单路径 */ + path: string + /** 菜单名称 */ + title: string +} diff --git a/src/common/utils/local-storage.ts b/src/common/utils/local-storage.ts index 3e7b543..9f752b7 100644 --- a/src/common/utils/local-storage.ts +++ b/src/common/utils/local-storage.ts @@ -2,6 +2,7 @@ import type { ThemeName } from "@@/composables/useTheme" import type { SidebarClosed, SidebarOpened } from "@@/constants/app-key" +import type { FavoriteMenu } from "@@/types/menu" import type { LayoutsConfig } from "@/layouts/config" import type { TagView } from "@/pinia/stores/tags-view" import { CacheKey } from "@@/constants/cache-key" @@ -79,3 +80,29 @@ export function setCachedViews(views: string[]) { localStorage.setItem(CacheKey.CACHED_VIEWS, JSON.stringify(views)) } // #endregion + +// #region 常用功能 +export function getFavoriteMenus() { + const json = localStorage.getItem(CacheKey.FAVORITE_MENUS) + return json ? (JSON.parse(json) as FavoriteMenu[]) : [] +} + +export function setFavoriteMenus(menus: FavoriteMenu[]) { + localStorage.setItem(CacheKey.FAVORITE_MENUS, JSON.stringify(menus)) +} +// #endregion + +// #region 保持登录(记住账号密码,对齐 school-iot-admin 的 localStorage.key) +export function getKeepLogin() { + const json = localStorage.getItem(CacheKey.KEEP_LOGIN) + return json ? (JSON.parse(json) as { username: string, password: string }) : null +} + +export function setKeepLogin(data: { username: string, password: string }) { + localStorage.setItem(CacheKey.KEEP_LOGIN, JSON.stringify(data)) +} + +export function removeKeepLogin() { + localStorage.removeItem(CacheKey.KEEP_LOGIN) +} +// #endregion diff --git a/src/http/axios.ts b/src/http/axios.ts index 7428fa8..a976ac2 100644 --- a/src/http/axios.ts +++ b/src/http/axios.ts @@ -38,8 +38,8 @@ function createInstance() { // Token 过期时 return useUserStore().logout() default: - // 不是正确的 code - ElMessage.error(apiData.message || "Error") + // 不是正确的 code(serverschool 信封的错误信息字段是 msg) + ElMessage.error(apiData.message || apiData.msg || "Error") return Promise.reject(apiData) } }, diff --git a/src/layouts/components/AppMain/index.vue b/src/layouts/components/AppMain/index.vue index ba2d2b1..8f00129 100644 --- a/src/layouts/components/AppMain/index.vue +++ b/src/layouts/components/AppMain/index.vue @@ -40,6 +40,8 @@ const settingsStore = useSettingsStore() .app-scrollbar { flex-grow: 1; overflow: auto; + // 内容区留白(对齐 school-iot-admin 的内容区内边距) + padding: 16px; @extend %scrollbar; display: flex; flex-direction: column; diff --git a/src/layouts/components/Logo/index.vue b/src/layouts/components/Logo/index.vue index f70bbeb..2d08dea 100644 --- a/src/layouts/components/Logo/index.vue +++ b/src/layouts/components/Logo/index.vue @@ -1,47 +1,74 @@ - - - - - - - - + + + + {{ PLATFORM_NAME }} + {{ PLATFORM_NAME_EN }} + + diff --git a/src/layouts/components/Sidebar/index.vue b/src/layouts/components/Sidebar/index.vue index be1ad04..883ed83 100644 --- a/src/layouts/components/Sidebar/index.vue +++ b/src/layouts/components/Sidebar/index.vue @@ -1,22 +1,13 @@ - - + !isTop.value ? "2px" : "0px") } } -.has-logo { - .el-scrollbar { - height: calc(100% - var(--v3-header-height)); - } +.sidebar-root { + height: 100%; } .el-scrollbar { @@ -109,6 +87,11 @@ const tipLineWidth = computed(() => !isTop.value ? "2px" : "0px") user-select: none; border: none; width: 100%; + // 菜单配色统一由 CSS 变量控制,方便多主题覆盖 + --el-menu-bg-color: var(--v3-sidebar-menu-bg-color); + --el-menu-text-color: var(--v3-sidebar-menu-text-color); + --el-menu-active-color: var(--v3-sidebar-menu-active-text-color); + --el-menu-hover-bg-color: var(--v3-sidebar-menu-hover-bg-color); } .el-menu--horizontal { @@ -121,22 +104,30 @@ const tipLineWidth = computed(() => !isTop.value ? "2px" : "0px") :deep(.el-menu--horizontal .el-menu-item) { height: v-bind(sidebarMenuItemHeight); line-height: v-bind(sidebarMenuItemHeight); + font-size: 16px; &.is-active, &:hover { background-color: v-bind(sidebarMenuHoverBgColor); } } -:deep(.el-sub-menu) { - &.is-active { - > .el-sub-menu__title { - color: v-bind(activeTextColor); - } +// 选中的菜单项(对齐 school-iot-admin:浅蓝底 + 左侧蓝色竖条 + 加粗) +:deep(.el-menu-item.is-active) { + @extend %tip-line; + background-color: v-bind(sidebarMenuActiveBgColor); + color: var(--el-menu-active-color); + font-weight: 700; + &:hover { + background-color: v-bind(sidebarMenuActiveBgColor); } } -:deep(.el-menu-item.is-active) { - @extend %tip-line; +:deep(.el-sub-menu) { + &.is-active { + > .el-sub-menu__title { + color: var(--el-menu-active-color); + } + } } .el-menu--collapse { @@ -146,5 +137,9 @@ const tipLineWidth = computed(() => !isTop.value ? "2px" : "0px") background-color: v-bind(sidebarMenuHoverBgColor); } } + // 折叠状态下隐藏常用功能入口(只保留图标) + :deep(.favorite-icon) { + display: none; + } } diff --git a/src/layouts/components/TagsView/index.vue b/src/layouts/components/TagsView/index.vue index 9f1ecd9..dddcdbe 100644 --- a/src/layouts/components/TagsView/index.vue +++ b/src/layouts/components/TagsView/index.vue @@ -217,12 +217,14 @@ listenerRouteChange((route) => { justify-content: center; position: relative; cursor: pointer; - height: 26px; + height: 28px; + padding: 0 10px; + font-size: 13px; border: 1px solid var(--v3-tagsview-tag-border-color); + border-bottom: none; border-radius: var(--v3-tagsview-tag-border-radius); - background-color: var(--v3-tagsview-tag-bg-color); - padding: 0 8px; - font-size: 12px; + background: var(--v3-tagsview-tag-bg); + color: #3a9bff; margin-left: 5px; &:first-of-type { margin-left: 5px; @@ -231,7 +233,7 @@ listenerRouteChange((route) => { margin-right: 5px; } &.active { - background-color: var(--v3-tagsview-tag-active-bg-color); + background: var(--v3-tagsview-tag-active-bg); color: var(--v3-tagsview-tag-active-text-color); border-color: var(--v3-tagsview-tag-active-border-color); } diff --git a/src/layouts/config.ts b/src/layouts/config.ts index 93a678b..e90237e 100644 --- a/src/layouts/config.ts +++ b/src/layouts/config.ts @@ -39,14 +39,14 @@ const DEFAULT_CONFIG: LayoutsConfig = { layoutMode: LayoutModeEnum.Left, showTagsView: true, fixedHeader: true, - showFooter: true, + showFooter: false, showLogo: true, showNotify: true, showThemeSwitch: true, showScreenfull: true, showSearchMenu: true, cacheTagsView: false, - showWatermark: true, + showWatermark: false, showGreyMode: false, showColorWeakness: false } diff --git a/src/layouts/modes/LeftMode.vue b/src/layouts/modes/LeftMode.vue index b88601e..b124a8a 100644 --- a/src/layouts/modes/LeftMode.vue +++ b/src/layouts/modes/LeftMode.vue @@ -3,7 +3,7 @@ import { useDevice } from "@@/composables/useDevice" import { useLayoutMode } from "@@/composables/useLayoutMode" import { useAppStore } from "@/pinia/stores/app" import { useSettingsStore } from "@/pinia/stores/settings" -import { AppMain, NavigationBar, Sidebar, TagsView } from "../components" +import { AppMain, Logo, NavigationBar, Sidebar, TagsView } from "../components" const { isMobile } = useDevice() @@ -13,7 +13,7 @@ const appStore = useAppStore() const settingsStore = useSettingsStore() -const { showTagsView, fixedHeader } = storeToRefs(settingsStore) +const { showTagsView, fixedHeader, showLogo } = storeToRefs(settingsStore) /** 定义计算属性 layoutClasses,用于控制布局的类名 */ const layoutClasses = computed(() => { @@ -36,15 +36,17 @@ function handleClickOutside() { + + + + + - - - - - - + + + @@ -59,6 +61,7 @@ $transition-time: 0.35s; @extend %clearfix; position: relative; width: 100%; + height: 100%; } .drawer-bg { @@ -70,65 +73,73 @@ $transition-time: 0.35s; z-index: 2001; } -.sidebar-container { - background-color: var(--v3-sidebar-menu-bg-color); - transition: width $transition-time; - width: var(--v3-sidebar-width); - height: 100%; - position: fixed; - top: 0; - bottom: 0; - left: 0; - z-index: 2002; - overflow: hidden; - border-right: var(--v3-sidebar-border-right); -} - -.main-container { - min-height: 100%; - transition: margin-left $transition-time; - margin-left: var(--v3-sidebar-width); +// 头部导航栏 +.layout-header { position: relative; + z-index: 2003; + display: flex; + align-items: center; + height: var(--v3-navigationbar-height); + background-color: var(--v3-header-bg-color); + color: var(--v3-navigationbar-text-color); + box-shadow: var(--v3-header-box-shadow); + border-bottom: var(--v3-header-border-bottom); + .logo { + flex: none; + padding-left: 20px; + } + .navigation-bar { + flex: 1; + min-width: 0; + } } .fixed-header { position: fixed !important; top: 0; + left: 0; right: 0; - z-index: 9; - width: calc(100% - var(--v3-sidebar-width)); +} + +// 左侧边栏 +.sidebar-container { + position: fixed; + top: var(--v3-navigationbar-height); + left: 0; + bottom: 0; + width: var(--v3-sidebar-width); + z-index: 2002; + overflow: hidden; + background-color: var(--v3-sidebar-menu-bg-color); + border-top-right-radius: 10px; + box-shadow: 0 0 10px rgba(32, 76, 131, 0.12); transition: width $transition-time; } -.layout-header { - position: relative; - z-index: 9; - background-color: var(--v3-header-bg-color); - box-shadow: var(--v3-header-box-shadow); - border-bottom: var(--v3-header-border-bottom); +// 主容器 +.main-container { + height: 100vh; + margin-left: var(--v3-sidebar-width); + display: flex; + flex-direction: column; + overflow: hidden; + transition: margin-left $transition-time; +} + +.has-fixed-header { + padding-top: var(--v3-navigationbar-height); +} + +.tags-view { + flex: none; } .app-main { - min-height: calc(100vh - var(--v3-navigationbar-height)); - position: relative; - overflow: hidden; -} - -.fixed-header + .app-main { - padding-top: var(--v3-navigationbar-height); - height: 100vh; - overflow: auto; -} - -.has-tags-view { - .app-main { - min-height: calc(100vh - var(--v3-header-height)); - } - .fixed-header + .app-main { - padding-top: var(--v3-header-height); - } + flex: 1; + min-height: 0; } +// 侧边栏折叠 .hide-sidebar { .sidebar-container { width: var(--v3-sidebar-hide-width); @@ -136,9 +147,6 @@ $transition-time: 0.35s; .main-container { margin-left: var(--v3-sidebar-hide-width); } - .fixed-header { - width: calc(100% - var(--v3-sidebar-hide-width)); - } } // 适配 mobile 端 @@ -150,12 +158,10 @@ $transition-time: 0.35s; .main-container { margin-left: 0px; } - .fixed-header { - width: 100%; - } &.open-sidebar { position: fixed; top: 0; + width: 100%; } &.hide-sidebar { .sidebar-container { @@ -164,12 +170,6 @@ $transition-time: 0.35s; transform: translate3d(calc(0px - var(--v3-sidebar-width)), 0, 0); } } - // 既是 mobile 又是顶部或混合布局模式 - &.no-left { - .sidebar-container { - background-color: var(--el-bg-color); - } - } } .without-animation { diff --git a/src/layouts/modes/LeftTopMode.vue b/src/layouts/modes/LeftTopMode.vue index 3698b40..7c09d30 100644 --- a/src/layouts/modes/LeftTopMode.vue +++ b/src/layouts/modes/LeftTopMode.vue @@ -19,18 +19,17 @@ const layoutClasses = computed(() => { - + - - - - + + + - - - + + + @@ -44,70 +43,75 @@ $transition-time: 0.35s; .app-wrapper { @extend %clearfix; width: 100%; + height: 100%; } .fixed-header { position: fixed; top: 0; - z-index: 1002; - width: 100%; + left: 0; + right: 0; + z-index: 2003; display: flex; + align-items: center; + height: var(--v3-navigationbar-height); .logo { flex: none; width: var(--v3-sidebar-width); + padding-left: 20px; } - .content { + .navigation-bar { flex: 1; - overflow: hidden; + min-width: 0; } } .layout-header { background-color: var(--v3-header-bg-color); + color: var(--v3-navigationbar-text-color); box-shadow: var(--v3-header-box-shadow); border-bottom: var(--v3-header-border-bottom); } .main-container { - min-height: 100%; + height: 100vh; + margin-left: var(--v3-sidebar-width); + padding-top: var(--v3-navigationbar-height); + display: flex; + flex-direction: column; + overflow: hidden; + transition: margin-left $transition-time; } .sidebar-container { - background-color: var(--el-menu-bg-color); - transition: width $transition-time; - width: var(--v3-sidebar-width); - height: 100%; position: fixed; + top: var(--v3-navigationbar-height); left: 0; - z-index: 1001; + bottom: 0; + width: var(--v3-sidebar-width); + z-index: 2002; overflow: hidden; - border-right: var(--v3-sidebar-border-right); - padding-top: var(--v3-navigationbar-height); + background-color: var(--v3-sidebar-menu-bg-color); + border-top-right-radius: 10px; + box-shadow: 0 0 10px rgba(32, 76, 131, 0.12); + transition: width $transition-time; +} + +.tags-view { + flex: none; } .app-main { - transition: padding-left $transition-time; - padding-top: var(--v3-navigationbar-height); - padding-left: var(--v3-sidebar-width); - height: 100vh; - overflow: auto; + flex: 1; + min-height: 0; } .hide-sidebar { .sidebar-container { width: var(--v3-sidebar-hide-width); } - .app-main { - padding-left: var(--v3-sidebar-hide-width); - } -} - -.has-tags-view { - .sidebar-container { - padding-top: var(--v3-header-height); - } - .app-main { - padding-top: var(--v3-header-height); + .main-container { + margin-left: var(--v3-sidebar-hide-width); } } diff --git a/src/layouts/modes/TopMode.vue b/src/layouts/modes/TopMode.vue index 12568bf..95e6530 100644 --- a/src/layouts/modes/TopMode.vue +++ b/src/layouts/modes/TopMode.vue @@ -9,16 +9,15 @@ const { showTagsView, showLogo } = storeToRefs(settingsStore) - + - - - - - + + + + @@ -32,44 +31,49 @@ $transition-time: 0.35s; .app-wrapper { @extend %clearfix; width: 100%; + height: 100%; } .fixed-header { position: fixed; top: 0; - z-index: 1002; - width: 100%; + left: 0; + right: 0; + z-index: 2003; + display: flex; + align-items: center; + height: var(--v3-navigationbar-height); .logo { - width: var(--v3-sidebar-width); + flex: none; + padding-left: 20px; } - .content { - display: flex; - .navigation-bar { - flex: 1; - } + .navigation-bar { + flex: 1; + min-width: 0; } } .layout-header { background-color: var(--v3-header-bg-color); + color: var(--v3-navigationbar-text-color); box-shadow: var(--v3-header-box-shadow); border-bottom: var(--v3-header-border-bottom); } .main-container { - min-height: 100%; + height: 100vh; + padding-top: var(--v3-navigationbar-height); + display: flex; + flex-direction: column; + overflow: hidden; +} + +.tags-view { + flex: none; } .app-main { - transition: padding-left $transition-time; - padding-top: var(--v3-navigationbar-height); - height: 100vh; - overflow: auto; -} - -.has-tags-view { - .app-main { - padding-top: var(--v3-header-height); - } + flex: 1; + min-height: 0; } diff --git a/src/pages/login/apis/index.ts b/src/pages/login/apis/index.ts index 3c559a3..a3c6b3f 100644 --- a/src/pages/login/apis/index.ts +++ b/src/pages/login/apis/index.ts @@ -1,19 +1,75 @@ -import type * as Auth from "./type" -import { request } from "@/http/axios" +import type * as Login from "./type" +import axios from "axios" -/** 获取登录验证码 */ -export function getCaptchaApi() { - return request({ - url: "auth/captcha", - method: "get" - }) +/** serverschool OAuth2 客户端信息(与 school-iot-admin 保持一致,公开客户端) */ +const OAUTH_CLIENT_ID = "client" +const OAUTH_CLIENT_SECRET = "123456" +const OAUTH_GRANT_TYPE = "password" + +/** serverschool 的 Result 信封(/wx、/sms 等老接口同样是这个结构) */ +export interface SchoolResult { + success: boolean + msg?: string + code?: number + data?: T } -/** 登录并返回 Token */ -export function loginApi(data: Auth.LoginRequestData) { - return request({ - url: "auth/login", - method: "post", - data - }) +const baseURL = import.meta.env.VITE_SCHOOL_BASE_URL + +/** + * 登录并返回 Token + * /oauth/token 返回的是 OAuth2 令牌结构(非 Result 信封格式),所以单独用 axios 请求 + * 账号或密码错误时后端返回 400,axios 会直接 reject + */ +export function loginApi(data: Login.LoginRequestData) { + return axios + .get(`${baseURL}/oauth/token`, { + params: { + username: data.username, + password: data.password, + client_id: OAUTH_CLIENT_ID, + client_secret: OAUTH_CLIENT_SECRET, + grant_type: OAUTH_GRANT_TYPE + }, + timeout: 10000 + }) + .then(res => res.data) +} + +/** + * 根据手机号查询账号列表(对齐 school-iot-admin 的 /wx/getAccoutList) + * 返回 [{ userName, teamName, ... }],查不到时 success=false + */ +export function getAccountListApi(phone: string) { + return axios + .get>(`${baseURL}/wx/getAccoutList`, { + params: { phone }, + timeout: 10000 + }) + .then(res => res.data) +} + +/** 发送短信验证码(POST /sms/sendCode?phone=) */ +export function sendSmsCodeApi(phone: string) { + return axios + .post>(`${baseURL}/sms/sendCode`, null, { + params: { phone }, + timeout: 15000 + }) + .then(res => res.data) +} + +/** 校验验证码并重置密码(POST /sms/verifyCode?userName=&newPwd=&phone=&code=) */ +export function verifySmsCodeApi(data: Login.ResetPasswordData) { + return axios + .post(`${baseURL}/sms/verifyCode`, null, { + params: { + userName: data.userName, + newPwd: data.newPassword, + phone: data.phone, + code: data.code + }, + timeout: 15000 + }) + .then(res => res.data) } diff --git a/src/pages/login/apis/type.ts b/src/pages/login/apis/type.ts index 86c0e2e..7c550fa 100644 --- a/src/pages/login/apis/type.ts +++ b/src/pages/login/apis/type.ts @@ -1,12 +1,36 @@ +/** 登录请求数据 */ export interface LoginRequestData { - /** admin 或 editor */ - username: "admin" | "editor" + /** 用户名(输入 11 位手机号可查询对应账号) */ + username: string /** 密码 */ password: string - /** 验证码 */ - code: string } -export type CaptchaResponseData = ApiResponseData +/** OAuth2 令牌响应(/oauth/token) */ +export interface OAuthToken { + access_token: string + token_type: string + refresh_token?: string + expires_in: number + scope?: string +} -export type LoginResponseData = ApiResponseData<{ token: string }> +/** 手机号查询出的账号(/wx/getAccoutList) */ +export interface AccountItem { + userName: string + /** 所属学校/分组名称 */ + teamName?: string + [key: string]: unknown +} + +/** 短信验证码重置密码(/sms/verifyCode) */ +export interface ResetPasswordData { + /** 用户名 */ + userName: string + /** 新密码 */ + newPassword: string + /** 手机号 */ + phone: string + /** 短信验证码 */ + code: string +} diff --git a/src/pages/login/components/Forget.vue b/src/pages/login/components/Forget.vue new file mode 100644 index 0000000..3d9a1ef --- /dev/null +++ b/src/pages/login/components/Forget.vue @@ -0,0 +1,278 @@ + + + + + + + + + + + + + + + + + + + + + + + + + {{ countdown > 0 ? `${countdown}s 后重试` : "获取验证码" }} + + + + + + + + + + + + 提交 + + + 返回登录 + + + + + + 关联的小程序旧密码失效,用修改后的密码进行登入 + + + + 确 定 + + + + + + + diff --git a/src/pages/login/images/login-background.png b/src/pages/login/images/login-background.png new file mode 100644 index 0000000..b752f29 Binary files /dev/null and b/src/pages/login/images/login-background.png differ diff --git a/src/pages/login/index.vue b/src/pages/login/index.vue index 1b48390..0be7882 100644 --- a/src/pages/login/index.vue +++ b/src/pages/login/index.vue @@ -1,13 +1,12 @@ - - - - - - - - - - - - - - - - - - - - - + + {{ LOGIN_TITLE }} + + + + + + 用户登录 + + + + + + + + {{ errorText }} + + + + + + + + + + - - - - - - - - - - - - 登 录 - - + + + + + {{ item.userName }} + {{ item.teamName }} + + + + + + + + + + + 保持登录 + + + 忘记密码 + + + + + 登 录 + + + + + + 忘记密码 + + + + + + 成都鸿兴运达科技公司 蜀ICP备19023702号-4 +
关联的小程序旧密码失效,用修改后的密码进行登入
成都鸿兴运达科技公司 蜀ICP备19023702号-4