feat(admin): 方案A—消息铃铛接真实待办聚合 + 清理空壳接口与空卡片
【消息铃铛(真实数据)】 - POST /api/message/list 改为待办聚合:待接单订单/待审核大神/待审核提现/待处理举报/待仲裁工单(复用 getDashboardTodos + 新增 pendingOrdersCount) - POST /api/message/read 持久化已读:新表 admin_message_reads 记录"已读时的数量",待办数量增加会重新变未读 - 两个接口均加 requireAdmin(待办数量属运营数据,不再匿名可见) 【删除空壳/死接口(前端已无调用方)】 - /api/user/save-info、/api/chat/list、/api/user/my-team/list、/api/user/latest-activity、/api/user/my-project/list 【后台页面清理】 - 「用户与个人中心 → 管理员资料」移除 我的项目/最近动态/我的团队/站内通知 四张空卡(保留真实的管理员资料头部) - 删除对应 4 个组件;api/user-center.ts 移除 queryMyProjectList/queryMyTeamList/queryLatestActivity/saveUserInfo;api/message.ts 移除 queryChatList 【验证】 - 新增 admin-todo-bell.spec.ts;更新 admin-demo-data-cleanup.spec.ts - 全量 101 套件 / 690 用例全绿;冒烟 8/8(未登录50008、待办数量、已读、新增后重新未读) - 后台 pnpm build 通过
这个提交包含在:
@@ -10,6 +10,10 @@ export interface MessageRecord {
|
||||
time: string;
|
||||
status: 0 | 1;
|
||||
messageType?: number;
|
||||
/** 待办跳转路径(可选) */
|
||||
link?: string;
|
||||
/** 待办数量(可选) */
|
||||
count?: number;
|
||||
}
|
||||
export type MessageListType = MessageRecord[];
|
||||
|
||||
@@ -24,15 +28,3 @@ interface MessageStatus {
|
||||
export function setMessageStatus(data: MessageStatus) {
|
||||
return axios.post<MessageListType>('/api/message/read', data);
|
||||
}
|
||||
|
||||
export interface ChatRecord {
|
||||
id: number;
|
||||
username: string;
|
||||
content: string;
|
||||
time: string;
|
||||
isCollect: boolean;
|
||||
}
|
||||
|
||||
export function queryChatList() {
|
||||
return axios.post<ChatRecord[]>('/api/chat/list');
|
||||
}
|
||||
|
||||
@@ -1,44 +1,5 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export interface MyProjectRecord {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
peopleNumber: number;
|
||||
contributors: {
|
||||
name: string;
|
||||
email: string;
|
||||
avatar: string;
|
||||
}[];
|
||||
}
|
||||
export function queryMyProjectList() {
|
||||
return axios.post('/api/user/my-project/list');
|
||||
}
|
||||
|
||||
export interface MyTeamRecord {
|
||||
id: number;
|
||||
avatar: string;
|
||||
name: string;
|
||||
peopleNumber: number;
|
||||
}
|
||||
export function queryMyTeamList() {
|
||||
return axios.post('/api/user/my-team/list');
|
||||
}
|
||||
|
||||
export interface LatestActivity {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
avatar: string;
|
||||
}
|
||||
export function queryLatestActivity() {
|
||||
return axios.post<LatestActivity[]>('/api/user/latest-activity');
|
||||
}
|
||||
|
||||
export function saveUserInfo() {
|
||||
return axios.post('/api/user/save-info');
|
||||
}
|
||||
|
||||
export interface BasicInfoModel {
|
||||
email: string;
|
||||
nickname: string;
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
<template>
|
||||
<a-card class="general-card" :title="$t('userInfo.title.latestActivity')">
|
||||
<template #extra>
|
||||
<a-link>{{ $t('userInfo.viewAll') }}</a-link>
|
||||
</template>
|
||||
<a-list :bordered="false">
|
||||
<a-list-item
|
||||
v-for="activity in activityList"
|
||||
:key="activity.id"
|
||||
action-layout="horizontal"
|
||||
>
|
||||
<a-skeleton
|
||||
v-if="loading"
|
||||
:loading="loading"
|
||||
:animation="true"
|
||||
class="skeleton-item"
|
||||
>
|
||||
<a-row :gutter="6">
|
||||
<a-col :span="2">
|
||||
<a-skeleton-shape shape="circle" />
|
||||
</a-col>
|
||||
<a-col :span="22">
|
||||
<a-skeleton-line :widths="['40%', '100%']" :rows="2" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-skeleton>
|
||||
<a-list-item-meta
|
||||
v-else
|
||||
:title="activity.title"
|
||||
:description="activity.description"
|
||||
>
|
||||
<template #avatar>
|
||||
<a-avatar>
|
||||
<img :src="activity.avatar" />
|
||||
</a-avatar>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { queryLatestActivity, LatestActivity } from '@/api/user-center';
|
||||
import useLoading from '@/hooks/loading';
|
||||
|
||||
const { loading, setLoading } = useLoading(true);
|
||||
const activityList = ref<LatestActivity[]>(new Array(7).fill({}));
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const { data } = await queryLatestActivity();
|
||||
activityList.value = data;
|
||||
} catch (err) {
|
||||
// you can report use errorHandler or other
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.latest-activity {
|
||||
&-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
.general-card :deep(.arco-list-item) {
|
||||
padding-left: 0;
|
||||
border-bottom: none;
|
||||
.arco-list-item-meta-content {
|
||||
flex: 1;
|
||||
padding-bottom: 27px;
|
||||
border-bottom: 1px solid var(--color-neutral-3);
|
||||
}
|
||||
.arco-list-item-meta-avatar {
|
||||
padding-bottom: 27px;
|
||||
}
|
||||
.skeleton-item {
|
||||
margin-top: 10px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid var(--color-neutral-3);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,27 +0,0 @@
|
||||
<template>
|
||||
<a-card class="general-card" :title="$t('userInfo.title.latestNotification')">
|
||||
<a-skeleton v-if="loading" :animation="true">
|
||||
<a-skeleton-line :rows="3" />
|
||||
</a-skeleton>
|
||||
<a-result v-else status="404">
|
||||
<template #subtitle>
|
||||
{{ $t('userInfo.nodata') }}
|
||||
</template>
|
||||
</a-result>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useLoading from '@/hooks/loading';
|
||||
|
||||
const { loading, setLoading } = useLoading(true);
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
}, 500);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.arco-result) {
|
||||
padding: 40px 32px 108px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,91 +0,0 @@
|
||||
<template>
|
||||
<a-card class="general-card" :title="$t('userInfo.title.myProject')">
|
||||
<template #extra>
|
||||
<a-link>{{ $t('userInfo.showMore') }}</a-link>
|
||||
</template>
|
||||
<a-row :gutter="16">
|
||||
<a-col
|
||||
v-for="(project, index) in projectList"
|
||||
:key="index"
|
||||
:xs="12"
|
||||
:sm="12"
|
||||
:md="12"
|
||||
:lg="12"
|
||||
:xl="8"
|
||||
:xxl="8"
|
||||
class="my-project-item"
|
||||
>
|
||||
<a-card>
|
||||
<a-skeleton v-if="loading" :loading="loading" :animation="true">
|
||||
<a-skeleton-line :rows="3" />
|
||||
</a-skeleton>
|
||||
<a-space v-else direction="vertical">
|
||||
<a-typography-text bold>{{ project.name }}</a-typography-text>
|
||||
<a-typography-text type="secondary">
|
||||
{{ project.description }}
|
||||
</a-typography-text>
|
||||
<a-space>
|
||||
<a-avatar-group :size="24">
|
||||
{{ project.contributors }}
|
||||
<a-avatar
|
||||
v-for="(contributor, idx) in project.contributors"
|
||||
:key="idx"
|
||||
:size="32"
|
||||
>
|
||||
<img alt="avatar" :src="contributor.avatar" />
|
||||
</a-avatar>
|
||||
</a-avatar-group>
|
||||
<a-typography-text type="secondary">
|
||||
等{{ project.peopleNumber }}人
|
||||
</a-typography-text>
|
||||
</a-space>
|
||||
</a-space>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { queryMyProjectList, MyProjectRecord } from '@/api/user-center';
|
||||
import useRequest from '@/hooks/request';
|
||||
|
||||
const defaultValue = Array(6).fill({} as MyProjectRecord);
|
||||
const { loading, response: projectList } = useRequest<MyProjectRecord[]>(
|
||||
queryMyProjectList,
|
||||
defaultValue
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
:deep(.arco-card-body) {
|
||||
min-height: 128px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.my-project {
|
||||
&-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&-title {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 18px !important;
|
||||
}
|
||||
|
||||
&-list {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&-item {
|
||||
// padding-right: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,64 +0,0 @@
|
||||
<template>
|
||||
<a-card
|
||||
class="general-card"
|
||||
:title="$t('userInfo.tab.title.team')"
|
||||
:header-style="{ paddingBottom: '18px' }"
|
||||
:body-style="{ paddingBottom: '12px' }"
|
||||
>
|
||||
<a-list :bordered="false">
|
||||
<a-list-item
|
||||
v-for="team in teamList"
|
||||
:key="team.id"
|
||||
action-layout="horizontal"
|
||||
>
|
||||
<a-skeleton v-if="loading" :loading="loading" :animation="true">
|
||||
<a-row :gutter="6">
|
||||
<a-col :span="6">
|
||||
<a-skeleton-shape shape="circle" />
|
||||
</a-col>
|
||||
<a-col :span="16">
|
||||
<a-skeleton-line :widths="['100%', '40%']" :rows="2" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-skeleton>
|
||||
<a-list-item-meta v-else :title="team.name">
|
||||
<template #avatar>
|
||||
<a-avatar>
|
||||
<img :src="team.avatar" />
|
||||
</a-avatar>
|
||||
</template>
|
||||
<template #description> 共{{ team.peopleNumber }}人 </template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { queryMyTeamList, MyTeamRecord } from '@/api/user-center';
|
||||
import useRequest from '@/hooks/request';
|
||||
|
||||
const defaultValue: MyTeamRecord[] = new Array(4).fill({});
|
||||
const { loading, response: teamList } = useRequest<MyTeamRecord[]>(
|
||||
queryMyTeamList,
|
||||
defaultValue
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.general-card {
|
||||
height: 356px;
|
||||
.arco-list-item {
|
||||
height: 72px;
|
||||
padding-left: 0;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--color-neutral-3);
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.arco-list-item-meta {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2,37 +2,11 @@
|
||||
<div class="container">
|
||||
<Breadcrumb :items="['menu.user', 'menu.user.info']" />
|
||||
<UserInfoHeader />
|
||||
<div class="content">
|
||||
<div class="content-left">
|
||||
<a-grid :cols="24" :col-gap="16" :row-gap="16">
|
||||
<a-grid-item :span="24">
|
||||
<MyProject />
|
||||
</a-grid-item>
|
||||
<a-grid-item :span="24">
|
||||
<LatestActivity />
|
||||
</a-grid-item>
|
||||
</a-grid>
|
||||
</div>
|
||||
<div class="content-right">
|
||||
<a-grid :cols="24" :row-gap="16">
|
||||
<a-grid-item :span="24">
|
||||
<MyTeam />
|
||||
</a-grid-item>
|
||||
<a-grid-item class="panel" :span="24">
|
||||
<LatestNotification />
|
||||
</a-grid-item>
|
||||
</a-grid>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UserInfoHeader from './components/user-info-header.vue';
|
||||
import LatestNotification from './components/latest-notification.vue';
|
||||
import MyProject from './components/my-project.vue';
|
||||
import LatestActivity from './components/latest-activity.vue';
|
||||
import MyTeam from './components/my-team.vue';
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -45,43 +19,4 @@
|
||||
.container {
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
margin-top: 12px;
|
||||
|
||||
&-left {
|
||||
flex: 1;
|
||||
margin-right: 16px;
|
||||
overflow: hidden;
|
||||
// background-color: var(--color-bg-2);
|
||||
|
||||
:deep(.arco-tabs-nav-tab) {
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&-right {
|
||||
width: 332px;
|
||||
}
|
||||
|
||||
.tab-pane-wrapper {
|
||||
padding: 0 16px 16px 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.mobile {
|
||||
.content {
|
||||
display: block;
|
||||
&-left {
|
||||
margin-right: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
&-right {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
在新工单中引用
屏蔽一个用户