mirror of
https://github.com/continew-org/continew-admin-ui.git
synced 2025-09-09 20:57:17 +08:00
feat: 重构个人消息中心,支持展示个人公告,并优化相关地址
This commit is contained in:
@@ -9,5 +9,6 @@ export * from './storage'
|
|||||||
export * from './option'
|
export * from './option'
|
||||||
export * from './smsConfig'
|
export * from './smsConfig'
|
||||||
export * from './smsLog'
|
export * from './smsLog'
|
||||||
export * from './user-center'
|
|
||||||
export * from './message'
|
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'
|
import http from '@/utils/http'
|
||||||
|
|
||||||
const BASE_URL = '/system/user'
|
const BASE_URL = '/user/profile'
|
||||||
|
|
||||||
/** @desc 上传头像 */
|
/** @desc 上传头像 */
|
||||||
export function uploadAvatar(data: FormData) {
|
export function uploadAvatar(data: FormData) {
|
||||||
@@ -30,7 +30,7 @@ export function updateUserEmail(data: { email: string, captcha: string, oldPassw
|
|||||||
|
|
||||||
/** @desc 获取绑定的三方账号 */
|
/** @desc 获取绑定的三方账号 */
|
||||||
export function listUserSocial() {
|
export function listUserSocial() {
|
||||||
return http.get<System.BindSocialAccountRes[]>(`${BASE_URL}/social`)
|
return http.get<T.BindSocialAccountRes[]>(`${BASE_URL}/social`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @desc 绑定三方账号 */
|
/** @desc 绑定三方账号 */
|
@@ -1,8 +1,6 @@
|
|||||||
import type * as T from './type'
|
import type * as T from './type'
|
||||||
import http from '@/utils/http'
|
import http from '@/utils/http'
|
||||||
|
|
||||||
export type * from './type'
|
|
||||||
|
|
||||||
const BASE_URL = '/system/user'
|
const BASE_URL = '/system/user'
|
||||||
|
|
||||||
/** @desc 查询用户列表 */
|
/** @desc 查询用户列表 */
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</a-col>
|
</a-col>
|
||||||
<div v-if="slots.left" class="gi-page-layout__divider" :class="{ none: isCollapsed || !isDesktop }">
|
<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-right v-if="isCollapsed" />
|
||||||
<icon-left v-else />
|
<icon-left v-else />
|
||||||
</div>
|
</div>
|
||||||
@@ -33,6 +33,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
margin: true,
|
margin: true,
|
||||||
padding: true,
|
padding: true,
|
||||||
gutter: false,
|
gutter: false,
|
||||||
|
defaultCollapsed: true,
|
||||||
leftColProps: () => ({}),
|
leftColProps: () => ({}),
|
||||||
rightColProps: () => ({}),
|
rightColProps: () => ({}),
|
||||||
leftStyle: () => ({}),
|
leftStyle: () => ({}),
|
||||||
@@ -68,6 +69,7 @@ interface Props {
|
|||||||
margin?: boolean
|
margin?: boolean
|
||||||
padding?: boolean
|
padding?: boolean
|
||||||
gutter?: boolean | number
|
gutter?: boolean | number
|
||||||
|
defaultCollapsed?: boolean
|
||||||
leftColProps?: ColProps
|
leftColProps?: ColProps
|
||||||
rightColProps?: ColProps
|
rightColProps?: ColProps
|
||||||
leftStyle?: CSSProperties
|
leftStyle?: CSSProperties
|
||||||
|
@@ -61,7 +61,7 @@
|
|||||||
v-bind="tableProps"
|
v-bind="tableProps"
|
||||||
:stripe="stripe"
|
:stripe="stripe"
|
||||||
:size="size"
|
:size="size"
|
||||||
:bordered="{ cell: isBordered, wrapper: isBordered }"
|
:bordered="{ cell: isBordered }"
|
||||||
:columns="visibleColumns"
|
:columns="visibleColumns"
|
||||||
:scrollbar="true"
|
:scrollbar="true"
|
||||||
:data="data"
|
:data="data"
|
||||||
@@ -270,11 +270,6 @@ defineExpose({
|
|||||||
height: 100%;
|
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,
|
: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) {
|
.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 = () => {
|
const open = () => {
|
||||||
window.open('/setting/message')
|
window.open('/user/message?tab=msg')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全部已读
|
// 全部已读
|
||||||
|
@@ -55,7 +55,7 @@
|
|||||||
<icon-down />
|
<icon-down />
|
||||||
</a-row>
|
</a-row>
|
||||||
<template #content>
|
<template #content>
|
||||||
<a-doption @click="router.push('/setting/profile')">
|
<a-doption @click="router.push('/user/profile')">
|
||||||
<span>个人中心</span>
|
<span>个人中心</span>
|
||||||
</a-doption>
|
</a-doption>
|
||||||
<a-divider :margin="0" />
|
<a-divider :margin="0" />
|
||||||
|
@@ -43,23 +43,29 @@ export const systemRoutes: RouteRecordRaw[] = [
|
|||||||
meta: { hidden: true },
|
meta: { hidden: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/setting',
|
path: '/user',
|
||||||
name: 'Setting',
|
name: 'User',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
meta: { hidden: true },
|
meta: { hidden: true },
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '/setting/profile',
|
path: '/user/profile',
|
||||||
name: 'SettingProfile',
|
name: 'UserProfile',
|
||||||
component: () => import('@/views/setting/profile/index.vue'),
|
component: () => import('@/views/user/profile/index.vue'),
|
||||||
meta: { title: '个人中心', showInTabs: false },
|
meta: { title: '个人中心', showInTabs: false },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/setting/message',
|
path: '/user/message',
|
||||||
name: 'SettingMessage',
|
name: 'UserMessage',
|
||||||
component: () => import('@/views/setting/message/index.vue'),
|
component: () => import('@/views/user/message/index.vue'),
|
||||||
meta: { title: '消息中心', showInTabs: false },
|
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' }"
|
:body-style="{ padding: '15px 20px 13px 20px' }"
|
||||||
>
|
>
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<a-link @click="router.replace({ path: '/system/notice' })">更多</a-link>
|
<a-link @click="open">更多</a-link>
|
||||||
</template>
|
</template>
|
||||||
<a-skeleton v-if="loading" :loading="loading" :animation="true">
|
<a-skeleton v-if="loading" :loading="loading" :animation="true">
|
||||||
<a-skeleton-line :rows="5" />
|
<a-skeleton-line :rows="5" />
|
||||||
@@ -31,16 +31,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a-card>
|
</a-card>
|
||||||
|
|
||||||
<NoticeDetailModal ref="NoticeDetailModalRef" />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { type DashboardNoticeResp, listDashboardNotice } from '@/apis'
|
import { type DashboardNoticeResp, listDashboardNotice } from '@/apis'
|
||||||
import { useDict } from '@/hooks/app'
|
import { useDict } from '@/hooks/app'
|
||||||
import NoticeDetailModal from '@/views/system/notice/NoticeDetailModal.vue'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const { notice_type } = useDict('notice_type')
|
const { notice_type } = useDict('notice_type')
|
||||||
|
|
||||||
const dataList = ref<DashboardNoticeResp[]>([])
|
const dataList = ref<DashboardNoticeResp[]>([])
|
||||||
@@ -56,10 +52,15 @@ const getDataList = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const NoticeDetailModalRef = ref<InstanceType<typeof NoticeDetailModal>>()
|
const router = useRouter()
|
||||||
// 详情
|
// 详情
|
||||||
const onDetail = (id: string) => {
|
const onDetail = (id: number) => {
|
||||||
NoticeDetailModalRef.value?.onDetail(id)
|
router.push({ path: '/user/notice', query: { id } })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开消息中心
|
||||||
|
const open = () => {
|
||||||
|
window.open('/user/message?tab=notice')
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
@@ -56,7 +56,7 @@ const handleBindSocial = () => {
|
|||||||
bindSocialAccount(source, othersQuery)
|
bindSocialAccount(source, othersQuery)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
router.push({
|
router.push({
|
||||||
path: '/setting/profile',
|
path: '/user/profile',
|
||||||
query: {
|
query: {
|
||||||
...othersQuery,
|
...othersQuery,
|
||||||
},
|
},
|
||||||
@@ -65,7 +65,7 @@ const handleBindSocial = () => {
|
|||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
router.push({
|
router.push({
|
||||||
path: '/setting/profile',
|
path: '/user/profile',
|
||||||
query: {
|
query: {
|
||||||
...othersQuery,
|
...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 RightBox from './Social.vue'
|
||||||
import PasswordPolicy from './Security.vue'
|
import PasswordPolicy from './Security.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'SettingProfile' })
|
defineOptions({ name: 'UserProfile' })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
Reference in New Issue
Block a user