---初始化后台管理web页面项目

This commit is contained in:
2025-08-20 14:39:30 +08:00
parent ad49711a7e
commit 87545a8baf
2057 changed files with 282864 additions and 213 deletions

View File

@ -0,0 +1,189 @@
<template>
<a-list :class="prefixCls" bordered :pagination="getPagination">
<template v-for="item in getData" :key="item.id">
<a-list-item class="list-item">
<a-list-item-meta>
<template #title>
<div class="title">
<a-typography-paragraph
@click="handleTitleClick(item)"
style="width: 100%; margin-bottom: 0 !important"
:style="{ cursor: isTitleClickable ? 'pointer' : '' }"
:delete="!!item.titleDelete"
:ellipsis="
$props.titleRows && $props.titleRows > 0
? { rows: $props.titleRows, tooltip: !!item.title }
: false
"
:content="item.title"
/>
<div class="extra" v-if="item.extra">
<a-tag class="tag" :color="item.color">
{{ item.extra }}
</a-tag>
</div>
</div>
</template>
<template #avatar>
<a-avatar v-if="item.avatar" class="avatar" :src="item.avatar" />
<span v-else> {{ item.avatar }}</span>
</template>
<template #description>
<div>
<div class="description" v-if="item.description">
<a-typography-paragraph
style="width: 100%; margin-bottom: 0 !important"
:ellipsis="
$props.descRows && $props.descRows > 0
? { rows: $props.descRows, tooltip: !!item.description }
: false
"
:content="item.description"
/>
</div>
<div class="datetime">
{{ item.datetime }}
</div>
</div>
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</template>
<script lang="ts">
import { computed, defineComponent, PropType, ref, watch, unref } from 'vue';
import { ListItem } from './data';
import { useDesign } from '/@/hooks/web/useDesign';
import { List, Avatar, Tag, Typography } from 'ant-design-vue';
import { isNumber } from '/@/utils/is';
export default defineComponent({
components: {
[Avatar.name]: Avatar,
[List.name]: List,
[List.Item.name]: List.Item,
AListItemMeta: List.Item.Meta,
ATypographyParagraph: Typography.Paragraph,
[Tag.name]: Tag,
},
props: {
list: {
type: Array as PropType<ListItem[]>,
default: () => [],
},
pageSize: {
type: [Boolean, Number] as PropType<Boolean | Number>,
default: 5,
},
currentPage: {
type: Number,
default: 1,
},
titleRows: {
type: Number,
default: 1,
},
descRows: {
type: Number,
default: 2,
},
onTitleClick: {
type: Function as PropType<(Recordable) => void>,
},
},
emits: ['update:currentPage'],
setup(props, { emit }) {
const { prefixCls } = useDesign('header-notify-list');
const current = ref(props.currentPage || 1);
const getData = computed(() => {
const { pageSize, list } = props;
if (pageSize === false) return [];
let size = isNumber(pageSize) ? pageSize : 5;
return list.slice(size * (unref(current) - 1), size * unref(current));
});
watch(
() => props.currentPage,
(v) => {
current.value = v;
},
);
const isTitleClickable = computed(() => !!props.onTitleClick);
const getPagination = computed(() => {
const { list, pageSize } = props;
if (pageSize > 0 && list && list.length > pageSize) {
return {
total: list.length,
pageSize,
//size: 'small',
current: unref(current),
onChange(page) {
current.value = page;
emit('update:currentPage', page);
},
};
} else {
return false;
}
});
function handleTitleClick(item: ListItem) {
props.onTitleClick && props.onTitleClick(item);
}
return { prefixCls, getPagination, getData, handleTitleClick, isTitleClickable };
},
});
</script>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-header-notify-list';
.@{prefix-cls} {
&::-webkit-scrollbar {
display: none;
}
::v-deep(.ant-pagination-disabled) {
display: inline-block !important;
}
&-item {
padding: 6px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s;
.title {
margin-bottom: 8px;
font-weight: normal;
.extra {
float: right;
margin-top: -1.5px;
margin-right: 0;
font-weight: normal;
.tag {
margin-right: 0;
}
}
.avatar {
margin-top: 4px;
}
.description {
font-size: 12px;
line-height: 18px;
}
.datetime {
margin-top: 4px;
font-size: 12px;
line-height: 18px;
}
}
}
}
</style>

