feat: 重构个人消息中心,支持展示个人公告,并优化相关地址

This commit is contained in:
2025-04-05 22:43:35 +08:00
parent ec43ba4c8f
commit 89d0d9ebb1
25 changed files with 586 additions and 257 deletions

View File

@@ -9,5 +9,6 @@ export * from './storage'
export * from './option'
export * from './smsConfig'
export * from './smsLog'
export * from './user-center'
export * from './message'
export * from './user-profile'
export * from './user-message'

View File

@@ -0,0 +1,14 @@
import type * as T from './type'
import http from '@/utils/http'
const BASE_URL = '/user/message'
/** @desc 分页查询用户公告 */
export function listUserNotice(query: T.NoticePageQuery) {
return http.get<PageRes<T.NoticeResp[]>>(`${BASE_URL}/notice`, query)
}
/** @desc 获取用户公告详情 */
export function getUserNotice(id: number) {
return http.get<T.NoticeResp>(`${BASE_URL}/notice/${id}`)
}

View File

@@ -1,7 +1,7 @@
import type * as System from './type'
import type * as T from './type'
import http from '@/utils/http'
const BASE_URL = '/system/user'
const BASE_URL = '/user/profile'
/** @desc 上传头像 */
export function uploadAvatar(data: FormData) {
@@ -30,7 +30,7 @@ export function updateUserEmail(data: { email: string, captcha: string, oldPassw
/** @desc 获取绑定的三方账号 */
export function listUserSocial() {
return http.get<System.BindSocialAccountRes[]>(`${BASE_URL}/social`)
return http.get<T.BindSocialAccountRes[]>(`${BASE_URL}/social`)
}
/** @desc 绑定三方账号 */

View File

@@ -1,8 +1,6 @@
import type * as T from './type'
import http from '@/utils/http'
export type * from './type'
const BASE_URL = '/system/user'
/** @desc 查询用户列表 */

View File

@@ -6,7 +6,7 @@
</div>
</a-col>
<div v-if="slots.left" class="gi-page-layout__divider" :class="{ none: isCollapsed || !isDesktop }">
<div class="gi-split-button" :class="{ none: isCollapsed || !isDesktop }" @click="toggleCollapsed">
<div v-if="defaultCollapsed" class="gi-split-button" :class="{ none: isCollapsed || !isDesktop }" @click="toggleCollapsed">
<icon-right v-if="isCollapsed" />
<icon-left v-else />
</div>
@@ -33,6 +33,7 @@ const props = withDefaults(defineProps<Props>(), {
margin: true,
padding: true,
gutter: false,
defaultCollapsed: true,
leftColProps: () => ({}),
rightColProps: () => ({}),
leftStyle: () => ({}),
@@ -68,6 +69,7 @@ interface Props {
margin?: boolean
padding?: boolean
gutter?: boolean | number
defaultCollapsed?: boolean
leftColProps?: ColProps
rightColProps?: ColProps
leftStyle?: CSSProperties

View File

@@ -61,7 +61,7 @@
v-bind="tableProps"
:stripe="stripe"
:size="size"
:bordered="{ cell: isBordered, wrapper: isBordered }"
:bordered="{ cell: isBordered }"
:columns="visibleColumns"
:scrollbar="true"
:data="data"
@@ -270,11 +270,6 @@ defineExpose({
height: 100%;
}
// 确保垂直滚动时右侧边框显示
:deep(.arco-table-scroll-y) {
border-right: 1px solid var(--color-border-table);
}
// 控制表格最后一行的下边框显示
:deep(.arco-table-border .arco-table-scroll-y .arco-table-body .arco-table-tr:last-of-type .arco-table-td,
.arco-table-border .arco-table-scroll-y tfoot .arco-table-tr:last-of-type .arco-table-td) {

View File

@@ -48,7 +48,7 @@ const getMessageData = async () => {
// 打开消息中心
const open = () => {
window.open('/setting/message')
window.open('/user/message?tab=msg')
}
// 全部已读

View File

@@ -55,7 +55,7 @@
<icon-down />
</a-row>
<template #content>
<a-doption @click="router.push('/setting/profile')">
<a-doption @click="router.push('/user/profile')">
<span>个人中心</span>
</a-doption>
<a-divider :margin="0" />

View File

@@ -43,23 +43,29 @@ export const systemRoutes: RouteRecordRaw[] = [
meta: { hidden: true },
},
{
path: '/setting',
name: 'Setting',
path: '/user',
name: 'User',
component: Layout,
meta: { hidden: true },
children: [
{
path: '/setting/profile',
name: 'SettingProfile',
component: () => import('@/views/setting/profile/index.vue'),
path: '/user/profile',
name: 'UserProfile',
component: () => import('@/views/user/profile/index.vue'),
meta: { title: '个人中心', showInTabs: false },
},
{
path: '/setting/message',
name: 'SettingMessage',
component: () => import('@/views/setting/message/index.vue'),
path: '/user/message',
name: 'UserMessage',
component: () => import('@/views/user/message/index.vue'),
meta: { title: '消息中心', showInTabs: false },
},
{
path: '/user/notice',
name: 'UserNotice',
component: () => import('@/views/user/message/components/detail/index.vue'),
meta: { title: '公告详情' },
},
],
},
{

View File

@@ -6,7 +6,7 @@
:body-style="{ padding: '15px 20px 13px 20px' }"
>
<template #extra>
<a-link @click="router.replace({ path: '/system/notice' })">更多</a-link>
<a-link @click="open">更多</a-link>
</template>
<a-skeleton v-if="loading" :loading="loading" :animation="true">
<a-skeleton-line :rows="5" />
@@ -31,16 +31,12 @@
</div>
</div>
</a-card>
<NoticeDetailModal ref="NoticeDetailModalRef" />
</template>
<script setup lang="ts">
import { type DashboardNoticeResp, listDashboardNotice } from '@/apis'
import { useDict } from '@/hooks/app'
import NoticeDetailModal from '@/views/system/notice/NoticeDetailModal.vue'
const router = useRouter()
const { notice_type } = useDict('notice_type')
const dataList = ref<DashboardNoticeResp[]>([])
@@ -56,10 +52,15 @@ const getDataList = async () => {
}
}
const NoticeDetailModalRef = ref<InstanceType<typeof NoticeDetailModal>>()
const router = useRouter()
// 详情
const onDetail = (id: string) => {
NoticeDetailModalRef.value?.onDetail(id)
const onDetail = (id: number) => {
router.push({ path: '/user/notice', query: { id } })
}
// 打开消息中心
const open = () => {
window.open('/user/message?tab=notice')
}
onMounted(() => {

View File

@@ -56,7 +56,7 @@ const handleBindSocial = () => {
bindSocialAccount(source, othersQuery)
.then(() => {
router.push({
path: '/setting/profile',
path: '/user/profile',
query: {
...othersQuery,
},
@@ -65,7 +65,7 @@ const handleBindSocial = () => {
})
.catch(() => {
router.push({
path: '/setting/profile',
path: '/user/profile',
query: {
...othersQuery,
},

View File

@@ -1,146 +0,0 @@
<template>
<GiPageLayout>
<GiTable
row-key="id"
title="消息中心"
:data="dataList"
:columns="columns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1000 }"
:pagination="pagination"
:disabled-tools="['size', 'setting']"
:disabled-column-keys="['name']"
:row-selection="{ type: 'checkbox', showCheckedAll: true }"
:selected-keys="selectedKeys"
@select-all="selectAll"
@select="select"
@refresh="search"
>
<template #toolbar-left>
<a-input v-model="queryForm.title" placeholder="请输入标题" allow-clear @change="search">
<template #prefix><icon-search /></template>
</a-input>
<a-select
v-model="queryForm.isRead"
placeholder="请选择状态"
allow-clear
style="width: 150px"
@change="search"
>
<a-option :value="false">未读</a-option>
<a-option :value="true">已读</a-option>
</a-select>
<a-button @click="reset">
<template #icon><icon-refresh /></template>
<template #default>重置</template>
</a-button>
</template>
<template #toolbar-right>
<a-button type="primary" status="danger" :disabled="!selectedKeys.length" :title="!selectedKeys.length ? '请选择' : ''" @click="onDelete">
<template #icon><icon-delete /></template>
<template #default>删除</template>
</a-button>
<a-button type="primary" :disabled="!selectedKeys.length" :title="!selectedKeys.length ? '请选择' : ''" @click="onRead">
<template #default>标记为已读</template>
</a-button>
<a-button type="primary" :disabled="selectedKeys.length" :title="!selectedKeys.length ? '请选择' : ''" @click="onReadAll">
<template #default>全部已读</template>
</a-button>
</template>
<template #title="{ record }">
<a-tooltip :content="record.content"><span>{{ record.title }}</span></a-tooltip>
</template>
<template #isRead="{ record }">
<a-tag :color="record.isRead ? '' : 'arcoblue'">
{{ record.isRead ? '已读' : '未读' }}
</a-tag>
</template>
<template #type="{ record }">
<GiCellTag :value="record.type" :dict="message_type" />
</template>
</GiTable>
</GiPageLayout>
</template>
<script setup lang="ts">
import type { TableInstance } from '@arco-design/web-vue'
import { Message, Modal } from '@arco-design/web-vue'
import { type MessageQuery, deleteMessage, listMessage, readMessage } from '@/apis'
import { useTable } from '@/hooks'
import { useDict } from '@/hooks/app'
defineOptions({ name: 'SystemMessage' })
const { message_type } = useDict('message_type')
const queryForm = reactive<MessageQuery>({
sort: ['createTime,desc'],
})
const {
tableData: dataList,
loading,
pagination,
selectedKeys,
select,
selectAll,
search,
handleDelete,
} = useTable((page) => listMessage({ ...queryForm, ...page }), { immediate: true })
const columns: TableInstance['columns'] = [
{
title: '序号',
width: 66,
align: 'center',
render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize),
},
{ title: '标题', dataIndex: 'title', slotName: 'title', minWidth: 100, ellipsis: true, tooltip: true },
{ title: '状态', dataIndex: 'isRead', slotName: 'isRead', align: 'center' },
{ title: '时间', dataIndex: 'createTime', width: 180 },
{ title: '类型', dataIndex: 'type', slotName: 'type', width: 180, ellipsis: true, tooltip: true },
]
// 重置
const reset = () => {
queryForm.title = undefined
queryForm.type = undefined
queryForm.isRead = undefined
search()
}
// 删除
const onDelete = () => {
if (!selectedKeys.value.length) {
return Message.warning('请选择数据')
}
return handleDelete(() => deleteMessage(selectedKeys.value), { showModal: false, multiple: true })
}
// 标记为已读
const onRead = async () => {
if (!selectedKeys.value.length) {
return Message.warning('请选择数据')
}
await readMessage(selectedKeys.value)
Message.success('操作成功')
search()
}
// 全部已读
const onReadAll = async () => {
Modal.warning({
title: '全部已读',
content: '确定要标记全部消息为已读吗?',
hideCancel: false,
maskClosable: false,
onOk: async () => {
await readMessage([])
Message.success('操作成功')
search()
},
})
}
</script>
<style scoped lang="scss"></style>

View File

@@ -1,77 +0,0 @@
<template>
<a-modal v-model:visible="visible" :width="width >= 600 ? 'auto' : '100%'" :footer="false" draggable @close="reset">
<a-typography :style="{ marginTop: '-40px', textAlign: 'center' }">
<a-typography-title>
{{ dataDetail?.title }}
</a-typography-title>
<a-typography-paragraph>
<div class="meta-data">
<a-space>
<span>
<icon-user class="icon" />
<span class="label">发布人</span>
<span>{{ dataDetail?.createUserString }}</span>
</span>
<a-divider direction="vertical" />
<span>
<icon-history class="icon" />
<span class="label">发布时间</span>
<span>{{ dataDetail?.effectiveTime ? dataDetail?.effectiveTime : dataDetail?.createTime }}</span>
</span>
</a-space>
</div>
</a-typography-paragraph>
</a-typography>
<a-divider />
<AiEditor :model-value="dataDetail?.content" />
<a-divider />
<div v-if="dataDetail?.updateTime" class="update-time-row">
<span>
<icon-schedule class="icon" />
<span>最后更新于</span>
<span>{{ dataDetail?.updateTime }}</span>
</span>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { useWindowSize } from '@vueuse/core'
import AiEditor from './detail/components/index.vue'
import { type NoticeResp, getNotice } from '@/apis/system'
const { width } = useWindowSize()
const dataDetail = ref<NoticeResp>({
content: '',
})
const visible = ref(false)
// 详情
const onDetail = async (id: string) => {
const { data } = await getNotice(id)
dataDetail.value = data
visible.value = true
}
// 重置
const reset = () => {
dataDetail.value = {
content: '',
}
}
defineExpose({ onDetail })
</script>
<style scoped lang="scss">
.arco-link {
color: rgb(var(--gray-8));
}
.icon {
margin-right: 3px;
}
.update-time-row {
text-align: right;
}
</style>

View File

@@ -0,0 +1,140 @@
<template>
<GiTable
row-key="id"
:data="dataList"
:columns="columns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1000 }"
:pagination="pagination"
:disabled-tools="['size', 'setting']"
:row-selection="{ type: 'checkbox', showCheckedAll: true }"
:selected-keys="selectedKeys"
@select-all="selectAll"
@select="select"
@refresh="search"
>
<template #toolbar-left>
<a-input-search v-model="queryForm.title" placeholder="搜索标题" allow-clear @search="search" />
<a-select
v-model="queryForm.isRead"
placeholder="全部状态"
allow-clear
style="width: 150px"
@change="search"
>
<a-option :value="false">未读</a-option>
<a-option :value="true">已读</a-option>
</a-select>
<a-button @click="reset">
<template #icon><icon-refresh /></template>
<template #default>重置</template>
</a-button>
</template>
<template #toolbar-right>
<a-button type="primary" status="danger" :disabled="!selectedKeys.length" :title="!selectedKeys.length ? '请选择' : ''" @click="onDelete">
<template #icon><icon-delete /></template>
<template #default>删除</template>
</a-button>
<a-button type="primary" :disabled="!selectedKeys.length" :title="!selectedKeys.length ? '请选择' : ''" @click="onRead">
<template #default>标记为已读</template>
</a-button>
<a-button type="primary" :disabled="selectedKeys.length" :title="!selectedKeys.length ? '请选择' : ''" @click="onReadAll">
<template #default>全部已读</template>
</a-button>
</template>
<template #title="{ record }">
<a-tooltip :content="record.content"><span>{{ record.title }}</span></a-tooltip>
</template>
<template #isRead="{ record }">
<a-tag :color="record.isRead ? '' : 'arcoblue'">
{{ record.isRead ? '已读' : '未读' }}
</a-tag>
</template>
<template #type="{ record }">
<GiCellTag :value="record.type" :dict="message_type" />
</template>
</GiTable>
</template>
<script setup lang="ts">
import type { TableInstance } from '@arco-design/web-vue'
import { Message, Modal } from '@arco-design/web-vue'
import { type MessageQuery, deleteMessage, listMessage, readMessage } from '@/apis'
import { useTable } from '@/hooks'
import { useDict } from '@/hooks/app'
defineOptions({ name: 'SystemMessage' })
const { message_type } = useDict('message_type')
const queryForm = reactive<MessageQuery>({
sort: ['createTime,desc'],
})
const {
tableData: dataList,
loading,
pagination,
selectedKeys,
select,
selectAll,
search,
handleDelete,
} = useTable((page) => listMessage({ ...queryForm, ...page }), { immediate: true })
const columns: TableInstance['collumns'] = [
{
title: '序号',
width: 66,
align: 'center',
render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize),
},
{ title: '标题', dataIndex: 'title', slotName: 'title', minWidth: 100, ellipsis: true, tooltip: true },
{ title: '状态', dataIndex: 'isRead', slotName: 'isRead', align: 'center' },
{ title: '时间', dataIndex: 'createTime', width: 180 },
{ title: '类型', dataIndex: 'type', slotName: 'type', width: 180, ellipsis: true, tooltip: true },
]
// 重置
const reset = () => {
queryForm.title = undefined
queryForm.type = undefined
queryForm.isRead = undefined
search()
}
// 删除
const onDelete = () => {
if (!selectedKeys.value.length) {
return Message.warning('请选择数据')
}
return handleDelete(() => deleteMessage(selectedKeys.value), { showModal: false, multiple: true })
}
// 标记为已读
const onRead = async () => {
if (!selectedKeys.value.length) {
return Message.warning('请选择数据')
}
await readMessage(selectedKeys.value)
Message.success('操作成功')
search()
}
// 全部已读
const onReadAll = async () => {
Modal.warning({
title: '全部已读',
content: '确定要标记全部消息为已读吗?',
hideCancel: false,
maskClosable: false,
onOk: async () => {
await readMessage([])
Message.success('操作成功')
search()
},
})
}
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,95 @@
<template>
<GiTable
row-key="id"
:data="dataList"
:columns="columns"
:loading="loading"
:scroll="{ x: '100%', y: '100%' }"
:pagination="pagination"
:disabled-tools="['size', 'setting']"
@refresh="search"
>
<template #toolbar-left>
<a-input-search v-model="queryForm.title" placeholder="搜索标题" allow-clear @search="search" />
<a-select
v-model="queryForm.type"
:options="notice_type"
placeholder="全部类型"
allow-clear
style="width: 150px"
@change="search"
/>
<a-button @click="reset">
<template #icon><icon-refresh /></template>
<template #default>重置</template>
</a-button>
</template>
<template #title="{ record }">
<a-link @click="onDetail(record)">
<a-typography-paragraph
class="link-text"
:ellipsis="{
rows: 1,
showTooltip: true,
css: true,
}"
>
{{ record.title }}
</a-typography-paragraph>
</a-link>
</template>
<template #type="{ record }">
<GiCellTag :value="record.type" :dict="notice_type" />
</template>
</GiTable>
</template>
<script setup lang="ts">
import type { TableInstance } from '@arco-design/web-vue'
import { type NoticeQuery, type NoticeResp, listUserNotice } from '@/apis/system'
import { useTable } from '@/hooks'
import { useDict } from '@/hooks/app'
defineOptions({ name: 'SystemMessage' })
const { notice_type } = useDict('notice_type')
const queryForm = reactive<NoticeQuery>({
sort: ['createTime,desc'],
})
const {
tableData: dataList,
loading,
pagination,
search,
} = useTable((page) => listUserNotice({ ...queryForm, ...page }), { immediate: true })
const columns: TableInstance['columns'] = [
{
title: '序号',
width: 66,
align: 'center',
render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize),
},
{ title: '标题', dataIndex: 'title', slotName: 'title', ellipsis: true, tooltip: true },
{ title: '类型', dataIndex: 'type', slotName: 'type', align: 'center' },
{ title: '发布人', dataIndex: 'createUserString', ellipsis: true, tooltip: true },
{ title: '发布时间', dataIndex: 'createTime', width: 180 },
]
// 重置
const reset = () => {
queryForm.title = undefined
queryForm.type = undefined
search()
}
const router = useRouter()
// 详情
const onDetail = (record: NoticeResp) => {
router.push({ path: '/user/notice', query: { id: record.id } })
}
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,123 @@
<!-- 未完善 -->
<template>
<div ref="divRef" class="container">
<div class="aie-container">
<div class="aie-header-panel" style="display: none;">
<div class="aie-container-header"></div>
</div>
<div class="aie-main">
<div class="aie-container-panel">
<div class="aie-container-main"></div>
</div>
</div>
<div class="aie-container-footer" style="display: none;"></div>
</div>
</div>
</template>
<script setup lang="ts">
import { AiEditor, type AiEditorOptions } from 'aieditor'
import 'aieditor/dist/style.css'
import { useAppStore } from '@/stores'
defineOptions({ name: 'AiEditor' })
const props = defineProps<{
modelValue: string
options?: AiEditorOptions
}>()
const aieditor = ref<AiEditor | null>(null)
const appStore = useAppStore()
const divRef = ref<any>()
const editorConfig = reactive<AiEditorOptions>({
element: '',
theme: appStore.theme,
placeholder: '请输入内容',
content: '',
editable: false,
})
const init = () => {
aieditor.value?.destroy()
aieditor.value = new AiEditor(editorConfig)
}
watch(() => props.modelValue, (value) => {
if (value !== aieditor.value?.getHtml()) {
editorConfig.content = value
init()
}
})
watch(() => appStore.theme, (value) => {
editorConfig.theme = value
init()
})
// 挂载阶段
onMounted(() => {
editorConfig.element = divRef.value
init()
})
// 销毁阶段
onUnmounted(() => {
aieditor.value?.destroy()
})
</script>
<style scoped lang="scss">
.container {
height: 100%;
width: 100%;
box-sizing: border-box;
}
.aie-header-panel {
position: sticky;
// top: 51px;
z-index: 1;
}
.aie-header-panel aie-header>div {
align-items: center;
justify-content: center;
padding: 10px 0;
}
.aie-container {
border: none !important;
}
.aie-container-panel {
width: calc(100% - 2rem - 2px);
max-width: 826.77px;
margin: 0rem auto;
border: 1px solid var(--color-border-1);
background-color: var() rgba($color: var(--color-bg-1), $alpha: 1.0);
height: 100%;
padding: 1rem;
z-index: 99;
overflow: auto;
box-sizing: border-box;
}
.aie-main {
position: relative;
overflow: hidden;
flex: 1;
box-sizing: border-box;
padding: 1rem 0px;
background-color: var(--color-bg-1);
}
.aie-directory {
position: absolute;
top: 30px;
left: 10px;
width: 260px;
z-index: 0;
}
.aie-title1 {
font-size: 14px;
font-weight: 500;
}
</style>

View File

@@ -0,0 +1,94 @@
<template>
<div ref="containerRef" class="detail">
<div class="detail_header">
<a-affix :target="(containerRef as HTMLElement)">
<a-page-header title="通知公告" subtitle="查看" @back="onBack">
</a-page-header>
</a-affix>
</div>
<div class="detail_content">
<h1 class="title">{{ form?.title }}</h1>
<div class="info">
<a-space>
<span>
<icon-user class="icon" />
<span class="label">发布人</span>
<span>{{ form?.createUserString }}</span>
</span>
<a-divider direction="vertical" />
<span>
<icon-history class="icon" />
<span class="label">发布时间</span>
<span>{{ form?.effectiveTime ? form?.effectiveTime : form?.createTime
}}</span>
</span>
<a-divider v-if="form?.updateTime" direction="vertical" />
<span v-if="form?.updateTime">
<icon-schedule class="icon" />
<span>更新时间</span>
<span>{{ form?.updateTime }}</span>
</span>
</a-space>
</div>
<div style="flex: 1;">
<AiEditor v-model="form.content" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import AiEditor from './components/index.vue'
import { getUserNotice } from '@/apis/system/user-message'
import { useTabsStore } from '@/stores'
import { useResetReactive } from '@/hooks'
defineOptions({ name: 'UserNotice' })
const route = useRoute()
const router = useRouter()
const tabsStore = useTabsStore()
const { id } = route.query
const containerRef = ref<HTMLElement | null>()
const [form, resetForm] = useResetReactive({
title: '',
createUserString: '',
effectiveTime: '',
createTime: '',
content: '',
})
// 回退
const onBack = () => {
router.back()
tabsStore.closeCurrent(route.path)
}
// 打开
const onOpen = async (id: string) => {
resetForm()
const { data } = await getUserNotice(id)
Object.assign(form, data)
}
onMounted(() => {
onOpen(id as string)
})
</script>
<style scoped lang="scss">
.detail_content {
.title {
text-align: center;
}
.info {
text-align: center;
}
.icon {
margin-right: 3px;
}
}
</style>

View File

@@ -0,0 +1,83 @@
<template>
<GiPageLayout :margin="true" :default-collapsed="false">
<template v-if="isDesktop" #left>
<a-tabs v-model:active-key="activeKey" position="left" hide-content @change="change">
<a-tab-pane v-for="(item) in menuList" :key="item.key" :title="item.name"></a-tab-pane>
</a-tabs>
</template>
<a-tabs v-if="!isDesktop" v-model:active-key="activeKey" type="card-gutter" style="margin-bottom: 10px" position="top" hide-content @change="change">
<a-tab-pane v-for="(item) in menuList" :key="item.key" :title="item.name"></a-tab-pane>
</a-tabs>
<transition name="fade-slide" mode="out-in" appear>
<component :is="menuList.find((item) => item.key === activeKey)?.value"></component>
</transition>
</GiPageLayout>
</template>
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'
import MyMessage from './components/MyMessage.vue'
import MyNotice from './components/MyNotice.vue'
import { useDevice } from '@/hooks'
defineOptions({ name: 'UserMessage' })
const { isDesktop } = useDevice()
const menuList = [
{ name: '我的消息', key: 'msg', value: MyMessage },
{ name: '我的公告', key: 'notice', value: MyNotice },
]
const route = useRoute()
const router = useRouter()
const activeKey = ref('msg')
watch(
() => route.query,
() => {
if (route.query.tab) {
activeKey.value = String(route.query.tab)
}
},
{ immediate: true },
)
const change = (key: string | number) => {
activeKey.value = key as string
router.replace({ path: route.path, query: { tab: key } })
}
</script>
<style scoped lang="scss">
:deep(.arco-tabs-nav-vertical.arco-tabs-nav-type-line .arco-tabs-tab) {
margin: 0;
padding: 8px 16px;
&:hover {
background: var(--color-fill-1);
.arco-tabs-tab-title {
&::before {
display: none !important;
}
}
}
&.arco-tabs-tab-active {
background: rgba(var(--primary-6), 0.08);
}
}
:deep(.arco-tabs-nav-vertical::before) {
left: 0;
display: none;
}
:deep(.arco-tabs-nav-vertical .arco-tabs-nav-ink) {
left: 0;
}
:deep(.arco-tabs-nav-vertical) {
float: none;
flex-direction: row;
}
</style>

View File

@@ -21,7 +21,7 @@ import LeftBox from './BasicInfo.vue'
import RightBox from './Social.vue'
import PasswordPolicy from './Security.vue'
defineOptions({ name: 'SettingProfile' })
defineOptions({ name: 'UserProfile' })
</script>
<style scoped lang="scss">