整合前端

This commit is contained in:
砂糖
2026-04-13 17:04:38 +08:00
parent 69609a2cb1
commit 5d4794c9bd
915 changed files with 144259 additions and 0 deletions

View File

@@ -0,0 +1,518 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="100px">
<el-form-item label="所属文章分类" prop="categoryId">
<el-select v-model="queryParams.categoryId" placeholder="请选择分类" clearable>
<el-option v-for="item in categoryOptions" :key="item.categoryId" :label="item.categoryName"
:value="item.categoryId" />
</el-select>
</el-form-item>
<el-form-item label="语言编码" prop="langCode">
<LanguageSelect v-model="queryParams.langCode" placeholder="请选择语种" />
</el-form-item>
<el-form-item label="文章标题" prop="title">
<el-input v-model="queryParams.title" placeholder="请输入文章标题" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="发布状态" prop="isPublished">
<el-select v-model="queryParams.isPublished" placeholder="请选择发布状态" clearable>
<el-option label="已发布" value="1" />
<el-option label="未发布" value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single"
@click="handleUpdate">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple"
@click="handleDelete">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport">导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="articleList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="所属文章分类" align="center" min-width="120">
<template slot-scope="scope">
{{ getCategoryName(scope.row.categoryId) }}
</template>
</el-table-column>
<el-table-column label="语言" align="center" width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.langCode === 'zh-CN' ? 'primary' : 'success'" size="small">{{ scope.row.langCode
}}</el-tag>
</template>
</el-table-column>
<el-table-column label="封面" align="center" width="80">
<template slot-scope="scope">
<image-preview :src="scope.row.cover" fit="contain" width="60" height="40" />
</template>
</el-table-column>
<el-table-column label="文章标题" prop="title" align="center" min-width="200" show-overflow-tooltip />
<el-table-column label="摘要" prop="summary" align="center" min-width="150" show-overflow-tooltip />
<el-table-column label="发布状态" align="center" width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.isPublished === '1' ? 'success' : 'info'" size="small">
{{ scope.row.isPublished === '1' ? '已发布' : '未发布' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="发布时间" align="center" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.publishedTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="关联文章" align="center" width="120">
<template slot-scope="scope">
<el-button v-if="scope.row.relatedArticleId" type="text" size="mini" @click="handleViewRelated(scope.row)">
查看关联
</el-button>
<span v-else></span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
<el-button size="mini" type="text" icon="el-icon-view" @click="handlePreview(scope.row)">预览</el-button>
<el-button size="mini" type="text" icon="el-icon-link" @click="handleCopyLink(scope.row)">复制链接</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
@pagination="getList" />
<!-- 添加或修改文章对话框 -->
<el-dialog :title="title" :visible.sync="open" width="900px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="100px" size="small">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="所属文章分类" prop="categoryId" required>
<el-select v-model="form.categoryId" placeholder="请选择分类" clearable>
<el-option v-for="item in categoryOptions" :key="item.categoryId" :label="item.categoryName"
:value="item.categoryId" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="语言编码" prop="langCode" required>
<LanguageSelect v-model="form.langCode" placeholder="请选择语种" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="文章标题" prop="title" required>
<el-input v-model="form.title" placeholder="请输入文章标题" maxlength="200" show-word-limit />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="关联文章ID" prop="relatedArticleId">
<el-input v-model="form.relatedArticleId" placeholder="请输入关联文章ID同一篇文章的其他语言版本" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="封面地址" prop="cover">
<el-input v-model="form.cover" placeholder="请输入封面地址" />
</el-form-item>
<el-form-item label="摘要" prop="summary">
<el-input v-model="form.summary" type="textarea" placeholder="请输入摘要" :rows="3" maxlength="500"
show-word-limit />
</el-form-item>
<el-form-item label="文章内容" prop="content" required>
<editor v-model="form.content" :min-height="300" />
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="发布状态" prop="isPublished">
<el-select v-model="form.isPublished" placeholder="请选择发布状态" clearable>
<el-option label="已发布" value="1" />
<el-option label="未发布" value="0" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="发布时间" prop="publishedTime">
<el-date-picker clearable v-model="form.publishedTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择发布时间" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="SEO 友好链接" prop="slug">
<el-input v-model="form.slug" placeholder="请输入SEO 友好链接" />
</el-form-item>
<el-form-item label="排序" prop="sortOrder">
<el-input-number v-model="form.sortOrder" :min="0" :max="9999" placeholder="请输入排序" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" :rows="2" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="cancel"> </el-button>
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
</div>
</el-dialog>
<!-- 文章预览对话框 -->
<el-dialog title="文章预览" :visible.sync="previewDialog" width="80%" top="5vh">
<div class="preview-container">
<h3 class="preview-title">{{ previewTitle }}</h3>
<div class="preview-meta">
<span class="meta-item">{{ parseTime(previewPublishedTime, '{y}-{m}-{d} {h}:{i}') }}</span>
<span class="meta-item">{{ previewLangCode }}</span>
</div>
<div v-html="previewContent" class="preview-content"></div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="previewDialog = false">关闭</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { addArticle, delArticle, getArticle, listArticle, updateArticle } from "@/api/site/article";
import { listArticleCategory } from "@/api/site/articleCategory";
import LanguageSelect from '@/components/LanguageSelect.vue';
export default {
name: "Article",
components: {
LanguageSelect
},
data () {
return {
// 按钮loading
buttonLoading: false,
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 文章表格数据
articleList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
categoryId: undefined,
langCode: undefined,
title: undefined,
isPublished: undefined
},
// 表单参数
form: {
articleId: undefined,
categoryId: undefined,
langCode: undefined,
slug: undefined,
title: undefined,
summary: undefined,
content: undefined,
isPublished: "0",
publishedTime: new Date().toISOString().slice(0, 19).replace('T', ' '),
sortOrder: 0,
delFlag: undefined,
remark: undefined,
createTime: undefined,
createBy: undefined,
updateTime: undefined,
updateBy: undefined,
cover: undefined,
relatedArticleId: undefined
},
// 表单校验
rules: {
categoryId: [
{ required: true, message: '请选择所属文章分类', trigger: 'change' }
],
langCode: [
{ required: true, message: '请选择语言编码', trigger: 'change' }
],
title: [
{ required: true, message: '请输入文章标题', trigger: 'blur' },
{ min: 1, max: 200, message: '标题长度不能超过200个字符', trigger: 'blur' }
],
content: [
{ required: true, message: '请输入文章内容', trigger: 'blur' }
]
},
categoryOptions: [],
previewDialog: false,
previewContent: "",
previewTitle: "",
previewPublishedTime: "",
previewLangCode: ""
};
},
created () {
this.getList();
this.getCategoryOptions();
},
methods: {
getCategoryOptions () {
listArticleCategory({}).then(res => {
this.categoryOptions = res.rows || [];
});
},
getCategoryName (id) {
const item = this.categoryOptions.find(opt => opt.categoryId === id);
return item ? item.categoryName : id;
},
handlePreview (row) {
if (row.content) {
this.previewTitle = row.title;
this.previewContent = row.content;
this.previewPublishedTime = row.publishedTime;
this.previewLangCode = row.langCode;
this.previewDialog = true;
} else {
getArticle(row.articleId).then(res => {
this.previewTitle = res.data.title;
this.previewContent = res.data.content;
this.previewPublishedTime = res.data.publishedTime;
this.previewLangCode = res.data.langCode;
this.previewDialog = true;
});
}
},
handleViewRelated (row) {
if (row.relatedArticleId) {
this.loading = true;
getArticle(row.relatedArticleId).then(res => {
this.loading = false;
this.previewTitle = res.data.title;
this.previewContent = res.data.content;
this.previewPublishedTime = res.data.publishedTime;
this.previewLangCode = res.data.langCode;
this.previewDialog = true;
}).catch(() => {
this.loading = false;
this.$message.error('获取关联文章失败');
});
}
},
/** 查询文章列表 */
getList () {
this.loading = true;
listArticle(this.queryParams).then(response => {
this.articleList = response.rows;
this.total = response.total;
this.loading = false;
});
},
// 取消按钮
cancel () {
this.open = false;
this.reset();
},
// 表单重置
reset () {
this.form = {
articleId: undefined,
categoryId: undefined,
langCode: undefined,
slug: undefined,
title: undefined,
summary: undefined,
content: undefined,
isPublished: "0",
publishedTime: new Date().toISOString().slice(0, 19).replace('T', ' '),
sortOrder: 0,
delFlag: undefined,
remark: undefined,
createTime: undefined,
createBy: undefined,
updateTime: undefined,
updateBy: undefined,
cover: undefined,
relatedArticleId: undefined
};
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery () {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery () {
this.resetForm("queryForm");
this.handleQuery();
},
// 多选框选中数据
handleSelectionChange (selection) {
this.ids = selection.map(item => item.articleId)
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 新增按钮操作 */
handleAdd () {
this.reset();
this.open = true;
this.title = "添加文章";
},
/** 修改按钮操作 */
handleUpdate (row) {
this.loading = true;
this.reset();
const articleId = row.articleId || this.ids
getArticle(articleId).then(response => {
this.loading = false;
this.form = response.data;
this.open = true;
this.title = "修改文章";
}).catch(() => {
this.loading = false;
this.$message.error('获取文章详情失败');
});
},
/** 提交按钮 */
submitForm () {
this.$refs["form"].validate(valid => {
if (valid) {
this.buttonLoading = true;
const articleData = {
...this.form
};
// 处理slug自动生成
if (!articleData.slug && articleData.title) {
articleData.slug = articleData.title.toLowerCase().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-').substring(0, 100);
}
const requestMethod = articleData.articleId ? updateArticle : addArticle;
requestMethod(articleData).then(response => {
this.$modal.msgSuccess(articleData.articleId ? "修改成功" : "新增成功");
this.open = false;
this.getList();
}).catch(() => {
this.$modal.msgError(articleData.articleId ? "修改失败" : "新增失败");
}).finally(() => {
this.buttonLoading = false;
});
}
});
},
/** 删除按钮操作 */
handleDelete (row) {
const articleIds = row.articleId || this.ids;
this.$modal.confirm('是否确认删除选中的文章数据?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.loading = true;
return delArticle(articleIds);
}).then(() => {
this.loading = false;
this.getList();
this.$modal.msgSuccess("删除成功");
this.ids = [];
this.single = true;
this.multiple = true;
}).catch(() => {
this.loading = false;
});
},
/** 导出按钮操作 */
handleExport () {
this.download('site/article/export', {
...this.queryParams
}, `article_${new Date().getTime()}.xlsx`)
},
handleCopyLink (row) {
const url = `/${row.categoryId}/${row.articleId}`;
if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(url).then(() => {
this.$message.success('复制成功');
}, () => {
this.$message.error('复制失败,请手动复制');
});
} else {
// 兼容性处理
const input = document.createElement('input');
input.value = url;
document.body.appendChild(input);
input.select();
try {
document.execCommand('copy');
this.$message.success('复制成功');
} catch (e) {
this.$message.error('复制失败,请手动复制');
}
document.body.removeChild(input);
}
},
/** 快速添加关联文章 */
quickAddRelatedArticle () {
if (this.form.articleId) {
this.reset();
this.form.relatedArticleId = this.form.articleId;
this.open = true;
this.title = "添加关联文章(其他语言版本)";
}
}
}
};
</script>
<style scoped>
.preview-container {
padding: 20px;
}
.preview-title {
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
text-align: center;
}
.preview-meta {
text-align: center;
color: #909399;
margin-bottom: 30px;
font-size: 14px;
}
.meta-item {
margin: 0 10px;
}
.preview-content {
line-height: 1.8;
font-size: 16px;
}
.preview-content img {
max-width: 100%;
height: auto;
margin: 10px 0;
}
</style>