This commit is contained in:
zhangzhiqiang_z 2024-01-02 16:18:27 +08:00
parent 633f2e7dc4
commit 7cedf9a874
58 changed files with 3127 additions and 0 deletions

37
cms/Addon.php Normal file
View File

@ -0,0 +1,37 @@
<?php
namespace addon\cms;
/**
* 插件安装之后单独的插件方法
* Class Manage
* @package addon\cms
*/
class Addon
{
/**
* 插件安装执行
*/
public function install()
{
return true;
}
/**
* 插件卸载执行
*/
public function uninstall()
{
return true;
}
/**
* 插件升级执行
*/
public function upgrade()
{
return true;
}
}

102
cms/admin/api/article.ts Normal file
View 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});
}

View File

@ -0,0 +1,17 @@
{
"name": "栏目名称",
"sort": "排序",
"isShow": "是否显示",
"namePlaceholder": "请输入栏目名称",
"sortPlaceholder": "请输入排序",
"isShowPlaceholder": "是否显示",
"addArticleCategory": "添加栏目",
"updateArticleCategory": "编辑栏目",
"articleCategoryDeleteTips": "确定要删除该栏目吗?",
"nameMax": "名称不能超过20个字符",
"sortNumber": "排序号必须是数字",
"sortBetween": "排序号不能超过10000",
"show": "显示",
"hide": "不显示",
"articleNumber": "文章数量"
}

View File

@ -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": "未读取到文章信息!"
}

View File

@ -0,0 +1,21 @@
{
"categoryName": "栏目",
"ID": "ID",
"title": "标题",
"intro": "简介",
"summary": "摘要",
"image": "封面",
"author": "作者",
"content": "文章内容",
"visit": "浏览量",
"visitVirtual": "初始浏览量",
"isShow": "是否显示",
"sort": "排序",
"createTime": "创建时间",
"updateTime": "更新时间",
"addArticle": "添加文章",
"updateArticle": "编辑文章",
"titlePlaceholder": "请输入文章标题",
"categoryIdPlaceholder": "请选择文章栏目",
"articleDeleteTips": "确定要删除该文章吗?"
}

View File

@ -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-[20px]">{{ 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="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 '@/cms/api/article'
import { ElMessageBox, FormInstance } from 'element-plus'
import EditCategory from '@/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>

View File

@ -0,0 +1,135 @@
<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 '@/cms/api/article'
let popTitle: string = '';
let 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, callback: any) => {
if (value === "" || isNaN(value)) {
callback(new Error(t('sortNumber')))
}
if (parseInt(value) > 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
let save = formData.category_id ? editArticleCategory : addArticleCategory
await formEl.validate(async (valid) => {
if (valid) {
loading.value = true
let data = formData
save(data).then(res => {
loading.value = false
showDialog.value = false
emit('complete')
}).catch(err => {
loading.value = false
// showDialog.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>

View File

@ -0,0 +1,176 @@
<template>
<div class="main-container">
<div class="detail-head">
<div class="left" @click="router.push({ path: '/cms/article/list' })">
<span class="iconfont iconxiangzuojiantou !text-xs"></span>
<span class="ml-[1px]">{{t('returnToPreviousPage')}}</span>
</div>
<span class="adorn">|</span>
<span class="right">{{ pageName }}</span>
</div>
<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="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 in categoryList" />
</el-select>
</el-form-item>
<el-form-item :label="t('intro')" prop="intro">
<el-input v-model="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="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="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="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, watch } from 'vue'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { getArticleInfo, getArticleCategoryAll, addArticle, editArticle } from '@/cms/api/article'
import { useRoute, useRouter } from 'vue-router'
import useAppStore from '@/stores/modules/app'
import { ElMessage } from 'element-plus'
const route = useRoute()
const router = useRouter()
const id: number = parseInt(route.query.id || 0)
const loading = ref(false)
const categoryList = ref([])
const appStore = useAppStore()
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(/&nbsp;/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></style>

View File

@ -0,0 +1,181 @@
<template>
<div class="main-container">
<el-card class="box-card !border-none" shadow="never">
<div class="flex justify-between items-center">
<span class="text-[20px]">{{ 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 in categoryList" />
</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 '@/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>

View File

@ -0,0 +1,177 @@
<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] article-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-press-escape="false" :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('topRounded')">
<el-slider v-model="diyStore.editComponent.topElementRounded" show-input size="small" class="ml-[10px] graphic-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] graphic-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 '@/cms/api/article'
const diyStore = useDiyStore()
diyStore.editComponent.ignore = []; //
//
diyStore.editComponent.verify = (index: number) => {
var 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">
.article-slider {
.el-slider__input {
width: 100px;
}
}
</style>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,108 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace addon\cms\app\adminapi\controller\article;
use addon\cms\app\service\admin\article\ArticleService;
use core\base\BaseAdminController;
use think\Response;
/**
* 文章控制器
* Class CmsArticle
* @package app\adminapi\controller\article
*/
class Article extends BaseAdminController
{
/**
* 文章列表
* @return Response
*/
public function lists()
{
$data = $this->request->params([
[ 'title', '' ],
[ 'category_id', '' ],
[ 'sort', '' ],
[ 'is_show', '' ],
]);
return success(( new ArticleService() )->getPage($data));
}
/**
* 文章详情
* @param int $id
* @return Response
*/
public function info(int $id)
{
return success(( new ArticleService() )->getInfo($id));
}
/**
* 添加文章
* @return Response
*/
public function add()
{
$data = $this->request->params([
[ 'title', '' ],
[ 'category_id', '' ],
[ 'intro', '' ],
[ 'summary', '' ],
[ 'image', '' ],
[ 'author', '' ],
[ 'content', '', false ],
[ 'visit_virtual', 0 ],
[ 'is_show', 1 ],
[ 'sort', 0 ],
]);
$this->validate($data, 'addon\cms\app\validate\article\Article.add');
$id = ( new ArticleService() )->add($data);
return success('ADD_SUCCESS', [ 'id' => $id ]);
}
/**
* 文章编辑
* @param int $id
* @return Response
*/
public function edit(int $id)
{
$data = $this->request->params([
[ 'title', '' ],
[ 'category_id', '' ],
[ 'intro', '' ],
[ 'summary', '' ],
[ 'image', '' ],
[ 'author', '' ],
[ 'content', '', false ],
[ 'visit_virtual', 0 ],
[ 'is_show', 1 ],
[ 'sort', 0 ],
]);
$this->validate($data, 'addon\cms\app\validate\article\Article.edit');
( new ArticleService() )->edit($id, $data);
return success('EDIT_SUCCESS');
}
/**
* 文章删除
* @param int $id
* @return Response
*/
public function del(int $id)
{
( new ArticleService() )->del($id);
return success('DELETE_SUCCESS');
}
}

View File

@ -0,0 +1,95 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace addon\cms\app\adminapi\controller\article;
use addon\cms\app\service\admin\article\ArticleCategoryService;
use core\base\BaseAdminController;
use think\Response;
class ArticleCategory extends BaseAdminController
{
/**
* 文章分类列表
* @return Response
*/
public function lists()
{
$data = $this->request->params([
[ 'name', '' ],
]);
return success(( new ArticleCategoryService() )->getPage($data));
}
/**
* 查询所有分类(文章添加,编辑,索引)
* @return Response
*/
public function all()
{
return success(( new ArticleCategoryService() )->getAll());
}
/**
* 文章分类详情
* @param int $id
* @return Response
*/
public function info(int $id)
{
return success(( new ArticleCategoryService() )->getInfo($id));
}
/**
* 添加文章分类
* @return Response
*/
public function add()
{
$data = $this->request->params([
[ 'name', '' ],
[ 'is_show', 1 ],
[ 'sort', 0 ],
]);
$this->validate($data, 'addon\cms\app\validate\article\ArticleCategory.add');
$id = ( new ArticleCategoryService() )->add($data);
return success('ADD_SUCCESS', [ 'id' => $id ]);
}
/**
* 文章分类编辑
* @param int $category_id //分类id
* @return Response
*/
public function edit(int $category_id)
{
$data = $this->request->params([
[ 'name', '' ],
[ 'is_show', 1 ],
[ 'sort', 0 ],
]);
$this->validate($data, 'addon\cms\app\validate\article\ArticleCategory.edit');
( new ArticleCategoryService() )->edit($category_id, $data);
return success('EDIT_SUCCESS');
}
/**
* 文章分类删除
* @param int $category_id
* @return Response
*/
public function del(int $category_id)
{
( new ArticleCategoryService() )->del($category_id);
return success('DELETE_SUCCESS');
}
}

View File

@ -0,0 +1,64 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
use think\facade\Route;
use app\adminapi\middleware\AdminCheckRole;
use app\adminapi\middleware\AdminCheckToken;
use app\adminapi\middleware\AdminLog;
/**
* 微官网
*/
Route::group('cms', function() {
/***************************************************** 文章管理 ****************************************************/
//文章列表
Route::get('article', 'addon\cms\app\adminapi\controller\article\Article@lists');
//文章详情
Route::get('article/:id', 'addon\cms\app\adminapi\controller\article\Article@info');
//添加文章
Route::post('article', 'addon\cms\app\adminapi\controller\article\Article@add');
//编辑文章
Route::put('article/:id', 'addon\cms\app\adminapi\controller\article\Article@edit');
//删除文章
Route::delete('article/:id', 'addon\cms\app\adminapi\controller\article\Article@del');
/***************************************************** 文章分类管理 ****************************************************/
//文章分类列表
Route::get('category', 'addon\cms\app\adminapi\controller\article\ArticleCategory@lists');
//所有文章分类
Route::get('category/all', 'addon\cms\app\adminapi\controller\article\ArticleCategory@all');
//文章分类详情
Route::get('category/:id', 'addon\cms\app\adminapi\controller\article\ArticleCategory@info');
//添加文章分类
Route::post('category', 'addon\cms\app\adminapi\controller\article\ArticleCategory@add');
//编辑文章分类
Route::put('category/:id', 'addon\cms\app\adminapi\controller\article\ArticleCategory@edit');
//删除文章分类
Route::delete('category/:category_id', 'addon\cms\app\adminapi\controller\article\ArticleCategory@del');
})->middleware([
AdminCheckToken::class,
AdminCheckRole::class,
AdminLog::class
]);

View File

@ -0,0 +1,59 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace addon\cms\app\api\controller\article;
use addon\cms\app\service\api\article\ArticleService;
use core\base\BaseApiController;
use think\Response;
/**
* 文章控制器
* Class CmsArticle
* @package app\api\controller\article
*/
class Article extends BaseApiController
{
/**
* 文章列表
* @return Response
*/
public function lists()
{
$data = $this->request->params([
[ 'title', '' ],
[ 'category_id', '' ],
]);
return success(( new ArticleService() )->getPage($data));
}
public function all()
{
$data = $this->request->params([
[ 'title', '' ],
[ 'category_id', '' ],
[ 'ids', [] ],
[ 'limit', 0 ]
]);
return success(( new ArticleService() )->getAll($data, $data[ 'limit' ]));
}
/**
* 文章详情
* @param int $id
* @return Response
*/
public function info(int $id)
{
return success(( new ArticleService() )->getInfo($id));
}
}

View File

@ -0,0 +1,48 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace addon\cms\app\api\controller\article;
use addon\cms\app\service\api\article\ArticleCategoryService;
use core\base\BaseApiController;
use think\Response;
/**
* 文章分类
* Class CmsArticleCategory
* @package app\api\controller\article
*/
class ArticleCategory extends BaseApiController
{
/**
* 文章分类列表
* @return Response
*/
public function lists()
{
$data = $this->request->params([
[ 'name', '' ],
]);
return success(( new ArticleCategoryService() )->getPage($data));
}
/**
* 文章分类详情
* @param int $id
* @return Response
*/
public function info(int $id)
{
return success(( new ArticleCategoryService() )->getInfo($id));
}
}

View File

@ -0,0 +1,44 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
use app\api\middleware\ApiCheckToken;
use app\api\middleware\ApiLog;
use app\api\middleware\ApiChannel;
use think\facade\Route;
/**
* 微官网
*/
Route::group('cms', function() {
/***************************************************** 文章管理 ****************************************************/
//文章列表
Route::get('article', 'addon\cms\app\api\controller\article\Article@lists');
//文章详情
Route::get('article/:id', 'addon\cms\app\api\controller\article\Article@info');
//文章
Route::get('article/all', 'addon\cms\app\api\controller\article\Article@all');
/***************************************************** 文章分类管理 ****************************************************/
//文章分类列表
Route::get('category', 'addon\cms\app\api\controller\article\ArticleCategory@lists');
//文章分类详情
Route::get('category/:id', 'addon\cms\app\api\controller\article\ArticleCategory@info');
})->middleware(ApiChannel::class)
->middleware(ApiCheckToken::class, false) //false表示不验证登录
->middleware(ApiLog::class);

View File

@ -0,0 +1,23 @@
<?php
return [
'BASIC' => [
'title' => get_lang('dict_diy.component_type_basic'),
'list' => [
'Article' => [
'title' => '文章',
'icon' => 'iconfont-iconwenzhang',
'path' => 'edit-article',
'support_page' => [],
'uses' => 0,
'sort' => 10007,
'value' => [
'sources' => 'initial',
'count' => 8,
'articleIds' => []
],
],
],
],
];

View File

@ -0,0 +1,18 @@
<?php
return [
'CMS_LINK' => [
'key' => 'cms',
'addon_title' => get_lang('dict_diy.cms_title'),
'title' => get_lang('dict_diy.cms_link'),
'child_list' => [
[
'name' => 'ARTICLE_LIST',
'title' => get_lang('dict_diy.cms_link_article_list'),
'url' => '/cms/pages/list',
'is_share' => 1,
'action' => ''
],
]
],
];

View File

@ -0,0 +1,4 @@
<?php
return [
];

View File

@ -0,0 +1,73 @@
<?php
return [
[
'menu_name' => '微官网',
'menu_key' => 'cms',
'menu_type' => 0,
'icon' => 'element-Tickets',
'api_url' => '',
'router_path' => 'cms',
'view_path' => '',
'methods' => '',
'sort' => 100,
'status' => 1,
'is_show' => 1,
'children' => [
[
'menu_name' => '文章管理',
'menu_short_name' => '文章',
'menu_key' => 'cms_article',
'menu_type' => 0,
'icon' => 'iconfont-iconwenzhangguanli1',
'api_url' => '',
'router_path' => 'article',
'view_path' => '',
'methods' => '',
'sort' => 98,
'status' => 1,
'is_show' => 1,
'children' => [
[
'menu_name' => '文章列表',
'menu_key' => 'cms_article_list',
'menu_type' => 1,
'icon' => 'element-ChatDotSquare',
'api_url' => 'cms/article',
'router_path' => 'list',
'view_path' => 'article/list',
'methods' => 'get',
'sort' => 100,
'status' => 1,
'is_show' => 1,
],
[
'menu_name' => '文章添加/编辑',
'menu_key' => 'cms_article_edit',
'menu_type' => 1,
'icon' => '',
'api_url' => 'cms/article',
'router_path' => 'edit',
'view_path' => 'article/edit',
'methods' => 'post',
'sort' => 90,
'status' => 1,
'is_show' => 0,
],
[
'menu_name' => '文章栏目',
'menu_key' => 'cms_article_category',
'menu_type' => 1,
'icon' => 'element-CollectionTag',
'api_url' => 'article/category',
'router_path' => 'category',
'view_path' => 'article/category',
'methods' => 'get',
'sort' => 80,
'status' => 1,
'is_show' => 1,
],
]
]
]
]
];

7
cms/app/event.php Normal file
View File

@ -0,0 +1,7 @@
<?php
return [
'listen' => [
'WapIndex' => [ 'addon\cms\app\listener\WapIndexListener' ],
]
];

14
cms/app/lang/en/api.php Normal file
View File

@ -0,0 +1,14 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
return [
];

20
cms/app/lang/en/dict.php Normal file
View File

@ -0,0 +1,20 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
return [
'dict_menu_admin' => [
'article' => 'article',
'article_list' => 'article list',
'article_edit' => 'article edit',
'article_category' => 'article category',
],
];

View File

@ -0,0 +1,27 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
return [
'validate_article' => [
'title_require' => 'title is require',
'title_max' => 'title must not be exceed 20 points',
'intro_max' => 'intro must not be exceed 50 points',
'summary_max' => 'summary must not be exceed 50 points',
'image_max' => 'image is exceed max',
'author_max' => 'author must not be exceed 20 points',
'is_show_number' => 'is_show must be a number',
'is_show_between' => 'is_show must be 0 or 1',
'sort_number' => 'sort must be a number',
'sort_between' => 'sort must not be exceed 10000',
'cate_name_require' => 'cate_name is require',
'cate_name_max' => 'cate_name must not be exceed 120 points',
]
];

View File

@ -0,0 +1,14 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
return [
];

View File

@ -0,0 +1,22 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
return [
'dict_diy' => [
'cms_title' => '微官网',
'cms_link' => '微官网链接',
'cms_link_article_list' => '文章资讯',
],
'dict_wap_index' => [
'cms' => '微官网',
'cms_desc' => '文章栏目管理',
],
];

View File

@ -0,0 +1,30 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
return [
'validate_article' => [
'title_require' => '文章标题必须填写',
'title_max' => '文章标题不能超过20个字符',
'intro_max' => '文章简介不能超过50个字符',
'summary_max' => '文章摘要不能超过50个字符',
'image_max' => '图片路径太长',
'author_max' => '文章作者不能超过20个字符',
'is_show_number' => '是否显示必须是数字',
'is_show_between' => '是否显示只能是0或者1',
'sort_number' => '排序号必须是数字',
'sort_between' => '排序号不能超过10000',
'cate_name_require' => '栏目名称必须填写',
'cate_name_max' => '栏目不能超过20个字符',
'category_id_require' => '文章栏目必须填写',
'category_id_num' => '文章栏目必须是整数',
'content_require' => '文章内容必须填写',
],
];

View File

@ -0,0 +1,31 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace addon\cms\app\listener;
/**
* 手机端首页加载事件
*/
class WapIndexListener
{
public function handle()
{
return [
[
'key' => 'cms',
"title" => get_lang("dict_wap_index.cms"),
'desc' => get_lang("dict_wap_index.cms_desc"),
"url" => "/cms/pages/list",
'icon'=>'addon/cms/icon.png'
],
];
}
}

View File

@ -0,0 +1,139 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace addon\cms\app\model\article;
use app\dict\sys\FileDict;
use core\base\BaseModel;
use think\db\Query;
use think\model\relation\HasOne;
/**
* 文章模型
* Class CmsArticle
* @package app\model\article
*/
class CmsArticle extends BaseModel
{
/**
* 数据表主键
* @var string
*/
protected $pk = 'id';
/**
* 模型名称
* @var string
*/
protected $name = 'cms_article';
/**
* @return HasOne
*/
public function cmsArticleCategory()
{
return $this->hasOne(CmsArticleCategory::class, 'category_id', 'category_id')->joinType('left')->withField('category_id, name')->bind([ 'category_name' => 'name' ]);
}
/**
* 文章分类搜索器
* @param $query
* @param $value
* @param $data
*/
public function searchCategoryIdAttr($query, $value, $data)
{
if ($value) {
$query->where('category_id', $value);
}
}
/**
* 文章标题搜索器
* @param $query
* @param $value
* @param $data
*/
public function searchTitleAttr($query, $value, $data)
{
if ($value) {
$query->where('title', 'like', '%' . $value . '%');
}
}
/**
* 文章标题搜索器
* @param $query
* @param $value
* @param $data
*/
public function searchIsShowAttr($query, $value, $data)
{
if ($value != '') {
$query->where('is_show', $value);
}
}
public function searchIdsAttr(Query $query, $value, $data)
{
if (!empty($value)) {
$query->whereIn('id', $data[ 'ids' ]);
}
}
/**
* 文章标题搜索器
* @param $query
* @param $value
* @param $data
*/
public function searchSortAttr($query, $value, $data)
{
if ($value) {
$query->where('sort', $value);
}
}
public function getArticleUrlAttr($value, $data)
{
$wap_domain = !empty(env("system.wap_domain")) ? preg_replace('#/$#', '', env("system.wap_domain")) : request()->domain();
$web_domain = !empty(env("system.web_domain")) ? preg_replace('#/$#', '', env("system.web_domain")) : request()->domain();
return [
'wap_url' => $wap_domain . "/wap/cms/pages/detail?id={$data['id']}",
'web_url' => $web_domain . "/web/article/detail?id={$data['id']}"
];
}
public function getImageThumbBigAttr($value, $data)
{
if ($data[ 'image' ] != '') {
return get_thumb_images($data[ 'image' ], FileDict::BIG);
}
}
public function getImageThumbMidAttr($value, $data)
{
if ($data[ 'image' ] != '') {
return get_thumb_images($data[ 'image' ], FileDict::MID);
}
}
public function getImageThumbSmallAttr($value, $data)
{
if ($data[ 'image' ] != '') {
return get_thumb_images($data[ 'image' ], FileDict::SMALL);
}
}
}

View File

@ -0,0 +1,54 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace addon\cms\app\model\article;
use core\base\BaseModel;
/**
* 文章栏目模型
* Class CmsArticleCategory
* @package app\model\article
*/
class CmsArticleCategory extends BaseModel
{
/**
* 数据表主键
* @var string
*/
protected $pk = 'category_id';
/**
* 模型名称
* @var string
*/
protected $name = 'cms_article_category';
/**
* 文章分类名称搜索器
* @param $query
* @param $value
* @param $data
*/
public function searchNameAttr($query, $value, $data)
{
if ($value) {
$query->where([ [ 'name', 'like', "%$value%" ] ]);
}
}
public function getArticleNumAttr($value, $data)
{
return ( new CmsArticle() )->where([ [ 'category_id', '=', $data[ 'category_id' ] ] ])->count();
}
}

View File

@ -0,0 +1,104 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace addon\cms\app\service\admin\article;
use addon\cms\app\model\article\CmsArticleCategory;
use core\base\BaseAdminService;
/**
* 文章分类(栏目)服务层
* Class ArticleService
* @package app\service\admin\article
*/
class ArticleCategoryService extends BaseAdminService
{
public function __construct()
{
parent::__construct();
$this->model = new CmsArticleCategory();
}
/**
* 获取文章分类列表
* @param array $where
* @return array
*/
public function getPage(array $where = [])
{
$field = 'category_id, site_id, name, sort, is_show, create_time, update_time';
$order = 'create_time desc';
$search_model = $this->model->where([['site_id', '=', $this->site_id]])->withSearch(['name'], $where)->field($field)->order($order)->append(["article_num"]);
return $this->pageQuery($search_model);
}
/**
* 查询所有分类(文章添加)
*/
public function getAll()
{
$field = 'category_id, site_id, name, sort';
$order = 'sort desc';
return $this->model->where([['site_id', '=', $this->site_id], ['is_show', '=', 1]])->field($field)->order($order)->select()->toArray();
}
/**
* 获取文章分类信息
* @param int $id
* @return array
*/
public function getInfo(int $id)
{
$field = 'category_id, site_id, name, sort, is_show, create_time, update_time';
return $this->model->field($field)->where([['category_id', '=', $id], ['site_id', '=', $this->site_id]])->append(["article_num"])->findOrEmpty()->toArray();
}
/**
* 添加文章分类
* @param array $data
* @return mixed
*/
public function add(array $data)
{
$data['site_id'] = $this->site_id;
$res = $this->model->create($data);
return $res->category_id;
}
/**
* 文章分类编辑
* @param int $id
* @param array $data
* @return true
*/
public function edit(int $id, array $data)
{
$data['update_time'] = time();
$this->model->where([['category_id', '=', $id], ['site_id', '=', $this->site_id]])->update($data);
return true;
}
/**
* 删除文章分类
* @param int $id
* @return bool
*/
public function del(int $id)
{
return $this->model->where([['category_id', '=', $id], ['site_id', '=', $this->site_id]])->delete();
}
}

View File

@ -0,0 +1,94 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace addon\cms\app\service\admin\article;
use addon\cms\app\model\article\CmsArticle;
use core\base\BaseAdminService;
/**
* 文章服务层
* Class ArticleService
* @package app\service\admin\article
*/
class ArticleService extends BaseAdminService
{
public function __construct()
{
parent::__construct();
$this->model = new CmsArticle();
}
/**
* 获取文章列表
* @param array $where
* @return array
*/
public function getPage(array $where = [])
{
$where[] = [ 'site_id', '=', $this->site_id ];
$field = 'id, category_id, site_id, title, intro, summary, image, author, content, visit, visit_virtual, is_show, sort, create_time, update_time';
$order = 'create_time desc';
$search_model = $this->model->where([ [ 'site_id', '=', $this->site_id ] ])->withSearch([ 'title', 'category_id', 'is_show'], $where)->with('articleCategory')->field($field)->order($order)->append(['article_url','image_thumb_small']);
return $this->pageQuery($search_model);
}
/**
* 获取文章信息
* @param int $id
* @return array
*/
public function getInfo(int $id)
{
$field = 'id, category_id, site_id, title, intro, summary, image, author, content, visit, visit_virtual, is_show, sort, create_time, update_time';
return $this->model->where([ [ 'id', '=', $id ], [ 'site_id', '=', $this->site_id ] ])->with('articleCategory')->field($field)->append(['image_thumb_small'])->findOrEmpty()->toArray();
}
/**
* 添加文章
* @param array $data
* @return mixed
*/
public function add(array $data)
{
$data[ 'site_id' ] = $this->site_id;
$data[ 'create_time' ] = time();
$res = $this->model->create($data);
return $res->id;
}
/**
* 文章编辑
* @param int $id
* @param array $data
* @return true
*/
public function edit(int $id, array $data)
{
$data[ 'update_time' ] = time();
$this->model->where([ [ 'id', '=', $id ], [ 'site_id', '=', $this->site_id ] ])->update($data);
return true;
}
/**
* 删除文章
* @param int $id
* @return bool
*/
public function del(int $id)
{
return $this->model->where([ [ 'id', '=', $id ], [ 'site_id', '=', $this->site_id ] ])->delete();
}
}

View File

@ -0,0 +1,53 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace addon\cms\app\service\api\article;
use addon\cms\app\model\article\CmsArticleCategory;
use core\base\BaseApiService;
/**
* 文章分类(栏目)服务层
* Class ArticleService
* @package app\service\api\article
*/
class ArticleCategoryService extends BaseApiService
{
public function __construct()
{
parent::__construct();
$this->model = new CmsArticleCategory();
}
/**
* 获取文章分类列表
* @param array $where
* @return array
*/
public function getPage(array $where = [])
{
$field = 'category_id, site_id, name, sort, is_show, create_time, update_time';
$order = 'create_time desc';
$search_model = $this->model->where([['site_id', '=', $this->site_id]])->withSearch(['name'], $where)->field($field)->order($order);
return $this->pageQuery($search_model);
}
/**
* 获取文章分类信息
* @param int $id
* @return array
*/
public function getInfo(int $id)
{
$field = 'category_id, site_id, name, sort, is_show, create_time, update_time';
return $this->model->field($field)->where([['category_id', '=', $id], ['site_id', '=', $this->site_id]])->findOrEmpty()->toArray();
}
}

View File

@ -0,0 +1,76 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace addon\cms\app\service\api\article;
use addon\cms\app\model\article\CmsArticle;
use core\base\BaseApiService;
use think\db\exception\DataNotFoundException;
use think\db\exception\DbException;
use think\db\exception\ModelNotFoundException;
/**
* 文章服务层
* Class ArticleService
* @package app\service\api\article
*/
class ArticleService extends BaseApiService
{
public function __construct()
{
parent::__construct();
$this->model = new CmsArticle();
}
/**
* 获取文章列表
* @param array $where
* @return array
*/
public function getPage(array $where = [])
{
$where[] = [ 'site_id', '=', $this->site_id ];
$field = 'id, category_id, site_id, title, intro, summary, image, author, content, visit, visit_virtual, is_show, sort, create_time, update_time';
$order = 'create_time desc';
$search_model = $this->model->where([ [ 'site_id', '=', $this->site_id ] ])->withSearch([ 'title', 'category_id'], $where)->with('articleCategory')->field($field)->order($order)->append(['image_thumb_mid']);
return $this->pageQuery($search_model);
}
/**
* 文章列表
* @param array $where
* @param int $limit
* @return array
* @throws DataNotFoundException
* @throws DbException
* @throws ModelNotFoundException
*/
public function getAll(array $where = [], int $limit = 0){
$where[] = [ 'site_id', '=', $this->site_id ];
$field = 'id, category_id, site_id, title, intro, summary, image, author, content, visit, visit_virtual, is_show, sort, create_time, update_time';
$order = 'create_time desc';
return $this->model->where([ [ 'site_id', '=', $this->site_id ] , ['is_show', '=', 1]])->withSearch([ 'title', 'category_id', 'ids' ], $where)->limit($limit)->with('articleCategory')->field($field)->append(['image_thumb_mid'])->order($order)->select()->toArray();
}
/**
* 获取文章信息
* @param int $id
* @return array
*/
public function getInfo(int $id)
{
$field = 'id, category_id, site_id, title, intro, summary, image, author, content, visit, visit_virtual, is_show, sort, create_time, update_time';
return $this->model->with('articleCategory')->field($field)->where([ [ 'id', '=', $id ], [ 'site_id', '=', $this->site_id ] ])->append(['image_thumb_big'])->findOrEmpty()->toArray();
}
}

View File

@ -0,0 +1,57 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace addon\cms\app\validate\article;
use think\Validate;
/**
* Class CmsArticle
* @package app\validate\article
*/
class Article extends Validate
{
//用户名或密码的规范可能是从数据库中获取的
protected $rule = [
'title' => 'require|max:20',
'intro' => 'max:50',
'summary' => 'max:50',
'image' => 'max:100',
'author' => 'max:20',
'is_show' => 'number|between:0,1',
'sort' => 'number|between:0,10000',
'category_id' => 'number|require',
'content' => 'require',
];
protected $message = [
'title.require' => 'validate_article.title_require',
'title.max' => 'validate_article.title_max',
'intro.max' => 'validate_article.intro_max',
'summary.max' => 'validate_article.summary_max',
'image.max' => 'validate_article.image_max',
'author.max' => 'validate_article.author_max',
'is_show.number' => 'validate_article.is_show_number',
'is_show.between' => 'validate_article.is_show_between',
'sort.number' => 'validate_article.sort_number',
'sort.between' => 'validate_article.sort_between',
'category_id.require' => 'validate_article.category_id_require',
'category_id.number' => 'validate_article.category_id_number',
'content.require' => 'validate_article.content_require',
];
protected $scene = [
'add' => ['title', 'intro', 'summary', 'image', 'author', 'is_show', 'sort', 'content', 'category_id'],
'edit' => ['title', 'intro', 'summary', 'image', 'author', 'is_show', 'sort', 'content', 'category_id'],
];
}

View File

@ -0,0 +1,45 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace addon\cms\app\validate\article;
use think\Validate;
/**
* 文章分类(栏目)验证
* Class CmsArticle
* @package app\validate\article
*/
class ArticleCategory extends Validate
{
//用户名或密码的规范可能是从数据库中获取的
protected $rule = [
'name' => 'require|max:20',
'is_show' => 'number|between:0,1',
'sort' => 'number|between:0,10000'
];
protected $message = [
'name.require' => 'validate_article.cate_name_require',
'name.max' => 'validate_article.cate_name_max',
'is_show.number' => 'validate_article.is_show_number',
'is_show.between' => 'validate_article.is_show_between',
'sort.number' => 'validate_article.sort_number',
'sort.between' => 'validate_article.sort_between',
];
protected $scene = [
'add' => ['name', 'is_show', 'sort'],
'edit' => ['name', 'is_show', 'sort'],
];
}

9
cms/info.json Normal file
View File

@ -0,0 +1,9 @@
{
"title": "微官网",
"desc": "文章栏目管理",
"key": "cms",
"version": "1.0.2",
"author": "niucloud",
"type": "app",
"support_app": ""
}

View File

@ -0,0 +1,19 @@
<?php
return [
'pages' => <<<EOT
// PAGE_BEGIN
{
"path": "{{addon_name}}/pages/list",
"style": {
"navigationBarTitleText": "%{{addon_name}}.pages.list%"
}
},
{
"path": "{{addon_name}}/pages/detail",
"style": {
"navigationBarTitleText": "%{{addon_name}}.pages.detail%"
}
}
// PAGE_END
EOT
];

BIN
cms/resource/cms.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
cms/resource/cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

BIN
cms/resource/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

39
cms/sql/install.sql Normal file
View File

@ -0,0 +1,39 @@
DROP TABLE IF EXISTS `{{prefix}}cms_article`;
CREATE TABLE `{{prefix}}cms_article` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '文章id',
site_id int NOT NULL DEFAULT 0,
`category_id` int(11) NOT NULL COMMENT '文章分类',
`title` varchar(255) NOT NULL COMMENT '文章标题',
`intro` varchar(255) NOT NULL DEFAULT '' COMMENT '简介',
`summary` varchar(255) NOT NULL DEFAULT '' COMMENT '文章摘要',
`image` varchar(128) NOT NULL DEFAULT '' COMMENT '文章图片',
`author` varchar(255) NOT NULL DEFAULT '' COMMENT '作者',
`content` text DEFAULT NULL COMMENT '文章内容',
`visit` int(11) NOT NULL DEFAULT '0' COMMENT '实际浏览量',
`visit_virtual` int(11) NOT NULL DEFAULT '0' COMMENT '初始浏览量',
`is_show` tinyint(4) NOT NULL DEFAULT '1' COMMENT '是否显示:1-是.0-否',
`sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序',
`create_time` int(11) NOT NULL DEFAULT '0' COMMENT '创建时间',
`update_time` int(11) NOT NULL DEFAULT '0' COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `IDX_article_category_id` (`category_id`),
KEY `IDX_article_create_time` (`create_time`),
KEY `IDX_article_is_show` (`is_show`),
KEY `IDX_ns_cms_article_sort` (`sort`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci COMMENT='文章表';
DROP TABLE IF EXISTS `{{prefix}}cms_article_category`;
CREATE TABLE `{{prefix}}cms_article_category` (
`category_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '文章分类id',
site_id int NOT NULL DEFAULT 0,
`name` varchar(255) NOT NULL DEFAULT '' COMMENT '分类名称',
`sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序',
`is_show` tinyint(4) NOT NULL DEFAULT '1' COMMENT '是否显示:1-是;0-否',
`create_time` int(11) NOT NULL DEFAULT '0' COMMENT '创建时间',
`update_time` int(11) NOT NULL DEFAULT '0' COMMENT '更新时间',
PRIMARY KEY (`category_id`),
KEY `create_time` (`create_time`),
KEY `is_show` (`is_show`),
KEY `sort` (`sort`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci COMMENT='文章分类表';

4
cms/sql/uninstall.sql Normal file
View File

@ -0,0 +1,4 @@
DROP TABLE IF EXISTS `{{prefix}}cms_article`;
DROP TABLE IF EXISTS `{{prefix}}cms_article_category`;

View File

@ -0,0 +1,29 @@
import request from '@/utils/request'
/**
*
*/
export function getArticleList(params: Record<string, any>) {
return request.get('cms/article', params)
}
/**
*
*/
export function getArticleAll(params: Record<string, any>) {
return request.get('cms/article/all', params)
}
/**
*
*/
export function getArticleDetail(id: number) {
return request.get(`cms/article/${id}`)
}
/**
*
*/
export function getArticleCategory() {
return request.get('cms/category')
}

View File

@ -0,0 +1,132 @@
<template>
<view :style="warpCss">
<view v-for="(item,index) in articleList" :key="item.id"
:class="['item flex align-center p-[20rpx]',{'border-solid border-t-0 border-l-0 border-r-0 border-b border-gray-200 mb-[20rpx]': articleList.length-1 !== index}] "
@click="toLink(item.id)" :style="itemCss">
<u--image width="260rpx" height="200rpx" :src="img(item.image)" v-if="item.image" model="aspectFill">
<template #error>
<u-icon name="photo" color="#999" size="50"></u-icon>
</template>
</u--image>
<view class="flex-1 flex flex-col justify-between ml-[20rpx]">
<view class="text-[32rpx] leading-[1.3] multi-hidden mt-[4rpx]">{{item.title}}</view>
<view class="text-[28rpx] using-hidden mb-[auto] mt-[20rpx] text-gray-500">{{item.summary}}</view>
<view class="text-[24rpx] text-gray-400 flex justify-between mt-[10rpx]">
<text>{{item.create_time}}</text>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
//
import { ref, computed, watch } from 'vue';
import { redirect, img } from '@/utils/common';
import useDiyStore from '@/app/stores/diy';
import { getArticleAll } from '@/cms/api/article';
const props = defineProps(['component', 'index', 'pullDownRefreshCount']);
const diyStore = useDiyStore();
const articleList = ref<Array<any>>([]);
const diyComponent = computed(() => {
if (diyStore.mode == 'decorate') {
return diyStore.value[props.index];
} else {
return props.component;
}
})
const warpCss = computed(() => {
var style = '';
if (diyComponent.value.componentBgColor) style += 'background-color:' + diyComponent.value.componentBgColor + ';';
if (diyComponent.value.topRounded) style += 'border-top-left-radius:' + diyComponent.value.topRounded * 2 + 'rpx;';
if (diyComponent.value.topRounded) style += 'border-top-right-radius:' + diyComponent.value.topRounded * 2 + 'rpx;';
if (diyComponent.value.bottomRounded) style += 'border-bottom-left-radius:' + diyComponent.value.bottomRounded * 2 + 'rpx;';
if (diyComponent.value.bottomRounded) style += 'border-bottom-right-radius:' + diyComponent.value.bottomRounded * 2 + 'rpx;';
return style;
})
const itemCss = computed(() => {
var style = '';
if (diyComponent.value.elementBgColor) style += 'background-color:' + diyComponent.value.elementBgColor + ';';
if (diyComponent.value.topElementRounded) style += 'border-top-left-radius:' + diyComponent.value.topElementRounded * 2 + 'rpx;';
if (diyComponent.value.topElementRounded) style += 'border-top-right-radius:' + diyComponent.value.topElementRounded * 2 + 'rpx;';
if (diyComponent.value.bottomElementRounded) style += 'border-bottom-left-radius:' + diyComponent.value.bottomElementRounded * 2 + 'rpx;';
if (diyComponent.value.bottomElementRounded) style += 'border-bottom-right-radius:' + diyComponent.value.bottomElementRounded * 2 + 'rpx;';
return style;
})
watch(
() => props.pullDownRefreshCount,
(newValue, oldValue) => {
//
}
)
const getArticleListFn = () => {
interface dataStructure {
ids ?: Array<number>,
limit ?: number
}
let data : dataStructure = {};
if (diyComponent.value.sources == "diy")
data.ids = diyComponent.value.articleIds;
else
data.limit = diyComponent.value.count;
interface takeDataStructure {
data : Array<Object>,
msg : string,
code : number
}
getArticleAll(data).then((res : takeDataStructure) => {
articleList.value = res.data;
});
}
const refresh = () => {
if (diyStore.mode == 'decorate') {
let obj = {
image: "",
summary: "文章摘要",
title: "文章标题",
create_time: "2023-03-28 09:00:00"
};
articleList.value.push(obj);
articleList.value.push(obj);
} else {
getArticleListFn();
}
}
refresh();
const toLink = (id : string) => {
redirect({ url: '/cms/pages/detail', param: { id } })
}
</script>
<style lang="scss" scoped>
/* 单行超出隐藏 */
.using-hidden {
word-break: break-all;
text-overflow: ellipsis;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
white-space: break-spaces;
}
/* 多行超出隐藏 */
.multi-hidden {
word-break: break-all;
text-overflow: ellipsis;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
</style>

View File

@ -0,0 +1,2 @@
{
}

View File

@ -0,0 +1,4 @@
{
"pages.list": "资讯中心",
"pages.detail": "文章详情"
}

View File

@ -0,0 +1,5 @@
{
"detail": "文章详情",
"abstract": "摘要",
"loadingText": "正在加载"
}

View File

@ -0,0 +1,7 @@
{
"list": "文章列表",
"noData": "~ 暂无数据 ~",
"all": "全部",
"end": "-- 到底了 --",
"searchPlaceholder": "请输入搜索关键词"
}

View File

@ -0,0 +1,65 @@
<template>
<view class="bg-white">
<block v-if="!loading">
<view class="border-solid border-t-0 border-l-0 border-r-0 border-b-[1px] border-gray-200 p-[10px]">
<view class="text-[16px]">
{{articleDetail.title}}
</view>
<view class="flex align-center justify-between text-[12px] text-gray-400 mt-[15px]">
<text>{{articleDetail.create_time}}</text>
</view>
</view>
<view class="mx-[10px] my-[10px] bg-gray-100 p-[8px] text-[14px] rounded-[5px] leading-[1.3]">
{{t('abstract')}}{{articleDetail.summary}}
</view>
<view class="px-[10px] pd-[10px]">
<u-parse :content="articleDetail.content" :tagStyle="style"></u-parse>
</view>
</block>
<u-loading-page bg-color="rgb(248,248,248)" :loading="loading" fontSize="16" color="#333" :loadingText="t('loadingText')"></u-loading-page>
</view>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { t } from '@/locale'
import { getArticleDetail } from '@/cms/api/article';
import { useShare } from '@/hooks/useShare'
const { setShare, onShareAppMessage, onShareTimeline } = useShare()
onShareAppMessage()
onShareTimeline()
let articleDetail = ref<Array<any>>([]);
let loading = ref<boolean>(true);
let style = {
h2: 'margin-bottom: 15px;',
p: 'margin-bottom: 10px;line-height: 1.5;',
img: 'margin: 10px 0;',
};
onLoad((option) => {
loading.value = true;
getArticleDetail(option.id).then((res) => {
articleDetail.value = res.data;
loading.value = false;
let share = {
title: articleDetail.value.title,
desc: articleDetail.value.intro,
url: articleDetail.value.image
}
uni.setNavigationBarTitle({
title: articleDetail.value.title
})
setShare({
wechat: {
...share
},
weapp: {
...share
}
});
});
})
</script>
<style lang="scss" scoped></style>

164
cms/uni-app/pages/list.vue Normal file
View File

@ -0,0 +1,164 @@
<template>
<view class="bg-gray-100 min-h-[100vh]">
<view class="fixed top-0 inset-x-0 z-10">
<view class='p-[10px] bg-white border-solid border-t-0 border-l-0 border-r-0 border-b-[1px] border-gray-200'>
<u-search :placeholder="t('searchPlaceholder')" actionText :actionStyle="{'width':0,'margin':0}" v-model="articleTitle" @clickIcon="searchFn"></u-search>
</view>
<scroll-view :scroll-x="true" :enable-flex="true"
class="nav-list bg-white align-center px-[10px] box-border">
<view class="flex scroll-view-wrap">
<view
:class="['nav-item text-[14px] mx-[5px] h-[30px] leading-[30px] my-[5px] border-t-0 border-l-0 border-r-0',{'border-solid border-b-[2px] active': currCategoryId==item.category_id}]"
@click="loadCategory(item.category_id)" v-for="(item,index) in categoryList"
:key="item.category_id">
{{item.name}}
</view>
</view>
</scroll-view>
</view>
<mescroll-body ref="mescrollRef" @init="mescrollInit" top="220rpx" @down="downCallback" @up="getArticleListFn">
<view v-for="(item,index) in articleList" :key="item.id"
:class="['bg-white flex align-center p-[10px]',{'border-solid border-t-0 border-l-0 border-r-0 border-b-[1px] border-gray-200': articleList.length-1 !== index}] "
@click="toLink(item.id)">
<u--image width="174rpx" height="174rpx" :src="img(item.image)" model="aspectFill">
<template #error>
<u-icon name="photo" color="#999" size="50"></u-icon>
</template>
</u--image>
<view class="flex-1 flex flex-col justify-between ml-[10px]">
<view class="text-[16px] leading-[1.3] multi-hidden mt-[2px]">{{item.title}}</view>
<view class="text-[14px] using-hidden mb-[10px] mt-[10px] text-gray-500">{{item.summary}}</view>
<view class="text-[12px] text-gray-400 flex justify-between mb-[5px]">
<text class="">{{item.create_time}}</text>
</view>
</view>
</view>
<mescroll-empty v-if="!articleList.length && loading"></mescroll-empty>
</mescroll-body>
<tabbar />
</view>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { t } from '@/locale'
import { redirect, img } from '@/utils/common';
import { getArticleList, getArticleCategory } from '@/cms/api/article';
import MescrollBody from '@/components/mescroll/mescroll-body/mescroll-body.vue';
import MescrollEmpty from '@/components/mescroll/mescroll-empty/mescroll-empty.vue';
import useMescroll from '@/components/mescroll/hooks/useMescroll.js';
import { onPageScroll, onReachBottom } from '@dcloudio/uni-app';
import { useShare } from '@/hooks/useShare'
const { mescrollInit, downCallback, getMescroll } = useMescroll(onPageScroll, onReachBottom);
const { setShare, onShareAppMessage, onShareTimeline } = useShare()
setShare()
onShareAppMessage()
onShareTimeline()
let categoryList = ref<Array<Object>>([]);
let articleList = ref<Array<any>>([]);
let currCategoryId = ref<number | string>('');
let articleTitle = ref<string>('');
let mescrollRef = ref(null);
let loading = ref<boolean>(false);
interface acceptingDataStructure {
data : acceptingDataItemStructure,
msg : string,
code : number
}
interface acceptingDataItemStructure {
data : object,
[propName : string] : number | string | object
}
onLoad(async () => {
await getArticleCategory().then((res : acceptingDataStructure) => {
const initData = { name: t("all"), category_id: '' };
categoryList.value.push(initData);
categoryList.value = categoryList.value.concat(res.data.data);
});
})
interface mescrollStructure {
num : number,
size : number,
endSuccess : Function,
[propName : string] : any
}
const getArticleListFn = (mescroll : mescrollStructure) => {
loading.value = false;
let data : object = {
category_id: currCategoryId.value,
title: articleTitle.value,
page: mescroll.num,
limit: mescroll.size
};
getArticleList(data).then((res : acceptingDataStructure) => {
let newArr = (res.data.data as Array<Object>);
//
if (mescroll.num == 1) {
articleList.value = []; //
}
articleList.value = articleList.value.concat(newArr);
mescroll.endSuccess(newArr.length);
loading.value = true;
}).catch(() => {
loading.value = true;
mescroll.endErr(); // ,
})
}
const loadCategory = (id : string) => {
currCategoryId.value = id;
getMescroll().resetUpScroll();
}
const searchFn = () => {
getMescroll().resetUpScroll();
}
const toLink = (id : string) => {
redirect({ url: '/cms/pages/detail', param: { id } })
}
onMounted(() => {
setTimeout(() => {
getMescroll().optUp.textNoMore = t("end");
}, 500)
});
</script>
<style lang="scss" scoped>
.nav-item.active {
color: $u-primary;
}
.scroll-view-wrap {
word-break: keep-all;
}
/* 单行超出隐藏 */
.using-hidden {
word-break: break-all;
text-overflow: ellipsis;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
white-space: break-spaces;
}
/* 多行超出隐藏 */
.multi-hidden {
word-break: break-all;
text-overflow: ellipsis;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
</style>

27
cms/web/api/article.ts Normal file
View File

@ -0,0 +1,27 @@
/**
*
*/
export function getArticleList(params: Record<string, any>) {
return request.get('cms/article', params)
}
/**
*
*/
export function getArticleAll(params: Record<string, any>) {
return request.get('cms/article/all', params)
}
/**
*
*/
export function getArticleDetail(id: number) {
return request.get(`cms/article/${id}`)
}
/**
*
*/
export function getArticleCategory() {
return request.get('cms/category')
}

View File

@ -0,0 +1,3 @@
{
"title": "文章"
}

View File

@ -0,0 +1,3 @@
{
"title": "文章"
}

View File

@ -0,0 +1,10 @@
{
"pages": {
"cms": {
"article": {
"list": "文章",
"detail": "文章"
}
}
}
}

View File

@ -0,0 +1,86 @@
<template>
<div class="w-full min-h-[100%] main-container pt-5">
<div class="mt-[20px] mb-[50px]" v-if="articleDetail">
<el-breadcrumb :separator-icon="ArrowRight">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item :to="{ path: '/cms/article/list' }">文章</el-breadcrumb-item>
<el-breadcrumb-item>{{ articleDetail.category_name }}</el-breadcrumb-item>
</el-breadcrumb>
<div>
<p class="py-[20px] text-center text-[24px]">{{ articleDetail.title }}</p>
<div class="flex justify-center">
<!-- <div class="mr-3 flex items-center text-gray-500 text-sm text-[#999]"><el-icon><View /></el-icon> <span class="ml-1">浏览量158</span></div> -->
<div class="mr-3 flex items-center text-gray-500 text-sm text-[#999]">
<el-icon>
<Clock />
</el-icon>
<span class="ml-1">时间{{ articleDetail.create_time }}</span>
</div>
</div>
<div class="mt-[50px]" v-html="articleDetail.content"></div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { getArticleDetail } from '@/cms/api/article'
import { ArrowRight } from '@element-plus/icons-vue'
import { nMounted } from 'vue';
import { useRoute } from 'vue-router';
const Route = useRoute(); //
const articleDetail = ref();
onMounted(() => {
obtainArticleInfo(Route.query.id)
});
const obtainArticleInfo = (id) => {
getArticleDetail(id).then(res => {
articleDetail.value = res.data;
})
}
</script>
<style lang="scss" scoped>
.index-carousel {
background-image: url('@/assets/images/index_carousel.png');
background-position: center center;
background-repeat: no-repeat;
background-size: cover;
}
.article-wrap {
span {
line-height: 1;
box-shadow: 0 0 5px var(--el-color-primary-light-7);
&.active {
background-image: linear-gradient(to right, var(--el-color-primary-light-5), var(--el-color-primary));
}
&:hover {
background-image: linear-gradient(to right, var(--el-color-primary-light-5), var(--el-color-primary));
color: #fff;
}
}
}
.tow-line-overflow {
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.text-color {
color: var(--el-color-primary);
}
.custom-tabs-label span {
font-size: 20px;
padding: 0px 10px;
}
</style>

View File

@ -0,0 +1,160 @@
<template>
<div class="w-full main-container pt-5">
<el-carousel height="350px" indicator-position="none" arrow="never">
<el-carousel-item>
<div class="h-full index-carousel"></div>
</el-carousel-item>
</el-carousel>
<div class="mt-[20px] mb-[50px]">
<div>
<div class="w-full">
<el-breadcrumb :separator-icon="ArrowRight">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item :to="{ path: '/cms/article/list' }">文章</el-breadcrumb-item>
<el-breadcrumb-item v-if="selectedCategoryName">{{ selectedCategoryName }}</el-breadcrumb-item>
</el-breadcrumb>
<div class="flex mt-[20px] items-start">
<div class="w-[50px]">类目</div>
<el-row>
<el-button class="mb-[10px]" @click="selectedCategory(categoryItem)" v-for="(categoryItem, categoryIndex) in activeCategoryLsit" :key="categoryIndex">{{ categoryItem.name }}</el-button>
</el-row>
</div>
<div class="article-list mb-[20px] cursor-pointer" v-for="(activeItem, activeIndex) in articleTableData.data" :key="activeIndex" @click="toLink(activeItem.id)">
<div class="flex justify-between relative py-[20px] border-b-1 border-gray-300 border-solid">
<div class="w-[150px] h-[150px] flex items-center">
<img :src="img(activeItem.image)" />
</div>
<div class="w-[1030px]">
<p class="text-xl font-bold">{{ activeItem.title }}</p>
<span class="overflow-ellipsis mt-2 mb-2 tow-line-overflow text-gray-500">{{ activeItem.intro }}</span>
</div>
<!-- <div class="activeBo flex items-right mt-2 justify-end absolute">
<span class="mr-5 text-sm text-gray-500">{{ activeItem.create_time }}</span>
<div class="mr-3 flex items-center text-gray-500 text-sm"><el-icon><View /></el-icon> <span class="ml-1">158</span></div>
<div class="mr-3 flex items-center text-gray-500 text-sm"><el-icon><Pointer /></el-icon> <span class="ml-1">22</span></div>
<div class="mr-3 flex items-center text-gray-500 text-sm"><el-icon><Star /></el-icon> <span class="ml-1">55</span></div>
<div class="flex items-center text-gray-500 text-sm"><el-icon><ChatDotRound /></el-icon> <span class="ml-1">655</span></div>
</div> -->
</div>
</div>
<el-pagination class="justify-center" @current-change="handleCurrentChange"
@size-change="handleSizeChange" :page-size="articleTableData.limit" background
layout="prev, pager, next" :total="articleTableData.total" />
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue'
import { getArticleCategory, getArticleList } from '@/cms/api/article'
import { ArrowRight } from '@element-plus/icons-vue'
import { useRouter } from 'vue-router';
const router = useRouter();
const activeCategoryLsit = ref([])
const selectedCategoryName = ref()
const articleTableData = reactive({
page: 1,
limit: 10,
total: 0,
loading: true,
data: [],
searchParam: {
title: '',
category_id: ''
}
})
/**
* 获取文章列表
*/
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 checkArticleCategory = () => {
getArticleCategory().then(res => {
activeCategoryLsit.value = res.data.data;
})
}
checkArticleCategory()
const selectedCategory = (item) => {
articleTableData.searchParam.category_id = item.category_id;
selectedCategoryName.value = item.name
}
const handleSizeChange = (val: number) => {
loadArticleList(val)
}
const handleCurrentChange = (val: number) => {
loadArticleList(val)
}
const toLink = (id) => {
router.push(`/cms/article/detail?id=${id}`)
}
</script>
<style lang="scss" scoped>
.index-carousel {
background-image: url('@/assets/images/index_carousel.png');
background-position: center center;
background-repeat: no-repeat;
background-size: cover;
}
.article-wrap {
span {
line-height: 1;
box-shadow: 0 0 5px var(--el-color-primary-light-7);
&.active {
background-image: linear-gradient(to right, var(--el-color-primary-light-5), var(--el-color-primary));
}
&:hover {
background-image: linear-gradient(to right, var(--el-color-primary-light-5), var(--el-color-primary));
color: #fff;
}
}
}
.tow-line-overflow {
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.text-color {
color: var(--el-color-primary);
}
.custom-tabs-label span {
font-size: 20px;
padding: 0px 10px;
}
.activeBo {
bottom: 20px;
right: 0px
}
</style>

10
cms/web/pages/routes.ts Normal file
View File

@ -0,0 +1,10 @@
export default [
{
path: "/cms/article/list",
component: () => import('~/cms/pages/article/list.vue')
},
{
path: "/cms/article/detail",
component: () => import('~/cms/pages/article/detail.vue')
}
]