fix: 完成: 诊所设置中心,消息中心
This commit is contained in:
157
apps/web-antd/src/components/modal/DoctorQrCodePreview.vue
Normal file
157
apps/web-antd/src/components/modal/DoctorQrCodePreview.vue
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Avatar, Button, Image } from 'ant-design-vue';
|
||||||
|
import html2canvas from 'html2canvas';
|
||||||
|
|
||||||
|
import { getDoctorInfoByStoreApi } from '#/views/doctor/doctor/api';
|
||||||
|
|
||||||
|
const qrCodeUrl = ref('');
|
||||||
|
const htmlToImage = ref('');
|
||||||
|
const previewAddress = ref('');
|
||||||
|
const goodAt = ref('');
|
||||||
|
const doctorInfo = ref();
|
||||||
|
const doctorId = ref(0);
|
||||||
|
const storeId = ref(0);
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
fullscreenButton: false,
|
||||||
|
footer: false,
|
||||||
|
showCancelButton: true,
|
||||||
|
showConfirmButton: true,
|
||||||
|
confirmText: '保存医生卡片图片',
|
||||||
|
cancelText: '保存医生二维码图片',
|
||||||
|
draggable: true,
|
||||||
|
onCancel() {
|
||||||
|
doctorInfo.value = null;
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onConfirm: async () => {
|
||||||
|
doctorInfo.value = null;
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onOpenChange(isOpen: boolean) {
|
||||||
|
if (isOpen) {
|
||||||
|
const { values } = modalApi.getData<Record<string, any>>();
|
||||||
|
if (values) {
|
||||||
|
console.log(values, 'ssssssssssssss')
|
||||||
|
if (values.doctor_id !== undefined) {
|
||||||
|
doctorId.value = values.doctor_id;
|
||||||
|
}
|
||||||
|
if (values.store_id !== undefined) {
|
||||||
|
storeId.value = values.store_id;
|
||||||
|
}
|
||||||
|
getDoctorInfoByStore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const getDoctorInfoByStore = () => {
|
||||||
|
getDoctorInfoByStoreApi({
|
||||||
|
doctor_id: doctorId.value,
|
||||||
|
store_id: storeId.value,
|
||||||
|
}).then((res) => {
|
||||||
|
doctorInfo.value = res;
|
||||||
|
goodAt.value = res.doctor.good_at;
|
||||||
|
previewAddress.value =
|
||||||
|
res.store.province.name + res.store.city.name + res.store.position;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 下载二维码
|
||||||
|
* @param name
|
||||||
|
* @param isQr
|
||||||
|
*/
|
||||||
|
const downloadQRCode = async (name: string, isQr = false) => {
|
||||||
|
const element = isQr ? qrCodeUrl.value : htmlToImage.value;
|
||||||
|
html2canvas(element, {
|
||||||
|
useCORS: true,
|
||||||
|
allowTaint: true,
|
||||||
|
logging: false,
|
||||||
|
}).then((canvas) => {
|
||||||
|
// 创建a标签下载
|
||||||
|
const link = document.createElement('a'); // 创建a标签
|
||||||
|
link.href = canvas.toDataURL(); // 是canvas对象的一种方法,用于将canvas对象转换为base64位编码
|
||||||
|
link.setAttribute('download', `${doctorInfo.value.doctor.name}${name}.png`); // 利用了a标签的download 来下载 canvas图片
|
||||||
|
link.style.display = 'none'; // 将图片隐藏起来
|
||||||
|
document.body.append(link); // 插入到其中
|
||||||
|
link.click();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Modal v-if="doctorInfo" title="医生二维码" class="w-[50%]">
|
||||||
|
<template #footer>
|
||||||
|
<Button type="primary" @click="downloadQRCode('二维码卡片下载')">
|
||||||
|
二维码卡片下载
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" @click="downloadQRCode('二维码下载', true)">
|
||||||
|
二维码下载
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
<div class="qrBox" style="text-align: center">
|
||||||
|
<div ref="htmlToImage" class="qrModal">
|
||||||
|
<div id="qrcode" ref="qrCodeUrl" class="qrcode">
|
||||||
|
<Avatar :src="doctorInfo.doctor.avatar" />
|
||||||
|
<div class="qrTitle">
|
||||||
|
{{ doctorInfo.doctor.name }} {{ doctorInfo.doctor.title.name }}
|
||||||
|
</div>
|
||||||
|
<div class="qrAddress">
|
||||||
|
<span class="addressTitle">诊所地址:</span>{{ previewAddress }}
|
||||||
|
</div>
|
||||||
|
<Image :preview="false" :src="doctorInfo.qr_code" class="mt-3" height="30" width="30" />
|
||||||
|
<h3 class="border-l-4 border-blue-500 pl-3 text-lg font-semibold">
|
||||||
|
擅长领域
|
||||||
|
</h3>
|
||||||
|
<div
|
||||||
|
class="prose max-w-none whitespace-pre-wrap"
|
||||||
|
style="text-indent: 2em"
|
||||||
|
>
|
||||||
|
{{ doctorInfo.doctor.good_at }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.qrModal {
|
||||||
|
// 宽度适应文本长度
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: center;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle at center,
|
||||||
|
rgba(173, 216, 230, 0.8),
|
||||||
|
rgba(135, 206, 235, 0.8),
|
||||||
|
rgba(100, 149, 237, 0.8)
|
||||||
|
);
|
||||||
|
padding: 20px 30px;
|
||||||
|
|
||||||
|
.qrTitle {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qrAddress {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addressTitle {
|
||||||
|
color: #3a8ee6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qrcode {
|
||||||
|
width: fit-content;
|
||||||
|
//width: 50%;
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px 30px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { NotificationItem } from '@vben/layouts';
|
import type { NotificationItem } from '@vben/layouts';
|
||||||
|
|
||||||
import { computed, ref, watch } from 'vue';
|
import {computed, onUnmounted, ref, watch} from 'vue';
|
||||||
|
|
||||||
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
|
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
|
||||||
|
|
||||||
@@ -19,41 +19,170 @@ import { preferences } from '@vben/preferences';
|
|||||||
import { useAccessStore, useUserStore } from '@vben/stores';
|
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||||
// import { openWindow } from '@vben/utils';
|
// import { openWindow } from '@vben/utils';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Bell,
|
||||||
|
CalendarClock,
|
||||||
|
Megaphone,
|
||||||
|
} from 'lucide-vue-next';
|
||||||
|
|
||||||
// import { $t } from '#/locales';
|
// import { $t } from '#/locales';
|
||||||
import { useAuthStore } from '#/store';
|
import { useAuthStore } from '#/store';
|
||||||
import LoginForm from '#/views/_core/authentication/login.vue';
|
import LoginForm from '#/views/_core/authentication/login.vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import {getNoticeListApi, readAllApi} from "#/views/notice/api";
|
||||||
|
import {formatTimeToRelative} from "#/util/tool";
|
||||||
|
import {notification} from "ant-design-vue";
|
||||||
|
|
||||||
const notifications = ref<NotificationItem[]>([
|
const router = useRouter();
|
||||||
// TODO 通知列表
|
|
||||||
// {
|
// 样式相关
|
||||||
// avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
|
const typeIcons = {
|
||||||
// date: '3小时前',
|
0: Megaphone,
|
||||||
// isRead: true,
|
1: Bell,
|
||||||
// message: '描述信息描述信息描述信息',
|
2: CalendarClock,
|
||||||
// title: '收到了 14 份新周报',
|
};
|
||||||
// },
|
|
||||||
// {
|
const typeColors = {
|
||||||
// avatar: 'https://avatar.vercel.sh/1',
|
0: 'text-sky-500',
|
||||||
// date: '刚刚',
|
1: 'text-amber-500',
|
||||||
// isRead: false,
|
2: 'text-emerald-500',
|
||||||
// message: '描述信息描述信息描述信息',
|
};
|
||||||
// title: '朱偏右 回复了你',
|
|
||||||
// },
|
const typeBgColors = {
|
||||||
// {
|
0: 'bg-sky-50',
|
||||||
// avatar: 'https://avatar.vercel.sh/1',
|
1: 'bg-amber-50',
|
||||||
// date: '2024-01-01',
|
2: 'bg-emerald-50',
|
||||||
// isRead: false,
|
};
|
||||||
// message: '描述信息描述信息描述信息',
|
|
||||||
// title: '曲丽丽 评论了你',
|
const typeBorderColors = {
|
||||||
// },
|
0: 'border-sky-200',
|
||||||
// {
|
1: 'border-amber-200',
|
||||||
// avatar: 'https://avatar.vercel.sh/satori',
|
2: 'border-emerald-200',
|
||||||
// date: '1天前',
|
};
|
||||||
// isRead: false,
|
|
||||||
// message: '描述信息描述信息描述信息',
|
// const notifications = ref<NotificationItem[]>([
|
||||||
// title: '代办提醒',
|
// // TODO 通知列表
|
||||||
// },
|
// {
|
||||||
]);
|
// avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
|
||||||
|
// date: '3小时前',
|
||||||
|
// isRead: true,
|
||||||
|
// message: '描述信息描述信息描述信息',
|
||||||
|
// title: '收到了 14 份新周报',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// avatar: 'https://avatar.vercel.sh/1',
|
||||||
|
// date: '刚刚',
|
||||||
|
// isRead: false,
|
||||||
|
// message: '描述信息描述信息描述信息',
|
||||||
|
// title: '朱偏右 回复了你',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// avatar: 'https://avatar.vercel.sh/1',
|
||||||
|
// date: '2024-01-01',
|
||||||
|
// isRead: false,
|
||||||
|
// message: '描述信息描述信息描述信息',
|
||||||
|
// title: '曲丽丽 评论了你',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// avatar: 'https://avatar.vercel.sh/satori',
|
||||||
|
// date: '1天前',
|
||||||
|
// isRead: false,
|
||||||
|
// message: '描述信息描述信息描述信息',
|
||||||
|
// title: '代办提醒',
|
||||||
|
// },
|
||||||
|
// ]);
|
||||||
|
const notifications = ref<NotificationItem[]>([]);
|
||||||
|
|
||||||
|
// 获取消息列表数据
|
||||||
|
const getNoticeList = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getNoticeListApi({
|
||||||
|
status: 0,
|
||||||
|
});
|
||||||
|
notifications.value = res.items.map(item => ({
|
||||||
|
id: item.id,
|
||||||
|
icon: typeIcons[item.type],
|
||||||
|
// 将API返回的数据映射到组件需要的格式
|
||||||
|
message: item.title,
|
||||||
|
title: getMessageTypeName(item.type),
|
||||||
|
date: formatTimeToRelative(item.created_at),
|
||||||
|
color: typeColors[item.type],
|
||||||
|
isRead: item.status
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取消息列表失败:', error);
|
||||||
|
} finally {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
getNoticeList();
|
||||||
|
// 每隔十秒刷新一次
|
||||||
|
let intervalId = setInterval(getNoticeList, 10_000);
|
||||||
|
|
||||||
|
// 封装定时器重启逻辑
|
||||||
|
const restartInterval = () => {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
getNoticeList(); // 立即获取最新数据
|
||||||
|
intervalId = setInterval(getNoticeList, 10_000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理跨标签页修改
|
||||||
|
const handleStorageChange = (event) => {
|
||||||
|
if (event.key === 'readStatus') {
|
||||||
|
restartInterval();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理当前标签页修改(核心新增部分)
|
||||||
|
const handleLocalChange = () => {
|
||||||
|
restartInterval();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加双重监听
|
||||||
|
window.addEventListener('storage', handleStorageChange); // 跨标签页监听
|
||||||
|
const originalSetItem = localStorage.setItem.bind(localStorage);
|
||||||
|
|
||||||
|
// 拦截当前页的 localStorage.setItem 操作
|
||||||
|
localStorage.setItem = (key, value) => {
|
||||||
|
originalSetItem(key, value);
|
||||||
|
if (key === 'readStatus') {
|
||||||
|
handleLocalChange(); // 手动触发当前页回调
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件卸载时清理
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
window.removeEventListener('storage', handleStorageChange);
|
||||||
|
localStorage.setItem = originalSetItem; // 恢复原始方法
|
||||||
|
});
|
||||||
|
|
||||||
|
// 消息类型定义
|
||||||
|
const messageTypes = [
|
||||||
|
{ id: -1, name: '全部消息' },
|
||||||
|
{ id: 0, name: '系统公告', icon: Megaphone },
|
||||||
|
{ id: 1, name: '系统通知', icon: Bell },
|
||||||
|
{ id: 2, name: '周报提醒', icon: CalendarClock },
|
||||||
|
];
|
||||||
|
// 根据类型ID获取类型名称
|
||||||
|
const getMessageTypeName = (typeId) => {
|
||||||
|
const type = messageTypes.find(t => t.id === typeId);
|
||||||
|
return type ? type.name : '未知类型';
|
||||||
|
};
|
||||||
|
const goNoticeList = () => {
|
||||||
|
// TODO 跳转通知列表
|
||||||
|
router.push('/notice/list');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理消息点击
|
||||||
|
const handleItemClick = (item) => {
|
||||||
|
notifications.value.forEach((value) => {
|
||||||
|
if (value.id === item.id) {
|
||||||
|
// 删除这个对象
|
||||||
|
value.status = 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
router.push(`/notice/detail/${item.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
@@ -111,7 +240,14 @@ async function handleLogout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleNoticeClear() {
|
function handleNoticeClear() {
|
||||||
notifications.value = [];
|
readAllApi().then(() => {
|
||||||
|
notifications.value = [];
|
||||||
|
// 成功后可以重新获取数据或者直接更新本地状态
|
||||||
|
notification.success({
|
||||||
|
message: '标记成功',
|
||||||
|
duration: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMakeAll() {
|
function handleMakeAll() {
|
||||||
@@ -154,6 +290,8 @@ watch(
|
|||||||
:notifications="notifications"
|
:notifications="notifications"
|
||||||
@clear="handleNoticeClear"
|
@clear="handleNoticeClear"
|
||||||
@make-all="handleMakeAll"
|
@make-all="handleMakeAll"
|
||||||
|
@read="handleItemClick"
|
||||||
|
@view-all="goNoticeList"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #extra>
|
<template #extra>
|
||||||
@@ -167,6 +305,5 @@ watch(
|
|||||||
<template #lock-screen>
|
<template #lock-screen>
|
||||||
<LockScreen :avatar @to-login="handleLogout" />
|
<LockScreen :avatar @to-login="handleLogout" />
|
||||||
</template>
|
</template>
|
||||||
<UpdatePasswordModals />
|
|
||||||
</BasicLayout>
|
</BasicLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -348,3 +348,28 @@ export function getRegisterStatus(status: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 把格式化时间转换成相对时间(刚刚,几分钟前,几小时前,几天前,几周前)
|
||||||
|
export function formatTimeToRelative(time: string) {
|
||||||
|
const now = new Date();
|
||||||
|
const date = new Date(time);
|
||||||
|
const diff = now.getTime() - date.getTime();
|
||||||
|
const seconds = Math.floor(diff / 1000);
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
const weeks = Math.floor(days / 7);
|
||||||
|
|
||||||
|
if(seconds < 60) {
|
||||||
|
return '刚刚';
|
||||||
|
} else if(minutes < 60) {
|
||||||
|
return `${minutes} 分钟前`;
|
||||||
|
} else if(hours < 24) {
|
||||||
|
return `${hours} 小时前`;
|
||||||
|
} else if(days < 7) {
|
||||||
|
return `${days} 天前`;
|
||||||
|
} else if (weeks < 4) {
|
||||||
|
return `${weeks} 周前`;
|
||||||
|
}
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,9 +28,11 @@ export async function saveServicePriceApi(data: Record<string, any>) {
|
|||||||
return requestClient.post<any>(`${prefix}save-service-price`, data);
|
return requestClient.post<any>(`${prefix}save-service-price`, data);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 创建医嘱
|
* 保存折扣比例
|
||||||
* @param content
|
* @param z_sale_percent
|
||||||
*/
|
*/
|
||||||
export async function createDoctorOrderApi(content: string) {
|
export async function saveZSalePercentApi(z_sale_percent: number | string) {
|
||||||
return requestClient.post<any>(`${prefix}create-doctor-order`, { content });
|
return requestClient.post<any>(`${prefix}save-zsale-percent`, {
|
||||||
|
z_sale_percent,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,41 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import { EllipsisText, Page } from '@vben/common-ui';
|
import { EllipsisText, Page, useVbenModal } from '@vben/common-ui';
|
||||||
import { useUserStore } from '@vben/stores';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
Avatar,
|
||||||
Button,
|
Button,
|
||||||
Card, Image,
|
Card,
|
||||||
Input,
|
Image,
|
||||||
InputGroup,
|
InputNumber, message,
|
||||||
InputNumber,
|
|
||||||
notification,
|
notification,
|
||||||
RadioButton,
|
RadioButton,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
Rate,
|
|
||||||
Switch,
|
|
||||||
Tag,
|
Tag,
|
||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
|
// eslint-disable-next-line n/no-extraneous-import
|
||||||
|
import html2canvas from 'html2canvas';
|
||||||
|
|
||||||
import {getStoreInfoApi} from "#/views/business/store/settings/api";
|
import DoctorQrCodePreview from '#/components/modal/DoctorQrCodePreview.vue';
|
||||||
import html2canvas from "html2canvas";
|
import {
|
||||||
|
getStoreInfoApi,
|
||||||
|
saveZSalePercentApi,
|
||||||
|
} from '#/views/business/store/settings/api';
|
||||||
|
|
||||||
const userInfoStore = useUserStore();
|
const [DoctorQrCodePreviewModal, DoctorQrCodePreviewModalApi] = useVbenModal({
|
||||||
|
connectedComponent: DoctorQrCodePreview,
|
||||||
|
});
|
||||||
|
|
||||||
|
const openDoctorQrCode = (doctorId: number, isNotQr = false) => {
|
||||||
|
if (isNotQr) return message.error('该医生暂时没有二维码!');
|
||||||
|
DoctorQrCodePreviewModalApi.setData({
|
||||||
|
values: {
|
||||||
|
doctor_id: doctorId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
DoctorQrCodePreviewModalApi.open();
|
||||||
|
};
|
||||||
|
|
||||||
// 映射数据(示例)
|
// 映射数据(示例)
|
||||||
const activeTabBar = ref(1);
|
const activeTabBar = ref(1);
|
||||||
@@ -38,6 +52,10 @@ const tabBar = [
|
|||||||
value: 3,
|
value: 3,
|
||||||
label: '诊所二维码',
|
label: '诊所二维码',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: 4,
|
||||||
|
label: '我的医生团队',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// 格式化日期
|
// 格式化日期
|
||||||
@@ -94,25 +112,19 @@ const downloadQRCode = async (name: string, isQr = false) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveSalePercent = () => {
|
const saveZSalePercent = () => {
|
||||||
saveServicePriceApi({
|
saveZSalePercentApi(data.value.z_sale_percent).then(() => {
|
||||||
z_sale_percent: data.z_sale_percent,
|
notification.success({
|
||||||
}).then((res) => {
|
message: '任务开始执行',
|
||||||
if (res.code === 200) {
|
duration: 2,
|
||||||
notification.success({
|
});
|
||||||
message: '保存成功',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page
|
<Page v-if="data" :title="`${data.name}的配置中心`" auto-content-height>
|
||||||
v-if="data"
|
<DoctorQrCodePreviewModal />
|
||||||
:title="`${data.name}的配置中心`"
|
|
||||||
auto-content-height
|
|
||||||
>
|
|
||||||
<div class="pb-2">
|
<div class="pb-2">
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
v-model:value="activeTabBar"
|
v-model:value="activeTabBar"
|
||||||
@@ -176,11 +188,8 @@ const saveSalePercent = () => {
|
|||||||
详细地址
|
详细地址
|
||||||
</h3>
|
</h3>
|
||||||
<EllipsisText :line="3" :tooltip="false" expand>
|
<EllipsisText :line="3" :tooltip="false" expand>
|
||||||
<div
|
<div style="text-indent: 2em">
|
||||||
style="text-indent: 2em"
|
{{ data.province.name }}{{ data.city.name }}{{ data.position }}
|
||||||
>
|
|
||||||
{{ data.province.name
|
|
||||||
}}{{ data.city.name }}{{ data.position }}
|
|
||||||
</div>
|
</div>
|
||||||
</EllipsisText>
|
</EllipsisText>
|
||||||
</div>
|
</div>
|
||||||
@@ -201,6 +210,17 @@ const saveSalePercent = () => {
|
|||||||
style="font-weight: bold"
|
style="font-weight: bold"
|
||||||
>{{ data.inquiry_count || 0 }}</span>
|
>{{ data.inquiry_count || 0 }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex w-1/2 items-center justify-between rounded-lg bg-gray-50 p-4 dark:bg-[#020817]"
|
||||||
|
style="margin-top: 0"
|
||||||
|
>
|
||||||
|
<span class="">医生数量</span>
|
||||||
|
<span
|
||||||
|
class="font-semibold text-blue-600"
|
||||||
|
style="font-weight: bold"
|
||||||
|
>{{ data.doctor_count || 0 }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2 rounded-lg bg-gray-50 p-4 dark:bg-[#020817]">
|
<div class="space-y-2 rounded-lg bg-gray-50 p-4 dark:bg-[#020817]">
|
||||||
<div class="flex justify-between text-sm">
|
<div class="flex justify-between text-sm">
|
||||||
@@ -222,14 +242,14 @@ const saveSalePercent = () => {
|
|||||||
<InputNumber
|
<InputNumber
|
||||||
v-model:value="data.z_sale_percent"
|
v-model:value="data.z_sale_percent"
|
||||||
style="min-width: 100px"
|
style="min-width: 100px"
|
||||||
|
@keydown.enter="saveZSalePercent"
|
||||||
>
|
>
|
||||||
<template #addonAfter> 元 </template>
|
<template #addonAfter> %</template>
|
||||||
</InputNumber>
|
</InputNumber>
|
||||||
<Button type="primary" @click="saveSalePercent">保存</Button>
|
<Button type="primary" @click="saveZSalePercent">保存</Button>
|
||||||
</Card>
|
</Card>
|
||||||
<Card v-else-if="activeTabBar === 3" title="诊所二维码" >
|
<Card v-else-if="activeTabBar === 3" title="诊所二维码">
|
||||||
<template #extra>
|
<template #extra>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
style="margin: 20px auto"
|
style="margin: 20px auto"
|
||||||
type="link"
|
type="link"
|
||||||
@@ -249,7 +269,12 @@ const saveSalePercent = () => {
|
|||||||
<div ref="htmlToImage" class="qrModal">
|
<div ref="htmlToImage" class="qrModal">
|
||||||
<div id="qrcode" ref="qrCodeUrl" class="qrcode">
|
<div id="qrcode" ref="qrCodeUrl" class="qrcode">
|
||||||
<div class="qrTitle">{{ data.name }}</div>
|
<div class="qrTitle">{{ data.name }}</div>
|
||||||
<Image :preview="false" :src="data.qr_code" height="30" width="30" />
|
<Image
|
||||||
|
:preview="false"
|
||||||
|
:src="data.qr_code"
|
||||||
|
height="30"
|
||||||
|
width="30"
|
||||||
|
/>
|
||||||
<div class="qrAddress">
|
<div class="qrAddress">
|
||||||
<span class="addressTitle">诊所地址:</span>{{ data.province.name
|
<span class="addressTitle">诊所地址:</span>{{ data.province.name
|
||||||
}}{{ data.city.name }}{{ data.position }}
|
}}{{ data.city.name }}{{ data.position }}
|
||||||
@@ -258,14 +283,111 @@ const saveSalePercent = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
<Card v-else-if="activeTabBar === 4" title="我的医生团队">
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
v-for="(doctor, index) in data.doctor_list"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<!-- 医生头像和信息 -->
|
||||||
|
<div class="mb-4 flex items-center">
|
||||||
|
<Avatar
|
||||||
|
:alt="doctor.doctor.name"
|
||||||
|
:class="{
|
||||||
|
'border-green-500': doctor.is_online,
|
||||||
|
'border-gray-300': !doctor.is_online,
|
||||||
|
}"
|
||||||
|
:src="doctor.doctor?.avatar"
|
||||||
|
class="h-16 w-16 rounded-full border-2 object-cover"
|
||||||
|
/>
|
||||||
|
<div class="ml-4">
|
||||||
|
<h3 class="text-lg font-semibold">
|
||||||
|
<span>{{ doctor.doctor?.name || '未知医生' }}</span>
|
||||||
|
<span
|
||||||
|
:class="doctor.is_online ? 'text-green-500' : 'text-gray-500'"
|
||||||
|
class="ml-2 text-sm"
|
||||||
|
>
|
||||||
|
●
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
最后登录:
|
||||||
|
<span>{{
|
||||||
|
new Date(doctor.last_login_time * 1000).toLocaleString()
|
||||||
|
}}</span>
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
手机号:
|
||||||
|
<span>{{ doctor.doctor.mobile }}</span>
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
专业:
|
||||||
|
|
||||||
|
<span
|
||||||
|
:class="
|
||||||
|
doctor.identity === 1
|
||||||
|
? 'bg-green-100 text-green-700'
|
||||||
|
: 'bg-gray-100 text-gray-700'
|
||||||
|
"
|
||||||
|
class="rounded px-2 py-1"
|
||||||
|
>
|
||||||
|
{{ doctor.identity === 1 ? '中医' : '西医' }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 二维码 -->
|
||||||
|
<div class="border-t pt-4"
|
||||||
|
@click="openDoctorQrCode(doctor.doctor.su_id, !doctor.qr_code)"
|
||||||
|
>
|
||||||
|
<p class="mb-2 text-sm text-gray-600">联系二维码:</p>
|
||||||
|
<template v-if="doctor.qr_code">
|
||||||
|
<img
|
||||||
|
:src="doctor.qr_code"
|
||||||
|
class="h-32 w-full rounded bg-gray-100 object-contain"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="!doctor.qr_code">
|
||||||
|
<div
|
||||||
|
class="flex h-32 items-center justify-center rounded bg-gray-100 text-gray-400"
|
||||||
|
>
|
||||||
|
暂无二维码
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 状态标签 -->
|
||||||
|
<div class="mt-4 flex justify-between text-sm">
|
||||||
|
<span
|
||||||
|
:class="
|
||||||
|
doctor.is_online
|
||||||
|
? 'bg-green-100 text-green-700'
|
||||||
|
: 'bg-gray-100 text-gray-700'
|
||||||
|
"
|
||||||
|
class="rounded px-2 py-1"
|
||||||
|
>
|
||||||
|
{{ doctor.is_online ? '在线' : '离线' }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
:class="
|
||||||
|
doctor.is_delete
|
||||||
|
? 'bg-red-100 text-red-700'
|
||||||
|
: 'bg-blue-100 text-blue-700'
|
||||||
|
"
|
||||||
|
class="rounded px-2 py-1"
|
||||||
|
>
|
||||||
|
{{ doctor.is_delete ? '已删除' : '正常' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
</Page>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
/* 自定义小标签样式 */
|
|
||||||
//.pill-info {
|
|
||||||
// @apply rounded-full bg-gray-100 px-3 py-1 text-sm ;
|
|
||||||
//}
|
|
||||||
|
|
||||||
/* 优化行高 */
|
/* 优化行高 */
|
||||||
.prose {
|
.prose {
|
||||||
@apply leading-relaxed;
|
@apply leading-relaxed;
|
||||||
@@ -275,15 +397,16 @@ const saveSalePercent = () => {
|
|||||||
.hover-animate {
|
.hover-animate {
|
||||||
@apply transition-transform duration-300 hover:scale-[1.02];
|
@apply transition-transform duration-300 hover:scale-[1.02];
|
||||||
}
|
}
|
||||||
|
|
||||||
.qrModal {
|
.qrModal {
|
||||||
// 宽度适应文本长度
|
// 宽度适应文本长度
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background: radial-gradient(
|
background: radial-gradient(
|
||||||
circle at center,
|
circle at center,
|
||||||
rgba(173, 216, 230, 0.8),
|
rgba(173, 216, 230, 0.8),
|
||||||
rgba(135, 206, 235, 0.8),
|
rgba(135, 206, 235, 0.8),
|
||||||
rgba(100, 149, 237, 0.8)
|
rgba(100, 149, 237, 0.8)
|
||||||
);
|
);
|
||||||
padding: 20px 30px;
|
padding: 20px 30px;
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,13 @@ export async function getDoctorList(data: any) {
|
|||||||
* 分页查询用户列表
|
* 分页查询用户列表
|
||||||
* @param data
|
* @param data
|
||||||
*/
|
*/
|
||||||
|
export async function getDoctorInfoByStoreApi(data: any) {
|
||||||
|
return requestClient.get<any>(`${prefix}doctor-info-by-store`, { params: data });
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 查询我的余额
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
export async function getDoctorAccountBalance(data: any = {}) {
|
export async function getDoctorAccountBalance(data: any = {}) {
|
||||||
return requestClient.get<any>(`${prefix}my-balance`, { params: data });
|
return requestClient.get<any>(`${prefix}my-balance`, { params: data });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -223,6 +223,7 @@ const createDoctorOrder = () => {
|
|||||||
v-if="data.service.register_status === 1"
|
v-if="data.service.register_status === 1"
|
||||||
v-model:value="data.service.register_price"
|
v-model:value="data.service.register_price"
|
||||||
style="min-width: 100px"
|
style="min-width: 100px"
|
||||||
|
@keydown.enter="saveServicePrice"
|
||||||
>
|
>
|
||||||
<template #addonAfter> 元 </template>
|
<template #addonAfter> 元 </template>
|
||||||
</InputNumber>
|
</InputNumber>
|
||||||
@@ -284,6 +285,7 @@ const createDoctorOrder = () => {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex w-1/2 items-center justify-between rounded-lg bg-gray-50 p-4 dark:bg-[#020817]"
|
class="flex w-1/2 items-center justify-between rounded-lg bg-gray-50 p-4 dark:bg-[#020817]"
|
||||||
|
style="margin-top: 0;"
|
||||||
>
|
>
|
||||||
<span class="">综合评分</span>
|
<span class="">综合评分</span>
|
||||||
<Rate :value="data.grade" disabled style="color: #455cda" />
|
<Rate :value="data.grade" disabled style="color: #455cda" />
|
||||||
@@ -291,6 +293,7 @@ const createDoctorOrder = () => {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex w-1/2 items-center justify-between rounded-lg bg-gray-50 p-4 dark:bg-[#020817]"
|
class="flex w-1/2 items-center justify-between rounded-lg bg-gray-50 p-4 dark:bg-[#020817]"
|
||||||
|
style="margin-top: 0;"
|
||||||
>
|
>
|
||||||
<span class="">平均回复</span>
|
<span class="">平均回复</span>
|
||||||
<span
|
<span
|
||||||
|
|||||||
48
apps/web-antd/src/views/notice/api/index.ts
Normal file
48
apps/web-antd/src/views/notice/api/index.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
const prefix = 'notice/';
|
||||||
|
/**
|
||||||
|
* 查询通知列表
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export async function getNoticeListApi(data: any) {
|
||||||
|
return requestClient.get<any>(`${prefix}list`, { params: data });
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 查询通知详情
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
export async function getNoticeDetailApi(id: number) {
|
||||||
|
const result = requestClient.get<any>(`${prefix}detail`, { params: {id} });
|
||||||
|
localStorage.setItem(
|
||||||
|
`readStatus`,
|
||||||
|
// 随机数
|
||||||
|
Math.random().toString(36).slice(2),
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 设置已读某条消息
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
export async function readApi(id: number) {
|
||||||
|
const result = requestClient.post<any>(`${prefix}read`, {id});
|
||||||
|
localStorage.setItem(
|
||||||
|
`readStatus`,
|
||||||
|
// 随机数
|
||||||
|
Math.random().toString(36).slice(2),
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 设置已读全部消息
|
||||||
|
*/
|
||||||
|
export async function readAllApi() {
|
||||||
|
const result = requestClient.post<any>(`${prefix}read-all`);
|
||||||
|
localStorage.setItem(
|
||||||
|
`readStatus`,
|
||||||
|
// 随机数
|
||||||
|
Math.random().toString(36).slice(2),
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
244
apps/web-antd/src/views/notice/compoents/detail.vue
Normal file
244
apps/web-antd/src/views/notice/compoents/detail.vue
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { Page } from '@vben/common-ui';
|
||||||
|
import {
|
||||||
|
ArrowLeft,
|
||||||
|
Bell,
|
||||||
|
CalendarClock,
|
||||||
|
Clock,
|
||||||
|
Megaphone,
|
||||||
|
User,
|
||||||
|
} from 'lucide-vue-next';
|
||||||
|
import {formatTimeToRelative} from "#/util/tool";
|
||||||
|
import {getNoticeDetailApi} from "#/views/notice/api";
|
||||||
|
|
||||||
|
// 假设有一个获取通知详情的API
|
||||||
|
// import { getNoticeDetailApi, markNoticeReadApi } from "#/views/notice/api";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const noticeId = ref(route.params.id);
|
||||||
|
|
||||||
|
const notice = ref(null);
|
||||||
|
const loading = ref(true);
|
||||||
|
const error = ref(null);
|
||||||
|
|
||||||
|
// 消息类型定义
|
||||||
|
const messageTypes = [
|
||||||
|
{ id: 0, name: '系统公告', icon: Megaphone, color: 'text-sky-500', bgColor: 'bg-sky-50', borderColor: 'border-sky-200' },
|
||||||
|
{ id: 1, name: '系统通知', icon: Bell, color: 'text-amber-500', bgColor: 'bg-amber-50', borderColor: 'border-amber-200' },
|
||||||
|
{ id: 2, name: '周报提醒', icon: CalendarClock, color: 'text-emerald-500', bgColor: 'bg-emerald-50', borderColor: 'border-emerald-200' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 获取通知详情
|
||||||
|
const getNoticeDetail = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 实际项目中应该调用API获取数据
|
||||||
|
// const res = await getNoticeDetailApi(noticeId);
|
||||||
|
// notice.value = res;
|
||||||
|
|
||||||
|
getNoticeDetailApi(noticeId.value).then((res) => {
|
||||||
|
notice.value = res;
|
||||||
|
loading.value = false;
|
||||||
|
})
|
||||||
|
// 模拟API响应
|
||||||
|
// setTimeout(() => {
|
||||||
|
// notice.value = {
|
||||||
|
// id: Number(noticeId.value),
|
||||||
|
// title: "更新诊所中药销售价格",
|
||||||
|
// detail: "您执行的任务已经完成",
|
||||||
|
// content: "执行成功,价格已经更新完成!\n本次更新了:748条数据!\n\n更新时间:2025-04-10 14:05:08\n\n请注意检查更新后的价格是否符合预期。如有问题,请联系系统管理员。",
|
||||||
|
// type: 1,
|
||||||
|
// user_id: 1,
|
||||||
|
// status: 0,
|
||||||
|
// created_at: "2025-04-10 14:05:08",
|
||||||
|
// admin: {
|
||||||
|
// id: 1,
|
||||||
|
// nick_name: "李琦"
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// // 如果通知未读,标记为已读
|
||||||
|
// if (notice.value && notice.value.status === 0) {
|
||||||
|
// markAsRead();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// loading.value = false;
|
||||||
|
// }, 800);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取通知详情失败:', err);
|
||||||
|
error.value = '获取通知详情失败,请稍后重试';
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 标记通知为已读
|
||||||
|
const markAsRead = async () => {
|
||||||
|
try {
|
||||||
|
// 实际项目中应该调用API
|
||||||
|
// await markNoticeReadApi(noticeId);
|
||||||
|
if (notice.value) {
|
||||||
|
notice.value.status = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('标记已读失败:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 返回列表
|
||||||
|
const goBack = () => {
|
||||||
|
router.push('/notice');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取类型信息
|
||||||
|
const getTypeInfo = (typeId) => {
|
||||||
|
return messageTypes.find(t => t.id === typeId) || messageTypes[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化内容,将换行符转换为<br>
|
||||||
|
const formatContent = (content) => {
|
||||||
|
return content ? content.replace(/\n/g, '<br>') : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getNoticeDetail();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page>
|
||||||
|
<template #title>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
@click="goBack"
|
||||||
|
class="flex h-8 w-8 items-center justify-center rounded-full text-gray-500 hover:bg-gray-100 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
<ArrowLeft class="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
<h1 class="text-xl font-semibold">通知详情</h1>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="mx-auto max-w-3xl">
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<div v-if="loading" class="flex justify-center py-20">
|
||||||
|
<div class="h-10 w-10 animate-spin rounded-full border-4 border-gray-200 border-t-sky-500"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 错误状态 -->
|
||||||
|
<div v-else-if="error" class="rounded-lg border border-red-200 bg-red-50 p-4 text-center text-red-600">
|
||||||
|
{{ error }}
|
||||||
|
<button
|
||||||
|
@click="getNoticeDetail"
|
||||||
|
class="mt-2 rounded-md bg-red-100 px-3 py-1 text-sm font-medium text-red-700 hover:bg-red-200"
|
||||||
|
>
|
||||||
|
重试
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 通知详情 -->
|
||||||
|
<div v-else-if="notice" class="space-y-6">
|
||||||
|
<!-- 标题和类型 -->
|
||||||
|
<div class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
|
||||||
|
<div class="mb-4 flex items-start justify-between">
|
||||||
|
<h2 class="text-xl font-bold text-gray-900">{{ notice.title }}</h2>
|
||||||
|
<div
|
||||||
|
:class="[getTypeInfo(notice.type).bgColor]"
|
||||||
|
class="flex items-center gap-1.5 rounded-full px-3 py-1"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="getTypeInfo(notice.type).icon"
|
||||||
|
:class="[getTypeInfo(notice.type).color]"
|
||||||
|
class="h-4 w-4"
|
||||||
|
/>
|
||||||
|
<span :class="[getTypeInfo(notice.type).color]" class="text-xs font-medium">
|
||||||
|
{{ getTypeInfo(notice.type).name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 元信息 -->
|
||||||
|
<div class="mb-6 flex flex-wrap items-center gap-x-6 gap-y-2 text-sm text-gray-500">
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
来自:
|
||||||
|
<User class="h-4 w-4" />
|
||||||
|
<span>萧康云医</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
收件人:
|
||||||
|
<User class="h-4 w-4" />
|
||||||
|
<span>{{ notice.admin?.nick_name || '系统' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6 flex flex-wrap items-center gap-x-6 gap-y-2 text-sm text-gray-500">
|
||||||
|
<div v-html="formatContent(notice.detail)"></div>
|
||||||
|
<div class="flex items-center gap-1.5" style="margin-left: auto;">
|
||||||
|
<Clock class="h-4 w-4" />
|
||||||
|
<span>{{ notice.created_at }}({{ formatTimeToRelative(notice.created_at) }})</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分隔线 -->
|
||||||
|
<div class="mb-6 h-px w-full bg-gray-100"></div>
|
||||||
|
|
||||||
|
<!-- 内容 -->
|
||||||
|
<div class="rounded-lg bg-gray-50 p-4">
|
||||||
|
<div class="prose prose-sm max-w-none text-gray-700" v-html="formatContent(notice.content)"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<button
|
||||||
|
@click="goBack"
|
||||||
|
class="flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<ArrowLeft class="h-4 w-4" />
|
||||||
|
返回列表
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<!-- 这里可以添加其他操作按钮,如删除、转发等 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 通知不存在 -->
|
||||||
|
<div v-else class="rounded-lg border border-gray-200 bg-white p-8 text-center shadow-sm">
|
||||||
|
<Bell class="mx-auto mb-4 h-12 w-12 text-gray-300" />
|
||||||
|
<h3 class="mb-2 text-lg font-medium text-gray-900">通知不存在</h3>
|
||||||
|
<p class="mb-4 text-gray-500">该通知可能已被删除或您没有权限查看</p>
|
||||||
|
<button
|
||||||
|
@click="goBack"
|
||||||
|
class="rounded-md bg-sky-500 px-4 py-2 text-sm font-medium text-white hover:bg-sky-600"
|
||||||
|
>
|
||||||
|
返回通知列表
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 平滑过渡效果 */
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保内容中的换行正确显示 */
|
||||||
|
:deep(.prose) {
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
586
apps/web-antd/src/views/notice/index.vue
Normal file
586
apps/web-antd/src/views/notice/index.vue
Normal file
@@ -0,0 +1,586 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch, onMounted } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { Page } from '@vben/common-ui';
|
||||||
|
import {
|
||||||
|
Bell,
|
||||||
|
CalendarClock,
|
||||||
|
CheckCircle2,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
Eye,
|
||||||
|
Filter,
|
||||||
|
Megaphone,
|
||||||
|
Search,
|
||||||
|
X,
|
||||||
|
} from 'lucide-vue-next';
|
||||||
|
import {getNoticeListApi, readAllApi, readApi} from "#/views/notice/api";
|
||||||
|
import {notification} from "ant-design-vue";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// 消息数据和分页状态
|
||||||
|
const messages = ref([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const currentPage = ref(1);
|
||||||
|
const pageSize = ref(5);
|
||||||
|
const total = ref(0);
|
||||||
|
const totalPages = ref(1);
|
||||||
|
|
||||||
|
// 筛选相关
|
||||||
|
const filterType = ref(-1); // -1表示全部
|
||||||
|
const searchQuery = ref('');
|
||||||
|
const showFilterDropdown = ref(false);
|
||||||
|
|
||||||
|
// 消息类型定义
|
||||||
|
const messageTypes = [
|
||||||
|
{ id: -1, name: '全部消息' },
|
||||||
|
{ id: 0, name: '系统公告', icon: Megaphone },
|
||||||
|
{ id: 1, name: '系统通知', icon: Bell },
|
||||||
|
{ id: 2, name: '周报提醒', icon: CalendarClock },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 获取消息列表数据
|
||||||
|
const getNoticeList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getNoticeListApi({
|
||||||
|
page: currentPage.value,
|
||||||
|
size: pageSize.value,
|
||||||
|
type: filterType.value === -1 ? undefined : filterType.value,
|
||||||
|
title: searchQuery.value || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
messages.value = res.items.map(item => ({
|
||||||
|
...item,
|
||||||
|
// 将API返回的数据映射到组件需要的格式
|
||||||
|
type_txt: getMessageTypeName(item.type),
|
||||||
|
date: item.created_at,
|
||||||
|
// status: 0表示未读,1表示已读
|
||||||
|
}));
|
||||||
|
|
||||||
|
total.value = res.total;
|
||||||
|
totalPages.value = res.page_count;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取消息列表失败:', error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据类型ID获取类型名称
|
||||||
|
const getMessageTypeName = (typeId) => {
|
||||||
|
const type = messageTypes.find(t => t.id === typeId);
|
||||||
|
return type ? type.name : '未知类型';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听筛选条件变化,重新获取数据
|
||||||
|
watch([filterType, searchQuery], () => {
|
||||||
|
currentPage.value = 1;
|
||||||
|
getNoticeList();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听页码变化,重新获取数据
|
||||||
|
watch(currentPage, () => {
|
||||||
|
getNoticeList();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 组件挂载时获取数据
|
||||||
|
onMounted(() => {
|
||||||
|
getNoticeList();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 未读消息数量
|
||||||
|
const unreadCount = computed(() => {
|
||||||
|
return messages.value.filter(msg => msg.status === 0).length;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 样式相关
|
||||||
|
const typeIcons = {
|
||||||
|
0: Megaphone,
|
||||||
|
1: Bell,
|
||||||
|
2: CalendarClock,
|
||||||
|
};
|
||||||
|
|
||||||
|
const typeColors = {
|
||||||
|
0: 'text-sky-500',
|
||||||
|
1: 'text-amber-500',
|
||||||
|
2: 'text-emerald-500',
|
||||||
|
};
|
||||||
|
|
||||||
|
const typeBgColors = {
|
||||||
|
0: 'bg-sky-50',
|
||||||
|
1: 'bg-amber-50',
|
||||||
|
2: 'bg-emerald-50',
|
||||||
|
};
|
||||||
|
|
||||||
|
const typeBorderColors = {
|
||||||
|
0: 'border-sky-200',
|
||||||
|
1: 'border-amber-200',
|
||||||
|
2: 'border-emerald-200',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理消息点击
|
||||||
|
const handleItemClick = (item) => {
|
||||||
|
router.push(`/notice/detail/${item.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 标记消息为已读
|
||||||
|
const markAsRead = async (event, item) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
try {
|
||||||
|
readApi(item.id).then(() => {
|
||||||
|
item.status = 1; // 临时更新UI状态
|
||||||
|
// 成功后可以重新获取数据或者直接更新本地状态
|
||||||
|
notification.success({
|
||||||
|
message: '标记成功',
|
||||||
|
duration: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('标记已读失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 标记所有消息为已读
|
||||||
|
const markAllAsRead = async () => {
|
||||||
|
try {
|
||||||
|
// 这里应该调用标记全部已读的API
|
||||||
|
// await markAllAsReadApi();
|
||||||
|
readAllApi().then(() => {
|
||||||
|
messages.value.forEach(msg => {
|
||||||
|
msg.status = 1;
|
||||||
|
});
|
||||||
|
// 成功后可以重新获取数据或者直接更新本地状态
|
||||||
|
notification.success({
|
||||||
|
message: '标记成功',
|
||||||
|
duration: 2,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
// 成功后可以重新获取数据或者直接更新本地状态
|
||||||
|
} catch (error) {
|
||||||
|
console.error('标记全部已读失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清除搜索
|
||||||
|
const clearSearch = () => {
|
||||||
|
searchQuery.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换筛选下拉菜单
|
||||||
|
const toggleFilterDropdown = () => {
|
||||||
|
showFilterDropdown.value = !showFilterDropdown.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 选择筛选类型
|
||||||
|
const selectFilterType = (type) => {
|
||||||
|
filterType.value = type;
|
||||||
|
showFilterDropdown.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 分页导航
|
||||||
|
const goToPage = (page) => {
|
||||||
|
if (page >= 1 && page <= totalPages.value) {
|
||||||
|
currentPage.value = page;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取当前选中的筛选类型名称
|
||||||
|
const currentFilterName = computed(() => {
|
||||||
|
const type = messageTypes.find(t => t.id === filterType.value);
|
||||||
|
return type ? type.name : '全部消息';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 展开消息详情
|
||||||
|
const expandedMessage = ref(null);
|
||||||
|
const toggleExpand = (event, id) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
expandedMessage.value = expandedMessage.value === id ? null : id;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page title="消息中心">
|
||||||
|
<template #extra>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-gray-500">共 {{ total }} 条消息</span>
|
||||||
|
<span
|
||||||
|
v-if="unreadCount > 0"
|
||||||
|
class="rounded-full bg-red-500 px-2 py-0.5 text-xs font-medium text-white"
|
||||||
|
>
|
||||||
|
{{ unreadCount }} 未读
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="mx-auto max-w-4xl">
|
||||||
|
<!-- 筛选和搜索工具栏 -->
|
||||||
|
<div
|
||||||
|
class="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
|
||||||
|
>
|
||||||
|
<!-- 筛选下拉菜单 -->
|
||||||
|
<div class="relative">
|
||||||
|
<button
|
||||||
|
class="flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2"
|
||||||
|
@click="toggleFilterDropdown"
|
||||||
|
>
|
||||||
|
<Filter class="h-4 w-4" />
|
||||||
|
{{ currentFilterName }}
|
||||||
|
<ChevronDown class="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="showFilterDropdown"
|
||||||
|
class="absolute left-0 top-full z-10 mt-1 w-56 origin-top-left rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||||
|
>
|
||||||
|
<div class="py-1">
|
||||||
|
<button
|
||||||
|
v-for="type in messageTypes"
|
||||||
|
:key="type.id"
|
||||||
|
:class="
|
||||||
|
filterType === type.id
|
||||||
|
? 'bg-gray-50 font-medium text-sky-600'
|
||||||
|
: 'text-gray-700'
|
||||||
|
"
|
||||||
|
class="flex w-full items-center gap-2 px-4 py-2 text-left text-sm hover:bg-gray-100"
|
||||||
|
@click="selectFilterType(type.id)"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="type.icon"
|
||||||
|
v-if="type.id !== -1 && type.icon"
|
||||||
|
:class="type.id !== -1 ? typeColors[type.id] : ''"
|
||||||
|
class="h-4 w-4"
|
||||||
|
/>
|
||||||
|
{{ type.name }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索框 -->
|
||||||
|
<div class="relative flex-1 sm:max-w-xs">
|
||||||
|
<div
|
||||||
|
class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"
|
||||||
|
>
|
||||||
|
<Search class="h-4 w-4 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-model="searchQuery"
|
||||||
|
class="block w-full rounded-lg border border-gray-200 bg-white py-2 pl-10 pr-10 text-sm text-gray-700 focus:border-sky-500 focus:outline-none focus:ring-1 focus:ring-sky-500"
|
||||||
|
placeholder="搜索消息..."
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="searchQuery"
|
||||||
|
class="absolute inset-y-0 right-0 flex items-center pr-3"
|
||||||
|
@click="clearSearch"
|
||||||
|
>
|
||||||
|
<X class="h-4 w-4 text-gray-400 hover:text-gray-600" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 标记全部已读按钮 -->
|
||||||
|
<button
|
||||||
|
v-if="unreadCount > 0"
|
||||||
|
class="flex items-center gap-1 rounded-lg bg-white px-3 py-2 text-sm font-medium text-gray-700 shadow-sm ring-1 ring-gray-200 hover:bg-gray-50"
|
||||||
|
@click="markAllAsRead"
|
||||||
|
>
|
||||||
|
<CheckCircle2 class="h-4 w-4 text-gray-500" />
|
||||||
|
标记全部已读
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 消息列表 -->
|
||||||
|
<div
|
||||||
|
class="overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm"
|
||||||
|
>
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<div v-if="loading" class="flex justify-center py-12">
|
||||||
|
<div class="h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-sky-500"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<div
|
||||||
|
v-else-if="messages.length === 0"
|
||||||
|
class="flex flex-col items-center justify-center py-16"
|
||||||
|
>
|
||||||
|
<Bell class="mb-4 h-16 w-16 text-gray-200" />
|
||||||
|
<p class="text-gray-500">暂无消息</p>
|
||||||
|
<p
|
||||||
|
v-if="searchQuery || filterType !== -1"
|
||||||
|
class="mt-2 text-sm text-gray-400"
|
||||||
|
>
|
||||||
|
尝试调整筛选条件
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 消息列表 -->
|
||||||
|
<ul v-else class="divide-y divide-gray-100">
|
||||||
|
<li
|
||||||
|
v-for="item in messages"
|
||||||
|
:key="item.id"
|
||||||
|
:class="[item.status === 0 ? 'bg-gray-50' : 'hover:bg-gray-50']"
|
||||||
|
class="group relative cursor-pointer transition-all duration-200"
|
||||||
|
@click="handleItemClick(item)"
|
||||||
|
>
|
||||||
|
<div class="flex items-start gap-4 p-4 sm:p-5">
|
||||||
|
<!-- 图标 -->
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
item.status === 1
|
||||||
|
? 'bg-gray-100 group-hover:bg-gray-200'
|
||||||
|
: typeBgColors[item.type],
|
||||||
|
]"
|
||||||
|
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full transition-all duration-200"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="typeIcons[item.type]"
|
||||||
|
:class="[
|
||||||
|
item.status === 1
|
||||||
|
? 'text-gray-400 group-hover:text-gray-600'
|
||||||
|
: typeColors[item.type],
|
||||||
|
]"
|
||||||
|
class="h-5 w-5 transition-all duration-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 内容 -->
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<div class="flex items-start justify-between gap-2">
|
||||||
|
<h3
|
||||||
|
:class="[
|
||||||
|
item.status === 0
|
||||||
|
? 'font-semibold text-gray-900'
|
||||||
|
: 'text-gray-700 group-hover:text-gray-900',
|
||||||
|
]"
|
||||||
|
class="text-sm font-medium transition-all duration-200 sm:text-base"
|
||||||
|
>
|
||||||
|
{{ item.title }}
|
||||||
|
</h3>
|
||||||
|
<span
|
||||||
|
class="shrink-0 whitespace-nowrap text-xs text-gray-400"
|
||||||
|
>{{ item.date }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-1 flex flex-wrap items-center gap-2">
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
item.status === 1
|
||||||
|
? 'border-gray-200 text-gray-500'
|
||||||
|
: `${typeColors[item.type]} ${typeBorderColors[item.type]}`,
|
||||||
|
]"
|
||||||
|
class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium transition-all duration-200"
|
||||||
|
>
|
||||||
|
{{ item.type_txt }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- 详情文本,只显示第一行,可展开 -->
|
||||||
|
<div class="mt-1 w-full">
|
||||||
|
<p
|
||||||
|
:class="[
|
||||||
|
item.status === 1 ? 'text-gray-500' : 'text-gray-700',
|
||||||
|
expandedMessage === item.id ? '' : 'line-clamp-1',
|
||||||
|
]"
|
||||||
|
class="text-xs transition-all duration-200"
|
||||||
|
>
|
||||||
|
{{ item.detail }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- <!– 展开/收起按钮 –>-->
|
||||||
|
<!-- <button-->
|
||||||
|
<!-- v-if="item.detail && item.detail.includes('\n')"-->
|
||||||
|
<!-- class="mt-1 flex items-center gap-1 text-xs text-sky-500 hover:text-sky-600"-->
|
||||||
|
<!-- @click="toggleExpand($event, item.id)"-->
|
||||||
|
<!-- >-->
|
||||||
|
<!-- <Eye class="h-3 w-3" />-->
|
||||||
|
<!-- {{ expandedMessage === item.id ? '收起' : '展开' }}-->
|
||||||
|
<!-- </button>-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="flex shrink-0 items-center gap-2">
|
||||||
|
<button
|
||||||
|
v-if="item.status === 0"
|
||||||
|
class="rounded-full p-1.5 text-gray-400 transition-all duration-200 hover:bg-gray-100 hover:text-gray-600"
|
||||||
|
title="标记为已读"
|
||||||
|
@click="markAsRead($event, item)"
|
||||||
|
>
|
||||||
|
<CheckCircle2 class="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
<ChevronRight
|
||||||
|
class="h-5 w-5 text-gray-300 transition-all duration-200 group-hover:text-gray-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 未读指示器 -->
|
||||||
|
<div
|
||||||
|
v-if="item.status === 0"
|
||||||
|
:class="[typeColors[item.type]]"
|
||||||
|
class="absolute bottom-0 left-0 top-0 w-1"
|
||||||
|
></div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- 分页控件 -->
|
||||||
|
<div
|
||||||
|
v-if="totalPages > 1"
|
||||||
|
class="flex items-center justify-between border-t border-gray-200 bg-gray-50 px-4 py-3 sm:px-6"
|
||||||
|
>
|
||||||
|
<div class="flex flex-1 justify-between sm:hidden">
|
||||||
|
<button
|
||||||
|
:class="[
|
||||||
|
currentPage === 1
|
||||||
|
? 'text-gray-300'
|
||||||
|
: 'text-gray-700 hover:bg-gray-50',
|
||||||
|
]"
|
||||||
|
:disabled="currentPage === 1"
|
||||||
|
class="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium"
|
||||||
|
@click="goToPage(currentPage - 1)"
|
||||||
|
>
|
||||||
|
上一页
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:class="[
|
||||||
|
currentPage === totalPages
|
||||||
|
? 'text-gray-300'
|
||||||
|
: 'text-gray-700 hover:bg-gray-50',
|
||||||
|
]"
|
||||||
|
:disabled="currentPage === totalPages"
|
||||||
|
class="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium"
|
||||||
|
@click="goToPage(currentPage + 1)"
|
||||||
|
>
|
||||||
|
下一页
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-gray-700">
|
||||||
|
显示第
|
||||||
|
<span class="font-medium">{{
|
||||||
|
(currentPage - 1) * pageSize + 1
|
||||||
|
}}</span>
|
||||||
|
至
|
||||||
|
<span class="font-medium">{{
|
||||||
|
Math.min(currentPage * pageSize, total)
|
||||||
|
}}</span>
|
||||||
|
条, 共
|
||||||
|
<span class="font-medium">{{ total }}</span>
|
||||||
|
条
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<nav
|
||||||
|
aria-label="Pagination"
|
||||||
|
class="isolate inline-flex -space-x-px rounded-md shadow-sm"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
:class="
|
||||||
|
currentPage === 1
|
||||||
|
? 'cursor-not-allowed'
|
||||||
|
: 'hover:text-gray-500'
|
||||||
|
"
|
||||||
|
:disabled="currentPage === 1"
|
||||||
|
class="relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0"
|
||||||
|
@click="goToPage(currentPage - 1)"
|
||||||
|
>
|
||||||
|
<span class="sr-only">上一页</span>
|
||||||
|
<ChevronLeft class="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 页码按钮 -->
|
||||||
|
<template v-for="page in totalPages" :key="page">
|
||||||
|
<button
|
||||||
|
v-if="
|
||||||
|
page === currentPage ||
|
||||||
|
page === 1 ||
|
||||||
|
page === totalPages ||
|
||||||
|
(page >= currentPage - 1 && page <= currentPage + 1)
|
||||||
|
"
|
||||||
|
:class="[
|
||||||
|
page === currentPage
|
||||||
|
? 'z-10 bg-sky-500 text-white focus:z-20 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-500'
|
||||||
|
: 'text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0',
|
||||||
|
]"
|
||||||
|
class="relative inline-flex items-center px-4 py-2 text-sm font-semibold"
|
||||||
|
@click="goToPage(page)"
|
||||||
|
>
|
||||||
|
{{ page }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 省略号 -->
|
||||||
|
<span
|
||||||
|
v-else-if="
|
||||||
|
(page === 2 && currentPage > 3) ||
|
||||||
|
(page === totalPages - 1 && currentPage < totalPages - 2)
|
||||||
|
"
|
||||||
|
class="relative inline-flex items-center px-4 py-2 text-sm font-semibold text-gray-700 ring-1 ring-inset ring-gray-300"
|
||||||
|
>
|
||||||
|
...
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<button
|
||||||
|
:class="
|
||||||
|
currentPage === totalPages
|
||||||
|
? 'cursor-not-allowed'
|
||||||
|
: 'hover:text-gray-500'
|
||||||
|
"
|
||||||
|
:disabled="currentPage === totalPages"
|
||||||
|
class="relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0"
|
||||||
|
@click="goToPage(currentPage + 1)"
|
||||||
|
>
|
||||||
|
<span class="sr-only">下一页</span>
|
||||||
|
<ChevronRight class="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.line-clamp-1 {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 平滑过渡效果 */
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 微妙的悬停动画 */
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(14, 165, 233, 0.2);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
box-shadow: 0 0 0 6px rgba(14, 165, 233, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(14, 165, 233, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.group:hover .pulse-on-hover {
|
||||||
|
animation: pulse 1.5s infinite;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -103,11 +103,19 @@ function handleClick(item: NotificationItem) {
|
|||||||
<span
|
<span
|
||||||
class="relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full"
|
class="relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full"
|
||||||
>
|
>
|
||||||
<img
|
|
||||||
:src="item.avatar"
|
<component
|
||||||
|
:is="item.icon"
|
||||||
|
v-if="item.icon"
|
||||||
|
:class="item.status === 1 ? 'text-gray-400 group-hover:text-gray-600': item.color"
|
||||||
class="aspect-square h-full w-full object-cover"
|
class="aspect-square h-full w-full object-cover"
|
||||||
role="img"
|
role="img"
|
||||||
/>
|
/>
|
||||||
|
<!-- <img-->
|
||||||
|
<!-- :src="item.avatar"-->
|
||||||
|
<!-- class="aspect-square h-full w-full object-cover"-->
|
||||||
|
<!-- role="img"-->
|
||||||
|
<!-- />-->
|
||||||
</span>
|
</span>
|
||||||
<div class="flex flex-col gap-1 leading-none">
|
<div class="flex flex-col gap-1 leading-none">
|
||||||
<p class="font-semibold">{{ item.title }}</p>
|
<p class="font-semibold">{{ item.title }}</p>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
interface NotificationItem {
|
interface NotificationItem {
|
||||||
avatar: string;
|
// avatar: string;
|
||||||
|
id: number;
|
||||||
|
icon: string;
|
||||||
|
color: string;
|
||||||
date: string;
|
date: string;
|
||||||
isRead?: boolean;
|
isRead?: boolean;
|
||||||
message: string;
|
message: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user