mirror of
https://github.com/KwiTsukasa/kt-template-admin.git
synced 2026-05-27 16:35:47 +08:00
feat(admin): 接入博客管理和按钮权限
This commit is contained in:
parent
4b4e6de601
commit
ce95d93c2b
1
apps/web-antdv-next/src/api/blog/index.ts
Normal file
1
apps/web-antdv-next/src/api/blog/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './wordpress';
|
||||||
151
apps/web-antdv-next/src/api/blog/wordpress.ts
Normal file
151
apps/web-antdv-next/src/api/blog/wordpress.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace WordpressBlogApi {
|
||||||
|
export interface PageResult<T> {
|
||||||
|
list: T[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RenderedField {
|
||||||
|
raw?: string;
|
||||||
|
rendered?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Article {
|
||||||
|
categories?: number[];
|
||||||
|
content?: RenderedField | string;
|
||||||
|
date?: string;
|
||||||
|
excerpt?: RenderedField | string;
|
||||||
|
id: number;
|
||||||
|
link?: string;
|
||||||
|
modified?: string;
|
||||||
|
slug?: string;
|
||||||
|
status?: string;
|
||||||
|
sticky?: boolean;
|
||||||
|
tags?: number[];
|
||||||
|
title?: RenderedField | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArticleBody {
|
||||||
|
categories?: number[];
|
||||||
|
content?: string;
|
||||||
|
excerpt?: string;
|
||||||
|
id?: number;
|
||||||
|
slug?: string;
|
||||||
|
status?: string;
|
||||||
|
sticky?: boolean;
|
||||||
|
tags?: number[];
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Term {
|
||||||
|
count?: number;
|
||||||
|
description?: string;
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
parent?: number;
|
||||||
|
slug?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TermBody {
|
||||||
|
description?: string;
|
||||||
|
id?: number;
|
||||||
|
name: string;
|
||||||
|
parent?: number;
|
||||||
|
slug?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TermQuery extends Recordable<any> {
|
||||||
|
hide_empty?: boolean;
|
||||||
|
pageNo?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
parent?: number | string;
|
||||||
|
search?: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getArticleList(params: Recordable<any>) {
|
||||||
|
return requestClient.get<
|
||||||
|
WordpressBlogApi.PageResult<WordpressBlogApi.Article>
|
||||||
|
>('/wordpress/article/list', { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getArticleDetail(id: number | string) {
|
||||||
|
return requestClient.get<WordpressBlogApi.Article>(
|
||||||
|
'/wordpress/article/detail',
|
||||||
|
{ params: { id } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createArticle(data: WordpressBlogApi.ArticleBody) {
|
||||||
|
return requestClient.post<WordpressBlogApi.Article>(
|
||||||
|
'/wordpress/article/save',
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateArticle(data: WordpressBlogApi.ArticleBody) {
|
||||||
|
return requestClient.post<WordpressBlogApi.Article>(
|
||||||
|
'/wordpress/article/update',
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteArticle(id: number | string, force = true) {
|
||||||
|
return requestClient.post<WordpressBlogApi.Article>(
|
||||||
|
`/wordpress/article/remove?id=${id}&force=${force}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCategoryList(params: WordpressBlogApi.TermQuery = {}) {
|
||||||
|
return requestClient.get<WordpressBlogApi.PageResult<WordpressBlogApi.Term>>(
|
||||||
|
'/wordpress/category/list',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCategory(data: WordpressBlogApi.TermBody) {
|
||||||
|
return requestClient.post<WordpressBlogApi.Term>(
|
||||||
|
'/wordpress/category/save',
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateCategory(data: WordpressBlogApi.TermBody) {
|
||||||
|
return requestClient.post<WordpressBlogApi.Term>(
|
||||||
|
'/wordpress/category/update',
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteCategory(id: number | string, force = true) {
|
||||||
|
return requestClient.post<WordpressBlogApi.Term>(
|
||||||
|
`/wordpress/category/remove?id=${id}&force=${force}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTagList(params: WordpressBlogApi.TermQuery = {}) {
|
||||||
|
return requestClient.get<WordpressBlogApi.PageResult<WordpressBlogApi.Term>>(
|
||||||
|
'/wordpress/tag/list',
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTag(data: WordpressBlogApi.TermBody) {
|
||||||
|
return requestClient.post<WordpressBlogApi.Term>('/wordpress/tag/save', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateTag(data: WordpressBlogApi.TermBody) {
|
||||||
|
return requestClient.post<WordpressBlogApi.Term>(
|
||||||
|
'/wordpress/tag/update',
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteTag(id: number | string, force = true) {
|
||||||
|
return requestClient.post<WordpressBlogApi.Term>(
|
||||||
|
`/wordpress/tag/remove?id=${id}&force=${force}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -10,9 +10,15 @@ export namespace AuthApi {
|
|||||||
/** 登录接口返回值 */
|
/** 登录接口返回值 */
|
||||||
export interface LoginResult {
|
export interface LoginResult {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
|
wordpressAvailable?: boolean;
|
||||||
wordpressAuth?: WordpressAuthResult['auth'] & {
|
wordpressAuth?: WordpressAuthResult['auth'] & {
|
||||||
user?: Record<string, any>;
|
user?: Record<string, any>;
|
||||||
};
|
};
|
||||||
|
wordpressError?: null | {
|
||||||
|
error?: any;
|
||||||
|
message?: string;
|
||||||
|
status?: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RefreshTokenResult {
|
export interface RefreshTokenResult {
|
||||||
|
|||||||
@ -3,6 +3,19 @@ import type { RouteRecordStringComponent } from '@vben/types';
|
|||||||
import { requestClient } from '#/api/request';
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
const SUPPORTED_ADMIN_MENU_NAMES = new Set([
|
const SUPPORTED_ADMIN_MENU_NAMES = new Set([
|
||||||
|
'Blog',
|
||||||
|
'BlogArticle',
|
||||||
|
'BlogArticleCreate',
|
||||||
|
'BlogArticleDelete',
|
||||||
|
'BlogArticleEdit',
|
||||||
|
'BlogCategory',
|
||||||
|
'BlogCategoryCreate',
|
||||||
|
'BlogCategoryDelete',
|
||||||
|
'BlogCategoryEdit',
|
||||||
|
'BlogTag',
|
||||||
|
'BlogTagCreate',
|
||||||
|
'BlogTagDelete',
|
||||||
|
'BlogTagEdit',
|
||||||
'System',
|
'System',
|
||||||
'SystemDept',
|
'SystemDept',
|
||||||
'SystemDeptCreate',
|
'SystemDeptCreate',
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
export * from './blog';
|
||||||
export * from './core';
|
export * from './core';
|
||||||
export * from './examples';
|
export * from './examples';
|
||||||
export * from './system';
|
export * from './system';
|
||||||
|
|||||||
40
apps/web-antdv-next/src/app.tsx
Normal file
40
apps/web-antdv-next/src/app.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { computed, defineComponent } from 'vue';
|
||||||
|
import { RouterView } from 'vue-router';
|
||||||
|
|
||||||
|
import { useAntdDesignTokens } from '@vben/hooks';
|
||||||
|
import { preferences, usePreferences } from '@vben/preferences';
|
||||||
|
|
||||||
|
import { App as AntdApp, ConfigProvider, theme } from 'antdv-next';
|
||||||
|
|
||||||
|
import { antdLocale } from '#/locales';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'App',
|
||||||
|
setup() {
|
||||||
|
const { isDark } = usePreferences();
|
||||||
|
const { tokens } = useAntdDesignTokens();
|
||||||
|
|
||||||
|
const tokenTheme = computed(() => {
|
||||||
|
const algorithm = isDark.value
|
||||||
|
? [theme.darkAlgorithm]
|
||||||
|
: [theme.defaultAlgorithm];
|
||||||
|
|
||||||
|
if (preferences.app.compact) {
|
||||||
|
algorithm.push(theme.compactAlgorithm);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
algorithm,
|
||||||
|
token: tokens,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
<ConfigProvider locale={antdLocale.value} theme={tokenTheme.value}>
|
||||||
|
<AntdApp>
|
||||||
|
<RouterView />
|
||||||
|
</AntdApp>
|
||||||
|
</ConfigProvider>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -1,39 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
import { useAntdDesignTokens } from '@vben/hooks';
|
|
||||||
import { preferences, usePreferences } from '@vben/preferences';
|
|
||||||
|
|
||||||
import { App, ConfigProvider, theme } from 'antdv-next';
|
|
||||||
|
|
||||||
import { antdLocale } from '#/locales';
|
|
||||||
|
|
||||||
defineOptions({ name: 'App' });
|
|
||||||
|
|
||||||
const { isDark } = usePreferences();
|
|
||||||
const { tokens } = useAntdDesignTokens();
|
|
||||||
|
|
||||||
const tokenTheme = computed(() => {
|
|
||||||
const algorithm = isDark.value
|
|
||||||
? [theme.darkAlgorithm]
|
|
||||||
: [theme.defaultAlgorithm];
|
|
||||||
|
|
||||||
// antd 紧凑模式算法
|
|
||||||
if (preferences.app.compact) {
|
|
||||||
algorithm.push(theme.compactAlgorithm);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
algorithm,
|
|
||||||
token: tokens,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ConfigProvider :locale="antdLocale" :theme="tokenTheme">
|
|
||||||
<App>
|
|
||||||
<RouterView />
|
|
||||||
</App>
|
|
||||||
</ConfigProvider>
|
|
||||||
</template>
|
|
||||||
@ -14,7 +14,7 @@ import { router } from '#/router';
|
|||||||
|
|
||||||
import { initComponentAdapter } from './adapter/component';
|
import { initComponentAdapter } from './adapter/component';
|
||||||
import { initSetupVbenForm } from './adapter/form';
|
import { initSetupVbenForm } from './adapter/form';
|
||||||
import App from './app.vue';
|
import App from './app';
|
||||||
import { initTimezone } from './timezone-init';
|
import { initTimezone } from './timezone-init';
|
||||||
|
|
||||||
async function bootstrap(namespace: string) {
|
async function bootstrap(namespace: string) {
|
||||||
|
|||||||
@ -15,7 +15,10 @@ import { $t } from '#/locales';
|
|||||||
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
|
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
|
||||||
|
|
||||||
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
||||||
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
|
const pageMap: ComponentRecordType = {
|
||||||
|
...import.meta.glob('../views/**/*.tsx'),
|
||||||
|
...import.meta.glob('../views/**/*.vue'),
|
||||||
|
};
|
||||||
|
|
||||||
const layoutMap: ComponentRecordType = {
|
const layoutMap: ComponentRecordType = {
|
||||||
BasicLayout,
|
BasicLayout,
|
||||||
|
|||||||
@ -35,14 +35,15 @@ const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
|
|||||||
/** 有权限校验的路由列表,包含动态路由和静态路由 */
|
/** 有权限校验的路由列表,包含动态路由和静态路由 */
|
||||||
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
|
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
|
||||||
|
|
||||||
const componentKeys: string[] = Object.keys(
|
const componentKeys: string[] = Object.keys({
|
||||||
import.meta.glob('../../views/**/*.vue'),
|
...import.meta.glob('../../views/**/*.tsx'),
|
||||||
)
|
...import.meta.glob('../../views/**/*.vue'),
|
||||||
|
})
|
||||||
.filter((item) => !item.includes('/modules/'))
|
.filter((item) => !item.includes('/modules/'))
|
||||||
.map((v) => {
|
.map((v) => {
|
||||||
const path = v.replace('../../views/', '/');
|
const path = v.replace('../../views/', '/');
|
||||||
return path.endsWith('.vue') ? path.slice(0, -4) : path;
|
return path.replace(/\.(tsx|vue)$/, '');
|
||||||
})
|
})
|
||||||
.filter((path) => path.startsWith('/system/'));
|
.filter((path) => path.startsWith('/blog/') || path.startsWith('/system/'));
|
||||||
|
|
||||||
export { accessRoutes, componentKeys, coreRouteNames, routes };
|
export { accessRoutes, componentKeys, coreRouteNames, routes };
|
||||||
|
|||||||
45
apps/web-antdv-next/src/router/routes/modules/blog.ts
Normal file
45
apps/web-antdv-next/src/router/routes/modules/blog.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
meta: {
|
||||||
|
icon: 'lucide:newspaper',
|
||||||
|
order: 100,
|
||||||
|
title: '博客管理',
|
||||||
|
},
|
||||||
|
name: 'Blog',
|
||||||
|
path: '/blog',
|
||||||
|
redirect: '/blog/article',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
component: () => import('#/views/blog/article/list'),
|
||||||
|
meta: {
|
||||||
|
icon: 'lucide:file-text',
|
||||||
|
title: '文章管理',
|
||||||
|
},
|
||||||
|
name: 'BlogArticle',
|
||||||
|
path: '/blog/article',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: () => import('#/views/blog/category/list'),
|
||||||
|
meta: {
|
||||||
|
icon: 'lucide:folder-tree',
|
||||||
|
title: '分类管理',
|
||||||
|
},
|
||||||
|
name: 'BlogCategory',
|
||||||
|
path: '/blog/category',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: () => import('#/views/blog/tag/list'),
|
||||||
|
meta: {
|
||||||
|
icon: 'lucide:tags',
|
||||||
|
title: '标签管理',
|
||||||
|
},
|
||||||
|
name: 'BlogTag',
|
||||||
|
path: '/blog/tag',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
581
apps/web-antdv-next/src/views/blog/article/list.tsx
Normal file
581
apps/web-antdv-next/src/views/blog/article/list.tsx
Normal file
@ -0,0 +1,581 @@
|
|||||||
|
import type { WordpressBlogApi } from '#/api/blog';
|
||||||
|
|
||||||
|
import { computed, defineComponent, onMounted, reactive, ref } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { useAccess } from '@vben/access';
|
||||||
|
import { Page } from '@vben/common-ui';
|
||||||
|
import { Plus, RotateCw } from '@vben/icons';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
Input,
|
||||||
|
message,
|
||||||
|
Modal,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Switch,
|
||||||
|
Table,
|
||||||
|
Tag,
|
||||||
|
TextArea,
|
||||||
|
} from 'antdv-next';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createArticle,
|
||||||
|
deleteArticle,
|
||||||
|
getArticleList,
|
||||||
|
getCategoryList,
|
||||||
|
getTagList,
|
||||||
|
updateArticle,
|
||||||
|
} from '#/api/blog';
|
||||||
|
|
||||||
|
type TermOption = {
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AButton = Button as any;
|
||||||
|
const AInput = Input as any;
|
||||||
|
const AModal = Modal as any;
|
||||||
|
const ASelect = Select as any;
|
||||||
|
const ASwitch = Switch as any;
|
||||||
|
const ATable = Table as any;
|
||||||
|
const ATextArea = TextArea as any;
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'BlogArticleList',
|
||||||
|
setup() {
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const { hasAccessByCodes } = useAccess();
|
||||||
|
|
||||||
|
const articleStatusOptions = [
|
||||||
|
{ color: 'success', label: '已发布', value: 'publish' },
|
||||||
|
{ color: 'default', label: '草稿', value: 'draft' },
|
||||||
|
{ color: 'warning', label: '待审核', value: 'pending' },
|
||||||
|
{ color: 'processing', label: '私有', value: 'private' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const saving = ref(false);
|
||||||
|
const modalOpen = ref(false);
|
||||||
|
const rows = ref<WordpressBlogApi.Article[]>([]);
|
||||||
|
const total = ref(0);
|
||||||
|
const editingId = ref<number>();
|
||||||
|
const categoryOptions = ref<TermOption[]>([]);
|
||||||
|
const tagOptions = ref<TermOption[]>([]);
|
||||||
|
|
||||||
|
const query = reactive({
|
||||||
|
categoryId: undefined as number | undefined,
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
search: '',
|
||||||
|
status: undefined as string | undefined,
|
||||||
|
tagId: undefined as number | undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = reactive<WordpressBlogApi.ArticleBody>({
|
||||||
|
categories: [],
|
||||||
|
content: '',
|
||||||
|
excerpt: '',
|
||||||
|
slug: '',
|
||||||
|
status: 'draft',
|
||||||
|
sticky: false,
|
||||||
|
tags: [],
|
||||||
|
title: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const modalTitle = computed(() =>
|
||||||
|
editingId.value ? '编辑文章' : '新建文章',
|
||||||
|
);
|
||||||
|
const canCreate = computed(() => hasAccessByCodes(['Blog:Article:Create']));
|
||||||
|
const canEdit = computed(() => hasAccessByCodes(['Blog:Article:Edit']));
|
||||||
|
const canDelete = computed(() => hasAccessByCodes(['Blog:Article:Delete']));
|
||||||
|
const canOperate = computed(() => canEdit.value || canDelete.value);
|
||||||
|
const columns = computed(() => {
|
||||||
|
const baseColumns = [
|
||||||
|
{ dataIndex: 'title', key: 'title', title: '标题' },
|
||||||
|
{ dataIndex: 'status', key: 'status', title: '状态', width: 110 },
|
||||||
|
{
|
||||||
|
dataIndex: 'categories',
|
||||||
|
key: 'categories',
|
||||||
|
title: '分类',
|
||||||
|
width: 180,
|
||||||
|
},
|
||||||
|
{ dataIndex: 'tags', key: 'tags', title: '标签', width: 180 },
|
||||||
|
{
|
||||||
|
dataIndex: 'modified',
|
||||||
|
key: 'modified',
|
||||||
|
title: '更新时间',
|
||||||
|
width: 180,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return canOperate.value
|
||||||
|
? [...baseColumns, { key: 'action', title: '操作', width: 150 }]
|
||||||
|
: baseColumns;
|
||||||
|
});
|
||||||
|
|
||||||
|
function getRenderedText(value?: string | WordpressBlogApi.RenderedField) {
|
||||||
|
if (!value) return '';
|
||||||
|
if (typeof value === 'string') return stripHtml(value);
|
||||||
|
return stripHtml(value.raw || value.rendered || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripHtml(value: string) {
|
||||||
|
return value
|
||||||
|
.replaceAll(/<[^>]+>/g, '')
|
||||||
|
.replaceAll(' ', ' ')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusOption(status?: string) {
|
||||||
|
return (
|
||||||
|
articleStatusOptions.find((item) => item.value === status) ||
|
||||||
|
articleStatusOptions[1]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTermLabel(options: TermOption[], value: number) {
|
||||||
|
return options.find((item) => item.value === value)?.label || `${value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRouteNumber(name: 'category' | 'tag') {
|
||||||
|
const value = route.query[name];
|
||||||
|
const rawValue = Array.isArray(value) ? value[0] : value;
|
||||||
|
const id = Number(rawValue);
|
||||||
|
|
||||||
|
return Number.isFinite(id) && id > 0 ? id : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readRouteFilters() {
|
||||||
|
query.categoryId = getRouteNumber('category');
|
||||||
|
query.tagId = getRouteNumber('tag');
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncRouteFilters() {
|
||||||
|
const nextQuery = { ...route.query };
|
||||||
|
|
||||||
|
if (query.categoryId) {
|
||||||
|
nextQuery.category = `${query.categoryId}`;
|
||||||
|
} else {
|
||||||
|
delete nextQuery.category;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.tagId) {
|
||||||
|
nextQuery.tag = `${query.tagId}`;
|
||||||
|
} else {
|
||||||
|
delete nextQuery.tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
return router.replace({
|
||||||
|
name: 'BlogArticle',
|
||||||
|
query: nextQuery,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTermOptions() {
|
||||||
|
const [categories, tags] = await Promise.all([
|
||||||
|
getCategoryList({ hide_empty: false, pageNo: 1, pageSize: 100 }),
|
||||||
|
getTagList({ hide_empty: false, pageNo: 1, pageSize: 100 }),
|
||||||
|
]);
|
||||||
|
categoryOptions.value = categories.list.map((item) => ({
|
||||||
|
label: item.name,
|
||||||
|
value: item.id,
|
||||||
|
}));
|
||||||
|
tagOptions.value = tags.list.map((item) => ({
|
||||||
|
label: item.name,
|
||||||
|
value: item.id,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadArticles() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const result = await getArticleList({
|
||||||
|
categories: query.categoryId ? `${query.categoryId}` : undefined,
|
||||||
|
pageNo: query.pageNo,
|
||||||
|
pageSize: query.pageSize,
|
||||||
|
search: query.search,
|
||||||
|
status: query.status || undefined,
|
||||||
|
tags: query.tagId ? `${query.tagId}` : undefined,
|
||||||
|
});
|
||||||
|
rows.value = result.list;
|
||||||
|
total.value = result.total;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchArticles() {
|
||||||
|
query.pageNo = 1;
|
||||||
|
await syncRouteFilters();
|
||||||
|
await loadArticles();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSearch() {
|
||||||
|
query.categoryId = undefined;
|
||||||
|
query.search = '';
|
||||||
|
query.status = undefined;
|
||||||
|
query.tagId = undefined;
|
||||||
|
query.pageNo = 1;
|
||||||
|
syncRouteFilters();
|
||||||
|
loadArticles();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function filterByCategory(id: number) {
|
||||||
|
query.categoryId = id;
|
||||||
|
query.pageNo = 1;
|
||||||
|
await syncRouteFilters();
|
||||||
|
await loadArticles();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function filterByTag(id: number) {
|
||||||
|
query.tagId = id;
|
||||||
|
query.pageNo = 1;
|
||||||
|
await syncRouteFilters();
|
||||||
|
await loadArticles();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreate() {
|
||||||
|
editingId.value = undefined;
|
||||||
|
Object.assign(form, {
|
||||||
|
categories: [],
|
||||||
|
content: '',
|
||||||
|
excerpt: '',
|
||||||
|
slug: '',
|
||||||
|
status: 'draft',
|
||||||
|
sticky: false,
|
||||||
|
tags: [],
|
||||||
|
title: '',
|
||||||
|
});
|
||||||
|
modalOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit(row: Record<string, any> | WordpressBlogApi.Article) {
|
||||||
|
const article = row as WordpressBlogApi.Article;
|
||||||
|
editingId.value = article.id;
|
||||||
|
Object.assign(form, {
|
||||||
|
categories: article.categories || [],
|
||||||
|
content: getRenderedText(article.content),
|
||||||
|
excerpt: getRenderedText(article.excerpt),
|
||||||
|
id: article.id,
|
||||||
|
slug: article.slug || '',
|
||||||
|
status: article.status || 'draft',
|
||||||
|
sticky: !!article.sticky,
|
||||||
|
tags: article.tags || [],
|
||||||
|
title: getRenderedText(article.title),
|
||||||
|
});
|
||||||
|
modalOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitArticle() {
|
||||||
|
if (!form.title.trim()) {
|
||||||
|
message.warning('请填写文章标题');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
saving.value = true;
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
...form,
|
||||||
|
id: editingId.value,
|
||||||
|
title: form.title.trim(),
|
||||||
|
};
|
||||||
|
await (editingId.value
|
||||||
|
? updateArticle(payload)
|
||||||
|
: createArticle(payload));
|
||||||
|
message.success('文章保存成功');
|
||||||
|
modalOpen.value = false;
|
||||||
|
loadArticles();
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmDelete(
|
||||||
|
row: Record<string, any> | WordpressBlogApi.Article,
|
||||||
|
) {
|
||||||
|
const article = row as WordpressBlogApi.Article;
|
||||||
|
Modal.confirm({
|
||||||
|
content: `确认删除文章「${getRenderedText(article.title) || article.id}」吗?`,
|
||||||
|
onOk: async () => {
|
||||||
|
await deleteArticle(article.id);
|
||||||
|
message.success('文章删除成功');
|
||||||
|
loadArticles();
|
||||||
|
},
|
||||||
|
title: '删除文章',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTableChange(pagination: any) {
|
||||||
|
query.pageNo = pagination.current || 1;
|
||||||
|
query.pageSize = pagination.pageSize || 10;
|
||||||
|
loadArticles();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
readRouteFilters();
|
||||||
|
await loadTermOptions();
|
||||||
|
await loadArticles();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
<Page autoContentHeight>
|
||||||
|
<div class="flex h-full min-h-0 flex-col gap-3">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-3 bg-card px-4 py-3">
|
||||||
|
<Space wrap>
|
||||||
|
<AInput
|
||||||
|
allowClear
|
||||||
|
class="w-[260px]"
|
||||||
|
onPressEnter={searchArticles}
|
||||||
|
onUpdate:value={(value: string) => {
|
||||||
|
query.search = value;
|
||||||
|
}}
|
||||||
|
placeholder="搜索标题或内容"
|
||||||
|
value={query.search}
|
||||||
|
/>
|
||||||
|
<ASelect
|
||||||
|
allowClear
|
||||||
|
class="w-[150px]"
|
||||||
|
onUpdate:value={(value: string | undefined) => {
|
||||||
|
query.status = value;
|
||||||
|
}}
|
||||||
|
options={articleStatusOptions}
|
||||||
|
placeholder="文章状态"
|
||||||
|
value={query.status}
|
||||||
|
/>
|
||||||
|
<ASelect
|
||||||
|
allowClear
|
||||||
|
class="w-[150px]"
|
||||||
|
onUpdate:value={(value: number | undefined) => {
|
||||||
|
query.categoryId = value;
|
||||||
|
}}
|
||||||
|
options={categoryOptions.value}
|
||||||
|
placeholder="文章分类"
|
||||||
|
value={query.categoryId}
|
||||||
|
/>
|
||||||
|
<ASelect
|
||||||
|
allowClear
|
||||||
|
class="w-[150px]"
|
||||||
|
onUpdate:value={(value: number | undefined) => {
|
||||||
|
query.tagId = value;
|
||||||
|
}}
|
||||||
|
options={tagOptions.value}
|
||||||
|
placeholder="文章标签"
|
||||||
|
value={query.tagId}
|
||||||
|
/>
|
||||||
|
<AButton onClick={searchArticles}>查询</AButton>
|
||||||
|
<AButton onClick={resetSearch}>
|
||||||
|
<RotateCw class="size-4" />
|
||||||
|
重置
|
||||||
|
</AButton>
|
||||||
|
</Space>
|
||||||
|
{canCreate.value ? (
|
||||||
|
<AButton onClick={openCreate} type="primary">
|
||||||
|
<Plus class="size-4" />
|
||||||
|
新建文章
|
||||||
|
</AButton>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="min-h-0 flex-1 bg-card p-4">
|
||||||
|
<ATable
|
||||||
|
columns={columns.value}
|
||||||
|
dataSource={rows.value}
|
||||||
|
loading={loading.value}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
pagination={{
|
||||||
|
current: query.pageNo,
|
||||||
|
pageSize: query.pageSize,
|
||||||
|
showSizeChanger: true,
|
||||||
|
total: total.value,
|
||||||
|
}}
|
||||||
|
rowKey="id"
|
||||||
|
scroll={{ x: 980 }}
|
||||||
|
v-slots={{
|
||||||
|
bodyCell: ({ column, record }: any) => {
|
||||||
|
if (column.key === 'title') {
|
||||||
|
return (
|
||||||
|
<div class="max-w-[420px]">
|
||||||
|
<div class="truncate font-medium">
|
||||||
|
{getRenderedText(record.title) || '-'}
|
||||||
|
</div>
|
||||||
|
{record.link ? (
|
||||||
|
<a
|
||||||
|
class="text-xs text-primary"
|
||||||
|
href={record.link}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
查看原文
|
||||||
|
</a>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (column.key === 'status') {
|
||||||
|
const status = getStatusOption(record.status);
|
||||||
|
return <Tag color={status?.color}>{status?.label}</Tag>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (column.key === 'categories') {
|
||||||
|
return record.categories?.length ? (
|
||||||
|
<Space size={[4, 4]} wrap>
|
||||||
|
{record.categories.map((item: number) => (
|
||||||
|
<Tag
|
||||||
|
class="cursor-pointer"
|
||||||
|
color="blue"
|
||||||
|
key={item}
|
||||||
|
onClick={() => filterByCategory(item)}
|
||||||
|
>
|
||||||
|
{getTermLabel(categoryOptions.value, item)}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
) : (
|
||||||
|
<span>-</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (column.key === 'tags') {
|
||||||
|
return record.tags?.length ? (
|
||||||
|
<Space size={[4, 4]} wrap>
|
||||||
|
{record.tags.map((item: number) => (
|
||||||
|
<Tag
|
||||||
|
class="cursor-pointer"
|
||||||
|
key={item}
|
||||||
|
onClick={() => filterByTag(item)}
|
||||||
|
>
|
||||||
|
{getTermLabel(tagOptions.value, item)}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
) : (
|
||||||
|
<span>-</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (column.key === 'action') {
|
||||||
|
return (
|
||||||
|
<Space>
|
||||||
|
{canEdit.value ? (
|
||||||
|
<AButton onClick={() => openEdit(record)} type="link">
|
||||||
|
编辑
|
||||||
|
</AButton>
|
||||||
|
) : null}
|
||||||
|
{canDelete.value ? (
|
||||||
|
<AButton
|
||||||
|
danger
|
||||||
|
onClick={() => confirmDelete(record)}
|
||||||
|
type="link"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</AButton>
|
||||||
|
) : null}
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AModal
|
||||||
|
confirmLoading={saving.value}
|
||||||
|
onOk={submitArticle}
|
||||||
|
onUpdate:open={(value: boolean) => {
|
||||||
|
modalOpen.value = value;
|
||||||
|
}}
|
||||||
|
open={modalOpen.value}
|
||||||
|
title={modalTitle.value}
|
||||||
|
width="760px"
|
||||||
|
>
|
||||||
|
<Form labelCol={{ span: 4 }} model={form} wrapperCol={{ span: 19 }}>
|
||||||
|
<FormItem label="标题" required>
|
||||||
|
<AInput
|
||||||
|
onUpdate:value={(value: string) => {
|
||||||
|
form.title = value;
|
||||||
|
}}
|
||||||
|
placeholder="请输入文章标题"
|
||||||
|
value={form.title}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="状态">
|
||||||
|
<ASelect
|
||||||
|
onUpdate:value={(value: string | undefined) => {
|
||||||
|
form.status = value;
|
||||||
|
}}
|
||||||
|
options={articleStatusOptions}
|
||||||
|
value={form.status}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="别名">
|
||||||
|
<AInput
|
||||||
|
onUpdate:value={(value: string | undefined) => {
|
||||||
|
form.slug = value;
|
||||||
|
}}
|
||||||
|
placeholder="可选,WordPress slug"
|
||||||
|
value={form.slug}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="分类">
|
||||||
|
<ASelect
|
||||||
|
mode="multiple"
|
||||||
|
onUpdate:value={(value: number[] | undefined) => {
|
||||||
|
form.categories = value;
|
||||||
|
}}
|
||||||
|
options={categoryOptions.value}
|
||||||
|
placeholder="选择分类"
|
||||||
|
value={form.categories}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="标签">
|
||||||
|
<ASelect
|
||||||
|
mode="multiple"
|
||||||
|
onUpdate:value={(value: number[] | undefined) => {
|
||||||
|
form.tags = value;
|
||||||
|
}}
|
||||||
|
options={tagOptions.value}
|
||||||
|
placeholder="选择标签"
|
||||||
|
value={form.tags}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="摘要">
|
||||||
|
<ATextArea
|
||||||
|
autoSize={{ maxRows: 4, minRows: 2 }}
|
||||||
|
onUpdate:value={(value: string | undefined) => {
|
||||||
|
form.excerpt = value;
|
||||||
|
}}
|
||||||
|
placeholder="可选,文章摘要"
|
||||||
|
value={form.excerpt}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="内容">
|
||||||
|
<ATextArea
|
||||||
|
autoSize={{ maxRows: 12, minRows: 6 }}
|
||||||
|
onUpdate:value={(value: string | undefined) => {
|
||||||
|
form.content = value;
|
||||||
|
}}
|
||||||
|
placeholder="支持 HTML 内容"
|
||||||
|
value={form.content}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="置顶">
|
||||||
|
<ASwitch
|
||||||
|
checked={form.sticky}
|
||||||
|
onUpdate:checked={(value: boolean) => {
|
||||||
|
form.sticky = value;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Form>
|
||||||
|
</AModal>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
10
apps/web-antdv-next/src/views/blog/category/list.tsx
Normal file
10
apps/web-antdv-next/src/views/blog/category/list.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
|
import TermManagement from '../modules/term-management';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'BlogCategoryList',
|
||||||
|
setup() {
|
||||||
|
return () => <TermManagement kind="category" title="分类" />;
|
||||||
|
},
|
||||||
|
});
|
||||||
406
apps/web-antdv-next/src/views/blog/modules/term-management.tsx
Normal file
406
apps/web-antdv-next/src/views/blog/modules/term-management.tsx
Normal file
@ -0,0 +1,406 @@
|
|||||||
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
|
import type { WordpressBlogApi } from '#/api/blog';
|
||||||
|
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
defineComponent,
|
||||||
|
onMounted,
|
||||||
|
reactive,
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
} from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { useAccess } from '@vben/access';
|
||||||
|
import { Page } from '@vben/common-ui';
|
||||||
|
import { Plus, RotateCw } from '@vben/icons';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
Input,
|
||||||
|
message,
|
||||||
|
Modal,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Table,
|
||||||
|
TextArea,
|
||||||
|
} from 'antdv-next';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createCategory,
|
||||||
|
createTag,
|
||||||
|
deleteCategory,
|
||||||
|
deleteTag,
|
||||||
|
getCategoryList,
|
||||||
|
getTagList,
|
||||||
|
updateCategory,
|
||||||
|
updateTag,
|
||||||
|
} from '#/api/blog';
|
||||||
|
|
||||||
|
const AButton = Button as any;
|
||||||
|
const AInput = Input as any;
|
||||||
|
const AModal = Modal as any;
|
||||||
|
const ASelect = Select as any;
|
||||||
|
const ATable = Table as any;
|
||||||
|
const ATextArea = TextArea as any;
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'BlogTermManagement',
|
||||||
|
props: {
|
||||||
|
kind: {
|
||||||
|
required: true,
|
||||||
|
type: String as PropType<'category' | 'tag'>,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
required: true,
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const { hasAccessByCodes } = useAccess();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const saving = ref(false);
|
||||||
|
const modalOpen = ref(false);
|
||||||
|
const rows = ref<WordpressBlogApi.Term[]>([]);
|
||||||
|
const total = ref(0);
|
||||||
|
const editingId = ref<number>();
|
||||||
|
|
||||||
|
const query = reactive({
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
search: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = reactive<WordpressBlogApi.TermBody>({
|
||||||
|
description: '',
|
||||||
|
name: '',
|
||||||
|
parent: undefined,
|
||||||
|
slug: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const modalTitle = computed(() =>
|
||||||
|
editingId.value ? `编辑${props.title}` : `新建${props.title}`,
|
||||||
|
);
|
||||||
|
const permissionModule = computed(() =>
|
||||||
|
props.kind === 'category' ? 'Blog:Category' : 'Blog:Tag',
|
||||||
|
);
|
||||||
|
const canCreate = computed(() =>
|
||||||
|
hasAccessByCodes([`${permissionModule.value}:Create`]),
|
||||||
|
);
|
||||||
|
const canEdit = computed(() =>
|
||||||
|
hasAccessByCodes([`${permissionModule.value}:Edit`]),
|
||||||
|
);
|
||||||
|
const canDelete = computed(() =>
|
||||||
|
hasAccessByCodes([`${permissionModule.value}:Delete`]),
|
||||||
|
);
|
||||||
|
const canViewArticles = computed(() =>
|
||||||
|
hasAccessByCodes(['Blog:Article:List']),
|
||||||
|
);
|
||||||
|
const canOperate = computed(
|
||||||
|
() => canViewArticles.value || canEdit.value || canDelete.value,
|
||||||
|
);
|
||||||
|
const columns = computed(() => {
|
||||||
|
const baseColumns = [
|
||||||
|
{ dataIndex: 'name', key: 'name', title: '名称' },
|
||||||
|
{ dataIndex: 'slug', key: 'slug', title: '别名', width: 180 },
|
||||||
|
{ dataIndex: 'count', key: 'count', title: '文章数', width: 100 },
|
||||||
|
{ dataIndex: 'description', key: 'description', title: '描述' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return canOperate.value
|
||||||
|
? [...baseColumns, { key: 'action', title: '操作', width: 220 }]
|
||||||
|
: baseColumns;
|
||||||
|
});
|
||||||
|
|
||||||
|
const parentOptions = computed(() =>
|
||||||
|
rows.value
|
||||||
|
.filter((item) => item.id !== editingId.value)
|
||||||
|
.map((item) => ({ label: item.name, value: item.id })),
|
||||||
|
);
|
||||||
|
|
||||||
|
function getRouteSearch() {
|
||||||
|
const value = route.query.search;
|
||||||
|
return Array.isArray(value) ? value[0] || '' : value || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestList() {
|
||||||
|
const params = {
|
||||||
|
hide_empty: false,
|
||||||
|
pageNo: query.pageNo,
|
||||||
|
pageSize: query.pageSize,
|
||||||
|
search: query.search,
|
||||||
|
};
|
||||||
|
return props.kind === 'category'
|
||||||
|
? await getCategoryList(params)
|
||||||
|
: await getTagList(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTerms() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const result = await requestList();
|
||||||
|
rows.value = result.list;
|
||||||
|
total.value = result.total;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSearch() {
|
||||||
|
query.search = '';
|
||||||
|
query.pageNo = 1;
|
||||||
|
loadTerms();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreate() {
|
||||||
|
editingId.value = undefined;
|
||||||
|
Object.assign(form, {
|
||||||
|
description: '',
|
||||||
|
name: '',
|
||||||
|
parent: undefined,
|
||||||
|
slug: '',
|
||||||
|
});
|
||||||
|
modalOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit(row: Record<string, any> | WordpressBlogApi.Term) {
|
||||||
|
const term = row as WordpressBlogApi.Term;
|
||||||
|
editingId.value = term.id;
|
||||||
|
Object.assign(form, {
|
||||||
|
description: term.description || '',
|
||||||
|
id: term.id,
|
||||||
|
name: term.name,
|
||||||
|
parent: term.parent || undefined,
|
||||||
|
slug: term.slug || '',
|
||||||
|
});
|
||||||
|
modalOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitTerm() {
|
||||||
|
if (!form.name.trim()) {
|
||||||
|
message.warning(`请填写${props.title}名称`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
saving.value = true;
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
...form,
|
||||||
|
id: editingId.value,
|
||||||
|
name: form.name.trim(),
|
||||||
|
};
|
||||||
|
if (props.kind === 'category') {
|
||||||
|
await (editingId.value
|
||||||
|
? updateCategory(payload)
|
||||||
|
: createCategory(payload));
|
||||||
|
} else {
|
||||||
|
await (editingId.value ? updateTag(payload) : createTag(payload));
|
||||||
|
}
|
||||||
|
message.success(`${props.title}保存成功`);
|
||||||
|
modalOpen.value = false;
|
||||||
|
loadTerms();
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmDelete(row: Record<string, any> | WordpressBlogApi.Term) {
|
||||||
|
const term = row as WordpressBlogApi.Term;
|
||||||
|
Modal.confirm({
|
||||||
|
content: `确认删除${props.title}「${term.name}」吗?`,
|
||||||
|
onOk: async () => {
|
||||||
|
await (props.kind === 'category'
|
||||||
|
? deleteCategory(term.id)
|
||||||
|
: deleteTag(term.id));
|
||||||
|
message.success(`${props.title}删除成功`);
|
||||||
|
loadTerms();
|
||||||
|
},
|
||||||
|
title: `删除${props.title}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openRelatedArticles(
|
||||||
|
row: Record<string, any> | WordpressBlogApi.Term,
|
||||||
|
) {
|
||||||
|
const term = row as WordpressBlogApi.Term;
|
||||||
|
router.push({
|
||||||
|
name: 'BlogArticle',
|
||||||
|
query:
|
||||||
|
props.kind === 'category'
|
||||||
|
? { category: `${term.id}` }
|
||||||
|
: { tag: `${term.id}` },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTableChange(pagination: any) {
|
||||||
|
query.pageNo = pagination.current || 1;
|
||||||
|
query.pageSize = pagination.pageSize || 10;
|
||||||
|
loadTerms();
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.kind,
|
||||||
|
() => {
|
||||||
|
query.search = getRouteSearch();
|
||||||
|
query.pageNo = 1;
|
||||||
|
loadTerms();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
query.search = getRouteSearch();
|
||||||
|
loadTerms();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
<Page autoContentHeight>
|
||||||
|
<div class="flex h-full min-h-0 flex-col gap-3">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-3 bg-card px-4 py-3">
|
||||||
|
<Space wrap>
|
||||||
|
<AInput
|
||||||
|
allowClear
|
||||||
|
class="w-[260px]"
|
||||||
|
onPressEnter={loadTerms}
|
||||||
|
onUpdate:value={(value: string) => {
|
||||||
|
query.search = value;
|
||||||
|
}}
|
||||||
|
placeholder={`搜索${props.title}名称`}
|
||||||
|
value={query.search}
|
||||||
|
/>
|
||||||
|
<AButton onClick={loadTerms}>查询</AButton>
|
||||||
|
<AButton onClick={resetSearch}>
|
||||||
|
<RotateCw class="size-4" />
|
||||||
|
重置
|
||||||
|
</AButton>
|
||||||
|
</Space>
|
||||||
|
{canCreate.value ? (
|
||||||
|
<AButton onClick={openCreate} type="primary">
|
||||||
|
<Plus class="size-4" />
|
||||||
|
新建{props.title}
|
||||||
|
</AButton>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="min-h-0 flex-1 bg-card p-4">
|
||||||
|
<ATable
|
||||||
|
columns={columns.value}
|
||||||
|
dataSource={rows.value}
|
||||||
|
loading={loading.value}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
pagination={{
|
||||||
|
current: query.pageNo,
|
||||||
|
pageSize: query.pageSize,
|
||||||
|
showSizeChanger: true,
|
||||||
|
total: total.value,
|
||||||
|
}}
|
||||||
|
rowKey="id"
|
||||||
|
scroll={{ x: 820 }}
|
||||||
|
v-slots={{
|
||||||
|
bodyCell: ({ column, record }: any) => {
|
||||||
|
if (column.key === 'description') {
|
||||||
|
return (
|
||||||
|
<span class="line-clamp-2">
|
||||||
|
{record.description || '-'}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (column.key === 'action') {
|
||||||
|
return (
|
||||||
|
<Space>
|
||||||
|
{canViewArticles.value ? (
|
||||||
|
<AButton
|
||||||
|
onClick={() => openRelatedArticles(record)}
|
||||||
|
type="link"
|
||||||
|
>
|
||||||
|
查看文章
|
||||||
|
</AButton>
|
||||||
|
) : null}
|
||||||
|
{canEdit.value ? (
|
||||||
|
<AButton onClick={() => openEdit(record)} type="link">
|
||||||
|
编辑
|
||||||
|
</AButton>
|
||||||
|
) : null}
|
||||||
|
{canDelete.value ? (
|
||||||
|
<AButton
|
||||||
|
danger
|
||||||
|
onClick={() => confirmDelete(record)}
|
||||||
|
type="link"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</AButton>
|
||||||
|
) : null}
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AModal
|
||||||
|
confirmLoading={saving.value}
|
||||||
|
onOk={submitTerm}
|
||||||
|
onUpdate:open={(value: boolean) => {
|
||||||
|
modalOpen.value = value;
|
||||||
|
}}
|
||||||
|
open={modalOpen.value}
|
||||||
|
title={modalTitle.value}
|
||||||
|
width="620px"
|
||||||
|
>
|
||||||
|
<Form labelCol={{ span: 5 }} model={form} wrapperCol={{ span: 18 }}>
|
||||||
|
<FormItem label="名称" required>
|
||||||
|
<AInput
|
||||||
|
onUpdate:value={(value: string) => {
|
||||||
|
form.name = value;
|
||||||
|
}}
|
||||||
|
placeholder={`请输入${props.title}名称`}
|
||||||
|
value={form.name}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="别名">
|
||||||
|
<AInput
|
||||||
|
onUpdate:value={(value: string | undefined) => {
|
||||||
|
form.slug = value;
|
||||||
|
}}
|
||||||
|
placeholder="可选,WordPress slug"
|
||||||
|
value={form.slug}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
{props.kind === 'category' ? (
|
||||||
|
<FormItem label="父级分类">
|
||||||
|
<ASelect
|
||||||
|
allowClear
|
||||||
|
onUpdate:value={(value: number | undefined) => {
|
||||||
|
form.parent = value;
|
||||||
|
}}
|
||||||
|
options={parentOptions.value}
|
||||||
|
placeholder="选择父级分类"
|
||||||
|
value={form.parent}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
) : null}
|
||||||
|
<FormItem label="描述">
|
||||||
|
<ATextArea
|
||||||
|
autoSize={{ maxRows: 6, minRows: 3 }}
|
||||||
|
onUpdate:value={(value: string | undefined) => {
|
||||||
|
form.description = value;
|
||||||
|
}}
|
||||||
|
placeholder="可选"
|
||||||
|
value={form.description}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Form>
|
||||||
|
</AModal>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
10
apps/web-antdv-next/src/views/blog/tag/list.tsx
Normal file
10
apps/web-antdv-next/src/views/blog/tag/list.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
|
import TermManagement from '../modules/term-management';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'BlogTagList',
|
||||||
|
setup() {
|
||||||
|
return () => <TermManagement kind="tag" title="标签" />;
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -8,6 +8,10 @@ import { z } from '#/adapter/form';
|
|||||||
import { getDeptList } from '#/api/system/dept';
|
import { getDeptList } from '#/api/system/dept';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
type PermissionOptions = {
|
||||||
|
canAccess?: (code: string) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取编辑表单的字段配置。如果没有使用多语言,可以直接export一个数组常量
|
* 获取编辑表单的字段配置。如果没有使用多语言,可以直接export一个数组常量
|
||||||
*/
|
*/
|
||||||
@ -76,7 +80,10 @@ export function useSchema(): VbenFormSchema[] {
|
|||||||
*/
|
*/
|
||||||
export function useColumns(
|
export function useColumns(
|
||||||
onActionClick?: OnActionClickFn<SystemDeptApi.SystemDept>,
|
onActionClick?: OnActionClickFn<SystemDeptApi.SystemDept>,
|
||||||
|
options: PermissionOptions = {},
|
||||||
): VxeTableGridOptions<SystemDeptApi.SystemDept>['columns'] {
|
): VxeTableGridOptions<SystemDeptApi.SystemDept>['columns'] {
|
||||||
|
const canAccess = options.canAccess || (() => true);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
align: 'left',
|
align: 'left',
|
||||||
@ -113,14 +120,19 @@ export function useColumns(
|
|||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
code: 'append',
|
code: 'append',
|
||||||
|
show: () => canAccess('System:Dept:Create'),
|
||||||
text: '新增下级',
|
text: '新增下级',
|
||||||
},
|
},
|
||||||
'edit', // 默认的编辑按钮
|
{
|
||||||
|
code: 'edit',
|
||||||
|
show: () => canAccess('System:Dept:Edit'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
code: 'delete', // 默认的删除按钮
|
code: 'delete', // 默认的删除按钮
|
||||||
disabled: (row: SystemDeptApi.SystemDept) => {
|
disabled: (row: SystemDeptApi.SystemDept) => {
|
||||||
return !!(row.children && row.children.length > 0);
|
return !!(row.children && row.children.length > 0);
|
||||||
},
|
},
|
||||||
|
show: () => canAccess('System:Dept:Delete'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import type {
|
|||||||
} from '#/adapter/vxe-table';
|
} from '#/adapter/vxe-table';
|
||||||
import type { SystemDeptApi } from '#/api/system/dept';
|
import type { SystemDeptApi } from '#/api/system/dept';
|
||||||
|
|
||||||
|
import { useAccess } from '@vben/access';
|
||||||
import { Page, useVbenModal } from '@vben/common-ui';
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
import { Plus } from '@vben/icons';
|
import { Plus } from '@vben/icons';
|
||||||
|
|
||||||
@ -22,6 +23,12 @@ const [FormModal, formModalApi] = useVbenModal({
|
|||||||
destroyOnClose: true,
|
destroyOnClose: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { hasAccessByCodes } = useAccess();
|
||||||
|
|
||||||
|
function hasPermission(code: string) {
|
||||||
|
return hasAccessByCodes([code]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 编辑部门
|
* 编辑部门
|
||||||
* @param row
|
* @param row
|
||||||
@ -94,7 +101,7 @@ function onActionClick({
|
|||||||
const [Grid, gridApi] = useVbenVxeGrid({
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
gridEvents: {},
|
gridEvents: {},
|
||||||
gridOptions: {
|
gridOptions: {
|
||||||
columns: useColumns(onActionClick),
|
columns: useColumns(onActionClick, { canAccess: hasPermission }),
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
keepSource: true,
|
keepSource: true,
|
||||||
pagerConfig: {
|
pagerConfig: {
|
||||||
@ -133,7 +140,11 @@ function refreshGrid() {
|
|||||||
<FormModal @success="refreshGrid" />
|
<FormModal @success="refreshGrid" />
|
||||||
<Grid table-title="部门列表">
|
<Grid table-title="部门列表">
|
||||||
<template #toolbar-tools>
|
<template #toolbar-tools>
|
||||||
<Button type="primary" @click="onCreate">
|
<Button
|
||||||
|
v-if="hasPermission('System:Dept:Create')"
|
||||||
|
type="primary"
|
||||||
|
@click="onCreate"
|
||||||
|
>
|
||||||
<Plus class="size-5" />
|
<Plus class="size-5" />
|
||||||
{{ $t('ui.actionTitle.create', [$t('system.dept.name')]) }}
|
{{ $t('ui.actionTitle.create', [$t('system.dept.name')]) }}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -3,6 +3,10 @@ import type { SystemMenuApi } from '#/api/system/menu';
|
|||||||
|
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
type PermissionOptions = {
|
||||||
|
canAccess?: (code: string) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export function getMenuTypeOptions() {
|
export function getMenuTypeOptions() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -23,7 +27,10 @@ export function getMenuTypeOptions() {
|
|||||||
|
|
||||||
export function useColumns(
|
export function useColumns(
|
||||||
onActionClick: OnActionClickFn<SystemMenuApi.SystemMenu>,
|
onActionClick: OnActionClickFn<SystemMenuApi.SystemMenu>,
|
||||||
|
options: PermissionOptions = {},
|
||||||
): VxeTableGridOptions<SystemMenuApi.SystemMenu>['columns'] {
|
): VxeTableGridOptions<SystemMenuApi.SystemMenu>['columns'] {
|
||||||
|
const canAccess = options.canAccess || (() => true);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
align: 'left',
|
align: 'left',
|
||||||
@ -92,10 +99,17 @@ export function useColumns(
|
|||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
code: 'append',
|
code: 'append',
|
||||||
|
show: () => canAccess('System:Menu:Create'),
|
||||||
text: '新增下级',
|
text: '新增下级',
|
||||||
},
|
},
|
||||||
'edit', // 默认的编辑按钮
|
{
|
||||||
'delete', // 默认的删除按钮
|
code: 'edit',
|
||||||
|
show: () => canAccess('System:Menu:Edit'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'delete',
|
||||||
|
show: () => canAccess('System:Menu:Delete'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
field: 'operation',
|
field: 'operation',
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import type {
|
|||||||
VxeTableGridOptions,
|
VxeTableGridOptions,
|
||||||
} from '#/adapter/vxe-table';
|
} from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { useAccess } from '@vben/access';
|
||||||
import { Page, useVbenDrawer } from '@vben/common-ui';
|
import { Page, useVbenDrawer } from '@vben/common-ui';
|
||||||
import { IconifyIcon, Plus } from '@vben/icons';
|
import { IconifyIcon, Plus } from '@vben/icons';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
@ -23,9 +24,15 @@ const [FormDrawer, formDrawerApi] = useVbenDrawer({
|
|||||||
destroyOnClose: true,
|
destroyOnClose: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { hasAccessByCodes } = useAccess();
|
||||||
|
|
||||||
|
function hasPermission(code: string) {
|
||||||
|
return hasAccessByCodes([code]);
|
||||||
|
}
|
||||||
|
|
||||||
const [Grid, gridApi] = useVbenVxeGrid({
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
gridOptions: {
|
gridOptions: {
|
||||||
columns: useColumns(onActionClick),
|
columns: useColumns(onActionClick, { canAccess: hasPermission }),
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
keepSource: true,
|
keepSource: true,
|
||||||
pagerConfig: {
|
pagerConfig: {
|
||||||
@ -115,7 +122,11 @@ function onDelete(row: SystemMenuApi.SystemMenu) {
|
|||||||
<FormDrawer @success="onRefresh" />
|
<FormDrawer @success="onRefresh" />
|
||||||
<Grid>
|
<Grid>
|
||||||
<template #toolbar-tools>
|
<template #toolbar-tools>
|
||||||
<Button type="primary" @click="onCreate">
|
<Button
|
||||||
|
v-if="hasPermission('System:Menu:Create')"
|
||||||
|
type="primary"
|
||||||
|
@click="onCreate"
|
||||||
|
>
|
||||||
<Plus class="size-5" />
|
<Plus class="size-5" />
|
||||||
{{ $t('ui.actionTitle.create', [$t('system.menu.name')]) }}
|
{{ $t('ui.actionTitle.create', [$t('system.menu.name')]) }}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -4,6 +4,10 @@ import type { SystemRoleApi } from '#/api';
|
|||||||
|
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
type PermissionOptions = {
|
||||||
|
canAccess?: (code: string) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export function useFormSchema(): VbenFormSchema[] {
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -77,7 +81,10 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
|||||||
export function useColumns<T = SystemRoleApi.SystemRole>(
|
export function useColumns<T = SystemRoleApi.SystemRole>(
|
||||||
onActionClick: OnActionClickFn<T>,
|
onActionClick: OnActionClickFn<T>,
|
||||||
onStatusChange?: (newStatus: any, row: T) => PromiseLike<boolean | undefined>,
|
onStatusChange?: (newStatus: any, row: T) => PromiseLike<boolean | undefined>,
|
||||||
|
options: PermissionOptions = {},
|
||||||
): VxeTableGridOptions['columns'] {
|
): VxeTableGridOptions['columns'] {
|
||||||
|
const canAccess = options.canAccess || (() => true);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
@ -117,6 +124,16 @@ export function useColumns<T = SystemRoleApi.SystemRole>(
|
|||||||
onClick: onActionClick,
|
onClick: onActionClick,
|
||||||
},
|
},
|
||||||
name: 'CellOperation',
|
name: 'CellOperation',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
code: 'edit',
|
||||||
|
show: () => canAccess('System:Role:Edit'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'delete',
|
||||||
|
show: () => canAccess('System:Role:Delete'),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
field: 'operation',
|
field: 'operation',
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import type {
|
|||||||
} from '#/adapter/vxe-table';
|
} from '#/adapter/vxe-table';
|
||||||
import type { SystemRoleApi } from '#/api';
|
import type { SystemRoleApi } from '#/api';
|
||||||
|
|
||||||
|
import { useAccess } from '@vben/access';
|
||||||
import { Page, useVbenDrawer } from '@vben/common-ui';
|
import { Page, useVbenDrawer } from '@vben/common-ui';
|
||||||
import { Plus } from '@vben/icons';
|
import { Plus } from '@vben/icons';
|
||||||
|
|
||||||
@ -24,6 +25,12 @@ const [FormDrawer, formDrawerApi] = useVbenDrawer({
|
|||||||
destroyOnClose: true,
|
destroyOnClose: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { hasAccessByCodes } = useAccess();
|
||||||
|
|
||||||
|
function hasPermission(code: string) {
|
||||||
|
return hasAccessByCodes([code]);
|
||||||
|
}
|
||||||
|
|
||||||
const [Grid, gridApi] = useVbenVxeGrid({
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
formOptions: {
|
formOptions: {
|
||||||
fieldMappingTime: [['createTime', ['startTime', 'endTime']]],
|
fieldMappingTime: [['createTime', ['startTime', 'endTime']]],
|
||||||
@ -31,7 +38,11 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
submitOnChange: true,
|
submitOnChange: true,
|
||||||
},
|
},
|
||||||
gridOptions: {
|
gridOptions: {
|
||||||
columns: useColumns(onActionClick, onStatusChange),
|
columns: useColumns(
|
||||||
|
onActionClick,
|
||||||
|
hasPermission('System:Role:Edit') ? onStatusChange : undefined,
|
||||||
|
{ canAccess: hasPermission },
|
||||||
|
),
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
keepSource: true,
|
keepSource: true,
|
||||||
proxyConfig: {
|
proxyConfig: {
|
||||||
@ -154,7 +165,11 @@ function onCreate() {
|
|||||||
<FormDrawer @success="onRefresh" />
|
<FormDrawer @success="onRefresh" />
|
||||||
<Grid :table-title="$t('system.role.list')">
|
<Grid :table-title="$t('system.role.list')">
|
||||||
<template #toolbar-tools>
|
<template #toolbar-tools>
|
||||||
<Button type="primary" @click="onCreate">
|
<Button
|
||||||
|
v-if="hasPermission('System:Role:Create')"
|
||||||
|
type="primary"
|
||||||
|
@click="onCreate"
|
||||||
|
>
|
||||||
<Plus class="size-5" />
|
<Plus class="size-5" />
|
||||||
{{ $t('ui.actionTitle.create', [$t('system.role.name')]) }}
|
{{ $t('ui.actionTitle.create', [$t('system.role.name')]) }}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -78,13 +78,13 @@ function convertRoutes(
|
|||||||
// 页面组件转换
|
// 页面组件转换
|
||||||
} else if (component) {
|
} else if (component) {
|
||||||
const normalizePath = normalizeViewPath(component);
|
const normalizePath = normalizeViewPath(component);
|
||||||
const pageKey = normalizePath.endsWith('.vue')
|
const pageKeys = getPageKeys(normalizePath);
|
||||||
? normalizePath
|
const pageKey = pageKeys.find((key) => pageMap[key]);
|
||||||
: `${normalizePath}.vue`;
|
|
||||||
if (pageMap[pageKey]) {
|
if (pageKey) {
|
||||||
route.component = pageMap[pageKey];
|
route.component = pageMap[pageKey];
|
||||||
} else {
|
} else {
|
||||||
console.error(`route component is invalid: ${pageKey}`, route);
|
console.error(`route component is invalid: ${pageKeys[0]}`, route);
|
||||||
route.component = pageMap['/_core/fallback/not-found.vue'];
|
route.component = pageMap['/_core/fallback/not-found.vue'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -105,4 +105,11 @@ function normalizeViewPath(path: string): string {
|
|||||||
// 这里耦合了vben-admin的目录结构
|
// 这里耦合了vben-admin的目录结构
|
||||||
return viewPath.replace(/^\/views/, '');
|
return viewPath.replace(/^\/views/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPageKeys(path: string): string[] {
|
||||||
|
if (/\.(tsx|vue)$/.test(path)) return [path];
|
||||||
|
|
||||||
|
return [`${path}.tsx`, `${path}.vue`];
|
||||||
|
}
|
||||||
|
|
||||||
export { generateRoutesByBackend };
|
export { generateRoutesByBackend };
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user