feat(notice): 顶栏通知铃铛 + 通知管理页
- ty-notice 铃铛(未读角标 + 列表 + 已读)+ notice store 轮询 - 通知管理页(ty-crud) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ff0e1dcd27
commit
64bbb0a987
6
components.d.ts
vendored
6
components.d.ts
vendored
@ -13,6 +13,7 @@ export {}
|
|||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
ChangePasswordForm: typeof import('./src/components/change-password-form.vue')['default']
|
ChangePasswordForm: typeof import('./src/components/change-password-form.vue')['default']
|
||||||
|
ElBadge: typeof import('element-plus/es')['ElBadge']
|
||||||
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
|
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
|
||||||
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
|
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
|
||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
@ -36,6 +37,7 @@ declare module 'vue' {
|
|||||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||||
ElOption: typeof import('element-plus/es')['ElOption']
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||||
|
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||||
ElResult: typeof import('element-plus/es')['ElResult']
|
ElResult: typeof import('element-plus/es')['ElResult']
|
||||||
@ -59,6 +61,7 @@ declare module 'vue' {
|
|||||||
TyForm: typeof import('./src/components/ty-form.vue')['default']
|
TyForm: typeof import('./src/components/ty-form.vue')['default']
|
||||||
TyHeader: typeof import('./src/components/ty-header.vue')['default']
|
TyHeader: typeof import('./src/components/ty-header.vue')['default']
|
||||||
TyMixbar: typeof import('./src/components/ty-mixbar.vue')['default']
|
TyMixbar: typeof import('./src/components/ty-mixbar.vue')['default']
|
||||||
|
TyNotice: typeof import('./src/components/ty-notice.vue')['default']
|
||||||
TySearchForm: typeof import('./src/components/ty-search-form.vue')['default']
|
TySearchForm: typeof import('./src/components/ty-search-form.vue')['default']
|
||||||
TySetting: typeof import('./src/components/ty-setting.vue')['default']
|
TySetting: typeof import('./src/components/ty-setting.vue')['default']
|
||||||
TySettingPanel: typeof import('./src/components/ty-setting-panel.vue')['default']
|
TySettingPanel: typeof import('./src/components/ty-setting-panel.vue')['default']
|
||||||
@ -74,6 +77,7 @@ declare module 'vue' {
|
|||||||
// For TSX support
|
// For TSX support
|
||||||
declare global {
|
declare global {
|
||||||
const ChangePasswordForm: typeof import('./src/components/change-password-form.vue')['default']
|
const ChangePasswordForm: typeof import('./src/components/change-password-form.vue')['default']
|
||||||
|
const ElBadge: typeof import('element-plus/es')['ElBadge']
|
||||||
const ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
|
const ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
|
||||||
const ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
|
const ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
|
||||||
const ElButton: typeof import('element-plus/es')['ElButton']
|
const ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
@ -97,6 +101,7 @@ declare global {
|
|||||||
const ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
const ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||||
const ElOption: typeof import('element-plus/es')['ElOption']
|
const ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
const ElPagination: typeof import('element-plus/es')['ElPagination']
|
const ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||||
|
const ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||||
const ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
const ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||||
const ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
const ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||||
const ElResult: typeof import('element-plus/es')['ElResult']
|
const ElResult: typeof import('element-plus/es')['ElResult']
|
||||||
@ -120,6 +125,7 @@ declare global {
|
|||||||
const TyForm: typeof import('./src/components/ty-form.vue')['default']
|
const TyForm: typeof import('./src/components/ty-form.vue')['default']
|
||||||
const TyHeader: typeof import('./src/components/ty-header.vue')['default']
|
const TyHeader: typeof import('./src/components/ty-header.vue')['default']
|
||||||
const TyMixbar: typeof import('./src/components/ty-mixbar.vue')['default']
|
const TyMixbar: typeof import('./src/components/ty-mixbar.vue')['default']
|
||||||
|
const TyNotice: typeof import('./src/components/ty-notice.vue')['default']
|
||||||
const TySearchForm: typeof import('./src/components/ty-search-form.vue')['default']
|
const TySearchForm: typeof import('./src/components/ty-search-form.vue')['default']
|
||||||
const TySetting: typeof import('./src/components/ty-setting.vue')['default']
|
const TySetting: typeof import('./src/components/ty-setting.vue')['default']
|
||||||
const TySettingPanel: typeof import('./src/components/ty-setting-panel.vue')['default']
|
const TySettingPanel: typeof import('./src/components/ty-setting-panel.vue')['default']
|
||||||
|
|||||||
89
src/api/notice.ts
Normal file
89
src/api/notice.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import { http } from '@/api/http'
|
||||||
|
|
||||||
|
export type NoticeStatus = 'enabled' | 'disabled'
|
||||||
|
|
||||||
|
export interface Notice {
|
||||||
|
id: string
|
||||||
|
tenantCode: string
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
status: NoticeStatus
|
||||||
|
createdAt: number
|
||||||
|
updatedAt: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MyNotice {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
status: string
|
||||||
|
createdAt: number
|
||||||
|
readAt: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NoticeListQuery {
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
title?: string
|
||||||
|
status?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NoticeListResult {
|
||||||
|
list: Notice[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MyNoticeListResult {
|
||||||
|
list: MyNotice[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NoticeUpsertData {
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
status?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 通知分页(管理侧)。 */
|
||||||
|
export function listNotices(query: NoticeListQuery, signal?: AbortSignal): Promise<NoticeListResult> {
|
||||||
|
const params: Record<string, unknown> = { page: query.page, pageSize: query.pageSize }
|
||||||
|
if (query.title) params.title = query.title
|
||||||
|
if (query.status) params.status = query.status
|
||||||
|
return http.get<NoticeListResult>('/notices', { params, signal })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createNotice(data: NoticeUpsertData): Promise<Notice> {
|
||||||
|
return http.post<Notice>('/notices', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateNotice(id: string, data: NoticeUpsertData): Promise<Notice> {
|
||||||
|
return http.patch<Notice>(`/notices/${id}`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteNotice(id: string): Promise<unknown> {
|
||||||
|
return http.delete(`/notices/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前用户可见通知(含已读状态)。 */
|
||||||
|
export function listMyNotices(query: { page: number; pageSize: number }, signal?: AbortSignal): Promise<MyNoticeListResult> {
|
||||||
|
return http.get<MyNoticeListResult>('/notices/mine', { params: query, signal })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 未读通知数。 */
|
||||||
|
export function getUnreadCount(signal?: AbortSignal): Promise<number> {
|
||||||
|
return http.get<number>('/notices/unread-count', { signal })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 标记单条已读。 */
|
||||||
|
export function markRead(id: string): Promise<unknown> {
|
||||||
|
return http.post(`/notices/${id}/read`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 标记全部已读。 */
|
||||||
|
export function markAllRead(): Promise<unknown> {
|
||||||
|
return http.post('/notices/read-all')
|
||||||
|
}
|
||||||
@ -49,6 +49,7 @@
|
|||||||
<button class="ty-header__action" type="button" title="布局设置" @click="emit('open-settings')">
|
<button class="ty-header__action" type="button" title="布局设置" @click="emit('open-settings')">
|
||||||
<icon type="iconify" icon="mdi:cog-outline" />
|
<icon type="iconify" icon="mdi:cog-outline" />
|
||||||
</button>
|
</button>
|
||||||
|
<ty-notice />
|
||||||
<el-dropdown trigger="click" @command="onUserCommand">
|
<el-dropdown trigger="click" @command="onUserCommand">
|
||||||
<span class="ty-header__user">
|
<span class="ty-header__user">
|
||||||
<icon type="iconify" icon="mdi:account-circle" />
|
<icon type="iconify" icon="mdi:account-circle" />
|
||||||
|
|||||||
128
src/components/ty-notice.vue
Normal file
128
src/components/ty-notice.vue
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
<template>
|
||||||
|
<el-popover placement="bottom" :width="340" trigger="click" @show="onOpen">
|
||||||
|
<template #reference>
|
||||||
|
<el-badge :value="unreadCount" :hidden="unreadCount === 0" :max="99" class="ty-notice__badge">
|
||||||
|
<button class="ty-notice__btn" type="button" title="通知">
|
||||||
|
<icon type="iconify" icon="mdi:bell-outline" />
|
||||||
|
</button>
|
||||||
|
</el-badge>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="ty-notice">
|
||||||
|
<div class="ty-notice__head">
|
||||||
|
<span class="ty-notice__head-title">通知</span>
|
||||||
|
<el-button v-if="unreadCount > 0" link type="primary" @click="onReadAll">全部已读</el-button>
|
||||||
|
</div>
|
||||||
|
<el-scrollbar max-height="320px">
|
||||||
|
<div v-if="list.length === 0" class="ty-notice__empty">暂无通知</div>
|
||||||
|
<div
|
||||||
|
v-for="n in list"
|
||||||
|
:key="n.id"
|
||||||
|
class="ty-notice__item"
|
||||||
|
:class="{ 'is-unread': n.readAt == null }"
|
||||||
|
@click="onRead(n)"
|
||||||
|
>
|
||||||
|
<div class="ty-notice__item-title">
|
||||||
|
<span v-if="n.readAt == null" class="ty-notice__dot" />
|
||||||
|
{{ n.title }}
|
||||||
|
</div>
|
||||||
|
<div class="ty-notice__item-content">{{ n.content }}</div>
|
||||||
|
<div class="ty-notice__item-time">{{ formatTime(n.createdAt) }}</div>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
</el-popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useNoticeStore } from '@/stores/modules/notice'
|
||||||
|
import type { MyNotice } from '@/api/notice'
|
||||||
|
|
||||||
|
defineOptions({ name: 'TyNotice' })
|
||||||
|
|
||||||
|
const noticeStore = useNoticeStore()
|
||||||
|
const unreadCount = computed(() => noticeStore.unreadCount)
|
||||||
|
const list = computed(() => noticeStore.list)
|
||||||
|
|
||||||
|
function formatTime(v: number): string {
|
||||||
|
const d = new Date(v)
|
||||||
|
const pad = (x: number) => String(x).padStart(2, '0')
|
||||||
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOpen(): void {
|
||||||
|
void noticeStore.fetchList()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onRead(n: MyNotice): Promise<void> {
|
||||||
|
if (n.readAt == null) await noticeStore.readOne(n.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onReadAll(): Promise<void> {
|
||||||
|
await noticeStore.readAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void noticeStore.fetchUnread()
|
||||||
|
noticeStore.startPolling()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
noticeStore.stopPolling()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.ty-notice__badge {
|
||||||
|
@apply inline-flex items-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ty-notice__btn {
|
||||||
|
@apply inline-flex items-center justify-center w-[34px] h-[34px] border-none rounded-base bg-transparent text-[18px] text-text-regular cursor-pointer transition-colors duration-200;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@apply bg-bg-page;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ty-notice__head {
|
||||||
|
@apply flex items-center justify-between px-3 py-2 border-b border-solid;
|
||||||
|
border-color: var(--wm-border-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ty-notice__head-title {
|
||||||
|
@apply font-semibold text-text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ty-notice__empty {
|
||||||
|
@apply py-8 text-center text-sm text-text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ty-notice__item {
|
||||||
|
@apply px-3 py-2.5 cursor-pointer transition-colors duration-150;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@apply bg-bg-page;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ty-notice__item-title {
|
||||||
|
@apply flex items-center gap-1.5 text-sm text-text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ty-notice__item-content {
|
||||||
|
@apply mt-1 truncate text-xs text-text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ty-notice__item-time {
|
||||||
|
@apply mt-1 text-xs;
|
||||||
|
color: var(--wm-text-color-placeholder, #909399);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ty-notice__dot {
|
||||||
|
@apply inline-block w-2 h-2 shrink-0 rounded-full;
|
||||||
|
background: var(--el-color-danger);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
63
src/pages/system/notice/index.vue
Normal file
63
src/pages/system/notice/index.vue
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<template>
|
||||||
|
<ty-crud
|
||||||
|
entity-name="通知"
|
||||||
|
:columns="columns"
|
||||||
|
:search-schema="searchSchema"
|
||||||
|
:form-schema="formSchema"
|
||||||
|
:list="list"
|
||||||
|
:create="create"
|
||||||
|
:update="update"
|
||||||
|
:remove="remove"
|
||||||
|
id-key="id"
|
||||||
|
name-key="title"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { JsonSchema, PageQuery, TableColumn } from '@/types/crud'
|
||||||
|
import { createNotice, deleteNotice, listNotices, updateNotice, type NoticeListQuery } from '@/api/notice'
|
||||||
|
|
||||||
|
defineOptions({ name: 'notice' })
|
||||||
|
|
||||||
|
const columns: TableColumn[] = [
|
||||||
|
{ prop: 'title', label: '标题', minWidth: 200 },
|
||||||
|
{ prop: 'content', label: '内容', minWidth: 300 },
|
||||||
|
{ prop: 'status', label: '状态', width: 90, align: 'center', type: 'tag', dictCode: 'common_status' },
|
||||||
|
{ prop: 'createdAt', label: '创建时间', width: 160, type: 'datetime' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const searchSchema: JsonSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
title: { type: 'string', title: '标题', placeholder: '请输入标题' },
|
||||||
|
status: { type: 'string', title: '状态', dictCode: 'common_status', placeholder: '全部' },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const formSchema: JsonSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
title: { type: 'string', title: '标题', placeholder: '请输入标题', minLength: 2 },
|
||||||
|
content: { type: 'string', title: '内容', format: 'textarea', placeholder: '请输入通知内容' },
|
||||||
|
status: { type: 'string', title: '状态', dictCode: 'common_status', default: 'enabled' },
|
||||||
|
},
|
||||||
|
required: ['title', 'content'],
|
||||||
|
}
|
||||||
|
|
||||||
|
function toListQuery(query: PageQuery): NoticeListQuery {
|
||||||
|
const str = (v: unknown): string | undefined => (typeof v === 'string' && v !== '' ? v : undefined)
|
||||||
|
return {
|
||||||
|
page: query.page,
|
||||||
|
pageSize: query.pageSize,
|
||||||
|
title: str(query.title),
|
||||||
|
status: str(query.status),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = (query: PageQuery, signal?: AbortSignal) => listNotices(toListQuery(query), signal)
|
||||||
|
const create = (data: Record<string, any>) =>
|
||||||
|
createNotice({ title: data.title, content: data.content, status: data.status })
|
||||||
|
const update = (id: string, data: Record<string, any>) =>
|
||||||
|
updateNotice(id, { title: data.title, content: data.content, status: data.status })
|
||||||
|
const remove = (id: string) => deleteNotice(id)
|
||||||
|
</script>
|
||||||
45
src/stores/modules/notice.ts
Normal file
45
src/stores/modules/notice.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { getUnreadCount, listMyNotices, markAllRead, markRead, type MyNotice } from '@/api/notice'
|
||||||
|
|
||||||
|
/** 站内通知:未读数 + 最近列表 + 轮询。 */
|
||||||
|
export const useNoticeStore = defineStore('noticeStore', () => {
|
||||||
|
const unreadCount = ref(0)
|
||||||
|
const list = ref<MyNotice[]>([])
|
||||||
|
let timer: number | null = null
|
||||||
|
|
||||||
|
async function fetchUnread(): Promise<void> {
|
||||||
|
unreadCount.value = await getUnreadCount()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchList(): Promise<void> {
|
||||||
|
const r = await listMyNotices({ page: 1, pageSize: 10 })
|
||||||
|
list.value = r.list
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readOne(id: string): Promise<void> {
|
||||||
|
await markRead(id)
|
||||||
|
await Promise.all([fetchUnread(), fetchList()])
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readAll(): Promise<void> {
|
||||||
|
await markAllRead()
|
||||||
|
await Promise.all([fetchUnread(), fetchList()])
|
||||||
|
}
|
||||||
|
|
||||||
|
function startPolling(interval = 30000): void {
|
||||||
|
stopPolling()
|
||||||
|
timer = window.setInterval(() => {
|
||||||
|
void fetchUnread()
|
||||||
|
}, interval)
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPolling(): void {
|
||||||
|
if (timer != null) {
|
||||||
|
clearInterval(timer)
|
||||||
|
timer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { unreadCount, list, fetchUnread, fetchList, readOne, readAll, startPolling, stopPolling }
|
||||||
|
})
|
||||||
13
typed-router.d.ts
vendored
13
typed-router.d.ts
vendored
@ -114,6 +114,13 @@ declare module 'vue-router/auto-routes' {
|
|||||||
Record<never, never>,
|
Record<never, never>,
|
||||||
| never
|
| never
|
||||||
>,
|
>,
|
||||||
|
'/system/notice/': RouteRecordInfo<
|
||||||
|
'/system/notice/',
|
||||||
|
'/system/notice',
|
||||||
|
Record<never, never>,
|
||||||
|
Record<never, never>,
|
||||||
|
| never
|
||||||
|
>,
|
||||||
'/system/org/': RouteRecordInfo<
|
'/system/org/': RouteRecordInfo<
|
||||||
'/system/org/',
|
'/system/org/',
|
||||||
'/system/org',
|
'/system/org',
|
||||||
@ -268,6 +275,12 @@ declare module 'vue-router/auto-routes' {
|
|||||||
views:
|
views:
|
||||||
| never
|
| never
|
||||||
}
|
}
|
||||||
|
'src/pages/system/notice/index.vue': {
|
||||||
|
routes:
|
||||||
|
| '/system/notice/'
|
||||||
|
views:
|
||||||
|
| never
|
||||||
|
}
|
||||||
'src/pages/system/org/index.vue': {
|
'src/pages/system/org/index.vue': {
|
||||||
routes:
|
routes:
|
||||||
| '/system/org/'
|
| '/system/org/'
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user