feat: 新增主题切换、个人中心页面,优化界面样式与登录流程
1. 新增Pinia主题状态管理,支持明暗主题切换并持久化配置 2. 新增个人中心页面,支持用户信息编辑、密码修改与主题设置 3. 登录后缓存用户信息到localStorage,完善用户操作链路 4. 重构菜单命令注册逻辑,引入tauri-helper简化开发 5. 优化全局样式系统,使用CSS变量实现主题切换 6. 新增用户详情API接口,完善后端服务能力 7. 修复S3 SDK导入冗余依赖问题,优化菜单更新SQL执行逻辑 8. 调整侧边栏用户菜单,新增个人中心跳转入口
This commit is contained in:
parent
577b82291b
commit
faf181e67a
|
|
@ -17,6 +17,7 @@ dependencies = [
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
|
"tauri-helper",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1204,6 +1205,25 @@ dependencies = [
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-deque"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-epoch",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-epoch"
|
||||||
|
version = "0.9.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-queue"
|
name = "crossbeam-queue"
|
||||||
version = "0.3.13"
|
version = "0.3.13"
|
||||||
|
|
@ -3827,6 +3847,26 @@ version = "0.6.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rayon"
|
||||||
|
version = "1.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
"rayon-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rayon-core"
|
||||||
|
version = "1.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-deque",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.18"
|
version = "0.5.18"
|
||||||
|
|
@ -5044,6 +5084,21 @@ dependencies = [
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-helper"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6b6ed54c4a04e658f8f1c57513abbeb5e2493c9db1d31987c9cc6db6c19a245c"
|
||||||
|
dependencies = [
|
||||||
|
"rayon",
|
||||||
|
"serde",
|
||||||
|
"syn 2.0.119",
|
||||||
|
"tauri_helper_core",
|
||||||
|
"tauri_helper_macros",
|
||||||
|
"toml 0.9.12+spec-1.1.0",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-macros"
|
name = "tauri-macros"
|
||||||
version = "2.6.3"
|
version = "2.6.3"
|
||||||
|
|
@ -5196,6 +5251,33 @@ dependencies = [
|
||||||
"toml 1.1.3+spec-1.1.0",
|
"toml 1.1.3+spec-1.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri_helper_core"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6da0ac433082491b94f3ba7f5087ee888608917bd371bb213ac16bc722547541"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"toml 0.9.12+spec-1.1.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri_helper_macros"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d6da533f4bd8a64d991a8ebca6abc2243d12eae7190c69ccdfa3483f82080b9"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-error",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"serde",
|
||||||
|
"syn 2.0.119",
|
||||||
|
"tauri_helper_core",
|
||||||
|
"thiserror 2.0.19",
|
||||||
|
"toml 0.9.12+spec-1.1.0",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.27.0"
|
version = "3.27.0"
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,12 @@ edition = "2021"
|
||||||
name = "DevBox_lib"
|
name = "DevBox_lib"
|
||||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["."]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2", features = [] }
|
tauri-build = { version = "2", features = [] }
|
||||||
|
tauri-helper = "0.2.1"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "2", features = [] }
|
tauri = { version = "2", features = [] }
|
||||||
|
|
@ -34,3 +38,4 @@ aws-sdk-s3 = "1.140.0"
|
||||||
aws-config = {version = "1.10.1", features = ["behavior-version-latest"] }
|
aws-config = {version = "1.10.1", features = ["behavior-version-latest"] }
|
||||||
aws-credential-types = "1.3.0"
|
aws-credential-types = "1.3.0"
|
||||||
aws-types = "1.5.0"
|
aws-types = "1.5.0"
|
||||||
|
tauri-helper = "0.2.1"
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
fn main() {
|
fn main() {
|
||||||
tauri_build::build()
|
// tauri_build::build()
|
||||||
|
tauri_helper::generate_command_file(tauri_helper::TauriHelperOptions::default());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -188,7 +188,7 @@ pub async fn menu_update(
|
||||||
pool: State<'_, MySqlPool>,
|
pool: State<'_, MySqlPool>,
|
||||||
params: MenuSaveReq,
|
params: MenuSaveReq,
|
||||||
) -> Result<ApiResponse<()>, String> {
|
) -> Result<ApiResponse<()>, String> {
|
||||||
let result = sqlx::query("UPDATE sys_menu SET parent_id = ?, name = ?, title = ?, path = ?, component = ?, icon = ?, perms = ?, menu_type = ?, sort = ?, visible = ?, status = ? WHERE id = ?")
|
sqlx::query("UPDATE sys_menu SET parent_id = ?, name = ?, title = ?, path = ?, component = ?, icon = ?, perms = ?, menu_type = ?, sort = ?, visible = ?, status = ? WHERE id = ?")
|
||||||
.bind(params.parent_id)
|
.bind(params.parent_id)
|
||||||
.bind(params.name)
|
.bind(params.name)
|
||||||
.bind(params.title)
|
.bind(params.title)
|
||||||
|
|
|
||||||
|
|
@ -25,22 +25,23 @@ pub fn run() {
|
||||||
app.manage(s3_client);
|
app.manage(s3_client);
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri_helper::tauri_collect_commands!())
|
||||||
commands::user::login,
|
// .invoke_handler(tauri::generate_handler![
|
||||||
commands::user::get_user_page,
|
// commands::user::login,
|
||||||
commands::user::register,
|
// commands::user::get_user_page,
|
||||||
commands::user::del_user,
|
// commands::user::register,
|
||||||
commands::user::update_user,
|
// commands::user::del_user,
|
||||||
commands::user::get_user_by_id,
|
// commands::user::update_user,
|
||||||
commands::role::role_save,
|
// commands::user::get_user_by_id,
|
||||||
commands::role::role_page,
|
// commands::role::role_save,
|
||||||
commands::role::role_update,
|
// commands::role::role_page,
|
||||||
commands::role::role_del,
|
// commands::role::role_update,
|
||||||
commands::menu::menu_list,
|
// commands::role::role_del,
|
||||||
commands::menu::menu_save,
|
// commands::menu::menu_list,
|
||||||
commands::menu::menu_update,
|
// commands::menu::menu_save,
|
||||||
commands::menu::menu_del,
|
// commands::menu::menu_update,
|
||||||
])
|
// commands::menu::menu_del,
|
||||||
|
// ])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use aws_credential_types::Credentials as AwsCreds;
|
use aws_credential_types::Credentials as AwsCreds;
|
||||||
use aws_sdk_s3::{
|
use aws_sdk_s3::{
|
||||||
config::{BehaviorVersion, Credentials, Region},
|
config::{BehaviorVersion, Region},
|
||||||
Client,
|
Client,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
30
src/App.vue
30
src/App.vue
|
|
@ -1,14 +1,30 @@
|
||||||
<template>
|
<template>
|
||||||
<n-message-provider>
|
<n-config-provider :theme="themeStore.naiveTheme" :theme-overrides="themeOverrides">
|
||||||
<n-dialog-provider>
|
<n-message-provider>
|
||||||
<div class="container">
|
<n-dialog-provider>
|
||||||
<router-view />
|
<div class="container">
|
||||||
</div>
|
<router-view />
|
||||||
</n-dialog-provider>
|
</div>
|
||||||
</n-message-provider>
|
</n-dialog-provider>
|
||||||
|
</n-message-provider>
|
||||||
|
</n-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import type { GlobalThemeOverrides } from 'naive-ui'
|
||||||
|
import { useThemeStore } from '@/stores/theme'
|
||||||
|
|
||||||
|
const themeStore = useThemeStore()
|
||||||
|
|
||||||
|
const themeOverrides = computed<GlobalThemeOverrides>(() => ({
|
||||||
|
common: {
|
||||||
|
primaryColor: '#0891B2',
|
||||||
|
primaryColorHover: '#0E7490',
|
||||||
|
primaryColorPressed: '#155E75',
|
||||||
|
primaryColorSuppl: '#06B6D4',
|
||||||
|
},
|
||||||
|
}))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,10 @@ const UserApi = {
|
||||||
update_user: async (params: UserSaveReq): Promise<ApiResponse<any>> => {
|
update_user: async (params: UserSaveReq): Promise<ApiResponse<any>> => {
|
||||||
return await invoke("update_user", { params })
|
return await invoke("update_user", { params })
|
||||||
},
|
},
|
||||||
|
// 根据ID获取用户信息
|
||||||
|
getUserById: async (id: number): Promise<ApiResponse<UserInfo>> => {
|
||||||
|
return await invoke("get_user_by_id", { id })
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default UserApi
|
export default UserApi
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="layout-shell">
|
<div class="layout-shell" :style="shellVars">
|
||||||
<n-layout has-sider class="layout-root">
|
<n-layout has-sider class="layout-root">
|
||||||
<!-- 侧边栏 -->
|
<!-- 侧边栏 -->
|
||||||
<LayoutSider />
|
<LayoutSider />
|
||||||
|
|
@ -27,11 +27,12 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { watch } from 'vue'
|
import { watch, computed } from 'vue'
|
||||||
import type { Component } from 'vue'
|
import type { Component } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { GridOutline, PeopleOutline, ShieldCheckmarkOutline } from '@vicons/ionicons5'
|
import { GridOutline, PeopleOutline, ShieldCheckmarkOutline } from '@vicons/ionicons5'
|
||||||
import { useLayoutStore } from '@/stores/layout'
|
import { useLayoutStore } from '@/stores/layout'
|
||||||
|
import { useThemeStore } from '@/stores/theme'
|
||||||
import LayoutSider from './components/LayoutSider.vue'
|
import LayoutSider from './components/LayoutSider.vue'
|
||||||
import LayoutHeader from './components/LayoutHeader.vue'
|
import LayoutHeader from './components/LayoutHeader.vue'
|
||||||
import LayoutTabs from './components/LayoutTabs.vue'
|
import LayoutTabs from './components/LayoutTabs.vue'
|
||||||
|
|
@ -39,6 +40,7 @@ import LayoutFooter from './components/LayoutFooter.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const layoutStore = useLayoutStore()
|
const layoutStore = useLayoutStore()
|
||||||
|
const themeStore = useThemeStore()
|
||||||
|
|
||||||
// 路由图标映射
|
// 路由图标映射
|
||||||
const routeIconMap: Record<string, Component> = {
|
const routeIconMap: Record<string, Component> = {
|
||||||
|
|
@ -58,6 +60,26 @@ watch(
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const shellVars = computed(() =>
|
||||||
|
themeStore.isDark
|
||||||
|
? {
|
||||||
|
'--color-bg': '#101014',
|
||||||
|
'--color-surface': '#18181c',
|
||||||
|
'--color-border': '#2a2a2f',
|
||||||
|
'--color-text': '#ffffffd9',
|
||||||
|
'--color-text-secondary': '#ffffff73',
|
||||||
|
'--color-primary': '#0891B2',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
'--color-bg': '#F2F3F5',
|
||||||
|
'--color-surface': '#FFFFFF',
|
||||||
|
'--color-border': '#E5E6EB',
|
||||||
|
'--color-text': '#1D2129',
|
||||||
|
'--color-text-secondary': '#86909C',
|
||||||
|
'--color-primary': '#0891B2',
|
||||||
|
}
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
@ -65,12 +87,6 @@ watch(
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
--color-bg: #F2F3F5;
|
|
||||||
--color-surface: #FFFFFF;
|
|
||||||
--color-border: #E5E6EB;
|
|
||||||
--color-text: #1D2129;
|
|
||||||
--color-text-secondary: #86909C;
|
|
||||||
--color-primary: #0891B2;
|
|
||||||
--header-height: 50px;
|
--header-height: 50px;
|
||||||
--tabs-height: 42px;
|
--tabs-height: 42px;
|
||||||
--footer-height: 36px;
|
--footer-height: 36px;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,11 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
|
<n-button quaternary circle size="small" @click="themeStore.toggle()">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon size="18" :component="themeStore.isDark ? SunnyOutline : MoonOutline" />
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
<n-button quaternary circle size="small" @click="toggleFullscreen">
|
<n-button quaternary circle size="small" @click="toggleFullscreen">
|
||||||
<template #icon><n-icon size="18" :component="ExpandOutline" /></template>
|
<template #icon><n-icon size="18" :component="ExpandOutline" /></template>
|
||||||
</n-button>
|
</n-button>
|
||||||
|
|
@ -56,13 +61,17 @@ import {
|
||||||
PersonOutline,
|
PersonOutline,
|
||||||
SettingsOutline,
|
SettingsOutline,
|
||||||
LogOutOutline,
|
LogOutOutline,
|
||||||
|
MoonOutline,
|
||||||
|
SunnyOutline,
|
||||||
} from '@vicons/ionicons5'
|
} from '@vicons/ionicons5'
|
||||||
import { useLayoutStore } from '@/stores/layout'
|
import { useLayoutStore } from '@/stores/layout'
|
||||||
|
import { useThemeStore } from '@/stores/theme'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const layoutStore = useLayoutStore()
|
const layoutStore = useLayoutStore()
|
||||||
|
const themeStore = useThemeStore()
|
||||||
|
|
||||||
const username = 'SparksFly'
|
const username = 'SparksFly'
|
||||||
const userInitial = computed(() => username.charAt(0).toUpperCase())
|
const userInitial = computed(() => username.charAt(0).toUpperCase())
|
||||||
|
|
@ -81,7 +90,9 @@ const userDropdownOptions: DropdownOption[] = [
|
||||||
]
|
]
|
||||||
|
|
||||||
function onUserAction(key: string) {
|
function onUserAction(key: string) {
|
||||||
if (key === 'logout') {
|
if (key === 'profile') {
|
||||||
|
router.push('/profile')
|
||||||
|
} else if (key === 'logout') {
|
||||||
message.success('已退出登录')
|
message.success('已退出登录')
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,9 @@ const userDropdownOptions: DropdownOption[] = [
|
||||||
]
|
]
|
||||||
|
|
||||||
function onUserAction(key: string) {
|
function onUserAction(key: string) {
|
||||||
if (key === 'logout') {
|
if (key === 'profile') {
|
||||||
|
router.push('/profile')
|
||||||
|
} else if (key === 'logout') {
|
||||||
message.success('已退出登录')
|
message.success('已退出登录')
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,12 @@ const routes = [
|
||||||
meta: { title: "菜单管理", icon: "MenuOutline", tabFixed: true },
|
meta: { title: "菜单管理", icon: "MenuOutline", tabFixed: true },
|
||||||
component: () => import("@/views/system/SysMenu.vue"),
|
component: () => import("@/views/system/SysMenu.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/profile",
|
||||||
|
name: "Profile",
|
||||||
|
meta: { title: "个人中心", icon: "PersonOutline", tabFixed: true },
|
||||||
|
component: () => import("@/views/system/Profile.vue"),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { darkTheme } from 'naive-ui'
|
||||||
|
import type { GlobalTheme } from 'naive-ui'
|
||||||
|
|
||||||
|
export const useThemeStore = defineStore('theme', () => {
|
||||||
|
const isDark = ref<boolean>(
|
||||||
|
localStorage.getItem('theme') === 'dark'
|
||||||
|
)
|
||||||
|
|
||||||
|
const naiveTheme = computed<GlobalTheme | null>(() =>
|
||||||
|
isDark.value ? darkTheme : null
|
||||||
|
)
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
isDark.value = !isDark.value
|
||||||
|
localStorage.setItem('theme', isDark.value ? 'dark' : 'light')
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isDark, naiveTheme, toggle }
|
||||||
|
})
|
||||||
|
|
@ -111,8 +111,8 @@ const repos = ref([
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page-dashboard {
|
.page-dashboard {
|
||||||
padding: 28px 24px;
|
padding: 28px 24px;
|
||||||
max-width: 1200px;
|
width: 100%;
|
||||||
margin: 0 auto;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
|
|
@ -124,14 +124,14 @@ const repos = ref([
|
||||||
font-family: 'Space Grotesk', system-ui, sans-serif;
|
font-family: 'Space Grotesk', system-ui, sans-serif;
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1D2129;
|
color: var(--color-text, #1D2129);
|
||||||
letter-spacing: -0.3px;
|
letter-spacing: -0.3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-subtitle {
|
.page-subtitle {
|
||||||
margin: 4px 0 0;
|
margin: 4px 0 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #86909C;
|
color: var(--color-text-secondary, #86909C);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── 统计卡片 ─── */
|
/* ─── 统计卡片 ─── */
|
||||||
|
|
@ -148,8 +148,8 @@ const repos = ref([
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background: #fff;
|
background: var(--color-surface, #fff);
|
||||||
border: 1px solid #E5E6EB;
|
border: 1px solid var(--color-border, #E5E6EB);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
transition: box-shadow 0.2s, transform 0.2s;
|
transition: box-shadow 0.2s, transform 0.2s;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
@ -179,14 +179,14 @@ const repos = ref([
|
||||||
font-family: 'Space Grotesk', system-ui, sans-serif;
|
font-family: 'Space Grotesk', system-ui, sans-serif;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1D2129;
|
color: var(--color-text, #1D2129);
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #86909C;
|
color: var(--color-text-secondary, #86909C);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-badge {
|
.stat-badge {
|
||||||
|
|
@ -220,8 +220,8 @@ const repos = ref([
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background: #fff;
|
background: var(--color-surface, #fff);
|
||||||
border: 1px solid #E5E6EB;
|
border: 1px solid var(--color-border, #E5E6EB);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
@ -238,7 +238,7 @@ const repos = ref([
|
||||||
font-family: 'Space Grotesk', system-ui, sans-serif;
|
font-family: 'Space Grotesk', system-ui, sans-serif;
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #1D2129;
|
color: var(--color-text, #1D2129);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-link {
|
.card-link {
|
||||||
|
|
@ -268,7 +268,7 @@ const repos = ref([
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #86909C;
|
color: var(--color-text-secondary, #86909C);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 仓库列表 */
|
/* 仓库列表 */
|
||||||
|
|
@ -281,7 +281,7 @@ const repos = ref([
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 12px 0;
|
padding: 12px 0;
|
||||||
border-bottom: 1px solid #E5E6EB;
|
border-bottom: 1px solid var(--color-border, #E5E6EB);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -305,12 +305,12 @@ const repos = ref([
|
||||||
.repo-name {
|
.repo-name {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #1D2129;
|
color: var(--color-text, #1D2129);
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo-desc {
|
.repo-desc {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #86909C;
|
color: var(--color-text-secondary, #86909C);
|
||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -321,7 +321,7 @@ const repos = ref([
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 40px 20px;
|
padding: 40px 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #86909C;
|
color: var(--color-text-secondary, #86909C);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,7 @@ const handleLogin = () => {
|
||||||
try {
|
try {
|
||||||
const res = await UserApi.login(formData.username, formData.password)
|
const res = await UserApi.login(formData.username, formData.password)
|
||||||
if (res.success === true) {
|
if (res.success === true) {
|
||||||
|
localStorage.setItem('userInfo', JSON.stringify(res.data))
|
||||||
message.success('登录成功')
|
message.success('登录成功')
|
||||||
router.push('/dashboard')
|
router.push('/dashboard')
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,452 @@
|
||||||
|
<template>
|
||||||
|
<div class="page-profile">
|
||||||
|
<div class="page-toolbar">
|
||||||
|
<h2 class="page-title">个人中心</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-spin :show="loading">
|
||||||
|
<div class="profile-layout">
|
||||||
|
<!-- 左侧信息卡 -->
|
||||||
|
<div class="profile-card card">
|
||||||
|
<div class="avatar-wrap">
|
||||||
|
<n-avatar round :size="80" :style="{ backgroundColor: '#0891B2', fontSize: '32px' }">
|
||||||
|
{{ userInitial }}
|
||||||
|
</n-avatar>
|
||||||
|
<div class="avatar-info">
|
||||||
|
<span class="display-name">{{ form.nick_name || form.user_name }}</span>
|
||||||
|
<n-tag type="info" size="small" round>{{ genderLabel }}</n-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-divider />
|
||||||
|
|
||||||
|
<div class="meta-list">
|
||||||
|
<div class="meta-item">
|
||||||
|
<n-icon :component="PersonOutline" size="16" class="meta-icon" />
|
||||||
|
<span class="meta-label">用户名</span>
|
||||||
|
<span class="meta-value">{{ form.user_name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<n-icon :component="MailOutline" size="16" class="meta-icon" />
|
||||||
|
<span class="meta-label">邮箱</span>
|
||||||
|
<span class="meta-value">{{ form.email || '未设置' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<n-icon :component="CallOutline" size="16" class="meta-icon" />
|
||||||
|
<span class="meta-label">手机</span>
|
||||||
|
<span class="meta-value">{{ form.phone || '未设置' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<n-icon :component="CalendarOutline" size="16" class="meta-icon" />
|
||||||
|
<span class="meta-label">年龄</span>
|
||||||
|
<span class="meta-value">{{ form.age || '未设置' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<n-icon :component="TimeOutline" size="16" class="meta-icon" />
|
||||||
|
<span class="meta-label">注册时间</span>
|
||||||
|
<span class="meta-value">{{ userInfo?.create_time || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧编辑区 -->
|
||||||
|
<div class="profile-edit card">
|
||||||
|
<n-tabs type="line" animated>
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<n-tab-pane name="info" tab="基本信息">
|
||||||
|
<n-form
|
||||||
|
ref="infoFormRef"
|
||||||
|
:model="form"
|
||||||
|
:rules="infoRules"
|
||||||
|
label-placement="left"
|
||||||
|
label-width="80"
|
||||||
|
require-mark-placement="right-hanging"
|
||||||
|
class="edit-form"
|
||||||
|
>
|
||||||
|
<n-form-item label="昵称" path="nick_name">
|
||||||
|
<n-input v-model:value="form.nick_name" placeholder="请输入昵称" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="邮箱" path="email">
|
||||||
|
<n-input v-model:value="form.email" placeholder="请输入邮箱" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="手机" path="phone">
|
||||||
|
<n-input v-model:value="form.phone" placeholder="请输入手机号" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="年龄" path="age">
|
||||||
|
<n-input-number v-model:value="form.age" :min="0" :max="150" :style="{ width: '100%' }" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="性别" path="sex">
|
||||||
|
<n-radio-group v-model:value="form.sex">
|
||||||
|
<n-radio :value="0">保密</n-radio>
|
||||||
|
<n-radio :value="1">男</n-radio>
|
||||||
|
<n-radio :value="2">女</n-radio>
|
||||||
|
</n-radio-group>
|
||||||
|
</n-form-item>
|
||||||
|
<div class="form-actions">
|
||||||
|
<n-button type="primary" :loading="saving" @click="saveInfo">保存修改</n-button>
|
||||||
|
</div>
|
||||||
|
</n-form>
|
||||||
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<!-- 修改密码 -->
|
||||||
|
<n-tab-pane name="password" tab="修改密码">
|
||||||
|
<n-form
|
||||||
|
ref="pwdFormRef"
|
||||||
|
:model="pwdForm"
|
||||||
|
:rules="pwdRules"
|
||||||
|
label-placement="left"
|
||||||
|
label-width="90"
|
||||||
|
require-mark-placement="right-hanging"
|
||||||
|
class="edit-form"
|
||||||
|
>
|
||||||
|
<n-form-item label="新密码" path="password">
|
||||||
|
<n-input
|
||||||
|
v-model:value="pwdForm.password"
|
||||||
|
type="password"
|
||||||
|
show-password-on="click"
|
||||||
|
placeholder="请输入新密码(至少6位)"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="确认密码" path="confirm">
|
||||||
|
<n-input
|
||||||
|
v-model:value="pwdForm.confirm"
|
||||||
|
type="password"
|
||||||
|
show-password-on="click"
|
||||||
|
placeholder="再次输入新密码"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<div class="form-actions">
|
||||||
|
<n-button type="primary" :loading="pwdSaving" @click="savePassword">更新密码</n-button>
|
||||||
|
</div>
|
||||||
|
</n-form>
|
||||||
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<!-- 外观设置 -->
|
||||||
|
<n-tab-pane name="appearance" tab="外观设置">
|
||||||
|
<div class="appearance-section">
|
||||||
|
<div class="appearance-item">
|
||||||
|
<div class="appearance-label">
|
||||||
|
<span class="appearance-title">深色模式</span>
|
||||||
|
<span class="appearance-desc">切换界面明暗主题</span>
|
||||||
|
</div>
|
||||||
|
<n-switch :value="themeStore.isDark" @update:value="themeStore.toggle()">
|
||||||
|
<template #checked-icon><n-icon :component="MoonOutline" /></template>
|
||||||
|
<template #unchecked-icon><n-icon :component="SunnyOutline" /></template>
|
||||||
|
</n-switch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-tab-pane>
|
||||||
|
</n-tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-spin>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
import type { FormInst, FormRules } from 'naive-ui'
|
||||||
|
import {
|
||||||
|
PersonOutline, MailOutline, CallOutline, CalendarOutline,
|
||||||
|
TimeOutline, MoonOutline, SunnyOutline,
|
||||||
|
} from '@vicons/ionicons5'
|
||||||
|
import { useThemeStore } from '@/stores/theme'
|
||||||
|
import UserApi from '@/api/system/user'
|
||||||
|
import type { UserInfo } from '@/api/system/user'
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
const themeStore = useThemeStore()
|
||||||
|
|
||||||
|
const userInfo = ref<UserInfo | null>(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
nick_name: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
age: 0,
|
||||||
|
sex: 0,
|
||||||
|
user_name: '',
|
||||||
|
avatar: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
function applyUserInfo(u: UserInfo) {
|
||||||
|
userInfo.value = u
|
||||||
|
form.nick_name = u.nick_name
|
||||||
|
form.email = u.email
|
||||||
|
form.phone = u.phone
|
||||||
|
form.age = u.age
|
||||||
|
form.sex = u.sex
|
||||||
|
form.user_name = u.user_name
|
||||||
|
form.avatar = u.avatar
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const stored = localStorage.getItem('userInfo')
|
||||||
|
if (!stored) {
|
||||||
|
message.warning('未找到登录信息,请重新登录')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const cached: UserInfo = JSON.parse(stored)
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await UserApi.getUserById(cached.id)
|
||||||
|
if (res.success && res.data) {
|
||||||
|
applyUserInfo(res.data)
|
||||||
|
localStorage.setItem('userInfo', JSON.stringify(res.data))
|
||||||
|
} else {
|
||||||
|
message.error(res.message || '获取用户信息失败')
|
||||||
|
applyUserInfo(cached)
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e?.toString() ?? '获取用户信息失败')
|
||||||
|
applyUserInfo(cached)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const userInitial = computed(() =>
|
||||||
|
(form.nick_name || form.user_name || 'U').charAt(0).toUpperCase()
|
||||||
|
)
|
||||||
|
|
||||||
|
const genderLabel = computed(() =>
|
||||||
|
({ 0: '保密', 1: '男', 2: '女' }[form.sex] ?? '保密')
|
||||||
|
)
|
||||||
|
|
||||||
|
const infoFormRef = ref<FormInst | null>(null)
|
||||||
|
const saving = ref(false)
|
||||||
|
|
||||||
|
const infoRules: FormRules = {
|
||||||
|
email: [{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' }],
|
||||||
|
phone: [{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }],
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveInfo() {
|
||||||
|
try { await infoFormRef.value?.validate() } catch { return }
|
||||||
|
if (!userInfo.value) { message.warning('未获取到用户信息'); return }
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const res = await UserApi.update_user({
|
||||||
|
id: userInfo.value.id,
|
||||||
|
nick_name: form.nick_name,
|
||||||
|
avatar: form.avatar,
|
||||||
|
user_name: form.user_name,
|
||||||
|
password: '',
|
||||||
|
email: form.email,
|
||||||
|
phone: form.phone,
|
||||||
|
age: form.age,
|
||||||
|
sex: form.sex,
|
||||||
|
creater: 0,
|
||||||
|
})
|
||||||
|
if (res.success) {
|
||||||
|
const refreshed = await UserApi.getUserById(userInfo.value.id)
|
||||||
|
if (refreshed.success && refreshed.data) {
|
||||||
|
applyUserInfo(refreshed.data)
|
||||||
|
localStorage.setItem('userInfo', JSON.stringify(refreshed.data))
|
||||||
|
}
|
||||||
|
message.success('保存成功')
|
||||||
|
} else {
|
||||||
|
message.error(res.message)
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e?.toString() ?? '保存失败')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pwdFormRef = ref<FormInst | null>(null)
|
||||||
|
const pwdSaving = ref(false)
|
||||||
|
const pwdForm = reactive({ password: '', confirm: '' })
|
||||||
|
|
||||||
|
const pwdRules: FormRules = {
|
||||||
|
password: [
|
||||||
|
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||||||
|
{ min: 6, message: '密码至少6位', trigger: 'blur' },
|
||||||
|
],
|
||||||
|
confirm: [
|
||||||
|
{ required: true, message: '请确认密码', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
validator: (_rule: any, value: string) =>
|
||||||
|
value === pwdForm.password ? true : new Error('两次密码不一致'),
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
async function savePassword() {
|
||||||
|
try { await pwdFormRef.value?.validate() } catch { return }
|
||||||
|
if (!userInfo.value) { message.warning('未获取到用户信息'); return }
|
||||||
|
pwdSaving.value = true
|
||||||
|
try {
|
||||||
|
const res = await UserApi.update_user({
|
||||||
|
id: userInfo.value.id,
|
||||||
|
nick_name: form.nick_name,
|
||||||
|
avatar: form.avatar,
|
||||||
|
user_name: form.user_name,
|
||||||
|
password: pwdForm.password,
|
||||||
|
email: form.email,
|
||||||
|
phone: form.phone,
|
||||||
|
age: form.age,
|
||||||
|
sex: form.sex,
|
||||||
|
creater: 0,
|
||||||
|
})
|
||||||
|
if (res.success) {
|
||||||
|
message.success('密码更新成功')
|
||||||
|
pwdForm.password = ''
|
||||||
|
pwdForm.confirm = ''
|
||||||
|
} else {
|
||||||
|
message.error(res.message)
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e?.toString() ?? '密码更新失败')
|
||||||
|
} finally {
|
||||||
|
pwdSaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page-profile {
|
||||||
|
padding: 24px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-toolbar {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Space Grotesk', system-ui, sans-serif;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text, #1D2129);
|
||||||
|
letter-spacing: -0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-layout {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--color-surface, #fff);
|
||||||
|
border: 1px solid var(--color-border, #E5E6EB);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-card {
|
||||||
|
width: 260px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-edit {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text, #1D2129);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-icon {
|
||||||
|
color: var(--color-text-secondary, #86909C);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-label {
|
||||||
|
color: var(--color-text-secondary, #86909C);
|
||||||
|
width: 48px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-value {
|
||||||
|
color: var(--color-text, #1D2129);
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-form {
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appearance-section {
|
||||||
|
padding-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appearance-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border: 1px solid var(--color-border, #E5E6EB);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appearance-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text, #1D2129);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appearance-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-secondary, #86909C);
|
||||||
|
display: block;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.page-profile { padding: 16px; }
|
||||||
|
.profile-layout { flex-direction: column; }
|
||||||
|
.profile-card { width: 100%; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -344,21 +344,21 @@ function handleDelete(row: MenuInfo) {
|
||||||
font-family: 'Space Grotesk', system-ui, sans-serif;
|
font-family: 'Space Grotesk', system-ui, sans-serif;
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1D2129;
|
color: var(--color-text, #1D2129);
|
||||||
letter-spacing: -0.3px;
|
letter-spacing: -0.3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-bar {
|
.search-bar {
|
||||||
background: #fff;
|
background: var(--color-surface, #fff);
|
||||||
border: 1px solid #E5E6EB;
|
border: 1px solid var(--color-border, #E5E6EB);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 14px 18px;
|
padding: 14px 18px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-card {
|
.table-card {
|
||||||
background: #fff;
|
background: var(--color-surface, #fff);
|
||||||
border: 1px solid #E5E6EB;
|
border: 1px solid var(--color-border, #E5E6EB);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
@ -367,8 +367,6 @@ function handleDelete(row: MenuInfo) {
|
||||||
--n-th-padding: 12px 16px;
|
--n-th-padding: 12px 16px;
|
||||||
--n-td-padding: 10px 16px;
|
--n-td-padding: 10px 16px;
|
||||||
--n-th-font-weight: 600;
|
--n-th-font-weight: 600;
|
||||||
--n-th-color: #F7F8FA;
|
|
||||||
--n-td-color-striped: #FAFBFC;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
|
|
||||||
|
|
@ -230,21 +230,21 @@ function handleDelete(row: RoleInfo) {
|
||||||
font-family: 'Space Grotesk', system-ui, sans-serif;
|
font-family: 'Space Grotesk', system-ui, sans-serif;
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1D2129;
|
color: var(--color-text, #1D2129);
|
||||||
letter-spacing: -0.3px;
|
letter-spacing: -0.3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-bar {
|
.search-bar {
|
||||||
background: #fff;
|
background: var(--color-surface, #fff);
|
||||||
border: 1px solid #E5E6EB;
|
border: 1px solid var(--color-border, #E5E6EB);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 14px 18px;
|
padding: 14px 18px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-card {
|
.table-card {
|
||||||
background: #fff;
|
background: var(--color-surface, #fff);
|
||||||
border: 1px solid #E5E6EB;
|
border: 1px solid var(--color-border, #E5E6EB);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
@ -253,8 +253,6 @@ function handleDelete(row: RoleInfo) {
|
||||||
--n-th-padding: 12px 16px;
|
--n-th-padding: 12px 16px;
|
||||||
--n-td-padding: 10px 16px;
|
--n-td-padding: 10px 16px;
|
||||||
--n-th-font-weight: 600;
|
--n-th-font-weight: 600;
|
||||||
--n-th-color: #F7F8FA;
|
|
||||||
--n-td-color-striped: #FAFBFC;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-footer {
|
.table-footer {
|
||||||
|
|
@ -262,7 +260,7 @@ function handleDelete(row: RoleInfo) {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 14px 20px;
|
padding: 14px 20px;
|
||||||
border-top: 1px solid #E5E6EB;
|
border-top: 1px solid var(--color-border, #E5E6EB);
|
||||||
}
|
}
|
||||||
|
|
||||||
.total-hint {
|
.total-hint {
|
||||||
|
|
|
||||||
|
|
@ -345,21 +345,21 @@ function handleDelete(row: UserInfo) {
|
||||||
font-family: 'Space Grotesk', system-ui, sans-serif;
|
font-family: 'Space Grotesk', system-ui, sans-serif;
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1D2129;
|
color: var(--color-text, #1D2129);
|
||||||
letter-spacing: -0.3px;
|
letter-spacing: -0.3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-bar {
|
.search-bar {
|
||||||
background: #fff;
|
background: var(--color-surface, #fff);
|
||||||
border: 1px solid #E5E6EB;
|
border: 1px solid var(--color-border, #E5E6EB);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 14px 18px;
|
padding: 14px 18px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-card {
|
.table-card {
|
||||||
background: #fff;
|
background: var(--color-surface, #fff);
|
||||||
border: 1px solid #E5E6EB;
|
border: 1px solid var(--color-border, #E5E6EB);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
@ -368,8 +368,6 @@ function handleDelete(row: UserInfo) {
|
||||||
--n-th-padding: 12px 16px;
|
--n-th-padding: 12px 16px;
|
||||||
--n-td-padding: 10px 16px;
|
--n-td-padding: 10px 16px;
|
||||||
--n-th-font-weight: 600;
|
--n-th-font-weight: 600;
|
||||||
--n-th-color: #F7F8FA;
|
|
||||||
--n-td-color-striped: #FAFBFC;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-footer {
|
.table-footer {
|
||||||
|
|
@ -377,12 +375,12 @@ function handleDelete(row: UserInfo) {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 14px 20px;
|
padding: 14px 20px;
|
||||||
border-top: 1px solid #E5E6EB;
|
border-top: 1px solid var(--color-border, #E5E6EB);
|
||||||
}
|
}
|
||||||
|
|
||||||
.total-hint {
|
.total-hint {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #86909C;
|
color: var(--color-text-secondary, #86909C);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue