完成单页管理

dev
truthhun 2 years ago
parent 4e500bbdfc
commit 590f4688c7

@ -219,6 +219,60 @@ func (s *AttachmentAPIService) UploadAvatar(ctx *gin.Context) {
s.uploadImage(ctx, model.AttachmentTypeAvatar)
}
// UploadArticle 上传文章相关图片和视频。这里不验证文件格式。
// 注意当前适配了wangeditor的接口规范如果需要适配其他编辑器需要修改此接口或者增加其他接口
func (s *AttachmentAPIService) UploadArticle(ctx *gin.Context) {
typ := ctx.Query("type")
if typ != "image" && typ != "video" {
ctx.JSON(http.StatusOK, map[string]interface{}{"errno": 1, "msg": "类型参数错误"})
return
}
userCliams, _, err := s.checkGinPermission(ctx)
if err != nil {
ctx.JSON(http.StatusOK, map[string]interface{}{"errno": 1, "msg": err.Error()})
return
}
name := "file"
fileHeader, err := ctx.FormFile(name)
if err != nil {
s.logger.Error("MultipartForm", zap.Error(err))
ctx.JSON(http.StatusOK, map[string]interface{}{"errno": 1, "msg": err.Error()})
return
}
attachment, err := s.saveFile(ctx, fileHeader)
if err != nil {
s.logger.Error("saveFile", zap.Error(err))
os.Remove("." + attachment.Path)
ctx.JSON(http.StatusOK, map[string]interface{}{"errno": 1, "msg": err.Error()})
return
}
attachment.UserId = userCliams.UserId
attachment.Type = model.AttachmentTypeArticle
err = s.dbModel.CreateAttachment(attachment)
if err != nil {
s.logger.Error("CreateAttachments", zap.Error(err))
ctx.JSON(http.StatusOK, map[string]interface{}{"errno": 1, "msg": err.Error()})
return
}
if typ == "image" {
ctx.JSON(http.StatusOK, map[string]interface{}{"errno": 0, "data": map[string]interface{}{
"url": attachment.Path,
"alt": attachment.Name,
// "href": "",
}})
} else {
ctx.JSON(http.StatusOK, map[string]interface{}{"errno": 0, "data": map[string]interface{}{
"url": attachment.Path,
// "poster": "",
}})
}
}
// UploadBanner 上传横幅创建横幅的时候要根据附件id更新附件的type_id字段
func (s *AttachmentAPIService) UploadBanner(ctx *gin.Context) {
s.uploadImage(ctx, model.AttachmentTypeBanner)

@ -15,7 +15,7 @@ require (
github.com/spf13/cobra v1.3.0
github.com/spf13/viper v1.10.1
go.uber.org/zap v1.21.0
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2
golang.org/x/net v0.1.0
google.golang.org/grpc v1.44.0
google.golang.org/protobuf v1.27.1
gorm.io/driver/mysql v1.3.2
@ -23,6 +23,8 @@ require (
)
require (
github.com/PuerkitoBio/goquery v1.8.0 // indirect
github.com/andybalholm/cascadia v1.3.1 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect
google.golang.org/genproto v0.0.0-20220228195345-15d65a4533f7 // indirect
@ -64,8 +66,8 @@ require (
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect
golang.org/x/sys v0.0.0-20211210111614-af8b64212486 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/sys v0.1.0 // indirect
golang.org/x/text v0.4.0 // indirect
gopkg.in/ini.v1 v1.66.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

@ -1,8 +1,11 @@
package model
import (
"path/filepath"
"strings"
"time"
"github.com/PuerkitoBio/goquery"
"go.uber.org/zap"
"gorm.io/gorm"
)
@ -31,6 +34,7 @@ func (m *DBModel) CreateArticle(article *Article) (err error) {
m.logger.Error("CreateArticle", zap.Error(err))
return
}
m.checkArticleFile(article)
return
}
@ -51,6 +55,8 @@ func (m *DBModel) UpdateArticle(article *Article, updateFields ...string) (err e
if err != nil {
m.logger.Error("UpdateArticle", zap.Error(err))
}
m.checkArticleFile(article)
return
}
@ -146,3 +152,37 @@ func (m *DBModel) DeleteArticle(ids []int64) (err error) {
}
return
}
// checkArticleFile 检查文章中的文件,包括音频视频和图片等
func (m *DBModel) checkArticleFile(article *Article) {
doc, err := goquery.NewDocumentFromReader(strings.NewReader(article.Content))
if err != nil {
m.logger.Error("checkArticleFile", zap.Error(err))
return
}
var (
hashes []string
tags = []string{"img", "video", "audio"}
)
for _, tag := range tags {
doc.Find(tag).Each(func(i int, selection *goquery.Selection) {
src, ok := selection.Attr("src")
if !ok {
src, ok = selection.Find("source").Attr("src")
}
if ok && strings.HasPrefix(src, "/uploads/") {
src = strings.Split(src, "?")[0]
hashes = append(hashes, strings.TrimSuffix(filepath.Base(src), filepath.Ext(src)))
}
})
}
if len(hashes) > 0 { // 更新内容ID
err = m.db.Model(&Attachment{}).Where("hash in (?) and type = ? and type_id = 0", hashes, AttachmentTypeArticle).Update("type_id", article.Id).Error
if err != nil {
m.logger.Error("checkArticleFile", zap.Error(err))
}
}
}

@ -33,7 +33,7 @@ type Attachment struct {
Hash string `form:"hash" json:"hash,omitempty" gorm:"column:hash;type:char(32);size:32;index:hash;comment:文件MD5;"`
UserId int64 `form:"user_id" json:"user_id,omitempty" gorm:"column:user_id;type:bigint(20);default:0;index:user_id;comment:用户 id;"`
TypeId int64 `form:"type_id" json:"type_id,omitempty" gorm:"column:type_id;type:bigint(20);default:0;comment:类型数据ID对应与用户头像时则为用户id对应为文档时则为文档ID;"`
Type int `form:"type" json:"type,omitempty" gorm:"column:type;type:smallint(5);default:0;comment:附件类型(0 位置1 头像2 文档3 文章附件 ...);"`
Type int `form:"type" json:"type,omitempty" gorm:"column:type;type:smallint(5);default:0;comment:附件类型(0 未知1 头像2 文档3 文章附件 ...);"`
Enable bool `form:"enable" json:"enable,omitempty" gorm:"column:enable;type:tinyint(3);default:1;comment:是否合法;"`
Path string `form:"path" json:"path,omitempty" gorm:"column:path;type:varchar(255);size:255;comment:文件存储路径;"`
Name string `form:"name" json:"name,omitempty" gorm:"column:name;type:varchar(255);size:255;comment:文件原名称;"`

@ -156,7 +156,7 @@ func (m *DBModel) CheckPermissionByGroupId(groupId []int64, method, path string)
// 校验当前登录了的用户所属用户组,是否有权限
var groupPermission GroupPermission
err = m.db.Where("group_id in (?) and permission_id = ?", groupId, permission.Id).First(&groupPermission).Error
if err != nil {
if err != nil && err != gorm.ErrRecordNotFound {
m.logger.Error("CheckPermissionByGroupId", zap.Error(err))
}

@ -25,6 +25,7 @@ func RegisterGinRouter(app *gin.Engine, dbModel *model.DBModel, logger *zap.Logg
checkPermissionGroup.POST("banner", attachmentAPIService.UploadBanner)
checkPermissionGroup.POST("document", attachmentAPIService.UploadDocument)
checkPermissionGroup.POST("category/cover", attachmentAPIService.UploadCategoryCover)
checkPermissionGroup.POST("article", attachmentAPIService.UploadArticle)
}
return

@ -73,7 +73,7 @@
@onCreated="onCreated"
/>
</el-form-item>
<el-form-item>
<el-form-item v-if="!isEditorFullScreen">
<el-button
type="primary"
class="btn-block"
@ -90,7 +90,6 @@
import { Boot } from '@wangeditor/editor'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import markdownModule from '@wangeditor/plugin-md'
import ctrlEnterModule from '@wangeditor/plugin-ctrl-enter'
import { createArticle, updateArticle } from '~/api/article'
export default {
@ -113,11 +112,12 @@ export default {
article: {},
editor: null,
toolbarConfig: {},
isEditorFullScreen: false,
editorConfig: {
placeholder: '请输入内容...',
MENU_CONF: {
uploadImage: {
server: '/api/v1/upload/article',
server: '/api/v1/upload/article?type=image',
fieldName: 'file',
maxFileSize: 20 * 1024 * 1024, // 20M
headers: {
@ -125,9 +125,12 @@ export default {
},
timeout: 30 * 1000, // 30s
withCredentials: false,
onFailed: (file, res) => {
this.$message.error(`${file.name}上传失败:${res.msg}`)
},
},
uploadVideo: {
server: '/api/v1/upload/article',
server: '/api/v1/upload/article?type=video',
fieldName: 'file',
maxFileSize: 1024 * 1024 * 1024, // 1GB
headers: {
@ -135,6 +138,9 @@ export default {
},
timeout: 600 * 1000, // 10min
withCredentials: false,
onFailed: (file, res) => {
this.$message.error(`${file.name}上传失败:${res.msg}`)
},
},
},
},
@ -151,7 +157,6 @@ export default {
},
created() {
Boot.registerModule(markdownModule)
Boot.registerModule(ctrlEnterModule)
this.article = { ...this.initArticle }
},
beforeDestroy() {
@ -162,6 +167,12 @@ export default {
methods: {
onCreated(editor) {
this.editor = Object.seal(editor) // Object.seal()
this.editor.on('fullScreen', () => {
this.isEditorFullScreen = true
})
this.editor.on('unFullScreen', () => {
this.isEditorFullScreen = false
})
},
onSubmit() {
this.$refs.formArticle.validate(async (valid) => {

@ -17,7 +17,6 @@
"@nuxtjs/pwa": "^3.3.5",
"@wangeditor/editor": "^5.1.22",
"@wangeditor/editor-for-vue": "^1.0.2",
"@wangeditor/plugin-ctrl-enter": "^1.1.2",
"@wangeditor/plugin-md": "^1.0.0",
"core-js": "^3.19.3",
"element-ui": "^2.15.6",
@ -49,4 +48,4 @@
"stylelint-config-prettier": "^8.0.2",
"stylelint-config-standard": "^21.0.0"
}
}
}

@ -1,5 +1,5 @@
<template>
<div>
<div class="page-admin-article">
<el-card shadow="never" class="search-card">
<FormSearch
:fields="searchFormFields"
@ -42,17 +42,18 @@
</el-pagination>
</div>
</el-card>
<el-dialog
<el-drawer
:title="article.id ? '编辑文章' : '新增文章'"
:visible.sync="formArticleVisible"
:size="'80%'"
:wrapper-closable="true"
>
<FormArticle
ref="articleForm"
:init-article="article"
@success="formSuccess"
/>
</el-dialog>
</el-drawer>
</div>
</template>
@ -122,7 +123,11 @@ export default {
this.article = { id: 0 }
this.formArticleVisible = true
this.$nextTick(() => {
this.$refs.articleForm.reset()
try {
this.$refs.articleForm.reset()
} catch (error) {
console.log(error)
}
})
},
async editRow(row) {
@ -209,4 +214,10 @@ export default {
},
}
</script>
<style></style>
<style lang="scss">
.page-admin-article {
.el-drawer__body {
padding: 0 20px;
}
}
</style>

@ -198,9 +198,9 @@ export default {
},
initTableListFields() {
this.tableListFields = [
{ prop: 'id', label: 'ID', width: 80, type: 'number', fixed: 'left' },
{ prop: 'type_name', label: '类型', width: 80, fixed: 'left' },
{ prop: 'name', label: '名称', minWidth: 150, fixed: 'left' },
{ prop: 'id', label: 'ID', width: 80, type: 'number' },
{ prop: 'type_name', label: '类型', width: 80 },
{ prop: 'name', label: '名称', minWidth: 200 },
{
prop: 'enable',
label: '是否合法',
@ -213,7 +213,7 @@ export default {
{ prop: 'width', label: '宽', width: 90 },
{ prop: 'height', label: '高', width: 90 },
{ prop: 'ext', label: '扩展', width: 90 },
{ prop: 'hash', label: 'HASH', width: 280 },
{ prop: 'hash', label: 'HASH', width: 290 },
{ prop: 'path', label: '存储路径', minWidth: 300 },
{ prop: 'description', label: '备注', width: 200 },
{ prop: 'created_at', label: '创建时间', width: 160, type: 'datetime' },

Loading…
Cancel
Save