feat: add article and production modules
This commit is contained in:
+94
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
|
||||
return new class {
|
||||
public function up(Builder $schema): void
|
||||
{
|
||||
if (!$schema->hasTable('article_category')) {
|
||||
$schema->create('article_category', function (Blueprint $table) {
|
||||
$table->bigInteger('id')->primary()->comment('主键');
|
||||
$table->string('name', 100)->comment('分类名称');
|
||||
$table->string('slug', 100)->nullable()->comment('分类标识');
|
||||
$table->bigInteger('parent_id')->default(0)->comment('父级分类ID');
|
||||
$table->integer('sort')->default(100)->comment('排序');
|
||||
$table->tinyInteger('enabled')->default(1)->comment('状态: 1启用 0禁用');
|
||||
$table->longText('description')->nullable()->comment('分类描述');
|
||||
$table->bigInteger('created_by')->nullable()->comment('创建者');
|
||||
$table->bigInteger('updated_by')->nullable()->comment('更新者');
|
||||
$table->bigInteger('created_at')->nullable()->comment('创建时间');
|
||||
$table->bigInteger('updated_at')->nullable()->comment('更新时间');
|
||||
$table->bigInteger('deleted_at')->nullable()->comment('删除时间');
|
||||
$table->index('parent_id');
|
||||
$table->index('slug');
|
||||
$table->index('enabled');
|
||||
});
|
||||
}
|
||||
|
||||
if (!$schema->hasTable('article_tag')) {
|
||||
$schema->create('article_tag', function (Blueprint $table) {
|
||||
$table->bigInteger('id')->primary()->comment('主键');
|
||||
$table->string('name', 100)->comment('标签名称');
|
||||
$table->string('slug', 100)->nullable()->comment('标签标识');
|
||||
$table->integer('sort')->default(100)->comment('排序');
|
||||
$table->integer('heat')->default(0)->comment('热度');
|
||||
$table->tinyInteger('enabled')->default(1)->comment('状态: 1启用 0禁用');
|
||||
$table->longText('description')->nullable()->comment('标签描述');
|
||||
$table->bigInteger('created_by')->nullable()->comment('创建者');
|
||||
$table->bigInteger('updated_by')->nullable()->comment('更新者');
|
||||
$table->bigInteger('created_at')->nullable()->comment('创建时间');
|
||||
$table->bigInteger('updated_at')->nullable()->comment('更新时间');
|
||||
$table->bigInteger('deleted_at')->nullable()->comment('删除时间');
|
||||
$table->index('slug');
|
||||
$table->index('enabled');
|
||||
$table->index('heat');
|
||||
});
|
||||
}
|
||||
|
||||
if (!$schema->hasTable('article_content')) {
|
||||
$schema->create('article_content', function (Blueprint $table) {
|
||||
$table->bigInteger('id')->primary()->comment('主键');
|
||||
$table->string('title', 200)->comment('文章标题');
|
||||
$table->string('slug', 200)->nullable()->comment('文章标识');
|
||||
$table->bigInteger('category_id')->default(0)->comment('分类ID');
|
||||
$table->string('cover', 255)->nullable()->comment('封面图');
|
||||
$table->string('summary', 500)->nullable()->comment('摘要');
|
||||
$table->longText('content')->nullable()->comment('文章内容');
|
||||
$table->tinyInteger('status')->default(1)->comment('状态: 0草稿 1发布 2下架');
|
||||
$table->tinyInteger('is_top')->default(0)->comment('是否置顶');
|
||||
$table->integer('sort')->default(100)->comment('排序');
|
||||
$table->integer('view_count')->default(0)->comment('浏览量');
|
||||
$table->bigInteger('published_at')->nullable()->comment('发布时间');
|
||||
$table->bigInteger('created_by')->nullable()->comment('创建者');
|
||||
$table->bigInteger('updated_by')->nullable()->comment('更新者');
|
||||
$table->bigInteger('created_at')->nullable()->comment('创建时间');
|
||||
$table->bigInteger('updated_at')->nullable()->comment('更新时间');
|
||||
$table->bigInteger('deleted_at')->nullable()->comment('删除时间');
|
||||
$table->index('category_id');
|
||||
$table->index('status');
|
||||
$table->index('is_top');
|
||||
$table->index('published_at');
|
||||
});
|
||||
}
|
||||
|
||||
if (!$schema->hasTable('article_content_tag')) {
|
||||
$schema->create('article_content_tag', function (Blueprint $table) {
|
||||
$table->bigInteger('id')->primary()->comment('主键');
|
||||
$table->bigInteger('content_id')->comment('文章ID');
|
||||
$table->bigInteger('tag_id')->comment('标签ID');
|
||||
$table->bigInteger('created_at')->nullable()->comment('创建时间');
|
||||
$table->index('content_id');
|
||||
$table->index('tag_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(Builder $schema): void
|
||||
{
|
||||
$schema->dropIfExists('article_content_tag');
|
||||
$schema->dropIfExists('article_content');
|
||||
$schema->dropIfExists('article_tag');
|
||||
$schema->dropIfExists('article_category');
|
||||
}
|
||||
};
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
|
||||
return new class {
|
||||
public function up(Builder $schema): void
|
||||
{
|
||||
if (!$schema->hasTable('article_content')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$schema->table('article_content', function (Blueprint $table) use ($schema) {
|
||||
if (!$schema->hasColumn('article_content', 'content_type')) {
|
||||
$table->string('content_type', 50)->default('article')->comment('内容类型')->after('title');
|
||||
}
|
||||
if (!$schema->hasColumn('article_content', 'channel')) {
|
||||
$table->string('channel', 100)->default('official')->comment('所属频道')->after('category_id');
|
||||
}
|
||||
if (!$schema->hasColumn('article_content', 'visibility')) {
|
||||
$table->tinyInteger('visibility')->default(1)->comment('可见范围: 1公开 2登录可见 3私密')->after('channel');
|
||||
}
|
||||
if (!$schema->hasColumn('article_content', 'author')) {
|
||||
$table->string('author', 100)->nullable()->comment('作者')->after('visibility');
|
||||
}
|
||||
if (!$schema->hasColumn('article_content', 'source')) {
|
||||
$table->string('source', 100)->default('original')->comment('内容来源')->after('author');
|
||||
}
|
||||
if (!$schema->hasColumn('article_content', 'offline_at')) {
|
||||
$table->bigInteger('offline_at')->nullable()->comment('下线时间')->after('published_at');
|
||||
}
|
||||
if (!$schema->hasColumn('article_content', 'seo_keywords')) {
|
||||
$table->string('seo_keywords', 255)->nullable()->comment('SEO关键词')->after('source');
|
||||
}
|
||||
if (!$schema->hasColumn('article_content', 'seo_title')) {
|
||||
$table->string('seo_title', 200)->nullable()->comment('SEO标题')->after('seo_keywords');
|
||||
}
|
||||
if (!$schema->hasColumn('article_content', 'seo_description')) {
|
||||
$table->string('seo_description', 500)->nullable()->comment('SEO描述')->after('seo_title');
|
||||
}
|
||||
if (!$schema->hasColumn('article_content', 'like_count')) {
|
||||
$table->integer('like_count')->default(0)->comment('点赞量')->after('view_count');
|
||||
}
|
||||
if (!$schema->hasColumn('article_content', 'comment_count')) {
|
||||
$table->integer('comment_count')->default(0)->comment('评论量')->after('like_count');
|
||||
}
|
||||
if (!$schema->hasColumn('article_content', 'favorite_count')) {
|
||||
$table->integer('favorite_count')->default(0)->comment('收藏量')->after('comment_count');
|
||||
}
|
||||
if (!$schema->hasColumn('article_content', 'is_recommend')) {
|
||||
$table->tinyInteger('is_recommend')->default(0)->comment('是否推荐')->after('is_top');
|
||||
}
|
||||
if (!$schema->hasColumn('article_content', 'allow_comment')) {
|
||||
$table->tinyInteger('allow_comment')->default(1)->comment('是否允许评论')->after('is_recommend');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(Builder $schema): void
|
||||
{
|
||||
if (!$schema->hasTable('article_content')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$dropColumns = [];
|
||||
foreach ([
|
||||
'content_type',
|
||||
'channel',
|
||||
'visibility',
|
||||
'author',
|
||||
'source',
|
||||
'offline_at',
|
||||
'seo_keywords',
|
||||
'seo_title',
|
||||
'seo_description',
|
||||
'like_count',
|
||||
'comment_count',
|
||||
'favorite_count',
|
||||
'is_recommend',
|
||||
'allow_comment',
|
||||
] as $column) {
|
||||
if ($schema->hasColumn('article_content', $column)) {
|
||||
$dropColumns[] = $column;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($dropColumns)) {
|
||||
$schema->table('article_content', function (Blueprint $table) use ($dropColumns) {
|
||||
$table->dropColumn($dropColumns);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 后台菜单配置
|
||||
* 安装时会自动加载
|
||||
*/
|
||||
|
||||
return [
|
||||
[
|
||||
'name' => '文章管理',
|
||||
'code' => 'article',
|
||||
'app' => 'article',
|
||||
'path' => '/article',
|
||||
'icon' => 'plugin',
|
||||
'children' => [
|
||||
[
|
||||
'name' => '内容中心',
|
||||
'code' => 'article:index',
|
||||
'app' => 'article',
|
||||
'path' => '/article/index',
|
||||
'component' => '/article/content/index',
|
||||
],
|
||||
[
|
||||
'name' => '内容分类',
|
||||
'code' => 'article:category:index',
|
||||
'app' => 'article',
|
||||
'path' => '/article/category',
|
||||
'component' => '/article/category/index',
|
||||
],
|
||||
[
|
||||
'name' => '标签管理',
|
||||
'code' => 'article:tag:index',
|
||||
'app' => 'article',
|
||||
'path' => '/article/tag',
|
||||
'component' => '/article/tag/index',
|
||||
],
|
||||
[
|
||||
'name' => '内容回收站',
|
||||
'code' => 'article:recycle:index',
|
||||
'app' => 'article',
|
||||
'path' => '/article/recycle',
|
||||
'component' => '/article/recycle/index',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 前端菜单配置
|
||||
*/
|
||||
|
||||
return [
|
||||
// 前端菜单配置
|
||||
];
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Web 菜单配置
|
||||
*/
|
||||
|
||||
return [
|
||||
// Web 菜单配置
|
||||
];
|
||||
@@ -0,0 +1,118 @@
|
||||
import BaseService from '@/api/core/base'
|
||||
import request from '@/utils/http'
|
||||
|
||||
export interface ArticleCategoryRow {
|
||||
id?: string | number
|
||||
name: string
|
||||
slug?: string
|
||||
parent_id?: string | number
|
||||
article_count?: number
|
||||
sort?: number
|
||||
enabled?: number
|
||||
description?: string
|
||||
created_date?: string
|
||||
updated_date?: string
|
||||
}
|
||||
|
||||
export interface ArticleTagRow {
|
||||
id?: string | number
|
||||
name: string
|
||||
slug?: string
|
||||
article_count?: number
|
||||
sort?: number
|
||||
heat?: number
|
||||
enabled?: number
|
||||
description?: string
|
||||
created_date?: string
|
||||
updated_date?: string
|
||||
}
|
||||
|
||||
export interface ArticleContentRow {
|
||||
id?: string | number
|
||||
title: string
|
||||
content_type?: string
|
||||
slug?: string
|
||||
category_id?: string | number
|
||||
category_name?: string
|
||||
channel?: string
|
||||
visibility?: number
|
||||
author?: string
|
||||
source?: string
|
||||
cover?: string
|
||||
seo_keywords?: string
|
||||
seo_title?: string
|
||||
seo_description?: string
|
||||
summary?: string
|
||||
content?: string
|
||||
status?: number
|
||||
is_top?: number
|
||||
is_recommend?: number
|
||||
allow_comment?: number
|
||||
sort?: number
|
||||
view_count?: number
|
||||
like_count?: number
|
||||
comment_count?: number
|
||||
favorite_count?: number
|
||||
published_at?: number | string
|
||||
offline_at?: number | string
|
||||
tag_ids?: Array<string | number>
|
||||
tag_names?: string
|
||||
created_date?: string
|
||||
updated_date?: string
|
||||
}
|
||||
|
||||
export const ArticleContentService = {
|
||||
...BaseService<ArticleContentRow>({ baseUrl: '/article/index' })
|
||||
}
|
||||
|
||||
export const ArticleCategoryService = {
|
||||
...BaseService<ArticleCategoryRow>({ baseUrl: '/article/category' })
|
||||
}
|
||||
|
||||
export const ArticleTagService = {
|
||||
...BaseService<ArticleTagRow>({ baseUrl: '/article/tag' })
|
||||
}
|
||||
|
||||
const pluginApi = {
|
||||
async test(params = {}) {
|
||||
return request.get<{ msg?: string; data?: unknown }>({
|
||||
url: '/article/index',
|
||||
params
|
||||
})
|
||||
},
|
||||
|
||||
contentList(params = {}) {
|
||||
return ArticleContentService.list(params as Record<string, any>)
|
||||
},
|
||||
|
||||
categoryList(params = {}) {
|
||||
return ArticleCategoryService.list(params as Record<string, any>)
|
||||
},
|
||||
|
||||
tagList(params = {}) {
|
||||
return ArticleTagService.list(params as Record<string, any>)
|
||||
},
|
||||
|
||||
async recycleList(params = {}) {
|
||||
return request.get<{ code: number; msg: string; data: Record<string, unknown>[] }>({
|
||||
url: '/article/recycle',
|
||||
params
|
||||
})
|
||||
},
|
||||
|
||||
async recycleRestore(id: string | number) {
|
||||
return request.put({
|
||||
url: `/article/recycle/${id}/restore`,
|
||||
params: { id }
|
||||
})
|
||||
},
|
||||
|
||||
async recycleDelete(id: string | number) {
|
||||
return request.del({
|
||||
url: `/article/recycle/${id}`,
|
||||
params: { id }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default pluginApi
|
||||
@@ -0,0 +1,136 @@
|
||||
{
|
||||
"article": {
|
||||
"title": "Article Management",
|
||||
"description": "Article content operation and management plugin",
|
||||
"card_title": "Article Plugin Data",
|
||||
"refresh_btn": "Refresh",
|
||||
"common": {
|
||||
"list_mode": "List Mode",
|
||||
"columns": {
|
||||
"index": "No."
|
||||
}
|
||||
},
|
||||
"modules": {
|
||||
"content": {
|
||||
"title": "Content Center",
|
||||
"description": "Manage articles, publish states, and content archives.",
|
||||
"search": {
|
||||
"keywords": "Keywords",
|
||||
"keywords_placeholder": "Please enter article title, category, or tags"
|
||||
},
|
||||
"form": {
|
||||
"title": "Content Title",
|
||||
"title_placeholder": "Please enter article title",
|
||||
"slug": "Content Slug",
|
||||
"slug_placeholder": "Please enter article slug",
|
||||
"content_type": "Content Type",
|
||||
"category_id": "Category",
|
||||
"category_id_placeholder": "Please select category",
|
||||
"channel": "Channel",
|
||||
"visibility": "Visibility",
|
||||
"author": "Author",
|
||||
"author_placeholder": "Please enter author",
|
||||
"source": "Source",
|
||||
"cover": "Cover",
|
||||
"cover_placeholder": "Please enter cover URL",
|
||||
"summary": "Summary",
|
||||
"summary_placeholder": "Please enter summary",
|
||||
"content": "Content",
|
||||
"content_placeholder": "Please enter content",
|
||||
"status": "Status",
|
||||
"is_top": "Pinned",
|
||||
"is_recommend": "Recommend",
|
||||
"allow_comment": "Comment Setting",
|
||||
"sort": "Sort Order",
|
||||
"sort_placeholder": "Please enter sort order",
|
||||
"view_count": "View Count",
|
||||
"like_count": "Like Count",
|
||||
"comment_count": "Comment Count",
|
||||
"favorite_count": "Favorite Count",
|
||||
"published_at": "Published At",
|
||||
"offline_at": "Offline At",
|
||||
"tag_ids": "Content Tags",
|
||||
"tag_ids_placeholder": "Please select content tags",
|
||||
"seo_keywords": "SEO Keywords",
|
||||
"seo_keywords_placeholder": "For search engine optimization",
|
||||
"seo_title": "SEO Title",
|
||||
"seo_title_placeholder": "Optional, defaults to content title",
|
||||
"seo_description": "SEO Description",
|
||||
"seo_description_placeholder": "Optional, highlight value and search keywords"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Article Title",
|
||||
"content_info": "Content Info",
|
||||
"category": "Category",
|
||||
"tags": "Tags",
|
||||
"status": "Status",
|
||||
"business_data": "Business Data",
|
||||
"time_info": "Time",
|
||||
"is_top": "Pinned",
|
||||
"updated_at": "Last Updated"
|
||||
}
|
||||
},
|
||||
"category": {
|
||||
"title": "Content Category",
|
||||
"description": "Manage article category structure and ordering.",
|
||||
"search": {
|
||||
"keywords": "Keywords",
|
||||
"keywords_placeholder": "Please enter category name or slug"
|
||||
},
|
||||
"form": {
|
||||
"name_placeholder": "Please enter category name",
|
||||
"slug_placeholder": "Please enter category slug",
|
||||
"parent_id": "Parent Category",
|
||||
"parent_top": "Top Category",
|
||||
"description": "Category Description",
|
||||
"description_placeholder": "Please enter category description"
|
||||
},
|
||||
"columns": {
|
||||
"name": "Category Name",
|
||||
"slug": "Category Slug",
|
||||
"article_count": "Content Count",
|
||||
"enabled": "Status",
|
||||
"sort": "Sort",
|
||||
"updated_at": "Last Updated"
|
||||
}
|
||||
},
|
||||
"tag": {
|
||||
"title": "Tag Management",
|
||||
"description": "Maintain article tags, popularity, and related article counts.",
|
||||
"search": {
|
||||
"keywords": "Keywords",
|
||||
"keywords_placeholder": "Please enter tag name or slug"
|
||||
},
|
||||
"form": {
|
||||
"name_placeholder": "Please enter tag name",
|
||||
"slug_placeholder": "Please enter tag slug",
|
||||
"description": "Tag Description",
|
||||
"description_placeholder": "Please enter tag description"
|
||||
},
|
||||
"columns": {
|
||||
"name": "Tag Name",
|
||||
"slug": "Tag Slug",
|
||||
"article_count": "Usage Count",
|
||||
"enabled": "Status",
|
||||
"heat": "Heat",
|
||||
"updated_at": "Last Updated"
|
||||
}
|
||||
},
|
||||
"recycle": {
|
||||
"title": "Content Recycle Bin",
|
||||
"description": "Manage deleted articles for later recovery or cleanup.",
|
||||
"search": {
|
||||
"keywords": "Keywords",
|
||||
"keywords_placeholder": "Please enter article title or deleter"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Article Title",
|
||||
"category": "Original Category",
|
||||
"deleted_by": "Deleted By",
|
||||
"deleted_at": "Deleted At",
|
||||
"expire_at": "Expire At"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"article": {
|
||||
"title": "Article Management",
|
||||
"description": "Article plugin demo page",
|
||||
"card_title": "Article Plugin Data",
|
||||
"refresh_btn": "Refresh"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"article": {
|
||||
"title": "文章管理",
|
||||
"description": "文章插件示例页面",
|
||||
"card_title": "文章插件数据",
|
||||
"refresh_btn": "刷新"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
{
|
||||
"article": {
|
||||
"title": "文章管理",
|
||||
"description": "文章内容运营与管理插件",
|
||||
"card_title": "文章插件数据",
|
||||
"refresh_btn": "刷新",
|
||||
"common": {
|
||||
"list_mode": "列表模式",
|
||||
"columns": {
|
||||
"index": "序号"
|
||||
}
|
||||
},
|
||||
"modules": {
|
||||
"content": {
|
||||
"title": "内容中心",
|
||||
"description": "管理文章内容、发布状态与内容归档。",
|
||||
"search": {
|
||||
"keywords": "关键字",
|
||||
"keywords_placeholder": "请输入文章标题、分类或标签"
|
||||
},
|
||||
"form": {
|
||||
"title": "内容标题",
|
||||
"title_placeholder": "请输入文章标题",
|
||||
"slug": "内容别名",
|
||||
"slug_placeholder": "请输入文章标识",
|
||||
"content_type": "内容类型",
|
||||
"category_id": "内容分类",
|
||||
"category_id_placeholder": "请选择内容分类",
|
||||
"channel": "所属频道",
|
||||
"visibility": "可见范围",
|
||||
"author": "作者",
|
||||
"author_placeholder": "请输入作者",
|
||||
"source": "内容来源",
|
||||
"cover": "封面图",
|
||||
"cover_placeholder": "请输入封面图地址",
|
||||
"summary": "摘要",
|
||||
"summary_placeholder": "请输入摘要",
|
||||
"content": "文章内容",
|
||||
"content_placeholder": "请输入文章内容",
|
||||
"status": "状态",
|
||||
"is_top": "是否置顶",
|
||||
"is_recommend": "推荐",
|
||||
"allow_comment": "评论设置",
|
||||
"sort": "内容排序",
|
||||
"sort_placeholder": "请输入排序",
|
||||
"view_count": "浏览量",
|
||||
"like_count": "点赞量",
|
||||
"comment_count": "评论量",
|
||||
"favorite_count": "收藏量",
|
||||
"published_at": "发布时间",
|
||||
"offline_at": "下线时间",
|
||||
"tag_ids": "内容标签",
|
||||
"tag_ids_placeholder": "请选择内容标签",
|
||||
"seo_keywords": "SEO 关键词",
|
||||
"seo_keywords_placeholder": "用于搜索引擎优化",
|
||||
"seo_title": "SEO 标题",
|
||||
"seo_title_placeholder": "可选,默认继承内容标题",
|
||||
"seo_description": "SEO 描述",
|
||||
"seo_description_placeholder": "可选,建议突出商业价值与检索关键词"
|
||||
},
|
||||
"columns": {
|
||||
"title": "文章标题",
|
||||
"content_info": "内容信息",
|
||||
"category": "内容分类",
|
||||
"tags": "标签",
|
||||
"status": "状态",
|
||||
"business_data": "经营数据",
|
||||
"time_info": "时间",
|
||||
"is_top": "置顶",
|
||||
"updated_at": "最后更新"
|
||||
}
|
||||
},
|
||||
"category": {
|
||||
"title": "内容分类",
|
||||
"description": "管理文章分类结构与分类排序。",
|
||||
"search": {
|
||||
"keywords": "关键字",
|
||||
"keywords_placeholder": "请输入分类名称或分类标识"
|
||||
},
|
||||
"form": {
|
||||
"name_placeholder": "请输入分类名称",
|
||||
"slug_placeholder": "请输入分类标识",
|
||||
"parent_id": "父级分类",
|
||||
"parent_top": "顶级分类",
|
||||
"description": "分类描述",
|
||||
"description_placeholder": "请输入分类描述"
|
||||
},
|
||||
"columns": {
|
||||
"name": "分类名称",
|
||||
"slug": "分类标识",
|
||||
"article_count": "内容数",
|
||||
"enabled": "状态",
|
||||
"sort": "排序",
|
||||
"updated_at": "最后更新"
|
||||
}
|
||||
},
|
||||
"tag": {
|
||||
"title": "标签管理",
|
||||
"description": "维护文章标签、热度与关联内容数量。",
|
||||
"search": {
|
||||
"keywords": "关键字",
|
||||
"keywords_placeholder": "请输入标签名称或标签标识"
|
||||
},
|
||||
"form": {
|
||||
"name_placeholder": "请输入标签名称",
|
||||
"slug_placeholder": "请输入标签标识",
|
||||
"description": "标签描述",
|
||||
"description_placeholder": "请输入标签描述"
|
||||
},
|
||||
"columns": {
|
||||
"name": "标签名称",
|
||||
"slug": "标签标识",
|
||||
"article_count": "使用次数",
|
||||
"enabled": "状态",
|
||||
"heat": "热度",
|
||||
"updated_at": "最后更新"
|
||||
}
|
||||
},
|
||||
"recycle": {
|
||||
"title": "内容回收站",
|
||||
"description": "管理已删除文章,支持后续恢复或清理。",
|
||||
"search": {
|
||||
"keywords": "关键字",
|
||||
"keywords_placeholder": "请输入文章标题或删除人"
|
||||
},
|
||||
"columns": {
|
||||
"title": "文章标题",
|
||||
"category": "原分类",
|
||||
"deleted_by": "删除人",
|
||||
"deleted_at": "删除时间",
|
||||
"expire_at": "过期清理时间"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { AppRouteRecord } from '@/types/router'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
|
||||
export default {
|
||||
path: '/article',
|
||||
name: 'Article',
|
||||
component: RoutesAlias.Layout,
|
||||
meta: {
|
||||
title: '文章管理',
|
||||
icon: 'ri:plugin-line',
|
||||
module: 'article'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
name: 'ArticleIndex',
|
||||
component: '/article/content/index',
|
||||
meta: {
|
||||
title: '内容中心',
|
||||
icon: 'ri:article-line',
|
||||
keepAlive: false,
|
||||
module: 'article'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'category',
|
||||
name: 'ArticleCategory',
|
||||
component: '/article/category/index',
|
||||
meta: {
|
||||
title: '内容分类',
|
||||
icon: 'ri:folder-2-line',
|
||||
keepAlive: false,
|
||||
module: 'article'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'tag',
|
||||
name: 'ArticleTag',
|
||||
component: '/article/tag/index',
|
||||
meta: {
|
||||
title: '标签管理',
|
||||
icon: 'ri:price-tag-3-line',
|
||||
keepAlive: false,
|
||||
module: 'article'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'recycle',
|
||||
name: 'ArticleRecycle',
|
||||
component: '/article/recycle/index',
|
||||
meta: {
|
||||
title: '内容回收站',
|
||||
icon: 'ri:delete-bin-6-line',
|
||||
keepAlive: false,
|
||||
module: 'article'
|
||||
}
|
||||
}
|
||||
]
|
||||
} as AppRouteRecord
|
||||
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div class="article-category-page art-full-height">
|
||||
<BasicCrud />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { useCrud } from '@/components/crud'
|
||||
import { crudSchema } from './schemas'
|
||||
|
||||
defineOptions({ name: 'ArticleCategoryList' })
|
||||
|
||||
const [BasicCrud] = useCrud({
|
||||
...crudSchema(),
|
||||
toolbarActions: [],
|
||||
dropDownToolbarActions: [],
|
||||
tableActions: [],
|
||||
dropDownActions: [],
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.article-category-page {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
import { CrudSchema } from '@/components/crud'
|
||||
import { DictEnum } from '@/enums'
|
||||
import { $t } from '@/locales'
|
||||
import { ArticleCategoryService } from '../../../../api/index'
|
||||
|
||||
export const crudSchema = (): CrudSchema => {
|
||||
return {
|
||||
api: ArticleCategoryService.list,
|
||||
dialogTitle: $t('article.modules.category.title'),
|
||||
crudApi: {
|
||||
add: ArticleCategoryService.create,
|
||||
edit: ArticleCategoryService.update,
|
||||
remove: ArticleCategoryService.delete,
|
||||
view: ArticleCategoryService.get,
|
||||
},
|
||||
useCrud: true,
|
||||
hasAdd: true,
|
||||
hasEdit: true,
|
||||
hasRemove: true,
|
||||
hasView: true,
|
||||
addAuth: 'article:category:create',
|
||||
editAuth: 'article:category:update',
|
||||
removeAuth: 'article:category:delete',
|
||||
viewAuth: 'article:category:read',
|
||||
rowKey: 'id',
|
||||
searchFormSchema: {
|
||||
labelWidth: '100px',
|
||||
schema: [
|
||||
{
|
||||
prop: 'LIKE_name',
|
||||
label: $t('article.modules.category.search.keywords'),
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
placeholder: $t('article.modules.category.search.keywords_placeholder'),
|
||||
},
|
||||
colSpan: 6,
|
||||
},
|
||||
],
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
type: 'selection',
|
||||
},
|
||||
{
|
||||
type: 'index',
|
||||
label: $t('article.common.columns.index'),
|
||||
width: 70,
|
||||
},
|
||||
{
|
||||
prop: 'name',
|
||||
label: $t('article.modules.category.columns.name'),
|
||||
minWidth: 220,
|
||||
},
|
||||
{
|
||||
prop: 'slug',
|
||||
label: $t('article.modules.category.columns.slug'),
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
prop: 'article_count',
|
||||
label: $t('article.modules.category.columns.article_count'),
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
prop: 'enabled',
|
||||
label: $t('article.modules.category.columns.enabled'),
|
||||
width: 120,
|
||||
component: 'ApiDict',
|
||||
componentProps: {
|
||||
code: DictEnum.SYS_ENABLED_STATUS,
|
||||
},
|
||||
},
|
||||
{
|
||||
prop: 'sort',
|
||||
label: $t('article.modules.category.columns.sort'),
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
prop: 'updated_date',
|
||||
label: $t('article.modules.category.columns.updated_at'),
|
||||
minWidth: 180,
|
||||
},
|
||||
],
|
||||
dialogProps: {
|
||||
width: '600px',
|
||||
},
|
||||
formSchema: {
|
||||
showDefaultActions: false,
|
||||
labelWidth: '100px',
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
show: false,
|
||||
ifDetail: true,
|
||||
colSpan: 24,
|
||||
prop: 'id',
|
||||
label: 'ID',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
prop: 'name',
|
||||
label: $t('article.modules.category.columns.name'),
|
||||
componentProps: {
|
||||
placeholder: $t('article.modules.category.form.name_placeholder'),
|
||||
maxlength: 100,
|
||||
},
|
||||
rules: [{ required: true, message: $t('article.modules.category.form.name_placeholder') }],
|
||||
colSpan: 12,
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
prop: 'slug',
|
||||
label: $t('article.modules.category.columns.slug'),
|
||||
componentProps: {
|
||||
placeholder: $t('article.modules.category.form.slug_placeholder'),
|
||||
maxlength: 100,
|
||||
},
|
||||
colSpan: 12,
|
||||
},
|
||||
{
|
||||
component: 'ApiTreeSelect',
|
||||
prop: 'parent_id',
|
||||
label: $t('article.modules.category.form.parent_id'),
|
||||
defaultValue: 0,
|
||||
componentProps: {
|
||||
api: '/article/category',
|
||||
requestMethod: 'get',
|
||||
params: {
|
||||
format: 'tree',
|
||||
},
|
||||
afterFetch: async (res: any) => {
|
||||
if (!Array.isArray(res)) {
|
||||
return res
|
||||
}
|
||||
return [
|
||||
{
|
||||
id: 0,
|
||||
name: $t('article.modules.category.form.parent_top'),
|
||||
},
|
||||
...res,
|
||||
]
|
||||
},
|
||||
clearable: true,
|
||||
checkStrictly: true,
|
||||
class: 'w-full',
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
},
|
||||
colSpan: 12,
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
prop: 'sort',
|
||||
label: $t('article.modules.category.columns.sort'),
|
||||
defaultValue: 100,
|
||||
componentProps: {
|
||||
min: 0,
|
||||
class: 'w-full',
|
||||
},
|
||||
colSpan: 12,
|
||||
},
|
||||
{
|
||||
component: 'ApiDict',
|
||||
prop: 'enabled',
|
||||
label: $t('article.modules.category.columns.enabled'),
|
||||
defaultValue: 1,
|
||||
componentProps: {
|
||||
code: DictEnum.SYS_ENABLED_STATUS,
|
||||
isBtn: true,
|
||||
renderType: 'RadioGroup',
|
||||
},
|
||||
colSpan: 24,
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
prop: 'description',
|
||||
label: $t('article.modules.category.form.description'),
|
||||
componentProps: {
|
||||
type: 'textarea',
|
||||
rows: 4,
|
||||
placeholder: $t('article.modules.category.form.description_placeholder'),
|
||||
},
|
||||
colSpan: 24,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div class="article-content-page art-full-height">
|
||||
<BasicCrud />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { useCrud } from '@/components/crud'
|
||||
import { crudSchema } from './schemas'
|
||||
|
||||
defineOptions({ name: 'ArticleContentList' })
|
||||
|
||||
const [BasicCrud] = useCrud({
|
||||
...crudSchema(),
|
||||
toolbarActions: [],
|
||||
dropDownToolbarActions: [],
|
||||
tableActions: [],
|
||||
dropDownActions: [],
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.article-content-page {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
+544
@@ -0,0 +1,544 @@
|
||||
import { CrudSchema } from '@/components/crud'
|
||||
import { DictEnum } from '@/enums'
|
||||
import { $t } from '@/locales'
|
||||
import { ArticleContentService } from '../../../../api/index'
|
||||
import { h } from 'vue'
|
||||
|
||||
const contentTypeOptions = [
|
||||
{ label: '文章', value: 'article' },
|
||||
{ label: '图文', value: 'news' },
|
||||
{ label: '公告', value: 'notice' },
|
||||
]
|
||||
|
||||
const channelOptions = [
|
||||
{ label: '官网内容', value: 'official' },
|
||||
{ label: '专题内容', value: 'topic' },
|
||||
{ label: '活动内容', value: 'activity' },
|
||||
]
|
||||
|
||||
const visibilityOptions = [
|
||||
{ label: '公开', value: 1 },
|
||||
{ label: '登录可见', value: 2 },
|
||||
{ label: '私密', value: 3 },
|
||||
]
|
||||
|
||||
const sourceOptions = [
|
||||
{ label: '原创', value: 'original' },
|
||||
{ label: '转载', value: 'repost' },
|
||||
{ label: '投稿', value: 'contribution' },
|
||||
]
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '草稿', value: 0 },
|
||||
{ label: '发布', value: 1 },
|
||||
{ label: '下线', value: 2 },
|
||||
]
|
||||
|
||||
const getOptionLabel = (options: Array<{ label: string; value: string | number }>, value: string | number) => {
|
||||
return options.find((item) => item.value === value)?.label || '-'
|
||||
}
|
||||
|
||||
const getStatusTagClass = (status: number) => {
|
||||
if (status === 1) {
|
||||
return 'inline-flex rounded-full bg-[#ecfdf5] px-2 py-[2px] text-xs text-[#10b981]'
|
||||
}
|
||||
if (status === 2) {
|
||||
return 'inline-flex rounded-full bg-[#fef2f2] px-2 py-[2px] text-xs text-[#ef4444]'
|
||||
}
|
||||
return 'inline-flex rounded-full bg-[#f4f4f5] px-2 py-[2px] text-xs text-[#909399]'
|
||||
}
|
||||
|
||||
export const crudSchema = (): CrudSchema => {
|
||||
return {
|
||||
api: ArticleContentService.list,
|
||||
dialogTitle: $t('article.modules.content.title'),
|
||||
exportFileName: '内容中心',
|
||||
exportFields: [
|
||||
{ field: 'title', title: '标题' },
|
||||
{ field: 'content_type', title: '内容类型' },
|
||||
{ field: 'category_name', title: '分类' },
|
||||
{ field: 'tag_names', title: '标签' },
|
||||
{ field: 'author', title: '作者' },
|
||||
{ field: 'channel', title: '频道' },
|
||||
{ field: 'visibility', title: '可见范围' },
|
||||
{ field: 'source', title: '来源' },
|
||||
{ field: 'status', title: '状态' },
|
||||
{ field: 'is_top', title: '置顶' },
|
||||
{ field: 'is_recommend', title: '推荐' },
|
||||
{ field: 'view_count', title: '浏览数' },
|
||||
{ field: 'like_count', title: '点赞数' },
|
||||
{ field: 'comment_count', title: '评论数' },
|
||||
{ field: 'favorite_count', title: '收藏数' },
|
||||
{ field: 'published_at', title: '发布时间' },
|
||||
{ field: 'updated_date', title: '更新时间' },
|
||||
{ field: 'summary', title: '摘要' }
|
||||
],
|
||||
crudApi: {
|
||||
add: ArticleContentService.create,
|
||||
edit: ArticleContentService.update,
|
||||
remove: ArticleContentService.remove,
|
||||
view: ArticleContentService.get,
|
||||
},
|
||||
useCrud: true,
|
||||
hasAdd: true,
|
||||
hasEdit: true,
|
||||
hasRemove: true,
|
||||
hasView: true,
|
||||
addAuth: 'article:create',
|
||||
editAuth: 'article:update',
|
||||
removeAuth: 'article:delete',
|
||||
viewAuth: 'article:read',
|
||||
rowKey: 'id',
|
||||
searchFormSchema: {
|
||||
labelWidth: '100px',
|
||||
schema: [
|
||||
{
|
||||
prop: 'LIKE_title',
|
||||
label: $t('article.modules.content.search.keywords'),
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
placeholder: $t('article.modules.content.search.keywords_placeholder'),
|
||||
},
|
||||
colSpan: 6,
|
||||
},
|
||||
],
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
type: 'index',
|
||||
label: $t('article.common.columns.index'),
|
||||
width: 70,
|
||||
},
|
||||
{
|
||||
prop: 'content_info',
|
||||
label: $t('article.modules.content.columns.content_info'),
|
||||
minWidth: 380,
|
||||
formatter: (row: any) => {
|
||||
const metaItems = [
|
||||
getOptionLabel(contentTypeOptions, row.content_type),
|
||||
row.author || '管理员',
|
||||
row.category_name,
|
||||
row.tag_names,
|
||||
].filter(Boolean)
|
||||
|
||||
const badgeNodes = [
|
||||
Number(row.is_top) === 1
|
||||
? h('span', { class: 'inline-flex items-center rounded bg-[#fff7e6] px-1.5 py-[1px] text-[10px] text-[#fa8c16]' }, '置顶')
|
||||
: null,
|
||||
Number(row.is_recommend) === 1
|
||||
? h('span', { class: 'inline-flex items-center rounded bg-[#ecfdf5] px-1.5 py-[1px] text-[10px] text-[#10b981]' }, '推荐')
|
||||
: null,
|
||||
].filter(Boolean)
|
||||
|
||||
return h('div', { class: 'flex gap-3 py-2' }, [
|
||||
row.cover
|
||||
? h('img', {
|
||||
src: row.cover,
|
||||
class: 'mt-1 h-14 w-20 flex-none rounded bg-[#f5f7fa] object-cover border border-[#f0f0f0]'
|
||||
})
|
||||
: h(
|
||||
'div',
|
||||
{
|
||||
class: 'mt-1 h-14 w-20 flex-none rounded border border-[#f0f0f0] bg-[#fafafa] text-[10px] text-[#c0c4cc] flex items-center justify-center'
|
||||
},
|
||||
'无上传封面'
|
||||
),
|
||||
h('div', { class: 'min-w-0 flex-1 leading-6' }, [
|
||||
h('div', { class: 'flex items-center gap-2 text-sm font-medium text-[#1f2937]' }, [
|
||||
h('span', { class: 'line-clamp-1' }, row.title || '-'),
|
||||
...badgeNodes
|
||||
]),
|
||||
h(
|
||||
'div',
|
||||
{ class: 'mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs text-[#909399]' },
|
||||
metaItems.length > 0 ? metaItems.map((item) => h('span', item)) : [h('span', '-')]
|
||||
),
|
||||
h('div', { class: 'mt-1 line-clamp-2 text-xs text-[#606266]' }, row.summary || row.slug || '-')
|
||||
])
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'status_info',
|
||||
label: $t('article.modules.content.columns.status'),
|
||||
width: 140,
|
||||
align: 'center',
|
||||
formatter: (row: any) => {
|
||||
const statusText = getOptionLabel(statusOptions, row.status)
|
||||
const visibilityText = getOptionLabel(visibilityOptions, row.visibility)
|
||||
const sourceText = getOptionLabel(sourceOptions, row.source)
|
||||
return h('div', { class: 'flex flex-col items-center py-2 text-sm leading-6' }, [
|
||||
h('div', [
|
||||
h('span', { class: getStatusTagClass(Number(row.status)) }, statusText)
|
||||
]),
|
||||
h('div', { class: 'mt-1 text-xs text-[#606266]' }, visibilityText),
|
||||
h('div', { class: 'mt-1 text-xs text-[#909399]' }, sourceText)
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'business_data',
|
||||
label: $t('article.modules.content.columns.business_data'),
|
||||
minWidth: 250,
|
||||
formatter: (row: any) => {
|
||||
const metricItems = [
|
||||
{ label: '浏览', value: row.view_count ?? 0 },
|
||||
{ label: '点赞', value: row.like_count ?? 0 },
|
||||
{ label: '评论', value: row.comment_count ?? 0 },
|
||||
{ label: '收藏', value: row.favorite_count ?? 0 },
|
||||
]
|
||||
|
||||
return h('div', { class: 'grid grid-cols-2 gap-2 py-2 text-xs text-[#606266]' }, [
|
||||
...metricItems.map((item) =>
|
||||
h('div', { class: 'flex items-center justify-between rounded border border-[#e5e7eb] bg-[#eef2f6] px-3 py-1.5 transition-colors hover:bg-[#e5e7eb]' }, [
|
||||
h('span', { class: 'text-[#909399]' }, item.label),
|
||||
h('span', item.value)
|
||||
])
|
||||
)
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'time_info',
|
||||
label: $t('article.modules.content.columns.time_info'),
|
||||
minWidth: 190,
|
||||
formatter: (row: any) => {
|
||||
return h('div', { class: 'py-2 text-xs leading-6 text-[#606266]' }, [
|
||||
h('div', [
|
||||
h('span', { class: 'text-[#909399]' }, '更新 '),
|
||||
h('span', row.updated_date || '-')
|
||||
]),
|
||||
h('div', { class: 'mt-1' }, [
|
||||
h('span', { class: 'text-[#909399]' }, '发布 '),
|
||||
h('span', row.created_date || '-')
|
||||
])
|
||||
])
|
||||
}
|
||||
},
|
||||
],
|
||||
tableActionColumn: {
|
||||
width: 170,
|
||||
},
|
||||
dialogProps: {
|
||||
width: '70%',
|
||||
},
|
||||
formSchema: {
|
||||
showDefaultActions: false,
|
||||
labelWidth: '100px',
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
show: false,
|
||||
ifDetail: true,
|
||||
colSpan: 24,
|
||||
prop: 'id',
|
||||
label: 'ID',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
prop: 'title',
|
||||
label: $t('article.modules.content.columns.title'),
|
||||
componentProps: {
|
||||
placeholder: $t('article.modules.content.form.title_placeholder'),
|
||||
maxlength: 200,
|
||||
},
|
||||
rules: [{ required: true, message: $t('article.modules.content.form.title_placeholder') }],
|
||||
colSpan: 12,
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
prop: 'slug',
|
||||
label: $t('article.modules.content.form.slug'),
|
||||
componentProps: {
|
||||
placeholder: $t('article.modules.content.form.slug_placeholder'),
|
||||
maxlength: 200,
|
||||
},
|
||||
colSpan: 12,
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
prop: 'content_type',
|
||||
label: $t('article.modules.content.form.content_type'),
|
||||
defaultValue: 'article',
|
||||
componentProps: {
|
||||
options: contentTypeOptions,
|
||||
class: 'w-full',
|
||||
},
|
||||
rules: [{ required: true, message: $t('article.modules.content.form.content_type') }],
|
||||
colSpan: 12,
|
||||
},
|
||||
{
|
||||
component: 'ApiTreeSelect',
|
||||
prop: 'category_id',
|
||||
label: $t('article.modules.content.form.category_id'),
|
||||
componentProps: {
|
||||
api: '/article/category',
|
||||
requestMethod: 'get',
|
||||
params: {
|
||||
format: 'tree',
|
||||
},
|
||||
teleported: true,
|
||||
clearable: true,
|
||||
checkStrictly: true,
|
||||
class: 'w-full',
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: $t('article.modules.content.form.category_id_placeholder'),
|
||||
},
|
||||
rules: [{ required: true, message: $t('article.modules.content.form.category_id') }],
|
||||
colSpan: 12,
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
prop: 'channel',
|
||||
label: $t('article.modules.content.form.channel'),
|
||||
defaultValue: 'official',
|
||||
componentProps: {
|
||||
options: channelOptions,
|
||||
class: 'w-full',
|
||||
},
|
||||
rules: [{ required: true, message: $t('article.modules.content.form.channel') }],
|
||||
colSpan: 12,
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
prop: 'author',
|
||||
label: $t('article.modules.content.form.author'),
|
||||
componentProps: {
|
||||
placeholder: $t('article.modules.content.form.author_placeholder'),
|
||||
maxlength: 100,
|
||||
},
|
||||
rules: [{ required: true, message: $t('article.modules.content.form.author_placeholder') }],
|
||||
colSpan: 12,
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
prop: 'visibility',
|
||||
label: $t('article.modules.content.form.visibility'),
|
||||
defaultValue: 1,
|
||||
componentProps: {
|
||||
options: visibilityOptions,
|
||||
class: 'w-full',
|
||||
},
|
||||
colSpan: 12,
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
prop: 'status',
|
||||
label: $t('article.modules.content.form.status'),
|
||||
defaultValue: 0,
|
||||
componentProps: {
|
||||
options: statusOptions,
|
||||
class: 'w-full',
|
||||
},
|
||||
colSpan: 12,
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
prop: 'source',
|
||||
label: $t('article.modules.content.form.source'),
|
||||
defaultValue: 'original',
|
||||
componentProps: {
|
||||
options: sourceOptions,
|
||||
class: 'w-full',
|
||||
},
|
||||
colSpan: 12,
|
||||
},
|
||||
{
|
||||
component: 'Avatar',
|
||||
prop: 'cover',
|
||||
label: $t('article.modules.content.form.cover'),
|
||||
componentProps: {
|
||||
shape: 'square',
|
||||
},
|
||||
colSpan: 24,
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
prop: 'summary',
|
||||
label: $t('article.modules.content.form.summary'),
|
||||
componentProps: {
|
||||
type: 'textarea',
|
||||
rows: 4,
|
||||
maxlength: 500,
|
||||
placeholder: $t('article.modules.content.form.summary_placeholder'),
|
||||
},
|
||||
colSpan: 24,
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
prop: 'tag_ids',
|
||||
label: $t('article.modules.content.form.tag_ids'),
|
||||
componentProps: {
|
||||
api: '/article/tag',
|
||||
requestMethod: 'get',
|
||||
params: {
|
||||
format: 'select',
|
||||
},
|
||||
labelField: 'label',
|
||||
valueField: 'value',
|
||||
multiple: true,
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
class: 'w-full',
|
||||
placeholder: $t('article.modules.content.form.tag_ids_placeholder'),
|
||||
},
|
||||
colSpan: 12,
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
prop: 'seo_keywords',
|
||||
label: $t('article.modules.content.form.seo_keywords'),
|
||||
componentProps: {
|
||||
placeholder: $t('article.modules.content.form.seo_keywords_placeholder'),
|
||||
maxlength: 255,
|
||||
},
|
||||
colSpan: 12,
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
prop: 'seo_title',
|
||||
label: $t('article.modules.content.form.seo_title'),
|
||||
componentProps: {
|
||||
placeholder: $t('article.modules.content.form.seo_title_placeholder'),
|
||||
maxlength: 200,
|
||||
},
|
||||
colSpan: 12,
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
prop: 'seo_description',
|
||||
label: $t('article.modules.content.form.seo_description'),
|
||||
componentProps: {
|
||||
placeholder: $t('article.modules.content.form.seo_description_placeholder'),
|
||||
maxlength: 500,
|
||||
},
|
||||
colSpan: 12,
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
prop: 'sort',
|
||||
label: $t('article.modules.content.form.sort'),
|
||||
defaultValue: 100,
|
||||
componentProps: {
|
||||
min: 0,
|
||||
class: 'w-full',
|
||||
},
|
||||
colSpan: 8,
|
||||
},
|
||||
{
|
||||
component: 'DatePicker',
|
||||
prop: 'published_at',
|
||||
label: $t('article.modules.content.form.published_at'),
|
||||
componentProps: {
|
||||
type: 'datetime',
|
||||
valueFormat: 'X',
|
||||
class: 'w-full',
|
||||
},
|
||||
colSpan: 8,
|
||||
},
|
||||
{
|
||||
component: 'DatePicker',
|
||||
prop: 'offline_at',
|
||||
label: $t('article.modules.content.form.offline_at'),
|
||||
componentProps: {
|
||||
type: 'datetime',
|
||||
valueFormat: 'X',
|
||||
class: 'w-full',
|
||||
},
|
||||
colSpan: 8,
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
prop: 'view_count',
|
||||
label: $t('article.modules.content.form.view_count'),
|
||||
defaultValue: 0,
|
||||
componentProps: {
|
||||
min: 0,
|
||||
class: 'w-full',
|
||||
},
|
||||
colSpan: 6,
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
prop: 'like_count',
|
||||
label: $t('article.modules.content.form.like_count'),
|
||||
defaultValue: 0,
|
||||
componentProps: {
|
||||
min: 0,
|
||||
class: 'w-full',
|
||||
},
|
||||
colSpan: 6,
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
prop: 'comment_count',
|
||||
label: $t('article.modules.content.form.comment_count'),
|
||||
defaultValue: 0,
|
||||
componentProps: {
|
||||
min: 0,
|
||||
class: 'w-full',
|
||||
},
|
||||
colSpan: 6,
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
prop: 'favorite_count',
|
||||
label: $t('article.modules.content.form.favorite_count'),
|
||||
defaultValue: 0,
|
||||
componentProps: {
|
||||
min: 0,
|
||||
class: 'w-full',
|
||||
},
|
||||
colSpan: 6,
|
||||
},
|
||||
{
|
||||
component: 'ApiDict',
|
||||
prop: 'is_top',
|
||||
label: $t('article.modules.content.form.is_top'),
|
||||
defaultValue: 0,
|
||||
componentProps: {
|
||||
code: DictEnum.SYS_YES_NO,
|
||||
isBtn: true,
|
||||
renderType: 'RadioGroup',
|
||||
},
|
||||
colSpan: 8,
|
||||
},
|
||||
{
|
||||
component: 'ApiDict',
|
||||
prop: 'is_recommend',
|
||||
label: $t('article.modules.content.form.is_recommend'),
|
||||
defaultValue: 0,
|
||||
componentProps: {
|
||||
code: DictEnum.SYS_YES_NO,
|
||||
isBtn: true,
|
||||
renderType: 'RadioGroup',
|
||||
},
|
||||
colSpan: 8,
|
||||
},
|
||||
{
|
||||
component: 'ApiDict',
|
||||
prop: 'allow_comment',
|
||||
label: $t('article.modules.content.form.allow_comment'),
|
||||
defaultValue: 1,
|
||||
componentProps: {
|
||||
code: DictEnum.SYS_YES_NO,
|
||||
isBtn: true,
|
||||
renderType: 'RadioGroup',
|
||||
},
|
||||
colSpan: 8,
|
||||
},
|
||||
{
|
||||
component: 'Editor',
|
||||
prop: 'content',
|
||||
label: $t('article.modules.content.form.content'),
|
||||
componentProps: {
|
||||
placeholder: $t('article.modules.content.form.content_placeholder'),
|
||||
},
|
||||
colSpan: 24,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<div class="article-container">
|
||||
<div class="plugin-header">
|
||||
<h1>{{ $t('article.title') }}</h1>
|
||||
<p>{{ $t('article.description') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="plugin-content">
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ $t('article.card_title') }}</span>
|
||||
<el-button type="primary" @click="fetchTestData">
|
||||
{{ $t('article.refresh_btn') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-loading="loading" class="card-body">
|
||||
<div v-if="error" class="error-message">
|
||||
<el-alert
|
||||
:title="error"
|
||||
type="error"
|
||||
show-icon
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="testData" class="plugin-data">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="测试消息">
|
||||
{{ testData.msg }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="数据">
|
||||
{{ testData.data }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<div v-else class="empty-state">
|
||||
<el-empty description="暂无数据" />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import pluginApi from '../../../api/index';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ArticleView',
|
||||
setup() {
|
||||
const loading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const testData = ref<{ msg?: string; data?: unknown } | null>(null);
|
||||
|
||||
const fetchTestData = async () => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const response = await pluginApi.test();
|
||||
testData.value = response;
|
||||
} catch (err: unknown) {
|
||||
error.value = err instanceof Error ? err.message : '网络错误';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 初始加载数据
|
||||
fetchTestData();
|
||||
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
testData,
|
||||
fetchTestData
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.article-container {
|
||||
padding: 20px;
|
||||
min-height: 80vh;
|
||||
}
|
||||
|
||||
.plugin-header {
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.plugin-header h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.plugin-header p {
|
||||
font-size: 16px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.plugin-content {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.plugin-data {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.plugin-data pre {
|
||||
background-color: #f5f7fa;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
min-height: 300px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div class="article-recycle-page art-full-height">
|
||||
<BasicCrud />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { useCrud } from '@/components/crud'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { $t } from '@/locales'
|
||||
import pluginApi from '../../../api/index'
|
||||
import { crudSchema } from './schemas'
|
||||
|
||||
defineOptions({ name: 'ArticleRecycleList' })
|
||||
|
||||
const [BasicCrud, crudApi] = useCrud({
|
||||
...crudSchema(),
|
||||
toolbarActions: [],
|
||||
dropDownToolbarActions: [],
|
||||
tableActions: [
|
||||
{
|
||||
label: '恢复',
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: 'ant-design:swap-outlined',
|
||||
auth: 'article:recycle:restore',
|
||||
onClick: async (e: Event, row: any) => {
|
||||
await pluginApi.recycleRestore(row.id)
|
||||
ElMessage.success($t('common.operation_success'))
|
||||
crudApi.reload()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '彻底删除',
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: 'ant-design:delete-outlined',
|
||||
auth: 'article:recycle:delete',
|
||||
onClick: async (e: Event, row: any) => {
|
||||
await ElMessageBox.confirm('确定彻底删除该内容吗?删除后不可恢复。', '二次确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确定删除',
|
||||
cancelButtonText: '取消'
|
||||
})
|
||||
await pluginApi.recycleDelete(row.id)
|
||||
ElMessage.success($t('common.operation_success'))
|
||||
crudApi.reload()
|
||||
}
|
||||
}
|
||||
],
|
||||
dropDownActions: [],
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.article-recycle-page {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import { CrudSchema } from '@/components/crud'
|
||||
import { $t } from '@/locales'
|
||||
import pluginApi from '../../../../api/index'
|
||||
|
||||
export const crudSchema = (): CrudSchema => {
|
||||
return {
|
||||
api: async (params: any) => {
|
||||
const response = await pluginApi.recycleList(params)
|
||||
const rows = Array.isArray(response) ? response : []
|
||||
return {
|
||||
recordCount: rows.length,
|
||||
totalPage: 1,
|
||||
rows,
|
||||
}
|
||||
},
|
||||
useCrud: false,
|
||||
searchFormSchema: {
|
||||
labelWidth: '100px',
|
||||
schema: [
|
||||
{
|
||||
prop: 'LIKE_title',
|
||||
label: $t('article.modules.recycle.search.keywords'),
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
placeholder: $t('article.modules.recycle.search.keywords_placeholder'),
|
||||
},
|
||||
colSpan: 6,
|
||||
},
|
||||
],
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
type: 'index',
|
||||
label: $t('article.common.columns.index'),
|
||||
width: 70,
|
||||
},
|
||||
{
|
||||
prop: 'title',
|
||||
label: $t('article.modules.recycle.columns.title'),
|
||||
minWidth: 220,
|
||||
},
|
||||
{
|
||||
prop: 'category',
|
||||
label: $t('article.modules.recycle.columns.category'),
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
prop: 'deleted_by',
|
||||
label: $t('article.modules.recycle.columns.deleted_by'),
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
prop: 'deleted_at',
|
||||
label: $t('article.modules.recycle.columns.deleted_at'),
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
prop: 'expire_at',
|
||||
label: $t('article.modules.recycle.columns.expire_at'),
|
||||
minWidth: 180,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div class="article-tag-page art-full-height">
|
||||
<BasicCrud />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { useCrud } from '@/components/crud'
|
||||
import { crudSchema } from './schemas'
|
||||
|
||||
defineOptions({ name: 'ArticleTagList' })
|
||||
|
||||
const [BasicCrud] = useCrud({
|
||||
...crudSchema(),
|
||||
toolbarActions: [],
|
||||
dropDownToolbarActions: [],
|
||||
tableActions: [],
|
||||
dropDownActions: [],
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.article-tag-page {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,169 @@
|
||||
import { CrudSchema } from '@/components/crud'
|
||||
import { DictEnum } from '@/enums'
|
||||
import { $t } from '@/locales'
|
||||
import { ArticleTagService } from '../../../../api/index'
|
||||
|
||||
export const crudSchema = (): CrudSchema => {
|
||||
return {
|
||||
api: ArticleTagService.list,
|
||||
dialogTitle: $t('article.modules.tag.title'),
|
||||
crudApi: {
|
||||
add: ArticleTagService.create,
|
||||
edit: ArticleTagService.update,
|
||||
remove: ArticleTagService.delete,
|
||||
view: ArticleTagService.get,
|
||||
},
|
||||
useCrud: true,
|
||||
hasAdd: true,
|
||||
hasEdit: true,
|
||||
hasRemove: true,
|
||||
hasView: true,
|
||||
addAuth: 'article:tag:create',
|
||||
editAuth: 'article:tag:update',
|
||||
removeAuth: 'article:tag:delete',
|
||||
viewAuth: 'article:tag:read',
|
||||
rowKey: 'id',
|
||||
searchFormSchema: {
|
||||
labelWidth: '100px',
|
||||
schema: [
|
||||
{
|
||||
prop: 'LIKE_name',
|
||||
label: $t('article.modules.tag.search.keywords'),
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
placeholder: $t('article.modules.tag.search.keywords_placeholder'),
|
||||
},
|
||||
colSpan: 6,
|
||||
},
|
||||
],
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
type: 'selection',
|
||||
},
|
||||
{
|
||||
type: 'index',
|
||||
label: $t('article.common.columns.index'),
|
||||
width: 70,
|
||||
},
|
||||
{
|
||||
prop: 'name',
|
||||
label: $t('article.modules.tag.columns.name'),
|
||||
minWidth: 220,
|
||||
},
|
||||
{
|
||||
prop: 'slug',
|
||||
label: $t('article.modules.tag.columns.slug'),
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
prop: 'article_count',
|
||||
label: $t('article.modules.tag.columns.article_count'),
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
prop: 'enabled',
|
||||
label: $t('article.modules.tag.columns.enabled'),
|
||||
width: 120,
|
||||
component: 'ApiDict',
|
||||
componentProps: {
|
||||
code: DictEnum.SYS_ENABLED_STATUS,
|
||||
},
|
||||
},
|
||||
{
|
||||
prop: 'heat',
|
||||
label: $t('article.modules.tag.columns.heat'),
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
prop: 'updated_date',
|
||||
label: $t('article.modules.tag.columns.updated_at'),
|
||||
minWidth: 180,
|
||||
},
|
||||
],
|
||||
dialogProps: {
|
||||
width: '600px',
|
||||
},
|
||||
formSchema: {
|
||||
showDefaultActions: false,
|
||||
labelWidth: '100px',
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
show: false,
|
||||
ifDetail: true,
|
||||
colSpan: 24,
|
||||
prop: 'id',
|
||||
label: 'ID',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
prop: 'name',
|
||||
label: $t('article.modules.tag.columns.name'),
|
||||
componentProps: {
|
||||
placeholder: $t('article.modules.tag.form.name_placeholder'),
|
||||
maxlength: 100,
|
||||
},
|
||||
rules: [{ required: true, message: $t('article.modules.tag.form.name_placeholder') }],
|
||||
colSpan: 12,
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
prop: 'slug',
|
||||
label: $t('article.modules.tag.columns.slug'),
|
||||
componentProps: {
|
||||
placeholder: $t('article.modules.tag.form.slug_placeholder'),
|
||||
maxlength: 100,
|
||||
},
|
||||
colSpan: 12,
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
prop: 'sort',
|
||||
label: $t('article.modules.tag.columns.heat'),
|
||||
defaultValue: 100,
|
||||
componentProps: {
|
||||
min: 0,
|
||||
class: 'w-full',
|
||||
},
|
||||
colSpan: 12,
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
prop: 'heat',
|
||||
label: $t('article.modules.tag.columns.heat'),
|
||||
defaultValue: 0,
|
||||
componentProps: {
|
||||
min: 0,
|
||||
class: 'w-full',
|
||||
},
|
||||
colSpan: 12,
|
||||
},
|
||||
{
|
||||
component: 'ApiDict',
|
||||
prop: 'enabled',
|
||||
label: $t('article.modules.tag.columns.enabled'),
|
||||
defaultValue: 1,
|
||||
componentProps: {
|
||||
code: DictEnum.SYS_ENABLED_STATUS,
|
||||
isBtn: true,
|
||||
renderType: 'RadioGroup',
|
||||
},
|
||||
colSpan: 24,
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
prop: 'description',
|
||||
label: $t('article.modules.tag.form.description'),
|
||||
componentProps: {
|
||||
type: 'textarea',
|
||||
rows: 4,
|
||||
placeholder: $t('article.modules.tag.form.description_placeholder'),
|
||||
},
|
||||
colSpan: 24,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<div class="article-container">
|
||||
<div class="plugin-header">
|
||||
<h1>{{ $t('article.title') }}</h1>
|
||||
<p>{{ $t('article.description') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="plugin-content">
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ $t('article.card_title') }}</span>
|
||||
<el-button type="primary" @click="fetchTestData">
|
||||
{{ $t('article.refresh_btn') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-loading="loading" class="card-body">
|
||||
<div v-if="error" class="error-message">
|
||||
<el-alert
|
||||
:title="error"
|
||||
type="error"
|
||||
show-icon
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="testData" class="plugin-data">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="测试消息">
|
||||
{{ testData.msg }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="数据">
|
||||
{{ testData.data }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<div v-else class="empty-state">
|
||||
<el-empty description="暂无数据" />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import pluginApi from '../../api/index';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ArticleView',
|
||||
setup() {
|
||||
const loading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const testData = ref<{ msg?: string; data?: unknown } | null>(null);
|
||||
|
||||
const fetchTestData = async () => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const response = await pluginApi.test();
|
||||
testData.value = response;
|
||||
} catch (err: unknown) {
|
||||
error.value = err instanceof Error ? err.message : '网络错误';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 初始加载数据
|
||||
fetchTestData();
|
||||
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
testData,
|
||||
fetchTestData
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.article-container {
|
||||
padding: 20px;
|
||||
min-height: 80vh;
|
||||
}
|
||||
|
||||
.plugin-header {
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.plugin-header h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.plugin-header p {
|
||||
font-size: 16px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.plugin-content {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.plugin-data {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.plugin-data pre {
|
||||
background-color: #f5f7fa;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
min-height: 300px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user