View File

@ -0,0 +1,58 @@
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
export interface ListItem {
id: string;
avatar?: string;
// 通知的标题内容
title: string;
// 是否在标题上显示删除线
titleDelete?: boolean;
datetime?: string;
type?: string;
read?: number;
description?: string;
clickClose?: boolean;
extra?: string;
color?: string;
taskId?: string;
processId?: string;
schemaId?: string;
timeFormat?: string;
}
export interface TabItem {
key: string;
name: string;
unreadNum: number;
list: ListItem[];
read?: ListItem[];
unreadlist?: ListItem[];
}
export const tabListData: TabItem[] = [
{
key: '1',
name: t('新闻'),
list: [],
unreadNum: 0,
},
{
key: '2',
name: t('通知公告'),
list: [],
unreadNum: 0,
},
{
key: '3',
name: t('日程'),
list: [],
unreadNum: 0,
},
{
key: '4',
name: t('工作流'),
list: [],
read: [],
unreadNum: 0,
},
];

View File

@ -0,0 +1,461 @@
<template>
<div :class="prefixCls">
<Popover title="" trigger="click" :overlayClassName="`${prefixCls}__overlay`">
<Badge :count="count" dot :numberStyle="numberStyle">
<Icon icon="ion:notifcations" size="22" />
</Badge>
<template #content>
<Tabs>
<template v-for="item in listData" :key="item.key">
<TabPane>
<template #tab>
{{ item.name }}
<span v-if="item.unreadNum !== 0">({{ item.unreadNum }})</span>
</template>
<div v-if="item.key === '4'" class="min-h-88">
<div>
<div class="list-item">
<span class="header-title">{{ t('流程审批') }}</span>
<router-link
class="opr"
:to="{
path: '/task/processtasks',
query: {},
}"
>
{{ t('查看更多') }}
</router-link>
</div>
<div v-if="item.list.length > 0">
<div class="readed-mark" v-for="it in item.list" :key="it.id">
<div
class="list-data-item"
:class="it.read ? 'readed' : ''"
@click="ApprovalHandle(it, item.key, 1)"
>
<span class="list-item-title">{{ it.title }}</span>
<span class="list-item-time">{{ it.timeFormat }}</span>
</div>
</div>
</div>
<a-empty :image="simpleImage" v-else />
</div>
<div>
<div class="list-item">
<span class="header-title">{{ t('流程传阅') }}</span>
<router-link
class="opr"
:to="{
path: '/task/processtasks',
query: { name: 'MyCirculation' },
}"
>
{{ t('查看更多') }}
</router-link>
</div>
<div v-if="item.read!.length>0">
<div class="list-item readed-mark" v-for="it in item.read" :key="it.id">
<div
class="list-data-item"
:class="it.read ? 'readed' : ''"
@click="ApprovalHandle(it, item.key, 2)"
>
<span class="list-item-title">{{ it.title }}</span>
<span class="list-item-time">{{ it.timeFormat }}</span>
</div>
</div>
</div>
<a-empty :image="simpleImage" v-else />
</div>
<div class="notice-footer">
<span @click="setReadAll(item.key)">{{ t('全部设置已读') }}</span>
</div>
</div>
<div v-else class="h-88">
<div v-if="item.list.length > 0" class="h-82">
<div
class="list-item readed-mark"
v-for="it in item.list"
:key="it.id"
:class="it.read ? 'readed' : ''"
@click="
() => {
it.read = 1;
setReadSingle(it.id, item.key);
}
"
>
<span class="list-item-title">
<a-tooltip>
<template #title>{{ it.title }}</template>
{{ it.title }}
</a-tooltip>
</span>
<span class="list-item-time">{{ it.timeFormat }}</span>
</div>
<div class="notice-footer"
><span @click="setReadAll(item.key)">{{ t('全部设置已读') }}</span
><span>{{ t('查看更多') }} </span></div
>
</div>
<a-empty :image="simpleImage" v-else />
</div>
</TabPane>
</template>
</Tabs>
<ApprovalProcess
v-if="Approval.visible"
:taskId="Approval.taskId || ''"
:processId="Approval.processId || ''"
:schemaId="Approval.schemaId || ''"
:visible="Approval.visible"
@close="
() => {
Approval.visible = false;
}
"
/>
<LookProcess
v-if="LookData.visible"
:taskId="LookData.taskId || ''"
:processId="LookData.processId || ''"
:visible="LookData.visible"
@close="
() => {
LookData.visible = false;
}
"
/>
</template>
</Popover>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref, onUnmounted } from 'vue';
import { Popover, Tabs, Badge } from 'ant-design-vue';
import { Icon } from '/@/components/Icon';
import { tabListData } from './data';
import { useDesign } from '/@/hooks/web/useDesign';
import {
getOaMessage,
getOaNews,
setOaRead,
setSingleRead,
setWorkReadAll,
getScheduleMsg,
setScheduleRead,
setScheduleReadAll,
} from '/@/api/system/login';
import { Empty } from 'ant-design-vue';
import ApprovalProcess from '/@/views/workflow/task/components/ApprovalProcess.vue';
import LookProcess from '/@/views/workflow/task/components/LookProcess.vue';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
export default defineComponent({
components: {
Popover,
Tabs,
TabPane: Tabs.TabPane,
Badge,
Icon,
ApprovalProcess,
LookProcess,
},
setup() {
const Approval = ref<{
taskId?: string;
processId?: string;
schemaId?: string;
visible: boolean;
}>({
visible: false,
});
const LookData = ref<{
taskId?: string;
processId?: string;
visible: boolean;
}>({
visible: false,
});
const { prefixCls } = useDesign('header-notify');
let times: any = ref();
const listData = ref(tabListData);
const simpleImage = ref(Empty.PRESENTED_IMAGE_SIMPLE);
getDatas();
times.value = setInterval(() => {
getDatas();
}, 10000);
async function getDatas() {
listData.value.forEach((o) => {
o.list = [];
o.unreadNum = 0;
if (o.read) o.read = [];
});
try {
let res = import.meta.env.VITE_GLOB_DISABLE_NEWS ? [] : await getOaNews(1);
res.list.forEach((o) => {
if (!o.readId) listData.value[0].unreadNum += 1;
listData.value[0].list.push({
id: o.id,
avatar: '',
title: o.briefHead,
description: '',
datetime: o.releaseTime,
color: '',
type: '3',
read: o.isRead,
});
});
let res1 = import.meta.env.VITE_GLOB_DISABLE_NEWS ? [] : await getOaNews(2);
res1.list.forEach((o) => {
if (!o.readId) listData.value[1].unreadNum += 1;
listData.value[1].list.push({
id: o.id,
avatar: '',
title: o.briefHead,
description: '',
datetime: o.releaseTime,
color: '',
type: '3',
read: o.isRead,
});
});
let res2 = import.meta.env.VITE_GLOB_DISABLE_NEWS ? [] : await getOaMessage();
res2.forEach((o) => {
if (o.messageType === 0) {
if (!o.isRead) listData.value[2].unreadNum += 1;
listData.value[2].list.push({
id: o.id,
avatar: '',
title: o.messageContent,
description: '',
datetime: o.sendTime,
timeFormat: o.timeFormat,
color: '',
type: '3',
read: o.isRead,
});
} else if (o.messageType == 1) {
if (!o.isRead) listData.value[3].unreadNum += 1;
listData.value[3].list.push({
id: o.id,
avatar: '',
title: o.messageContent,
description: '',
datetime: o.sendTime,
timeFormat: o.timeFormat,
color: '',
type: '3',
taskId: o.objectId,
processId: o.processId,
schemaId: o.schemaId,
read: o.isRead,
});
} else if (o.messageType == 2) {
if (!o.isRead) listData.value[3].unreadNum += 1;
listData.value[3].read?.push({
id: o.id,
avatar: '',
title: o.messageContent,
description: '',
datetime: o.sendTime,
read: o.isRead,
timeFormat: o.timeFormat,
color: '',
type: '3',
taskId: o.objectId,
processId: o.processId,
});
}
});
let res3 = await getScheduleMsg();
res3.list.forEach((item) => (item.read = item.isRead));
listData.value[2].unreadNum = res3.list.filter((x) => !x.isRead).length;
listData.value[2].list.push(...res3.list);
} catch (error) {
clearInterval(times.value);
}
}
const count = computed(() => {
let count = 0;
for (let i = 0; i < listData.value.length; i++) {
count += listData.value[i].unreadNum;
}
return count;
});
async function setReadAll(type) {
if (type == 1 || type == 2) {
let ids: string[] = [];
listData.value[type - 1].list.forEach((o) => {
o.read = 1;
ids.push(o.id);
});
await setOaRead(ids);
} else if (type == 3) {
await setScheduleReadAll();
listData.value[type - 1].list.forEach((o) => {
o.read = 1;
});
} else if (type == 4) {
listData.value[type - 1].list.forEach((o) => {
o.read = 1;
});
await setWorkReadAll();
}
listData.value[type - 1].unreadNum = 0;
}
async function setReadSingle(ids, num) {
if (num == 3) {
await setScheduleRead([ids]);
} else if (num == 4) {
await setSingleRead(ids);
} else {
await setOaRead([ids]);
}
if (listData.value[num - 1].unreadNum > 0) listData.value[num - 1].unreadNum -= 1;
}
onUnmounted(() => {
clearInterval(times.value);
});
function ApprovalHandle(it, key, type) {
if (type == 1) {
Approval.value = {
taskId: it.taskId,
processId: it.processId,
schemaId: it.schemaId,
visible: true,
};
} else {
LookData.value = {
taskId: it.taskId,
processId: it.processId,
visible: true,
};
}
it.read = 1;
setReadSingle(it.id, key);
}
return {
prefixCls,
listData,
count,
setReadAll,
simpleImage,
numberStyle: {
top: '18px',
right: '8px',
},
setReadSingle,
ApprovalHandle,
Approval,
LookData,
t,
};
},
});
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-header-notify';
.@{prefix-cls} {
padding-top: 2px;
&__overlay {
width: 380px;
}
.ant-tabs-content {
width: 300px;
}
.ant-badge {
font-size: 18px;
.ant-badge-multiple-words {
padding: 0 4px;
}
svg {
width: 0.9em;
}
}
}
</style>
<style scoped lang="less">
.list-item {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #f2f2f2;
font-weight: 400;
font-size: 12px;
height: 36px;
cursor: pointer;
.header-title {
font-weight: 700;
font-size: 14px;
}
.opr {
color: #02a7f0;
cursor: pointer;
font-size: 12px;
}
&.readed {
color: gray;
}
}
.list-data-item {
display: flex;
height: 36px;
cursor: pointer;
border-bottom: 1px solid #f2f2f2;
justify-content: space-between;
align-items: center;
&.readed {
color: gray;
}
}
.list-item-title {
width: 280px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.list-item-time {
text-align: right;
}
.readed-mark {
color: #02a7f0;
}
.notice-footer {
display: flex;
justify-content: space-around;
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 40px;
line-height: 40px;
border-bottom: 1px solid #f2f2f2;
font-size: 12px;
color: #02a7f0;
cursor: pointer;
}
</style>