Compare commits

...

4 Commits

Author SHA1 Message Date
chansee97
cf41abc5a5 fix: 移除demo接口 2025-09-04 18:50:16 +08:00
chansee97
4b7946867e fix: 修改Put方法为Patch 2025-09-04 18:23:56 +08:00
chansee97
41d4e9a0d5 feat: 完善个人中心 2025-09-04 18:07:00 +08:00
chansee97
c168e92135 fix: 隐藏tabbar滚动条 2025-09-04 14:17:00 +08:00
16 changed files with 500 additions and 182 deletions

View File

@ -1,31 +0,0 @@
import { request } from '../utils/alova'
/* 示例列表接口 */
export function fetchUserList() {
return request.Get('/userList')
}
// 获取所有路由信息
export function fetchAllRoutes() {
return request.Get<Api.Response<AppRoute.RowRoute[]>>('/getUserRoutes')
}
// 获取所有用户信息
export function fetchUserPage() {
return request.Get<Api.Response<Entity.User[]>>('/userPage')
}
// 获取所有角色列表
export function fetchRoleList() {
return request.Get<Api.Response<Entity.Role[]>>('/role/list')
}
/**
*
*
* @param code -
* @returns
*/
export function fetchDictList(code?: string) {
const params = { code }
return request.Get<Api.Response<Entity.DictType[]>>('/dict/list', { params })
}

View File

@ -1,4 +1,3 @@
export * from './login'
export * from './demo'
export * from './test'
export * from './system'

View File

