🧹 清理重复配置文件
- 删除根目录中重复的 NestJS 配置文件 - 删除 tsconfig.json, tsconfig.build.json, eslint.config.mjs, .prettierrc - 保留 wwjcloud-nest/ 目录中的完整配置 - 避免配置冲突,确保项目结构清晰
This commit is contained in:
102
admin-vben/apps/web-antd/src/addon/cms/api/article.ts
Normal file
102
admin-vben/apps/web-antd/src/addon/cms/api/article.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/***************************************************** 文章表 ****************************************************/
|
||||
|
||||
/**
|
||||
* 获取文章表列表
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export function getArticleList(params: Record<string, any>) {
|
||||
return request.get(`cms/article`, {params})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文章表详情
|
||||
* @param id 文章表id
|
||||
* @returns
|
||||
*/
|
||||
export function getArticleInfo(id: number) {
|
||||
return request.get(`cms/article/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加文章表
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export function addArticle(params: Record<string, any>) {
|
||||
return request.post('cms/article', params, {showSuccessMessage: true})
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑文章表
|
||||
* @param params
|
||||
*/
|
||||
export function editArticle(params: Record<string, any>) {
|
||||
return request.put(`cms/article/${params.id}`, params, {showSuccessMessage: true})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文章表
|
||||
* @param id
|
||||
* @returns
|
||||
*/
|
||||
export function deleteArticle(id: number) {
|
||||
return request.delete(`cms/article/${id}`, {showSuccessMessage: true})
|
||||
}
|
||||
|
||||
/***************************************************** 文章分类管理 ****************************************************/
|
||||
|
||||
/**
|
||||
* 获取文章分类列表
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export function getArticleCategoryList(params: Record<string, any>) {
|
||||
return request.get(`cms/category`, {params})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取文章全部分类
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export function getArticleCategoryAll(params: Record<string, any>) {
|
||||
return request.get(`cms/category/all`, params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文章分类详情
|
||||
* @param category_id
|
||||
*/
|
||||
export function getArticleCategoryInfo(category_id: number) {
|
||||
return request.get(`cms/category/${category_id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加文章分类
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export function addArticleCategory(params: Record<string, any>) {
|
||||
return request.post('cms/category', params, {showSuccessMessage: true})
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑文章分类
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export function editArticleCategory(params: Record<string, any>) {
|
||||
return request.put(`cms/category/${params.category_id}`, params, {showSuccessMessage: true})
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章分类删除
|
||||
* @param category_id
|
||||
*/
|
||||
export function deleteArticleCategory(category_id: number) {
|
||||
return request.delete(`cms/category/${category_id}`, {showSuccessMessage: true});
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "栏目名称",
|
||||
"sort": "排序",
|
||||
"isShow": "是否显示",
|
||||
"namePlaceholder": "请输入栏目名称",
|
||||
"sortPlaceholder": "请输入排序",
|
||||
"isShowPlaceholder": "是否显示",
|
||||
"addArticleCategory": "添加栏目",
|
||||
"updateArticleCategory": "编辑栏目",
|
||||
"articleCategoryDeleteTips": "确定要删除该栏目吗?",
|
||||
"nameMax": "名称不能超过20个字符",
|
||||
"sortNumber": "排序号必须是数字",
|
||||
"sortBetween": "排序号不能超过10000",
|
||||
"show": "显示",
|
||||
"hide": "不显示",
|
||||
"articleNumber": "文章数量"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"categoryName": "文章栏目",
|
||||
"title": "文章标题",
|
||||
"intro": "简介",
|
||||
"summary": "文章摘要",
|
||||
"image": "文章图片",
|
||||
"author": "作者",
|
||||
"content": "文章内容",
|
||||
"visit": "实际浏览量",
|
||||
"visitVirtual": "初始浏览量",
|
||||
"isShow": "是否显示",
|
||||
"sort": "排序",
|
||||
"categoryIdPlaceholder": "请选择文章栏目",
|
||||
"titlePlaceholder": "请输入文章标题",
|
||||
"introPlaceholder": "请输入简介",
|
||||
"summaryPlaceholder": "请输入文章摘要",
|
||||
"imagePlaceholder": "请上传文章图片",
|
||||
"authorPlaceholder": "请输入作者",
|
||||
"contentPlaceholder": "请输入文章内容",
|
||||
"visitPlaceholder": "请输入实际浏览量",
|
||||
"visitVirtualPlaceholder": "请输入初始浏览量",
|
||||
"isShowPlaceholder": "是否显示",
|
||||
"sortPlaceholder": "请输入排序",
|
||||
"addArticle": "添加文章",
|
||||
"updateArticle": "编辑文章",
|
||||
"titleMax": "文章标题不能超过20个字符",
|
||||
"introMax": "文章简介不能超过50个字符",
|
||||
"summaryMax": "文章摘要不能超过50个字符",
|
||||
"imageMax": "图片路径太长",
|
||||
"authorMax": "文章作者不能超过20个字符",
|
||||
"isShowNumber": "是否显示必须是数字",
|
||||
"isShowBetween": "是否显示只能是0或者1",
|
||||
"sortNumber": "排序号必须是数字",
|
||||
"sortBetween": "排序号需要在0-10000之间",
|
||||
"articleNull": "未读取到文章信息!"
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"categoryName": "栏目",
|
||||
"ID": "ID",
|
||||
"title": "标题",
|
||||
"intro": "简介",
|
||||
"summary": "摘要",
|
||||
"image": "封面",
|
||||
"author": "作者",
|
||||
"content": "文章内容",
|
||||
"visit": "浏览量",
|
||||
"visitVirtual": "初始浏览量",
|
||||
"isShow": "是否显示",
|
||||
"sort": "排序",
|
||||
"createTime": "创建时间",
|
||||
"updateTime": "更新时间",
|
||||
"addArticle": "添加文章",
|
||||
"updateArticle": "编辑文章",
|
||||
"titlePlaceholder": "请输入文章标题",
|
||||
"categoryIdPlaceholder": "请选择文章栏目",
|
||||
"articleDeleteTips": "确定要删除该文章吗?"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"articleData": "文章数据",
|
||||
"articleStyle": "文章样式",
|
||||
"articleBgColor": "文章背景",
|
||||
"articleNum": "文章数量",
|
||||
"selectArticleTips": "文章选择",
|
||||
"articleTitle": "标题",
|
||||
"articleImage": "封面",
|
||||
"articleCategoryName": "栏目",
|
||||
"articleSummary": "摘要",
|
||||
"selectArticleTip": "请选择文章"
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div class="main-container">
|
||||
<el-card class="box-card !border-none" shadow="never">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-page-title">{{ pageName }}</span>
|
||||
<el-button type="primary" @click="addEvent">{{ t('addArticleCategory') }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card class="box-card !border-none my-[10px] table-search-wrap" shadow="never">
|
||||
<el-form :inline="true" :model="categoryTableData.searchParam" ref="searchFormRef">
|
||||
<el-form-item :label="t('name')" prop="name">
|
||||
<el-input v-model.trim="categoryTableData.searchParam.name" :placeholder="t('namePlaceholder')" class="w-[190px]" prefix-icon="Search" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="loadCategoryList()">{{ t('search') }}</el-button>
|
||||
<el-button @click="resetForm(searchFormRef)">{{ t('reset') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<div class="mt-[10px]">
|
||||
<el-table :data="categoryTableData.data" size="large" v-loading="categoryTableData.loading">
|
||||
<template #empty>
|
||||
<span>{{ !categoryTableData.loading ? t('emptyData') : '' }}</span>
|
||||
</template>
|
||||
<el-table-column prop="name" :label="t('name')" min-width="150" />
|
||||
<el-table-column prop="article_num" :label="t('articleNumber')" min-width="140" />
|
||||
<el-table-column prop="is_show" :label="t('isShow')" min-width="150">
|
||||
<template #default="{ row }">
|
||||
{{ row.is_show == 1 ? t('show') : t('hide') }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="sort" :label="t('sort')" min-width="120" />
|
||||
|
||||
<el-table-column :label="t('operation')" fixed="right" width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="editEvent(row)">{{ t('edit') }}</el-button>
|
||||
<el-button type="primary" link @click="deleteEvent(row.category_id)">{{ t('delete') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
</el-table>
|
||||
<div class="mt-[16px] flex justify-end">
|
||||
<el-pagination v-model:current-page="categoryTableData.page" v-model:page-size="categoryTableData.limit" layout="total, sizes, prev, pager, next, jumper" :total="categoryTableData.total" @size-change="loadCategoryList()" @current-change="loadCategoryList" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<edit-category ref="editCategoryDialog" @complete="loadCategoryList()" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { getArticleCategoryList, deleteArticleCategory } from '@/addon/cms/api/article'
|
||||
import { ElMessageBox, FormInstance } from 'element-plus'
|
||||
import EditCategory from '@/addon/cms/views/article/components/edit-category.vue'
|
||||
import { debounce } from '@/utils/common'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const pageName = route.meta.title
|
||||
|
||||
const categoryTableData = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0,
|
||||
loading: true,
|
||||
data: [],
|
||||
searchParam: {
|
||||
name: ''
|
||||
}
|
||||
})
|
||||
|
||||
const searchFormRef = ref<FormInstance>()
|
||||
|
||||
const resetForm = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl.resetFields()
|
||||
loadCategoryList()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文章分类列表
|
||||
*/
|
||||
const loadCategoryList = debounce((page: number = 1) => {
|
||||
categoryTableData.loading = true
|
||||
categoryTableData.page = page
|
||||
|
||||
getArticleCategoryList({
|
||||
page: categoryTableData.page,
|
||||
limit: categoryTableData.limit,
|
||||
...categoryTableData.searchParam
|
||||
}).then(res => {
|
||||
categoryTableData.loading = false
|
||||
categoryTableData.data = res.data.data
|
||||
categoryTableData.total = res.data.total
|
||||
}).catch(() => {
|
||||
categoryTableData.loading = false
|
||||
})
|
||||
})
|
||||
loadCategoryList()
|
||||
|
||||
const editCategoryDialog: Record<string, any> | null = ref(null)
|
||||
|
||||
/**
|
||||
* 添加文章分类
|
||||
*/
|
||||
const addEvent = () => {
|
||||
editCategoryDialog.value.setFormData()
|
||||
editCategoryDialog.value.showDialog = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑文章分类
|
||||
* @param data
|
||||
*/
|
||||
const editEvent = (data: any) => {
|
||||
editCategoryDialog.value.setFormData(data)
|
||||
editCategoryDialog.value.showDialog = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文章分类
|
||||
*/
|
||||
const deleteEvent = (id: number) => {
|
||||
ElMessageBox.confirm(t('articleCategoryDeleteTips'), t('warning'),
|
||||
{
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
deleteArticleCategory(id).then(() => {
|
||||
loadCategoryList()
|
||||
}).catch(() => {
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" :title="popTitle" width="500px" :destroy-on-close="true">
|
||||
<el-form :model="formData" label-width="90px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
|
||||
<el-form-item :label="t('name')" prop="name">
|
||||
<el-input v-model="formData.name" clearable :placeholder="t('namePlaceholder')" class="input-width" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('sort')" prop="sort">
|
||||
<el-input-number v-model="formData.sort" :min="0" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('isShow')">
|
||||
<el-radio-group v-model="formData.is_show" :placeholder="t('isShowPlaceholder')">
|
||||
<el-radio :label="1">{{ t('show') }}</el-radio>
|
||||
<el-radio :label="0">{{ t('hidden') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { addArticleCategory, editArticleCategory, getArticleCategoryInfo } from '@/addon/cms/api/article'
|
||||
|
||||
let popTitle: string = ''
|
||||
|
||||
const showDialog = ref(false)
|
||||
const loading = ref(true)
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
category_id: '',
|
||||
name: '',
|
||||
sort: '',
|
||||
is_show: 1
|
||||
}
|
||||
const formData: Record<string, any> = reactive({ ...initialFormData })
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
name: [
|
||||
{ required: true, message: t('namePlaceholder'), trigger: 'blur' },
|
||||
{
|
||||
validator: (rule: any, value: string, callback: any) => {
|
||||
if (value.length > 20) {
|
||||
callback(new Error(t('nameMax')))
|
||||
}
|
||||
|
||||
callback()
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
sort: [
|
||||
{
|
||||
validator: (rule: any, value: string | number, callback: any) => {
|
||||
if (value === '' || isNaN(value as number)) {
|
||||
callback(new Error(t('sortNumber')))
|
||||
}
|
||||
if (parseInt(value as string) > 10000) {
|
||||
callback(new Error(t('sortBetween')))
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['complete'])
|
||||
|
||||
/**
|
||||
* 确认
|
||||
* @param formEl
|
||||
*/
|
||||
const confirm = async (formEl: FormInstance | undefined) => {
|
||||
if (loading.value || !formEl) return
|
||||
const save = formData.category_id ? editArticleCategory : addArticleCategory
|
||||
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
|
||||
const data = formData
|
||||
|
||||
save(data).then(res => {
|
||||
loading.value = false
|
||||
showDialog.value = false
|
||||
emit('complete')
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const setFormData = async (row: any = null) => {
|
||||
loading.value = true
|
||||
Object.assign(formData, initialFormData)
|
||||
popTitle = t('addArticleCategory')
|
||||
if (row) {
|
||||
popTitle = t('updateArticleCategory')
|
||||
const data = await (await getArticleCategoryInfo(row.category_id)).data
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (data[key] != undefined) formData[key] = data[key]
|
||||
})
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
175
admin-vben/apps/web-antd/src/addon/cms/views/article/edit.vue
Normal file
175
admin-vben/apps/web-antd/src/addon/cms/views/article/edit.vue
Normal file
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<div class="main-container">
|
||||
<el-card class="card !border-none mb-[15px]" shadow="never">
|
||||
<el-page-header :content="pageName" :icon="ArrowLeft" @back="router.push({ path: '/cms/article/list' })" />
|
||||
</el-card>
|
||||
|
||||
<el-card class="box-card !border-none" shadow="never">
|
||||
<el-form :model="formData" label-width="90px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
|
||||
<el-form-item :label="t('title')" prop="title">
|
||||
<el-input v-model.trim="formData.title" clearable :placeholder="t('titlePlaceholder')" class="input-width" maxlength="20" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('categoryName')" prop="category_id">
|
||||
<el-select v-model="formData.category_id" clearable :placeholder="t('categoryIdPlaceholder')" class="input-width">
|
||||
<el-option :label="item['name']" :value="item['category_id']" v-for="(item,index) in categoryList" :key="index" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('intro')" prop="intro">
|
||||
<el-input v-model.trim="formData.intro" type="textarea" rows="4" clearable :placeholder="t('introPlaceholder')" class="input-width" maxlength="50" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('summary')" prop="summary">
|
||||
<el-input v-model.trim="formData.summary" type="textarea" rows="4" clearable :placeholder="t('summaryPlaceholder')" class="input-width" maxlength="50" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('image')">
|
||||
<upload-image v-model="formData.image" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('author')" prop="author">
|
||||
<el-input v-model.trim="formData.author" clearable :placeholder="t('authorPlaceholder')" class="input-width" maxlength="20" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('content')" prop="content">
|
||||
<editor v-model="formData.content" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('visitVirtual')">
|
||||
<el-input v-model.trim="formData.visit_virtual" clearable :placeholder="t('visitVirtualPlaceholder')" class="input-width" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('isShow')">
|
||||
<el-radio-group v-model="formData.is_show" :placeholder="t('isShowPlaceholder')">
|
||||
<el-radio :label="1">{{ t('show') }}</el-radio>
|
||||
<el-radio :label="0">{{ t('hidden') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('sort')" prop="sort">
|
||||
<el-input-number v-model="formData.sort" :min="0" />
|
||||
</el-form-item>
|
||||
|
||||
</el-form>
|
||||
</el-card>
|
||||
<div class="fixed-footer-wrap">
|
||||
<div class="fixed-footer">
|
||||
<el-button type="primary" @click="onSave(formRef)">{{ t('save') }}</el-button>
|
||||
<el-button @click="back()">{{ t('cancel') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { getArticleInfo, getArticleCategoryAll, addArticle, editArticle } from '@/addon/cms/api/article'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const id: number = parseInt(route.query.id as string || '0')
|
||||
const loading = ref(false)
|
||||
const categoryList = ref([])
|
||||
const pageName = route.meta.title
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: '',
|
||||
category_id: '',
|
||||
title: '',
|
||||
intro: '',
|
||||
summary: '',
|
||||
image: '',
|
||||
author: '',
|
||||
content: '',
|
||||
visit: '',
|
||||
visit_virtual: '',
|
||||
is_show: 1,
|
||||
sort: 0
|
||||
}
|
||||
|
||||
const formData: Record<string, any> = reactive({ ...initialFormData })
|
||||
|
||||
const setFormData = async (id: number = 0) => {
|
||||
loading.value = true
|
||||
Object.assign(formData, initialFormData)
|
||||
if (id) {
|
||||
const data = await (await getArticleInfo(id)).data
|
||||
if (!data || Object.keys(data).length == 0) {
|
||||
ElMessage.error(t('articleNull'))
|
||||
setTimeout(() => {
|
||||
router.go(-1)
|
||||
}, 2000)
|
||||
return false
|
||||
}
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (data[key] != undefined) formData[key] = data[key]
|
||||
})
|
||||
loading.value = false
|
||||
} else {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
if (id) setFormData(id)
|
||||
|
||||
const setCategoryList = async () => {
|
||||
categoryList.value = await (await getArticleCategoryAll({})).data
|
||||
// if (!id && categoryList.value.length > 0) formData.category_id = categoryList.value[0].category_id
|
||||
}
|
||||
setCategoryList()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
title: [
|
||||
{ required: true, message: t('titlePlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
category_id: [
|
||||
{ required: true, message: t('categoryIdPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
content: [
|
||||
{ required: true, message: t('contentPlaceholder'), trigger: 'blur' },
|
||||
{
|
||||
validator: (rule: any, value: string, callback: any) => {
|
||||
const content = value.replace(/<[^<>]+>/g, '').replace(/ /gi, '')
|
||||
if (!content && value.indexOf('img') === -1) {
|
||||
callback(new Error(t('contentPlaceholder')))
|
||||
} else callback()
|
||||
},
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const onSave = async (formEl: FormInstance | undefined) => {
|
||||
if (loading.value || !formEl) return
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
const data = formData
|
||||
const save = id ? editArticle : addArticle
|
||||
save(data).then(res => {
|
||||
loading.value = false
|
||||
back()
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const back = () => {
|
||||
router.push({ path: '/cms/article/list' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.edui-default .edui-editor {
|
||||
border: none!important;
|
||||
z-index: 1!important;
|
||||
}
|
||||
</style>
|
||||
182
admin-vben/apps/web-antd/src/addon/cms/views/article/list.vue
Normal file
182
admin-vben/apps/web-antd/src/addon/cms/views/article/list.vue
Normal file
@@ -0,0 +1,182 @@
|
||||
<template>
|
||||
<div class="main-container">
|
||||
<el-card class="box-card !border-none" shadow="never">
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-page-title">{{ pageName }}</span>
|
||||
<el-button type="primary" @click="addEvent">{{ t('addArticle') }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card class="box-card !border-none my-[10px] table-search-wrap" shadow="never">
|
||||
<el-form :inline="true" :model="articleTableData.searchParam" ref="searchFormRef">
|
||||
<el-form-item :label="t('title')" prop="title">
|
||||
<el-input v-model="articleTableData.searchParam.title" :placeholder="t('titlePlaceholder')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('categoryName')" prop="category_id">
|
||||
<el-select v-model="articleTableData.searchParam.category_id" clearable :placeholder="t('categoryIdPlaceholder')" class="input-width">
|
||||
<el-option :label="t('selectPlaceholder')" value="" />
|
||||
<el-option :label="item['name']" :value="item['category_id']" v-for="(item,index) in categoryList" :key="index"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="loadArticleList()">{{ t('search') }}</el-button>
|
||||
<el-button @click="resetForm(searchFormRef)">{{ t('reset') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<div class="mt-[10px]">
|
||||
<el-table :data="articleTableData.data" size="large" v-loading="articleTableData.loading">
|
||||
<template #empty>
|
||||
<span>{{ !articleTableData.loading ? t('emptyData') : '' }}</span>
|
||||
</template>
|
||||
|
||||
<el-table-column prop="id" :show-overflow-tooltip="true" :label="t('ID')" width="100" />
|
||||
|
||||
<el-table-column prop="category_name" :label="t('categoryName')" width="120" />
|
||||
|
||||
<el-table-column prop="title" :show-overflow-tooltip="true" :label="t('title')" width="180">
|
||||
<template #default="{ row }">
|
||||
<a :href="row.article_url.web_url" target="_blank">{{ row.title }}</a>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('image')" min-width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-image class="w-12 h-12" v-if="row.image_thumb_small" :src="img(row.image_thumb_small)" fit="contain" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="visit" :label="t('visit')" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<span>{{ parseInt(row.visit + row.visit_virtual) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('isShow')" min-width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.is_show == 1">{{ t('show') }}</span>
|
||||
<span v-if="row.is_show == 0">{{t('hidden')}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="sort" :label="t('sort')" width="100" align="center" />
|
||||
|
||||
<el-table-column :label="t('createTime')" min-width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.create_time || '' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('operation')" fixed="right" align="right" width="130">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="editEvent(row)">{{ t('edit') }}</el-button>
|
||||
<el-button type="primary" link @click="deleteEvent(row.id)">{{ t('delete') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
</el-table>
|
||||
<div class="mt-[16px] flex justify-end">
|
||||
<el-pagination v-model:current-page="articleTableData.page" v-model:page-size="articleTableData.limit" layout="total, sizes, prev, pager, next, jumper" :total="articleTableData.total" @size-change="loadArticleList()" @current-change="loadArticleList" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { getArticleList, deleteArticle, getArticleCategoryAll } from '@/addon/cms/api/article'
|
||||
import { img } from '@/utils/common'
|
||||
import { ElMessageBox, FormInstance } from 'element-plus'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const pageName = route.meta.title
|
||||
|
||||
const articleTableData = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0,
|
||||
loading: true,
|
||||
data: [],
|
||||
searchParam: {
|
||||
title: '',
|
||||
category_id: ''
|
||||
}
|
||||
})
|
||||
const categoryList = ref([])
|
||||
|
||||
const searchFormRef = ref<FormInstance>()
|
||||
|
||||
const setCategoryList = async () => {
|
||||
categoryList.value = await (await getArticleCategoryAll({})).data
|
||||
}
|
||||
setCategoryList()
|
||||
|
||||
/**
|
||||
* 获取文章列表
|
||||
*/
|
||||
const loadArticleList = (page: number = 1) => {
|
||||
articleTableData.loading = true
|
||||
articleTableData.page = page
|
||||
|
||||
getArticleList({
|
||||
page: articleTableData.page,
|
||||
limit: articleTableData.limit,
|
||||
...articleTableData.searchParam
|
||||
}).then(res => {
|
||||
articleTableData.loading = false
|
||||
articleTableData.data = res.data.data
|
||||
articleTableData.total = res.data.total
|
||||
}).catch(() => {
|
||||
articleTableData.loading = false
|
||||
})
|
||||
}
|
||||
loadArticleList()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
/**
|
||||
* 添加文章
|
||||
*/
|
||||
const addEvent = () => {
|
||||
router.push('/cms/article/edit')
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑文章
|
||||
* @param data
|
||||
*/
|
||||
const editEvent = (data: any) => {
|
||||
router.push(`/cms/article/edit?id=${data.id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文章
|
||||
*/
|
||||
const deleteEvent = (id: number) => {
|
||||
ElMessageBox.confirm(t('articleDeleteTips'), t('warning'),
|
||||
{
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
deleteArticle(id).then(() => {
|
||||
loadArticleList()
|
||||
}).catch(() => {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const resetForm = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl.resetFields()
|
||||
loadArticleList()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('articleData') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
<el-form-item :label="t('dataSources')">
|
||||
<el-radio-group v-model="diyStore.editComponent.sources">
|
||||
<el-radio :label="'initial'">{{ t('defaultSources') }}</el-radio>
|
||||
<el-radio :label="'diy'">{{ t('manualSelectionSources') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('articleNum')" v-show="diyStore.editComponent.sources == 'initial'">
|
||||
<el-slider v-model="diyStore.editComponent.count" show-input size="small" class="ml-[10px] diy-nav-slider" :min="1" :max="30" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('manualSelectionSources')" v-show="diyStore.editComponent.sources == 'diy'" class=" flex">
|
||||
<span @click="showArticle" class="cursor-pointer flex-1" :class="{ 'text-primary': diyStore.editComponent.articleIds.length > 0 }">{{ diyStore.editComponent.articleIds.length > 0 ? t('selected') + diyStore.editComponent.articleIds.length + t('piece') : t('selectPlaceholder') }}</span>
|
||||
<el-icon>
|
||||
<ArrowRight />
|
||||
</el-icon>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="showDialog" :title="t('selectArticleTips')" width="60%" :close-on-click-modal="false">
|
||||
|
||||
<div>
|
||||
<el-table :data="articleTableData.data" size="large" v-loading="articleTableData.loading" @selection-change="handleSelectionChange">
|
||||
<template #empty>
|
||||
<span>{{ !articleTableData.loading ? t('emptyData') : '' }}</span>
|
||||
</template>
|
||||
|
||||
<el-table-column type="selection" width="55" />
|
||||
|
||||
<el-table-column prop="title" :show-overflow-tooltip="true" :label="t('articleTitle')" width="140" />
|
||||
|
||||
<el-table-column :label="t('articleImage')" min-width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-image class="w-12 h-12" v-if="row.image" :src="img(row.image)" fit="contain" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="category_name" :label="t('articleCategoryName')" align="center" min-width="140" />
|
||||
|
||||
<el-table-column prop="summary" :label="t('articleSummary')" width="180" :show-overflow-tooltip="true" />
|
||||
|
||||
<el-table-column :label="t('createTime')" min-width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.create_time || '' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
</el-table>
|
||||
<div class="mt-[16px] flex justify-end">
|
||||
<el-pagination v-model:current-page="articleTableData.page" v-model:page-size="articleTableData.limit"
|
||||
layout="total, sizes, prev, pager, next, jumper" :total="articleTableData.total"
|
||||
@size-change="loadArticleList()" @current-change="loadArticleList" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="save">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('articleStyle') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
<el-form-item :label="t('articleBgColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.elementBgColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('textColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.textColor"/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('topRounded')">
|
||||
<el-slider v-model="diyStore.editComponent.topElementRounded" show-input size="small" class="ml-[10px] diy-nav-slider" :max="50" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('bottomRounded')">
|
||||
<el-slider v-model="diyStore.editComponent.bottomElementRounded" show-input size="small" class="ml-[10px] diy-nav-slider" :max="50" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
import { ref, reactive } from 'vue'
|
||||
import { img } from '@/utils/common'
|
||||
import { getArticleList } from '@/addon/cms/api/article'
|
||||
|
||||
const diyStore: any = useDiyStore()
|
||||
diyStore.editComponent.ignore = [] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
if (diyStore.value[index].sources === 'diy' && diyStore.value[index].articleIds.length === 0) {
|
||||
res.code = false
|
||||
res.message = t('selectArticleTip')
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
const showDialog = ref(false)
|
||||
|
||||
const showArticle = () => {
|
||||
showDialog.value = true
|
||||
}
|
||||
|
||||
const articleTableData = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0,
|
||||
loading: true,
|
||||
data: [],
|
||||
searchParam: {
|
||||
title: '',
|
||||
category_id: '',
|
||||
is_show: 1
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取文章列表
|
||||
*/
|
||||
const loadArticleList = (page: number = 1) => {
|
||||
articleTableData.loading = true
|
||||
articleTableData.page = page
|
||||
|
||||
getArticleList({
|
||||
page: articleTableData.page,
|
||||
limit: articleTableData.limit,
|
||||
...articleTableData.searchParam
|
||||
}).then(res => {
|
||||
articleTableData.loading = false
|
||||
articleTableData.data = res.data.data
|
||||
articleTableData.total = res.data.total
|
||||
}).catch(() => {
|
||||
articleTableData.loading = false
|
||||
})
|
||||
}
|
||||
|
||||
loadArticleList()
|
||||
|
||||
const multipleSelection: any = ref([])
|
||||
|
||||
const handleSelectionChange = (val: any[]) => {
|
||||
multipleSelection.value = val
|
||||
}
|
||||
|
||||
const save = () => {
|
||||
diyStore.editComponent.articleIds = []
|
||||
multipleSelection.value.forEach((item: any) => {
|
||||
diyStore.editComponent.articleIds.push(item.id)
|
||||
})
|
||||
showDialog.value = false
|
||||
}
|
||||
|
||||
defineExpose({})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
28
admin-vben/apps/web-antd/src/addon/example/router/index.ts
Normal file
28
admin-vben/apps/web-antd/src/addon/example/router/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
export const NO_LOGIN_ROUTES: string[] = ['/home/example/public'];
|
||||
|
||||
export const ROUTE: RouteRecordRaw[] = [
|
||||
{
|
||||
name: 'ExampleAddonRoot',
|
||||
path: '/home/example',
|
||||
meta: { title: '示例插件', app: 'home' },
|
||||
component: () => import('#/layouts/app/home.vue'),
|
||||
children: [
|
||||
{
|
||||
name: 'ExampleAddonPublic',
|
||||
path: 'public',
|
||||
meta: { title: '公开页面' },
|
||||
component: () => import('../views/public.vue'),
|
||||
},
|
||||
{
|
||||
name: 'ExampleAddonDashboard',
|
||||
path: 'dashboard',
|
||||
meta: { title: '插件仪表盘' },
|
||||
component: () => import('../views/dashboard.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default ROUTE;
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<h3>示例插件 - 仪表盘(需登录)</h3>
|
||||
<p>登录后访问:/home/example/dashboard</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<h3>示例插件 - 公开页面(免登录)</h3>
|
||||
<p>这是一个无需登录即可访问的示例页面:/home/example/public</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
Reference in New Issue
Block a user