# 一、企业网站后台内容管理(网站的新闻、产品、内容等等信息)表 news
# 1、表设计
企业网站后台内容管理(网站的新闻、产品、内容等等信息)表 news 字段设计(更多字段根据业务需求来扩展)
表名:news
字段名 数据类型 描述 空 默认值 字段含义
id int(20) 主键、自增长、UNSIGNED无符号 否 无 category_id int(20) 否 0 外键,关联哪个分类,属于哪个分类下面的信息内容 name varchar(100) 否 内容标题、新闻标题、产品名称等 enname varchar(255) 是 标题、名称等英文 status int(1) 是 1 内容显示状态 0不显示 1显示 等等状态 order int(11) 是 50 文章、内容排序,默认50 description varchar(255) 是 内容摘要,同时也是百度等搜索引擎抓取的描述信息 keywords varchar(255) 是 百度等搜素引擎的关键字 maincontent text 是 主体内容(可为空,比如当对产品描述时候仅仅说一下产品名称内容不想填) attachment varchar(255) 是 内容附件,比如:封面图、文件等等,或者指明文章出自哪个网址链接,一般是网址 lookcount int(11) 是 0 文章、内容点击量,默认0 manager_id int(20) 否 0 哪个发布的内容,一般关联管理员表,外键 timestamp datetime 否 CURRENT_TIMESTAMP 时间戳(可用于统计内容发布数量等) ... create_time datetime 否 CURRENT_TIMESTAMP 数据创建时间 update_time datetime 否 CURRENT_TIMESTAMP 数据更新时间 额外说明:
mysql每行最大只能存65535个字节。假设是utf-8编码,每个字符占3个字节。varchar存储最大字符数为(65535-2-1)/3=21844字符长度
# 2、创建迁移文件、执行迁移命令创建数据表 news
在数据库创建数据表的方式很多,在上面定义了表字段之后,可以使用
phpmyAdmin、数据库插件执行sql语句创建等等方式,但是建议大家通过:创建迁移文件、执行迁移命令创建数据表。涉及的知识点:
- 章节2:egg.js基础-五、eggjs项目中sequelize模型创建mysql数据库
创建迁移文件 命令:
npx sequelize migration:generate --name=init-news创建迁移文件:
'use strict'; /** @type {import('sequelize-cli').Migration} */ module.exports = { async up(queryInterface, Sequelize) { const { INTEGER, STRING, DATE, ENUM, TEXT, BIGINT } = Sequelize; // 创建表 --- 类似我们sql语句定义表结构 await queryInterface.createTable('news', { id: { type: INTEGER(20).UNSIGNED, primaryKey: true, autoIncrement: true, comment: '内容主键id' }, category_id: { type: INTEGER(20).UNSIGNED, allowNull: false, defaultValue: 0, comment: '分类id', references: { //关联关系 model: 'category', //关联的表 key: 'id' //关联表的主键 }, onDelete: 'cascade', //删除时操作 onUpdate: 'restrict', // 更新时操作 }, name: { type: STRING(100), allowNull: false, defaultValue: '', comment: '内容标题、新闻标题、产品名称等' }, enname: { type: STRING, allowNull: true, defaultValue: '', comment: '标题、名称等英文' }, status: { type: INTEGER(1), allowNull: false, defaultValue: 1, comment: '内容显示状态 0不显示 1显示 等等状态' }, order: { type: INTEGER,//不限定长度.默认int(11) allowNull: true, defaultValue: 50, comment: '文章、内容排序,默认50' }, description: { type: STRING,//不限定长度.默认varchar(255) allowNull: true, defaultValue: '', comment: '内容摘要' }, keywords: { type: STRING,//不限定长度.默认varchar(255) allowNull: true, defaultValue: '', comment: '关键字' }, maincontent: { type: TEXT, allowNull: true, defaultValue: '', comment: '主体内容(可为空,比如当对产品描述时候仅仅说一下产品名称内容不想填)' }, attachment: { type: STRING,//不限定长度.默认varchar(255) allowNull: true, defaultValue: '', comment: '内容附件,比如:封面图、文件等等,或者指明文章出自哪个网址链接,一般是网址' }, lookcount: { type: INTEGER, allowNull: true, defaultValue: 0, comment: '文章、内容点击量,默认0' }, manager_id: { type: INTEGER(20).UNSIGNED, allowNull: false, defaultValue: 0, comment: '哪个发布的内容,一般关联管理员表,外键', references: { //关联关系 model: 'manager', //关联的表 key: 'id' //关联表的主键 }, onDelete: 'cascade', //删除时操作 onUpdate: 'restrict', // 更新时操作 }, timestamp : { type: DATE, allowNull: false, defaultValue:Sequelize.fn('NOW'), comment: '时间戳(可用于统计内容发布数量等)', }, create_time: { type: DATE, allowNull: false, defaultValue: Sequelize.fn('NOW') }, update_time: { type: DATE, allowNull: false, defaultValue: Sequelize.fn('NOW') } }); }, async down(queryInterface, Sequelize) { await queryInterface.dropTable('news') } };执行迁移文件命令生成数据库表:
// 升级数据库-创建数据表 npx sequelize db:migrate // 如果有问题需要回滚,可以通过 `db:migrate:undo` 回退一个变更 npx sequelize db:migrate:undo // 可以通过 `db:migrate:undo:all` 回退到初始状态 npx sequelize db:migrate:undo:all
# 3、创建 news表 的模型
模型文件主要是用于处理数据库表的增删改查等操作
app/model/news.js
'use strict';
module.exports = app => {
const { INTEGER, STRING, DATE, ENUM, TEXT, BIGINT } = app.Sequelize;
const News = app.model.define('news', {
id: {
type: INTEGER(20).UNSIGNED,
primaryKey: true,
autoIncrement: true,
comment: '内容主键id'
},
category_id: {
type: INTEGER(20).UNSIGNED,
allowNull: false,
defaultValue: 0,
comment: '分类id',
references: { //关联关系
model: 'category', //关联的表
key: 'id' //关联表的主键
},
onDelete: 'cascade', //删除时操作
onUpdate: 'restrict', // 更新时操作
},
name: {
type: STRING(100),
allowNull: false,
defaultValue: '',
comment: '内容标题、新闻标题、产品名称等'
},
enname: {
type: STRING,
allowNull: true,
defaultValue: '',
comment: '标题、名称等英文'
},
status: {
type: INTEGER(1),
allowNull: false,
defaultValue: 1,
comment: '内容显示状态 0不显示 1显示 等等状态'
},
order: {
type: INTEGER,//不限定长度.默认int(11)
allowNull: true,
defaultValue: 50,
comment: '文章、内容排序,默认50'
},
description: {
type: STRING,//不限定长度.默认varchar(255)
allowNull: true,
defaultValue: '',
comment: '内容摘要'
},
keywords: {
type: STRING,//不限定长度.默认varchar(255)
allowNull: true,
defaultValue: '',
comment: '关键字'
},
maincontent: {
type: TEXT,
allowNull: true,
defaultValue: '',
comment: '主体内容(可为空,比如当对产品描述时候仅仅说一下产品名称内容不想填)'
},
attachment: {
type: STRING,//不限定长度.默认varchar(255)
allowNull: true,
defaultValue: '',
comment: '内容附件,比如:封面图、文件等等,或者指明文章出自哪个网址链接,一般是网址'
},
lookcount: {
type: INTEGER,
allowNull: true,
defaultValue: 0,
comment: '文章、内容点击量,默认0'
},
manager_id: {
type: INTEGER(20).UNSIGNED,
allowNull: false,
defaultValue: 0,
comment: '哪个发布的内容,一般关联管理员表,外键',
references: { //关联关系
model: 'manager', //关联的表
key: 'id' //关联表的主键
},
onDelete: 'cascade', //删除时操作
onUpdate: 'restrict', // 更新时操作
},
timestamp : {
type: DATE,
allowNull: false,
defaultValue:app.Sequelize.fn('NOW'),
get(){
let data = this.getDataValue('timestamp') ;
/*
//如果想转换成年月日时分秒,可以使用moment.js库等其他时间库
//我们这里带领大家回忆一下js基础,就手动拼接一下
let year = data.getFullYear();
let month = ("0" + (data.getMonth() + 1)).slice(-2);
let day = ("0" + data.getDate()).slice(-2);
let hours = ("0" + data.getHours()).slice(-2);
let minutes = ("0" + data.getMinutes()).slice(-2);
let seconds = ("0" + data.getSeconds()).slice(-2);
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
*/
//如果想转成时间戳
return (new Date(data)).getTime();
}
},
// sex: { type: ENUM, values: ['男','女','保密'], allowNull: true, defaultValue: '保密', comment: '留言用户性别'},
create_time: {
type: DATE,
allowNull: false,
defaultValue: app.Sequelize.fn('NOW'),
get() {
return app.formatTime(this.getDataValue('create_time'));
}
},
update_time: { type: DATE, allowNull: false, defaultValue: app.Sequelize.fn('NOW') }
});
// 模型关联关系
News.associate = function (models) {
// 关联分类 反向一对多(一个分类可以有多个新闻内容,分类对于新闻内容是一对多的关系,反过来新闻内容属于分类belongsTo,就是反向一对多)
News.belongsTo(app.model.Category);
// 关联管理员 反向一对多(一个管理员可以发布很多新闻,管理员对于新闻内容就是一对多的关系,反过来新闻内容属于管理员belongsTo,就是反向一对多)
News.belongsTo(app.model.Manager);
}
return News;
}
# 4、完成基本的新闻内容列表展示、及创建发布新闻内容界面
控制器
app/controller/admin/news.js
'use strict';
const Controller = require('egg').Controller;
class NewsController extends Controller {
//内容列表界面
async index() {
const { ctx, app } = this;
//分页:可以提炼成一个公共方法page(模型名称,where条件,其他参数options)
let data = await ctx.page('News',{},{
// 关联查询
include:[
{
model:app.model.Category,// 需要查询的模型
attributes:['id','name','enname'],// 查询的字段
},
{
model:app.model.Manager,// 需要查询的模型
attributes:['id','username','avatar'],// 查询的字段
},
],
});
data = JSON.parse(JSON.stringify(data));
// console.log(data);return;
//渲染公共模版
await ctx.renderTemplate({
title: '信息内容列表',//现在网页title,面包屑导航title,页面标题
data,
tempType: 'table', //模板类型:table表格模板 ,form表单模板
table: {
//表格上方按钮,没有不要填buttons
buttons: [
{
url: '/admin/news/create',//新增路径
desc: '发布内容',//新增 //按钮名称
// icon: 'fa fa-plus fa-lg',//按钮图标
}
],
//表头
columns: [
{
title: '标题/名称',
key: 'name',
class: 'text-left',//可选
},
{
title: '所属栏目',
// key: 'category_id',
class: 'text-center',//可选
render(item) {
return `
<span>${item.category.name}</span>
`;
}
},
{
title: '浏览量',
key: 'lookcount',
class: 'text-center',//可选
},
{
title: '哪个管理员发布的',
// key: 'username',
render(item) {
return `
<h2 class="table-avatar">
<a href="#" class="avatar avatar-sm mr-2">
<img
class="avatar-img rounded-circle"
src="${item.manager.avatar}"
alt="User Image"></a>
<a href="#"> ${item.manager.username}
<span>管理员id:${item.manager.id}</span></a>
</h2>
`;
},
},
{
title: '创建时间',
key: 'create_time',
width: 200,//可选
class: 'text-center',//可选
},
{
title: '操作',
class: 'text-right',//可选
action: {
//修改
edit: function (id) {
return `/admin/news/edit/${id}`;
},
//删除
delete: function (id) {
return `/admin/news/delete/${id}`;
}
}
},
],
},
});
}
//新增内容界面
async create() {
const { ctx, app } = this;
// 渲染模版前先拿到所有分类
let data = await ctx.service.category.dropdown_Categorylist();
console.log(data);
data.shift();//删除自定义的第一项
console.log(data);
//渲染公共模版
await ctx.renderTemplate({
title: '发布内容',//现在网页title,面包屑导航title,页面标题
tempType: 'form', //模板类型:table表格模板 ,form表单模板
form: {
//提交地址
action: "/admin/news/save",
// 字段
fields: [
{
label: '请选择一个分类栏目发布',
type: 'dropdown', //下拉框
name: 'category_id',
default: JSON.stringify(data),
placeholder: '请选择',
},
{
label: '标题/名称',
type: 'text',
name: 'name',
placeholder: '请输入标题/名称',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '标题/名称英文',
type: 'text',
name: 'enname',
placeholder: '请输入标题/名称英文,选填',
},
{
label: '内容摘要',
type: 'text',
name: 'description',
placeholder: '请输入内容摘要,选填',
},
{
label: '关键字',
type: 'text',
name: 'keywords',
placeholder: '请输入关键字(逗号隔开),选填',
},
{
label: '主体内容',
type: 'text',
name: 'maincontent',
placeholder: '请输入主体内容,选填',
},
{
label: '内容附件',
type: 'text',
name: 'attachment',
placeholder: '请输入附件如:封面图、文件、网址等,选填',
},
{
label: '点击量',
type: 'text',
name: 'lookcount',
default:0,
placeholder: '请输入点击量,选填,默认0',
},
{
label: '排序',
type: 'number',
name: 'order',
placeholder: '请输入排序(数字越小越排在前面)',
default:50,
},
{
label: '内容显示状态',
type: 'btncheck', //按钮组选择
name: 'status',
default: JSON.stringify([
{ value: 1, name: '显示', checked: true },
{ value: 0, name: '隐藏' },
]),
placeholder: '内容显示状态 0不显示 1显示 等等状态',
},
],
},
//新增成功之后跳转到哪个页面
successUrl: '/admin/news/index',
});
}
}
module.exports = NewsController;
路由
app/router/admin/admin.js
//新闻内容列表页面
router.get('/admin/news/index', controller.admin.news.index);
//发布新闻内容界面
router.get('/admin/news/create', controller.admin.news.create);
表单模版微调
app/view/admin/layout/_form.html
...
{# 如果是下拉框类型 #}
<div class="dropdown">
<button type="button" class="btn dropdown-toggle dropdown-{{item.name}}" data-toggle="dropdown" id="dropdownData">
{{item.placeholder if item.placeholder else '一级分类'}}
</button>
...
分类控制器调整
app/controller/admin/category.js
//修改分类界面
...
// 字段
fields: [
{
label: '放在哪个分类里面',
type: 'dropdown', //下拉框
name: 'pid',
default: JSON.stringify(data),
placeholder: '不调整(如需调整请选择)',//统一改成placeholder提示
// id:id,//有id说明操作是修改不是新增
},
...
# 5、完成发布新闻内容的数据提交,以及删除单个新闻内容
控制器
app/controller/admin/news.js
//发布新闻内容提交数据
async save() {
//一般处理流程
//1.参数验证
this.ctx.validate({
name: {
type: 'string', //参数类型
required: true, //是否必须
// defValue: '',
desc: '标题/名称' //字段含义
},
enname: {
type: 'string',
required: false,
defValue: '',
desc: '标题/名称英文'
},
status: {
type: 'int',
required: true,
defValue: 1,
desc: '内容显示状态 0不显示 1显示 等等状态'
},
category_id: {
type: 'int',
required: true,
defValue: 0,
desc: '分类栏目id'
},
description: {
type: 'string',
required: false,
defValue: '',
desc: '内容摘要'
},
order: {
type: 'int',
required: false,
defValue: 50,
desc: '排序'
},
keywords: {
type: 'string',
required: false,
defValue: '',
desc: '关键字'
},
maincontent: {
type: 'string',
required: false,
defValue: '',
desc: '主体内容'
},
attachment: {
type: 'string',
required: false,
defValue: '',
desc: '内容附件'
},
lookcount: {
type: 'int',
required: false,
defValue: 0,
desc: '点击量/浏览量'
},
});
//先判断一下直播功能中的礼物账号是否存在,不存在在写入数据库
//2.写入数据库
//3.成功之后给页面反馈
let { name, enname, status, category_id,description,order,keywords, maincontent,attachment,lookcount} = this.ctx.request.body;
if (await this.app.model.News.findOne({
where: {
name,
category_id
}
})) {
return this.ctx.apiFail('已存在同一个分类的内容,不能重复发布');
}
//否则不存在则写入数据库
//拿到管理员的登录信息
const manager_id = this.ctx.session.auth.id;
const res = await this.app.model.News.create({
name,
enname,
status,
category_id,
description,
order,
keywords, maincontent,attachment,lookcount,
manager_id
});
this.ctx.apiSuccess('发布内容成功');
}
//删除新闻内容
async delete() {
const { ctx, app } = this;
const id = ctx.params.id;
await app.model.News.destroy({
where: {
id
}
});
//提示
ctx.toast('内容删除成功', 'success');
//跳转
ctx.redirect('/admin/news/index');
}
路由
app/router/admin/admin.js
//发布新闻内容提交数据
router.post('/admin/news/save', controller.admin.news.save);
//删除新闻内容功能
router.get('/admin/news/delete/:id', controller.admin.news.delete);
表单模版微调
app/view/admin/layout/_form.html
...
methods:{
submit(){
...
for(const key in this.form){
let value = this.form[key];
console.log('key的值:'+ value);
if(typeof value == 'string' && value.length > 0 && value.indexOf('"')>-1){
this.form[key] = JSON.parse(value.replaceAll('"','"'))[0].value;
}
}
...
# 6、发布新闻内容界面的内容摘要换成文本域标签,内容附件换成自定义的按钮组选择附件内容
控制器
app/controller/admin/news.js
//发布新闻内容界面
async create() {
...
{
label: '内容摘要',
type: 'textarea',
name: 'description',
placeholder: '请输入内容摘要,选填',
},
...
{
label: '内容附件',
type: 'diy',//自定义类型模版
name: 'attachment',
placeholder: '请输入内容附件,选填',
render(item) {
const name = item.name;
console.log(name);
return `
<div class="btn-group btn-group-${name}">
<button type="button" class="btn btn-success"
@click="diyBtnGroup('btn-group-${name}','btn-group-${name}-check',0)">文件</button>
<button type="button" class="btn btn-light"
@click="diyBtnGroup('btn-group-${name}','btn-group-${name}-check',1)">网址</button>
</div>
<div class="btn-group-${name}-check">
<div>
<input class="form-control" type="file" name="${name}"
@change="uploadFile($event,'${name}',(e)=>{
console.log(e);
})" />
<div v-if="uploadedFiles.hasOwnProperty('${name}')">
<div v-if="uploadedFiles['${name}'].endsWith('.jpg') ||
uploadedFiles['${name}'].endsWith('.jpeg') ||
uploadedFiles['${name}'].endsWith('.png') ||
uploadedFiles['${name}'].endsWith('.gif') ">
<img :src="uploadedFiles['${name}']" class="mt-2 p-1 rounded border avatar-lg" />
</div>
<div v-else>
<span class="text-success">附件上传成功!</span>
</div>
</div>
</div>
<div style="display:none;">
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text">网址</span>
</div>
<input type="text" class="form-control" placeholder="请输入网址"
name='${name}' v-model="form.${name}" />
</div>
</div>
</div>
`;
}
},
...
}
表单模版调整
app/view/admin/layout/_form.html
...
{# 如果是文本域类型 #}
{% elif item.type == 'textarea' %}
<textarea class="form-control" rows="5" name="{{item.name}}"
placeholder="{{item.placeholder}}..."
v-model="form.{{item.name}}"></textarea>
{# 如果是自定义类型模版 #}
{% elif item.type == 'diy' %}
{% if item.render %}
{{ item.render(item) | safe }}
{% endif %}
...
methods:{
...
data(){
return {
...
uploadedFiles: {}, //存储每个item.name对应的已上传文件路径
}
},
...
methods:{
...
uploadFile(e,name,callback=null){
...
$.ajax({
...
success: (response, stutas, xhr)=> {
...
// 使用$set将上传成功的文件URL添加到uploadedFiles对象
this.$set(this.uploadedFiles, name, response.data.url);
if(callback && typeof callback == 'function'){
callback({
event:e,
name:name,
data:response.data
});
}
}
});
},
...
//自定义的按钮点击效果
diyBtnGroup(clickparentClassname, classname,index){
$('.' + classname).children().eq(index).show().siblings().hide();
$('.'+ clickparentClassname).children().eq(index)
.addClass('btn-success').siblings().removeClass('btn-success').addClass('btn-light');
}
}
}
# 二、初步引入富文本编辑器
我们在后台发布文章,文章主体内容一般会用到类似我们word文档形式的编辑器,常用的富文本编辑器有:
UEditor、tinymce、Kindeditor、Simditor、CKEditor、wangEditor、Suneditor、froala等等。大家可以自由选择,我们本套课程采用tinymce作为富文本编辑器进行讲解。其他编辑器,感兴趣的同学可以去学习摸索一下,我们目前只需要学会怎么使用即可。
# tinymce 编辑器说明
tinymce 编辑器的文档说明,可查阅: http://tinymce.ax-z.cn/quick-start.php (opens new window)
# 如何使用
控制器
app/controller/admin/news.js
...
{
label: '主体内容',
type: 'editor', // 富文本编辑器
name: 'maincontent',
placeholder: '请输入主体内容,选填',
},
...
表单模版调整
app/view/admin/layout/_form.html
...
{# 如果是富文本编辑器 #}
{% elif item.type == 'editor' %}
{# {% include "admin/layout/_editor.html" %} #}
<script src="/public/editor/tinymce/js/tinymce/tinymce.min.js"></script>
<textarea id="{{item.name}}" name="{{item.name}}" :value="form.{{item.name}}"
placeholder="{{item.placeholder }}..." >{{form[item.name] if form[item.name]}}</textarea>
<script>
tinymce.init({
selector: '#{{ item.name }}',
language: 'zh_CN',
init_instance_callback: function (editor) {
// 监听内容改变事件,并更新 Vue 数据模型
editor.on('Change', () => {
console.log('监听内容改变事件', editor.getContent());
// 使用 Vue.set 或 $set 方法更新数据模型
Vueapp.$set(Vueapp.form, "{{item.name}}", editor.getContent());
});
}
});
</script>
...
关于编辑器的引入:
大家去群里面下载本节课的课件,我给大家提供了编辑器的所有文件,大家将文件放在
app/public/editor里面即可
# 三、富文本编辑器常用配置说明及上传图片
富文本编辑器的各项配置,查阅 http://tinymce.ax-z.cn/general/basic-setup.php (opens new window)
# 常用配置说明及上传图片:
- 新建一个模版
app/view/admin/layout/_editor.html app/view/admin/layout/_form.html表单模版引入
{# 如果是富文本编辑器 #}
{% elif item.type == 'editor' %}
{% include "admin/layout/_editor.html" %}
app/view/admin/layout/_editor.html富文本编辑器模版
<script src="/public/editor/tinymce/js/tinymce/tinymce.min.js"></script>
<textarea id="{{item.name}}" name="{{item.name}}" :value="form.{{item.name}}"
placeholder="{{item.placeholder}}">{{form[item.name] if form[item.name]}}</textarea>
<script>
tinymce.init({
selector: '#{{item.name}}',
language: 'zh_CN',
branding: false, // 添加此行以移除 Logo
promotion: false, // 隐藏“Upgrade”按钮等商业推广内容
//toolbar:
// 保留默认工具栏,并添加 'image' 按钮
toolbar: `code undo redo | styleselect | bold italic underline | alignleft aligncenter alignright alignjustify |
bullist numlist outdent indent | link imageUpload`,
//菜单栏配置 menu和menubar [看文档http://tinymce.ax-z.cn/general/basic-setup.php]
// menubar: false, // 隐藏菜单栏
//plugins: ['image', 'table', 'code'],
plugins: 'image,table,code',
//上传图片:http://tinymce.ax-z.cn/general/upload-images.php
//images_upload_url: '/uploadStreamSingleToServerDiy/adminImg?_csrf={{ctx.csrf|safe}}',
//自定义上传 images_upload_handler
//images_upload_handler: function (blobInfo, succFun, failFun) {},
setup:(editor)=>{
console.log(editor.ui.registry);
editor.ui.registry.addButton("imageUpload",{
tooltip:"插入图片",
icon:"image",
onAction:()=>{
console.log('点击了插入图片');
// editor.insertContent('<img src="https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png" style="width:100%;" /">');
// editor.insertContent('<p><img style="width: 100%;" src="../../public/uploads/Diy/adminImg/20240416/1713253836023_11465325.png"></p>');
// 创建一个隐藏的文件输入元素
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*'; // 限制仅允许选择图片文件
// 监听文件选择事件
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (file) {
try {
console.log(file);
let formData = new FormData();
formData.append('file', file);
$.ajax({
type: 'POST',
url: "/uploadStreamSingleToServerDiy/adminImg?_csrf={{ctx.csrf|safe}}",
processData: false, // 告诉jQuery不要去处理发送的数据
data: formData,
contentType: false, // 告诉jQuery不要去设置Content-Type请求头
success: (response, stutas, xhr)=> {
//console.log(response)
//console.log(response.data.url)
if(response.data.url){
// 使用服务器返回的图片URL插入图片到编辑器内容中
const imgHtml = `<img src="${response.data.url}" />`;
console.log('imgHtml',imgHtml);
editor.insertContent(imgHtml);
}
},
error:function(e){
console.log(e)
}
});
} catch (error) {
console.error('Error occurred during image upload:', error);
}
}
});
// 触发文件选择对话框
fileInput.click();
}
});
},
init_instance_callback: function (editor) {
// 监听内容改变事件,并更新 Vue 数据模型
editor.on('Change',()=>{
console.log('监听内容改变事件', editor.getContent());
// 使用 Vue.set 或 $set 方法更新数据模型
// Vueapp.$set(Vueapp.form,"{{item.name}}",editor.getContent());
// 修正图片src路径
const contentWithCorrectedPaths = editor.getContent().
replace(/(<img[^>]*src=")(\.\.\/)+([^"]*)/,(_, prefix, relativePath, url) => `${prefix}/${url}`);
console.log('修正图片src路径', contentWithCorrectedPaths);
// 使用 Vue.set 或 $set 方法更新数据模型
Vueapp.$set(Vueapp.form, "{{item.name}}", contentWithCorrectedPaths);
});
}
});
</script>
# 完整配置参考
如果想让编辑器的
工具栏和菜单栏更丰富,可以参考一下配置,更多配置查阅编辑器文档 http://tinymce.ax-z.cn/general/basic-setup.php (opens new window)
// 配置
const init = {
language_url: '/tinymce/langs/zh-Hans.js', // 中文语言包路径
language: "zh-Hans",
skin_url: '/tinymce/skins/ui/oxide', // 编辑器皮肤样式
content_css: "/tinymce/skins/content/default/content.min.css",
menubar: false, // 隐藏菜单栏
autoresize_bottom_margin: 50,
max_height: 500,
min_height: 450,
// height: 320,
toolbar_mode: "none",
plugins:
'wordcount visualchars visualblocks template searchreplace save quickbars preview pagebreak nonbreaking media insertdatetime importcss image help fullscreen directionality codesample code charmap link code table lists advlist anchor autolink autoresize autosave',
toolbar:
"formats undo redo fontsizeselect fontselect ltr rtl searchreplace media imageUpload | outdent indent aligncenter alignleft alignright alignjustify lineheight underline quicklink h2 h3 blockquote numlist bullist table removeformat forecolor backcolor bold italic strikethrough hr link preview fullscreen help ",
content_style: "p {margin: 5px 0; font-size: 14px}",
fontsize_formats: "12px 14px 16px 18px 24px 36px 48px 56px 72px",
font_formats: "微软雅黑=Microsoft YaHei,Helvetica Neue,PingFang SC,sans-serif;苹果苹方= PingFang SC, Microsoft YaHei, sans- serif; 宋体 = simsun, serif; 仿宋体 = FangSong, serif; 黑体 = SimHei, sans - serif; Arial = arial, helvetica, sans - serif;Arial Black = arial black, avant garde;Book Antiqua = book antiqua, palatino; ",
branding: false,
elementpath: false,
resize: false, // 禁止改变大小
statusbar: false, // 隐藏底部状态栏
// 自定义按钮
setup: (editor) => {
//console.log(editor);
editor.ui.registry.addButton('imageUpload', {
tooltip: '插入图片',
icon: 'image',
onAction: (e) => {
console.log('插入图片')
// let url = "https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png"
// editor.insertContent(` <img src="${url}" style="width:100%;"> `)
}
})
}
}
tinymce.init; // 初始化
# 四、修改新闻内容
控制器
app/controller/admin/news.js
//修改新闻内容界面
async edit() {
const { ctx, app } = this;
const id = ctx.params.id;
let currentdata = await app.model.News.findOne({
where: {
id
}
});
if (!currentdata) {
return ctx.apiFail('该内容不存在');
}
currentdata = JSON.parse(JSON.stringify(currentdata));
console.log('当前内容数据', currentdata);
// return;
// 渲染模版前先拿到所有分类
let data = await ctx.service.category.dropdown_Categorylist();
data.shift();//删除数组第一个元素
console.log('下拉框显示的所有分类', JSON.stringify(data));
// return;
//渲染公共模版
await ctx.renderTemplate({
id,
title: '修改内容',//现在网页title,面包屑导航title,页面标题
tempType: 'form', //模板类型:table表格模板 ,form表单模板
form: {
//修改直播功能中的礼物提交地址
action: '/admin/news/update/' + id,
// 字段
fields: [
{
label: '请选择一个分类栏目发布',
type: 'dropdown', //下拉框
name: 'category_id',
default: JSON.stringify(data),
placeholder: '请选择',
},
{
label: '标题/名称',
type: 'text',
name: 'name',
placeholder: '请输入标题/名称',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '标题/名称英文',
type: 'text',
name: 'enname',
placeholder: '请输入标题/名称英文,选填',
},
{
label: '内容摘要',
type: 'textarea',
name: 'description',
placeholder: '请输入内容摘要,选填',
},
{
label: '关键字',
type: 'text',
name: 'keywords',
placeholder: '请输入关键字(用逗号隔开),选填',
},
{
label: '主体内容',
type: 'editor', //富文本编辑器
name: 'maincontent',
placeholder: '请输入主体内容,选填',
},
{
label: '内容附件',
type: 'diy', //自定义类型组件
name: 'attachment',
placeholder: '请输入内容附件,选填',
render(item){
const name = item.name;
console.log(name);
return `
<div class="btn-group btn-group-${name}">
<button type="button" class="btn btn-success"
@click="diyBtnGroup('btn-group-${name}','btn-group-${name}-check',0)">文件</button>
<button type="button" class="btn btn-light"
@click="diyBtnGroup('btn-group-${name}','btn-group-${name}-check',1)">网址</button>
</div>
<div class="btn-group-${name}-check">
<div>
<input class="form-control" type="file" name="${name}"
@change="uploadFile($event,'${name}',(e)=>{
console.log(e);
})" />
<div v-if="uploadedFiles.hasOwnProperty('${name}') && uploadedFiles['${name}'] ">
<div v-if="uploadedFiles['${name}'].endsWith('.jpg') ||
uploadedFiles['${name}'].endsWith('.jpeg') ||
uploadedFiles['${name}'].endsWith('.png') ||
uploadedFiles['${name}'].endsWith('.gif') ">
<img :src="uploadedFiles['${name}']" class="mt-2 p-1 rounded border avatar-lg" >
</div>
<div v-else>
<span class="text-success">附件上传成功!</span>
</div>
</div>
</div>
<div style="display:none;">
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text">链接地址</span>
</div>
<input type="text" class="form-control"
placeholder="请输入链接地址" name='${name}'
v-model="form.${name}" />
</div>
</div>
</div>
`;
}
},
{
label: '点击量/浏览量',
type: 'text',
name: 'lookcount',
default:0,
placeholder: '请输入点击量/浏览量,选填',
},
{
label: '排序',
type: 'number',
name: 'order',
placeholder: '请输入排序',
default:50,
},
{
label: '内容显示状态',
type: 'btncheck', //按钮组选择
name: 'status',
default: JSON.stringify([
{ value: 1, name: '显示', checked: true },
{ value: 0, name: '隐藏' },
]),
placeholder: '内容显示状态 0不显示 1显示 等等状态',
},
],
//修改内容默认值
data:currentdata,
},
//修改成功之后跳转到哪个页面
successUrl: '/admin/news/index',
});
}
//修改新闻内容提交数据
async update() {
const { ctx, app } = this;
//1.参数验证
this.ctx.validate({
id: {
type: 'int',
required: true,
desc: '新闻内容id'
},
name: {
type: 'string', //参数类型
required: true, //是否必须
// defValue: '',
desc: '标题/名称' //字段含义
},
enname: {
type: 'string',
required: false,
defValue: '',
desc: '标题/名称英文'
},
status: {
type: 'int',
required: true,
defValue: 1,
desc: '内容显示状态 0不显示 1显示 等等状态'
},
category_id: {
type: 'int',
required: true,
defValue: 0,
desc: '分类id'
},
description: {
type: 'string',
required: false,
defValue: '',
desc: '内容摘要'
},
order: {
type: 'int',
required: false,
defValue: 50,
desc: '排序'
},
keywords: {
type: 'string',
required: false,
defValue: '',
desc: '关键字'
},
maincontent: {
type: 'string',
required: false,
defValue: '',
desc: '主体内容'
},
attachment: {
type: 'string',
required: false,
defValue: '',
desc: '内容附件'
},
lookcount: {
type: 'int',
required: false,
defValue: 0,
desc: '点击量/浏览量'
}
});
// 参数
const id = ctx.params.id;
let { name, enname, status, category_id,description,order,
keywords,maincontent,attachment, lookcount} = this.ctx.request.body;
// 先看一下是否存在
const news = await app.model.News.findOne({ where: { id } });
if (!news) {
return ctx.apiFail('该内容记录不存在');
}
//存在,内容名称可以一样,只要保证它在不同的分类下面
const Op = this.app.Sequelize.Op;//拿Op,固定写法
//先查一下修改的内容名称,是否已经存在,如果存在,但是只要不放在同一分类下还是可以的
const hasdata = await app.model.News.findOne({
where: {
name,
id: {
[Op.ne]: id
},
}
});
if (hasdata && hasdata.category_id == category_id) {
return ctx.apiFail('同一个分类下不能有相同的内容名称');
}
// 修改数据
news.name = name;
news.enname = enname;
news.status = status;
news.category_id = category_id;
news.description = description;
news.order = order;
news.keywords = keywords;
news.maincontent = maincontent;
news.attachment = attachment;
news.lookcount = lookcount;
await news.save();
/*
//update方法批量修改字段
let params = this.ctx.request.body;
await news.update(params,{
//fields:['name'],//指定修改字段
});
*/
// 给一个反馈
ctx.apiSuccess('修改内容成功');
}
路由
app/router/admin/admin.js
//修改新闻内容界面
router.get('/admin/news/edit/:id', controller.admin.news.edit);
//修改新闻内容提交数据
router.post('/admin/news/update/:id', controller.admin.news.update);
细节调整
app/view/admin/layout/_form.html表单模版对编辑器内容的渲染处理
...
form:{
{% for item in form.fields %}
{{item.name}} : `{{ (form.data[item.name]|safe) if form.data[item.name] else item.default }}`,
{% endfor %}
},
...
app/view/admin/layout/_editor.html富文本编辑器模版,对编辑器多张图片都要修正路径,正则表达式加上g进行全局匹配
...
// 修正图片src路径
const contentWithCorrectedPaths = editor.getContent().
replace(/(<img[^>]*src=")(\.\.\/)+([^"]*)/g,(_, prefix, relativePath, url) => `${prefix}/${url}`);
...
# 五、新闻列表页面调整标题展示、新增是否显示按钮组,并对按钮组方法封装成公共方法
- 控制器
app/controller/admin/news.js
//新闻内容列表界面
async index() {
...
{
title: '标题/名称',
// key: 'name',
class: 'text-left',//可选
render(item) {
let type = '';
let imgHTML = '';
if (item.attachment.endsWith('.jpg') || item.attachment.endsWith('.jpeg') ||
item.attachment.endsWith('.png') || item.attachment.endsWith('.gif')) {
type = 'image';
imgHTML = `
<a href="#" class="avatar avatar-sm mr-2">
<img class="avatar-img" src="${item.attachment}" alt="User Image">
</a>
`;
}
return `
<h2 class="table-avatar">
${imgHTML}
<a href="#">
${item.name.substring(0,20)}
<span>${item.description.substring(0,20)}</span>
</a>
</h2>
`;
}
},
...
{
title: '显示状态',
key: 'status',
width: 200,//可选
class: 'text-center',//可选
hidekeyData: true,//是否隐藏key对应的数据
render(item) {
console.log('显示状态里面每个item', item);
let arr = [
{ value: 1, name: '显示' },
{ value: 0, name: '隐藏' },
];
let str = `<div class="btn-group btn-group-${item.id}">`;
for (let i = 0; i < arr.length; i++) {
str += `<button type="button" class="btn btn-light" data="${item.status}"
value="${arr[i].value}"
@click="changeBtnStatus('status','btn-group-${item.id}',${arr[i].value},
${i},${item.id},'news','News')">${arr[i].name}</button>`;
}
str += `</div>`;
return str;
}
},
...
}
- 同时分类控制器的changeBtnStatus方法也需要修改:
app/controller/admin/category.js
//分类列表页面
async index() {
...
{
title: '可用状态',
key: 'status',
width: 200,//可选
class: 'text-center',//可选
hidekeyData: true,//是否隐藏key对应的数据
render(item) {
console.log('可用状态里面每个item', item);
let arr = [
{ value: 1, name: '可用' },
{ value: 0, name: '不可用' },
];
let str = `<div class="btn-group btn-group-${item.id}">`;
for (let i = 0; i < arr.length; i++) {
str += `<button type="button" class="btn btn-light" data="${item.status}"
value="${arr[i].value}"
@click="changeBtnStatus('status','btn-group-${item.id}',${arr[i].value},
${i},${item.id},'category','Category')">${arr[i].name}</button>`;
}
str += `</div>`;
return str;
}
},
...
}
app/view/admin/layout/_table.html表格模版调整
//点击按钮组
changeBtnStatus(keyname,classname,btnValue,index,id,keytable,model){
// console.log(keyname,classname,btnValue,index,id,keytable,model);
$.ajax({
type: 'POST',
// url: "/admin/category/updateStatus?_csrf={{ctx.csrf|safe}}",
url: `/admin/commonUpdateById?_csrf={{ctx.csrf|safe}}`,
contentType:'application/json;charset=UTF-8;',
data:JSON.stringify({
id:id,
// status:btnValue
keyname:keyname,
keyvalue:btnValue,
keytable:keytable,
model:model
}),
...
});
}
- 封装一个公共方法
app/controller/admin/home.js
// 公共方法,根据id修改某张表中的某个字段
async commonUpdateById(){
const { id, keyname,keyvalue,model} = this.ctx.request.body;
//1.参数验证
this.ctx.validate({
id: {
type: 'int',
required: true,
// defValue: 0,
desc: '分类id'
},
keyname: {
type: 'string',
required: true,
},
keyvalue: {
type: 'string',
required: true,
defValue: '',
},
model: {
type: 'string',
required: true,
},
});
// console.log('获取的参数',id, keyname,keyvalue,model);return;
let data = await this.app.model[model].findByPk(id);
if (!data) {
return this.ctx.apiFail('数据不存在');
}
data[keyname] = keyvalue;
await data.save();
this.ctx.apiSuccess('修改成功');
}
- 公共方法路由
app/router/admin/admin.js
// 公共方法,根据id修改某张表中的某个字段
router.post('/admin/commonUpdateById', controller.admin.home.commonUpdateById);