@ -39,7 +39,7 @@ export function getDeptById(id: number) {
* PUT /dept/{id}
*/
export function updateDept(id: number, data: Partial<Entity.Dept>) {
return request.Put<Api.Response<Entity.Dept>>(`/dept/${id}`, data)
return request.Patch<Api.Response<Entity.Dept>>(`/dept/${id}`, data)
}
/**

View File

@ -51,7 +51,7 @@ export function getDictTypeById(id: number) {
* PUT /dict/types/{id}
*/
export function updateDictType(id: number, data: Partial<Entity.DictType>) {
return request.Put<Api.Response<Entity.DictType>>(`/dict/types/${id}`, data)
return request.Patch<Api.Response<Entity.DictType>>(`/dict/types/${id}`, data)
}
/**
@ -95,7 +95,7 @@ export function getDictDataById(id: number) {
* PUT /dict/data/{id}
*/
export function updateDictData(id: number, data: Partial<Entity.DictData>) {
return request.Put<Api.Response<Entity.DictData>>(`/dict/data/${id}`, data)
return request.Patch<Api.Response<Entity.DictData>>(`/dict/data/${id}`, data)
}
/**

View File

@ -30,7 +30,7 @@ export function getMenuById(id: number) {
* PUT /menu/{id}
*/
export function updateMenu(id: number, data: Partial<Entity.Menu>) {
return request.Put<Api.Response<Entity.Menu>>(`/menu/${id}`, data)
return request.Patch<Api.Response<Entity.Menu>>(`/menu/${id}`, data)
}
/**

View File

@ -46,7 +46,7 @@ export function getRoleById(id: number) {
* PUT /role/{id}
*/
export function updateRole(id: number, data: Partial<Entity.Role>) {
return request.Put<Api.Response<Entity.Role>>(`/role/${id}`, data)
return request.Patch<Api.Response<Entity.Role>>(`/role/${id}`, data)
}
/**

View File

@ -38,7 +38,7 @@ export function getUserById(id: number) {
* PUT /user/{id}
*/
export function updateUser(id: number, data: Partial<Entity.User>) {
return request.Put<Api.Response<Entity.User>>(`/user/${id}`, data)
return request.Patch<Api.Response<Entity.User>>(`/user/${id}`, data)
}
/**
@ -48,3 +48,14 @@ export function updateUser(id: number, data: Partial<Entity.User>) {
export function deleteUser(id: number) {
return request.Delete<Api.Response<boolean>>(`/user/${id}`)
}
/**
*
* Patch /user/password
*/
export interface UserPasswordParams {
oldPassword: string
newPassword: string
}
export function updateUserPassword(data: UserPasswordParams) {
return request.Patch<Api.Response<boolean>>(`/user/password`, data)
}

View File

@ -23,7 +23,7 @@ export function fetchDelete() {
}
/* put方法测试 */
export function fetchPut(data: any) {
return request.Put('/putAPI', data)
return request.Patch('/putAPI', data)
}
/* 不携带token的接口 */
export function withoutToken() {

View File

@ -114,7 +114,17 @@ useDraggable(el, tabs, {
</script>
<template>
<n-scrollbar ref="scrollbar" class="relative flex h-full tab-bar-scroller-wrapper" content-class="h-full pr-34 tab-bar-scroller-content" :x-scrollable="true" @wheel="onWheel">
<n-scrollbar
ref="scrollbar"
class="relative flex h-full tab-bar-scroller-wrapper"
content-class="h-full pr-34 tab-bar-scroller-content"
x-scrollable
:theme-overrides="{
color: 'transrparent',
colorHover: 'transrparent',
}"
@wheel="onWheel"
>
<div class="p-l-2 flex wh-full relative">
<div class="flex items-end">
<TabBarItem

View File

@ -0,0 +1,43 @@
<script setup lang="ts">
//
const settings = ref({
systemUpdateEmail: true,
securityAlertEmail: true,
})
</script>
<template>
<div class="space-y-4">
<!-- 邮件通知 -->
<n-card title="邮件通知" size="small">
<n-list>
<n-list-item>
<n-thing>
<template #header>
系统更新通知
</template>
<template #description>
系统有重要更新时发送邮件通知
</template>
</n-thing>
<template #suffix>
<n-switch v-model:value="settings.systemUpdateEmail" />
</template>
</n-list-item>
<n-list-item>
<n-thing>
<template #header>
安全警报
</template>
<template #description>
检测到安全威胁时立即发送邮件
</template>
</n-thing>
<template #suffix>
<n-switch v-model:value="settings.securityAlertEmail" />
</template>
</n-list-item>
</n-list>
</n-card>
</div>
</template>

View File

@ -61,7 +61,6 @@ async function updateUserInfo() {
:model="formData"
label-placement="left"
label-width="80px"
class="max-w-400px"
>
<n-grid :cols="1">
<n-grid-item>

View File

@ -0,0 +1,47 @@
<script setup lang="ts">
//
const preferences = ref({
aiAssistant: false,
predictiveLoading: false,
})
</script>
<template>
<div class="space-y-4">
<!-- 实验性功能 -->
<n-card title="实验性功能" size="small">
<n-alert type="warning" style="margin-bottom: 16px;">
以下功能仍在测试阶段可能会影响系统稳定性
</n-alert>
<n-list>
<n-list-item>
<n-thing>
<template #header>
AI 助手
</template>
<template #description>
启用智能助手功能提供操作建议
</template>
</n-thing>
<template #suffix>
<n-switch v-model:value="preferences.aiAssistant" />
</template>
</n-list-item>
<n-list-item>
<n-thing>
<template #header>
预测性加载
</template>
<template #description>
根据使用习惯预加载可能需要的内容
</template>
</n-thing>
<template #suffix>
<n-switch v-model:value="preferences.predictiveLoading" />
</template>
</n-list-item>
</n-list>
</n-card>
</div>
</template>

View File

@ -0,0 +1,162 @@
<script setup lang="ts">
import { ref } from 'vue'
import type { FormInst, FormRules } from 'naive-ui'
import { useMessage } from 'naive-ui'
import { updateUserPassword } from '@/api/system/user'
const message = useMessage()
const formRef = ref<FormInst | null>(null)
const loading = ref(false)
//
const passwordForm = ref({
oldPassword: '',
newPassword: '',
confirmPassword: '',
})
//
const loginNotificationEnabled = ref(true)
//
const passwordRules: FormRules = {
oldPassword: [
{ required: true, message: '请输入当前密码', trigger: 'blur' },
],
newPassword: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' },
],
confirmPassword: [
{ required: true, message: '请确认新密码', trigger: 'blur' },
{
validator: (rule, value) => {
return value === passwordForm.value.newPassword
},
message: '两次输入的密码不一致',
trigger: 'blur',
},
],
}
//
async function handleUpdatePassword() {
try {
await formRef.value?.validate()
loading.value = true
await updateUserPassword({
oldPassword: passwordForm.value.oldPassword,
newPassword: passwordForm.value.newPassword,
})
message.success('密码更新成功')
resetForm()
}
finally {
loading.value = false
}
}
//
function resetForm() {
passwordForm.value = {
oldPassword: '',
newPassword: '',
confirmPassword: '',
}
formRef.value?.restoreValidation()
}
</script>
<template>
<div class="space-y-4">
<!-- 密码更新 -->
<n-card title="密码更新" size="small">
<n-form
ref="formRef"
:model="passwordForm"
:rules="passwordRules"
label-placement="left"
label-width="120px"
require-mark-placement="right-hanging"
>
<n-form-item label="当前密码" path="oldPassword">
<n-input
v-model:value="passwordForm.oldPassword"
type="password"
placeholder="请输入当前密码"
show-password-on="click"
/>
</n-form-item>
<n-form-item label="新密码" path="newPassword">
<n-input
v-model:value="passwordForm.newPassword"
type="password"
placeholder="请输入新密码"
show-password-on="click"
/>
</n-form-item>
<n-form-item label="确认新密码" path="confirmPassword">
<n-input
v-model:value="passwordForm.confirmPassword"
type="password"
placeholder="请再次输入新密码"
show-password-on="click"
/>
</n-form-item>
<n-form-item label=" ">
<n-space>
<n-button
type="primary"
:loading="loading"
@click="handleUpdatePassword"
>
更新密码
</n-button>
<n-button @click="resetForm">
重置
</n-button>
</n-space>
</n-form-item>
</n-form>
</n-card>
<!-- 账户安全 -->
<n-card title="账户安全" size="small">
<n-list>
<n-list-item>
<n-thing>
<template #header>
登录设备管理
</template>
<template #description>
管理已登录的设备可以远程注销可疑设备
</template>
</n-thing>
<template #suffix>
<n-button size="small" secondary>
查看设备
</n-button>
</template>
</n-list-item>
<n-list-item>
<n-thing>
<template #header>
登录通知
</template>
<template #description>
新设备登录时发送邮件通知
</template>
</n-thing>
<template #suffix>
<n-switch v-model:value="loginNotificationEnabled" />
</template>
</n-list-item>
</n-list>
</n-card>
</div>
</template>

View File

@ -1,13 +1,65 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { computed, h, onMounted, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useAuthStore } from '@/store'
import PersonalInfo from './components/PersonalInfo.vue'
import SecuritySettings from './components/SecuritySettings.vue'
import NotificationSettings from './components/NotificationSettings.vue'
import PreferenceSettings from './components/PreferenceSettings.vue'
import IconUser from '~icons/icon-park-outline/user'
import IconLock from '~icons/icon-park-outline/lock'
import IconRemind from '~icons/icon-park-outline/remind'
import IconSetting from '~icons/icon-park-outline/setting-one'
const authStore = useAuthStore()
const { userInfo } = storeToRefs(authStore)
//
const activeTab = ref('profile')
//
const activeMenu = ref('profile')
// -
const pageConfig = [
{
key: 'profile',
label: '个人信息',
title: '个人信息',
icon: IconUser,
component: PersonalInfo,
},
{
key: 'security',
label: '安全设置',
title: '安全设置',
icon: IconLock,
component: SecuritySettings,
},
{
key: 'notification',
label: '通知设置',
title: '通知设置',
icon: IconRemind,
component: NotificationSettings,
},
{
key: 'preference',
label: '偏好设置',
title: '偏好设置',
icon: IconSetting,
component: PreferenceSettings,
},
]
// -
const menuOptions = pageConfig.map(({ key, label, icon }) => ({
key,
label,
icon: () => h(icon),
}))
// -
const currentPageConfig = computed(() => {
return pageConfig.find(config => config.key === activeMenu.value)
})
//
function getGreeting() {
@ -34,83 +86,46 @@ onMounted(async () => {
</script>
<template>
<n-space vertical>
<!-- 欢迎标题 -->
<n-card>
<n-flex align="center" justify="space-between">
<div>
<n-h2>
<n-card>
<n-flex :wrap="false" style="height: 100%;">
<!-- 左侧区域 -->
<div class="w-[220px] border-r border-[var(--n-border-color)] flex flex-col">
<!-- 左上头像和招呼 -->
<n-flex
align="center" justify="center" vertical
class="p-3 border-b border-[var(--n-border-color)]"
:size="8"
>
<n-avatar
round
:size="80"
:src="userInfo?.avatar || `https://api.dicebear.com/9.x/adventurer-neutral/svg?seed=${userInfo!.username}`"
/>
<n-h3 class="m-0">
{{ getGreeting() }}{{ userInfo?.nickName || userInfo.username }}
</n-h2>
<n-p class="text-sm opacity-90 m-0">
</n-h3>
<n-text>
欢迎使用个人中心
</n-p>
</div>
<n-avatar
round
:size="64"
:src="userInfo?.avatar || `https://api.dicebear.com/9.x/adventurer-neutral/svg?seed=${userInfo!.username}`"
</n-text>
</n-flex>
<!-- 左下菜单 -->
<n-menu
v-model:value="activeMenu"
class="flex-1"
:options="menuOptions"
:icon-size="16"
/>
</n-flex>
</n-card>
</div>
<!-- 主要内容区域 -->
<n-card>
<n-tabs
v-model:value="activeTab"
type="line"
placement="left"
tab-style="min-height: 30px;"
pane-style="padding-left: 1rem;"
>
<n-tab-pane name="profile">
<template #tab>
<icon-park-outline-user class="mr-2" />
个人信息
</template>
<n-h5>
个人信息
</n-h5>
<n-divider />
<PersonalInfo />
</n-tab-pane>
<n-tab-pane name="security">
<template #tab>
<icon-park-outline-lock class="mr-2" />
安全设置
</template>
<n-h5>
安全设置
</n-h5>
<n-divider />
<n-empty description="功能开发中..." />
</n-tab-pane>
<n-tab-pane name="notification">
<template #tab>
<icon-park-outline-remind class="mr-2" />
通知设置
</template>
<n-h5>
通知设置
</n-h5>
<n-divider />
<n-empty description="功能开发中..." />
</n-tab-pane>
<n-tab-pane name="preference">
<template #tab>
<icon-park-outline-setting-one class="mr-2" />
偏好设置
</template>
<n-h5>
偏好设置
</n-h5>
<n-divider />
<n-empty description="功能开发中..." />
</n-tab-pane>
</n-tabs>
</n-card>
</n-space>
<!-- 右侧功能区域 -->
<div class="flex-1 p-2">
<n-h4 class="flex items-center gap-2">
<component :is="currentPageConfig!.icon" /> {{ currentPageConfig!.title }}
</n-h4>
<n-divider />
<component :is="currentPageConfig!.component" />
</div>
</n-flex>
</n-card>
</template>

View File

@ -1,109 +1,172 @@
<script setup lang="ts">
import { fetchDictList } from '@/api'
import { getDictTypeList } from '@/api'
import { useDict } from '@/hooks/useDict'
import { useDictStore } from '@/store'
const { dict } = useDictStore()
const dictStore = useDictStore()
const dictKey = ref('')
const options = ref()
const subOptions = ref()
const currentDict = ref()
const selectedDictType = ref('')
const selectedDictValue = ref('')
const dictTypeOptions = ref<Array<{ label: string; value: string }>>([])
const displayData = ref<Entity.DictData[] | Record<string, any>>([])
async function getAlldict() {
const { data } = await fetchDictList()
options.value = data.map((item) => {
// 使 useDict hook
const dictUtils = computed(() => {
if (!selectedDictType.value) {
return {
label: item.label,
value: item.code,
rawData: ref([]),
enumMap: ref({}),
valueMap: ref({}),
labelMap: ref({}),
options: ref([])
}
})
}
return useDict(selectedDictType.value)
})
//
async function getAllDictTypes() {
try {
const { data } = await getDictTypeList()
dictTypeOptions.value = data.list.map((item: Entity.DictType) => ({
label: item.name,
value: item.type,
}))
}
catch (error) {
console.error('获取字典类型失败:', error)
}
}
function changeSelect(v: string) {
dict(v).then((data) => {
currentDict.value = data
subOptions.value = data.data()
//
function changeSelect(dictType: string) {
selectedDictType.value = dictType
selectedDictValue.value = ''
//
nextTick(() => {
if (dictUtils.value.rawData.value.length > 0) {
displayData.value = dictUtils.value.rawData.value
}
})
}
onMounted(() => {
getAlldict()
getAllDictTypes()
})
const data = ref()
async function getDict() {
data.value = currentDict.value.data()
//
function showRawData() {
displayData.value = dictUtils.value.rawData.value
}
async function getEnum() {
data.value = currentDict.value.enum()
// value -> name
function showEnumMap() {
displayData.value = dictUtils.value.enumMap.value
}
async function getValueMap() {
data.value = currentDict.value.valueMap()
// value -> dictData
function showValueMap() {
displayData.value = dictUtils.value.valueMap.value
}
async function getLabelMap() {
data.value = currentDict.value.labelMap()
// name -> dictData
function showLabelMap() {
displayData.value = dictUtils.value.labelMap.value
}
const dictValue = ref()
//
function showOptions() {
displayData.value = dictUtils.value.options.value
}
// 使 enumMap
const dictLabel = computed(() => {
if (data.value && data.value[dictValue.value]) {
return data.value[dictValue.value].label
}
return '--'
if (!selectedDictValue.value || !dictUtils.value.enumMap.value) return '--'
return dictUtils.value.enumMap.value[selectedDictValue.value] || '--'
})
const enumValue = ref()
//
function clearCache() {
dictStore.cleanDict()
window.$message?.success('字典缓存已清理')
}
const enumLabel = computed(() => {
if (data.value && data.value[enumValue.value]) {
return data.value[enumValue.value]
//
function removeCache() {
if (selectedDictType.value) {
dictStore.removeDict(selectedDictType.value)
window.$message?.success(`已移除 ${selectedDictType.value} 的缓存`)
}
return '--'
})
}
</script>
<template>
<n-card title="字典">
<n-card title="字典示">
<n-flex vertical>
<n-flex>
<n-select v-model:value="dictKey" class="w-1/3" :options="options" @update:value="changeSelect" />
子字典下拉框
<n-select class="w-1/3" :options="subOptions" />
<n-flex align="center">
<n-select
v-model:value="selectedDictType"
:options="dictTypeOptions"
placeholder="请选择字典类型"
@update:value="changeSelect"
/>
<n-select
v-model:value="selectedDictValue"
:options="dictUtils.options.value"
placeholder="请选择字典项"
/>
</n-flex>
<n-flex>
<n-button @click="getDict">
获取当前字典数据
<n-button @click="showRawData">
显示原始数据 (rawData)
</n-button>
<n-button @click="getEnum">
获取当前字典枚举
<n-button @click="showEnumMap">
显示枚举映射 (enumMap)
</n-button>
<n-button @click="getValueMap">
获取当前字典ValueMap
<n-button @click="showValueMap">
显示值映射 (valueMap)
</n-button>
<n-button @click="getLabelMap">
获取当前字典LabelMap
<n-button @click="showLabelMap">
显示标签映射 (labelMap)
</n-button>
<n-button @click="showOptions">
显示选项格式 (options)
</n-button>
</n-flex>
<n-flex>
<n-button @click="clearCache" type="warning">
清理所有缓存
</n-button>
<n-button @click="removeCache" type="error" :disabled="!selectedDictType">
移除当前字典缓存
</n-button>
</n-flex>
<pre class="bg-#eee:30">
{{ data }}
</pre>
<pre class="bg-gray-100 p-4 rounded text-sm overflow-auto max-h-96">
{{ JSON.stringify(displayData, null, 2) }}</pre>
<n-flex align="center">
<n-input-number v-model:value="dictValue" :min="0" />
<n-flex align="center" v-if="selectedDictValue">
<n-text>选中值: {{ selectedDictValue }}</n-text>
<n-text type="info">
Map回显结果 {{ dictLabel }}
</n-text>
</n-flex>
<n-flex align="center">
<n-input-number v-model:value="enumValue" :min="0" />
<n-text type="info">
Enum回显结果 {{ enumLabel }}
对应标签: {{ dictLabel }}
</n-text>
</n-flex>
<n-divider />
<n-text depth="3">useDict Hook 使用说明</n-text>
<n-ul>
<n-li><n-text code>useDict(dictType)</n-text> - 使用字典类型获取字典工具对象</n-li>
<n-li><n-text code>rawData</n-text> - 原始字典数据数组 Entity.DictData[]</n-li>
<n-li><n-text code>enumMap</n-text> - 枚举映射 { value: name }</n-li>
<n-li><n-text code>valueMap</n-text> - 值映射 { value: dictData }</n-li>
<n-li><n-text code>labelMap</n-text> - 标签映射 { name: dictData }</n-li>
<n-li><n-text code>options</n-text> - 选项数组 [{ label: name, value }]</n-li>
<n-li>字典数据会自动缓存60分钟支持手动清理缓存</n-li>
<n-li>推荐使用 useDict hook 而不是直接调用 API store</n-li>
</n-ul>
</n-flex>
</n-card>
</template>

View File

@ -21,7 +21,7 @@ const modalForm = createProModalForm<Partial<Entity.DictData>>({
omitEmptyString: false,
initialValues: {
sort: 0,
status: 1,
status: 0,
},
onSubmit: submitModal,
})