mirror of
https://github.com/continew-org/continew-admin-ui.git
synced 2025-09-08 22:57:11 +08:00
feat: 重构个人消息中心,支持展示个人公告,并优化相关地址
This commit is contained in:
@@ -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'
|
||||
|
14
src/apis/system/user-message.ts
Normal file
14
src/apis/system/user-message.ts
Normal 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}`)
|
||||
}
|
@@ -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 绑定三方账号 */
|
@@ -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 查询用户列表 */
|
||||
|
@@ -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
|
||||
|
@@ -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) {
|
||||
|
@@ -48,7 +48,7 @@ const getMessageData = async () => {
|
||||
|
||||
// 打开消息中心
|
||||
const open = () => {
|
||||
window.open('/setting/message')
|
||||
window.open('/user/message?tab=msg')
|
||||
}
|
||||
|
||||
// 全部已读
|
||||
|
@@ -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" />
|
||||
|
@@ -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: '公告详情' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@@ -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(() => {
|
||||
|
@@ -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,
|
||||
},
|
||||
|
@@ -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>
|
@@ -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>
|
140
src/views/user/message/components/MyMessage.vue
Normal file
140
src/views/user/message/components/MyMessage.vue
Normal 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>
|
95
src/views/user/message/components/MyNotice.vue
Normal file
95
src/views/user/message/components/MyNotice.vue
Normal 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>
|
123
src/views/user/message/components/detail/components/index.vue
Normal file
123
src/views/user/message/components/detail/components/index.vue
Normal 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>
|
94
src/views/user/message/components/detail/index.vue
Normal file
94
src/views/user/message/components/detail/index.vue
Normal 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>
|
83
src/views/user/message/index.vue
Normal file
83
src/views/user/message/index.vue
Normal 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>
|
@@ -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">
|
Reference in New Issue
Block a user