初步完成横幅管理功能

dev
truthhun 2 years ago
parent 01ff237e03
commit 1b8746750f

4
.gitignore vendored

@ -10,4 +10,6 @@ go.sum
.vscode
ginrpc*
dist
output
output
cache
uploads

@ -0,0 +1,77 @@
syntax = "proto3";
import "google/protobuf/timestamp.proto";
import "gogoproto/gogo.proto";
// import "validate/validate.proto";
import "google/api/annotations.proto";
import "google/protobuf/empty.proto";
package api.v1;
option go_package = "moredoc/api/v1;v1";
option java_multiple_files = true;
option java_package = "api.v1";
message Banner {
int64 id = 1;
string title = 2;
string path = 3;
int32 sort = 4;
int32 status = 5;
int32 type = 6;
string url = 7;
string description = 8;
google.protobuf.Timestamp created_at = 9 [ (gogoproto.stdtime) = true ];
google.protobuf.Timestamp updated_at = 10 [ (gogoproto.stdtime) = true ];
}
message DeleteBannerRequest {repeated int64 id = 1; }
message GetBannerRequest { int64 id = 1; }
message ListBannerRequest {
int64 page = 1;
int64 size = 2;
repeated int32 type = 3;
repeated int32 status = 4;
string wd = 5;
}
message ListBannerReply {
int64 total = 1;
repeated Banner banner = 2;
}
service BannerAPI {
rpc CreateBanner(Banner) returns (Banner) {
option (google.api.http) = {
post : '/api/v1/banner',
body : '*',
};
}
rpc UpdateBanner(Banner) returns (Banner) {
option (google.api.http) = {
put : '/api/v1/banner',
body : '*',
};
}
rpc DeleteBanner(DeleteBannerRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {
delete : '/api/v1/banner',
};
}
rpc GetBanner(GetBannerRequest) returns (Banner) {
option (google.api.http) = {
get : '/api/v1/banner',
};
}
rpc ListBanner(ListBannerRequest) returns (ListBannerReply) {
option (google.api.http) = {
get : '/api/v1/banner/list',
};
}
}

@ -193,15 +193,6 @@ func (s *AttachmentAPIService) UploadCategoryCover(ctx *gin.Context) {
func (s *AttachmentAPIService) uploadImage(ctx *gin.Context, attachmentType int) {
name := "file"
switch attachmentType {
case model.AttachmentTypeBanner:
name = "banner"
case model.AttachmentTypeAvatar:
name = "avatar"
case model.AttachmentTypeCategoryCover:
name = "cover"
}
userCliams, statusCodes, err := s.checkGinPermission(ctx)
if err != nil {
ctx.JSON(statusCodes, ginResponse{Code: statusCodes, Message: err.Error(), Error: err.Error()})
@ -227,7 +218,7 @@ func (s *AttachmentAPIService) uploadImage(ctx *gin.Context, attachmentType int)
ctx.JSON(http.StatusBadRequest, ginResponse{Code: http.StatusBadRequest, Message: err.Error(), Error: err.Error()})
return
}
attachment.Type = model.AttachmentTypeAvatar
attachment.Type = attachmentType
attachment.UserId = userCliams.UserId
if attachmentType == model.AttachmentTypeAvatar {
@ -251,14 +242,12 @@ func (s *AttachmentAPIService) uploadImage(ctx *gin.Context, attachmentType int)
// saveFile 保存文件。文件以md5值命名以及存储
// 同时,返回附件信息
func (s *AttachmentAPIService) saveFile(ctx *gin.Context, fileHeader *multipart.FileHeader) (attachment *model.Attachment, err error) {
cacheDir := fmt.Sprintf("cache/%s", time.Now().Format("2006/01/02"))
cacheDir := fmt.Sprintf("cache/uploads/%s", time.Now().Format("2006/01/02"))
os.MkdirAll(cacheDir, os.ModePerm)
ext := strings.ToLower(filepath.Ext(fileHeader.Filename))
cachePath := fmt.Sprintf("%s/%s%s", cacheDir, uuid.Must(uuid.NewV4()).String(), ext)
defer func() {
if err != nil {
os.Remove(cachePath)
}
os.Remove(cachePath)
}()
// 保存到临时文件
@ -277,7 +266,7 @@ func (s *AttachmentAPIService) saveFile(ctx *gin.Context, fileHeader *multipart.
savePath := fmt.Sprintf("uploads/%s/%s%s", strings.Join(strings.Split(md5hash, "")[0:5], "/"), md5hash, ext)
os.MkdirAll(filepath.Dir(savePath), os.ModePerm)
err = os.Rename(cachePath, savePath)
err = util.CopyFile(cachePath, savePath)
if err != nil {
s.logger.Error("Rename", zap.Error(err), zap.String("cachePath", cachePath), zap.String("savePath", savePath))
return

@ -0,0 +1,136 @@
package biz
import (
"context"
pb "moredoc/api/v1"
"moredoc/middleware/auth"
"moredoc/model"
"moredoc/util"
"go.uber.org/zap"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
type BannerAPIService struct {
pb.UnimplementedBannerAPIServer
dbModel *model.DBModel
logger *zap.Logger
}
func NewBannerAPIService(dbModel *model.DBModel, logger *zap.Logger) (service *BannerAPIService) {
return &BannerAPIService{dbModel: dbModel, logger: logger.Named("BannerAPIService")}
}
func (s *BannerAPIService) checkPermission(ctx context.Context) (*auth.UserClaims, error) {
return checkGRPCPermission(s.dbModel, ctx)
}
// CreateBanner 创建横幅
func (s *BannerAPIService) CreateBanner(ctx context.Context, req *pb.Banner) (*pb.Banner, error) {
_, err := s.checkPermission(ctx)
if err != nil {
return nil, err
}
var banner model.Banner
util.CopyStruct(req, &banner)
err = s.dbModel.CreateBanner(&banner)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
pbBanner := &pb.Banner{}
util.CopyStruct(&banner, pbBanner)
return pbBanner, nil
}
// UpdateBanner 更新横幅
func (s *BannerAPIService) UpdateBanner(ctx context.Context, req *pb.Banner) (*pb.Banner, error) {
_, err := s.checkPermission(ctx)
if err != nil {
return nil, err
}
var banner model.Banner
util.CopyStruct(req, &banner)
err = s.dbModel.UpdateBanner(&banner)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
pbBanner := &pb.Banner{}
util.CopyStruct(&banner, pbBanner)
return pbBanner, nil
}
func (s *BannerAPIService) DeleteBanner(ctx context.Context, req *pb.DeleteBannerRequest) (*emptypb.Empty, error) {
_, err := s.checkPermission(ctx)
if err != nil {
return nil, err
}
err = s.dbModel.DeleteBanner(req.Id)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
return &emptypb.Empty{}, nil
}
func (s *BannerAPIService) GetBanner(ctx context.Context, req *pb.GetBannerRequest) (*pb.Banner, error) {
fields := []string{"id", "title", "path", "url"}
if _, errPermission := s.checkPermission(ctx); errPermission == nil {
fields = []string{}
}
banner, err := s.dbModel.GetBanner(req.Id, fields...)
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
pbBanner := &pb.Banner{}
util.CopyStruct(banner, pbBanner)
return pbBanner, nil
}
// GetBanners 获取横幅列表
func (s *BannerAPIService) ListBanner(ctx context.Context, req *pb.ListBannerRequest) (*pb.ListBannerReply, error) {
var opt = &model.OptionGetBannerList{
Page: int(req.Page),
Size: int(req.Size_),
WithCount: true,
SelectFields: []string{"id", "title", "path", "url"}, // 对于非权限用户,可查询的字段
QueryIn: make(map[string][]interface{}),
}
if len(req.Type) > 0 {
opt.QueryIn["type"] = util.Slice2Interface(req.Type)
}
_, errPermission := s.checkPermission(ctx)
if errPermission != nil {
opt.QueryIn["status"] = []interface{}{model.BannerStatusNormal} // 非权限用户,只能查询正常状态的横幅
} else {
opt.SelectFields = []string{} // 不限字段
if len(req.Status) > 0 {
opt.QueryIn["status"] = util.Slice2Interface(req.Status)
}
if req.Wd != "" {
opt.QueryLike = map[string][]interface{}{"title": {req.Wd}, "description": {req.Wd}}
}
}
banners, total, err := s.dbModel.GetBannerList(opt)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
var pbBanner []*pb.Banner
util.CopyStruct(banners, &pbBanner)
return &pb.ListBannerReply{Total: total, Banner: pbBanner}, nil
}

@ -7,6 +7,8 @@ import (
"gorm.io/gorm"
)
// TODO: 附件管理需要有一个定时任务定时根据type和type_id清理无效的附件数据同时删除无效的文件
const (
AttachmentTypeAvatar = iota // 用户头像
AttachmentTypeDocument // 文档
@ -134,7 +136,11 @@ func (m *DBModel) GetAttachmentList(opt *OptionGetAttachmentList) (attachmentLis
}
// TODO: 没有排序参数的话,可以自行指定排序字段
db = m.generateQuerySort(db, tableName, opt.Sort)
if len(opt.Sort) > 0 {
db = m.generateQuerySort(db, tableName, opt.Sort)
} else {
db = db.Order("id desc")
}
db = db.Offset((opt.Page - 1) * opt.Size).Limit(opt.Size)

@ -1,39 +1,30 @@
package model
import (
"fmt"
"strings"
"time"
"go.uber.org/zap"
"gorm.io/gorm"
)
const (
BannerStatusNormal = iota
BannerStatusDisabled
)
type Banner struct {
Id int64 `form:"id" json:"id,omitempty" gorm:"primaryKey;autoIncrement;column:id;comment:;"`
Title string `form:"title" json:"title,omitempty" gorm:"column:title;type:varchar(255);size:255;comment:横幅名称;"`
Path string `form:"path" json:"path,omitempty" gorm:"column:path;type:varchar(255);size:255;comment:横幅地址;"`
Sort int `form:"sort" json:"sort,omitempty" gorm:"column:sort;type:int(11);size:11;default:0;comment:排序,值越大越靠前;"`
Status int8 `form:"status" json:"status,omitempty" gorm:"column:status;type:tinyint(4);size:4;default:0;comment:0 正常1禁用;"`
Category int8 `form:"category" json:"category,omitempty" gorm:"column:category;type:tinyint(4);size:4;default:0;comment:0 PC横幅1 H5横幅2 小程序横幅3 APP横幅;"`
CreatedAt *time.Time `form:"created_at" json:"created_at,omitempty" gorm:"column:created_at;type:datetime;comment:创建时间;"`
UpdatedAt *time.Time `form:"updated_at" json:"updated_at,omitempty" gorm:"column:updated_at;type:datetime;comment:更新时间;"`
Url string `form:"url" json:"url,omitempty" gorm:"column:url;type:varchar(255);size:255;comment:横幅跳转地址;"`
Id int64 `form:"id" json:"id,omitempty" gorm:"primaryKey;autoIncrement;column:id;comment:;"`
Title string `form:"title" json:"title,omitempty" gorm:"column:title;type:varchar(255);size:255;comment:横幅名称;"`
Description string `form:"description" json:"description,omitempty" gorm:"column:description;type:varchar(255);size:255;comment:横幅描述、备注;"`
Path string `form:"path" json:"path,omitempty" gorm:"column:path;type:varchar(255);size:255;comment:横幅地址;"`
Sort int `form:"sort" json:"sort,omitempty" gorm:"column:sort;type:int(11);size:11;default:0;comment:排序,值越大越靠前;"`
Status int8 `form:"status" json:"status,omitempty" gorm:"column:status;type:tinyint(4);size:4;default:0;comment:0 正常1禁用;"`
Type int8 `form:"type" json:"type,omitempty" gorm:"column:type;type:tinyint(4);size:4;default:0;comment:0 网站横幅1 小程序横幅2 APP横幅;"`
CreatedAt *time.Time `form:"created_at" json:"created_at,omitempty" gorm:"column:created_at;type:datetime;comment:创建时间;"`
UpdatedAt *time.Time `form:"updated_at" json:"updated_at,omitempty" gorm:"column:updated_at;type:datetime;comment:更新时间;"`
Url string `form:"url" json:"url,omitempty" gorm:"column:url;type:varchar(255);size:255;comment:横幅跳转地址;"`
}
// 这里是proto文件中的结构体可以根据需要删除或者调整
//message Banner {
// int64 id = 1;
// string title = 2;
// string path = 3;
// int32 sort = 4;
// int32 status = 5;
// int32 category = 6;
// google.protobuf.Timestamp created_at = 7 [ (gogoproto.stdtime) = true ];
// google.protobuf.Timestamp updated_at = 8 [ (gogoproto.stdtime) = true ];
// string url = 9;
//}
func (Banner) TableName() string {
return tablePrefix + "banner"
}
@ -91,37 +82,12 @@ type OptionGetBannerList struct {
}
// GetBannerList 获取Banner列表
func (m *DBModel) GetBannerList(opt OptionGetBannerList) (bannerList []Banner, total int64, err error) {
func (m *DBModel) GetBannerList(opt *OptionGetBannerList) (bannerList []Banner, total int64, err error) {
db := m.db.Model(&Banner{})
tableName := Banner{}.TableName()
for field, rangeValue := range opt.QueryRange {
fields := m.FilterValidFields(Banner{}.TableName(), field)
if len(fields) == 0 {
continue
}
if rangeValue[0] != nil {
db = db.Where(fmt.Sprintf("%s >= ?", field), rangeValue[0])
}
if rangeValue[1] != nil {
db = db.Where(fmt.Sprintf("%s <= ?", field), rangeValue[1])
}
}
for field, values := range opt.QueryIn {
fields := m.FilterValidFields(Banner{}.TableName(), field)
if len(fields) == 0 {
continue
}
db = db.Where(fmt.Sprintf("%s in (?)", field), values)
}
for field, values := range opt.QueryLike {
fields := m.FilterValidFields(Banner{}.TableName(), field)
if len(fields) == 0 {
continue
}
db = db.Where(strings.TrimSuffix(fmt.Sprintf(strings.Join(make([]string, len(values)+1), "%s like ? or"), field), "or"), values...)
}
db = m.generateQueryIn(db, tableName, opt.QueryIn)
db = m.generateQueryLike(db, tableName, opt.QueryLike)
if len(opt.Ids) > 0 {
db = db.Where("id in (?)", opt.Ids)
@ -135,33 +101,14 @@ func (m *DBModel) GetBannerList(opt OptionGetBannerList) (bannerList []Banner, t
}
}
opt.SelectFields = m.FilterValidFields(Banner{}.TableName(), opt.SelectFields...)
opt.SelectFields = m.FilterValidFields(tableName, opt.SelectFields...)
if len(opt.SelectFields) > 0 {
db = db.Select(opt.SelectFields)
}
if len(opt.Sort) > 0 {
var sorts []string
for _, sort := range opt.Sort {
slice := strings.Split(sort, " ")
if len(m.FilterValidFields(Banner{}.TableName(), slice[0])) == 0 {
continue
}
if len(slice) == 2 {
sorts = append(sorts, fmt.Sprintf("%s %s", slice[0], slice[1]))
} else {
sorts = append(sorts, fmt.Sprintf("%s desc", slice[0]))
}
}
if len(sorts) > 0 {
db = db.Order(strings.Join(sorts, ","))
}
}
db = db.Offset((opt.Page - 1) * opt.Size).Limit(opt.Size)
err = db.Find(&bannerList).Error
err = db.Order("status asc, sort desc").Find(&bannerList).Error
if err != nil && err != gorm.ErrRecordNotFound {
m.logger.Error("GetBannerList", zap.Error(err))
}
@ -169,8 +116,7 @@ func (m *DBModel) GetBannerList(opt OptionGetBannerList) (bannerList []Banner, t
}
// DeleteBanner 删除数据
// TODO: 删除数据之后,存在 banner_id 的关联表,需要删除对应数据,同时相关表的统计数值,也要随着减少
func (m *DBModel) DeleteBanner(ids []interface{}) (err error) {
func (m *DBModel) DeleteBanner(ids []int64) (err error) {
err = m.db.Where("id in (?)", ids).Delete(&Banner{}).Error
if err != nil {
m.logger.Error("DeleteBanner", zap.Error(err))

@ -78,6 +78,7 @@ func Run(cfg *conf.Config, logger *zap.Logger) {
// 根目录访问静态文件,要放在 grpc 服务的前面
// 可以在 dist 目录下创建一个 index.html 文件并添加内容,然后访问 http://ip:port
app.Use(static.Serve("/uploads", static.LocalFile("./uploads", true)))
app.Use(static.Serve("/", static.LocalFile("./dist", true)))
app.NoRoute(func(ctx *gin.Context) {
http.ServeFile(ctx.Writer, ctx.Request, "./dist/index.html")

@ -50,5 +50,14 @@ func RegisterGRPCService(dbModel *model.DBModel, logger *zap.Logger, endpoint st
return
}
// 横幅API接口服务
bannerAPIService := biz.NewBannerAPIService(dbModel, logger)
v1.RegisterBannerAPIServer(grpcServer, bannerAPIService)
err = v1.RegisterBannerAPIHandlerFromEndpoint(context.Background(), gwmux, endpoint, dialOpts)
if err != nil {
logger.Error("RegisterBannerAPIHandlerFromEndpoint", zap.Error(err))
return
}
return
}

@ -2,6 +2,9 @@ package util
import (
"context"
"fmt"
"io"
"os"
"strings"
jsoniter "github.com/json-iterator/go"
@ -78,3 +81,23 @@ func Slice2Interface[T Any](slice []T) (values []interface{}) {
}
return
}
// CopyFile 复制文件
func CopyFile(src, dst string) error {
inputFile, err := os.Open(src)
if err != nil {
return fmt.Errorf("couldn't open source file: %s", err)
}
outputFile, err := os.Create(dst)
if err != nil {
inputFile.Close()
return fmt.Errorf("couldn't open dest file: %s", err)
}
defer outputFile.Close()
_, err = io.Copy(outputFile, inputFile)
inputFile.Close()
if err != nil {
return fmt.Errorf("writing to output file failed: %s", err)
}
return nil
}

@ -0,0 +1,43 @@
import service from '~/utils/request'
export const createBanner = (data) => {
return service({
url: '/api/v1/banner',
method: 'post',
data,
})
}
export const updateBanner = (data) => {
return service({
url: '/api/v1/banner',
method: 'put',
data,
})
}
export const deleteBanner = (params) => {
return service({
url: '/api/v1/banner',
method: 'delete',
params,
})
}
export const getBanner = (params) => {
return service({
url: '/api/v1/banner',
method: 'get',
params,
})
}
export const listBanner = (params) => {
return service({
url: '/api/v1/banner/list',
method: 'get',
params,
})
}

@ -0,0 +1,208 @@
<template>
<div class="com-form-banner">
<el-form
ref="formBanner"
label-position="top"
label-width="80px"
:model="banner"
>
<el-form-item
label="图片"
prop="path"
:rules="[
{ required: true, message: '请上传横幅图片', trigger: 'blur' },
]"
>
<UploadImage
:action="'/api/v1/upload/banner'"
:image="banner.path"
:width="'600px'"
@success="success"
/>
</el-form-item>
<el-form-item
label="名称"
prop="title"
:rules="[
{ required: true, message: '请输入横幅名称', trigger: 'blur' },
]"
>
<el-input
v-model="banner.title"
clearable
placeholder="请输入横幅名称"
></el-input>
</el-form-item>
<el-form-item
label="链接"
prop="url"
:rules="[
{
required: true,
message: '请输入链接地址',
trigger: 'blur',
},
]"
>
<el-input
v-model="banner.url"
clearable
placeholder="请输入链接地址"
></el-input>
</el-form-item>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="类型" prop="type">
<el-select
v-model="banner.type"
clearable
placeholder="请选择横幅类型"
>
<el-option
v-for="opt in bannerTypeOptions"
:key="'type-' + opt.value"
:label="opt.label"
:value="opt.value"
></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="排序(值越大越靠前)" prop="sort">
<el-input-number
v-model="banner.sort"
:min="0"
:step="1"
></el-input-number>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="是否启用" prop="status">
<el-switch
v-model="banner.status"
style="display: block; margin-top: 8px"
active-color="#ff4949"
inactive-color="#13ce66"
active-text="否"
inactive-text="是"
>
</el-switch>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="描述">
<el-input
v-model="banner.description"
type="textarea"
rows="5"
placeholder="请输入附件相关描述或备注"
></el-input>
</el-form-item>
<el-form-item>
<el-button
type="primary"
class="btn-block"
icon="el-icon-check"
:loading="loading"
@click="onSubmit"
>提交</el-button
>
</el-form-item>
</el-form>
</div>
</template>
<script>
import UploadImage from './UploadImage.vue'
import { createBanner, updateBanner } from '~/api/banner'
import { bannerTypeOptions } from '~/utils/enum'
export default {
name: 'FormBanner',
components: { UploadImage },
props: {
initBanner: {
type: Object,
default: () => {
return {
id: 0,
title: '',
description: '',
}
},
},
},
data() {
return {
loading: false,
banner: {
id: 0,
title: '',
description: '',
path: '',
type: 0,
status: 0,
},
bannerTypeOptions,
}
},
watch: {
initBanner: {
handler(val) {
const banner = { ...this.banner, ...val }
this.banner = banner
},
immediate: true,
},
},
created() {
const banner = { ...this.banner, ...this.initBanner }
this.banner = banner
},
methods: {
onSubmit() {
this.$refs.formBanner.validate(async (valid) => {
if (!valid) {
return
}
this.loading = true
const banner = { ...this.banner }
banner.status = banner.status ? 1 : 0
if (this.banner.id > 0) {
const res = await updateBanner(banner)
if (res.status === 200) {
this.$message.success('修改成功')
this.resetFields()
this.$emit('success', res.data)
} else {
this.$message.error(res.data.message)
}
} else {
const res = await createBanner(banner)
if (res.status === 200) {
this.$message.success('添加成功')
this.resetFields()
this.$emit('success', res.data)
} else {
this.$message.error(res.data.message)
}
}
this.loading = false
})
},
clearValidate() {
this.$refs.formBanner.clearValidate()
},
resetFields() {
this.$refs.formBanner.resetFields()
},
reset() {
this.resetFields()
this.clearValidate()
},
success(res) {
this.banner.path = res.data.path
console.log(res)
},
},
}
</script>

@ -56,6 +56,14 @@
scope.row[item.prop] || '-'
}}</span>
</span>
<span v-else-if="['link', 'url'].includes(item.type)">
<a :href="scope.row[item.prop]" target="_blank">
<i class="el-icon-link"></i> {{ scope.row[item.prop] }}</a
>
</span>
<span v-else-if="item.type === 'banner'">
<UploadImage :disabled="true" :image="scope.row[item.prop]" />
</span>
<!-- 字符串更多则需要继续扩展 -->
<span v-else>{{ scope.row[item.prop] || '-' }}</span>
</template>
@ -97,9 +105,11 @@
</div>
</template>
<script>
import UploadImage from './UploadImage.vue'
import { formatDatetime, formatBytes } from '~/utils/utils'
export default {
name: 'ComTableList',
components: { UploadImage },
props: {
tableData: {
type: Array,

@ -0,0 +1,76 @@
<template>
<div class="com-upload-image">
<el-upload
class="image-uploader"
:action="action"
:headers="{ authorization: `bearer ${token}` }"
:show-file-list="false"
:on-success="success"
accept="image/jpeg,image/png,image/gif,image/jpg"
:multiple="false"
:disabled="disabled"
>
<el-image
v-if="disabled"
:src="image"
:style="'width:' + width + ';height:' + height"
>
<div slot="error" class="image-slot">
<img :src="errorImage" />
</div>
</el-image>
<el-tooltip v-else content="点击上传图片" placement="top">
<el-image :src="image" :style="'width:' + width + ';height:' + height">
<div slot="error" class="image-slot">
<img :src="errorImage" />
</div>
</el-image>
</el-tooltip>
</el-upload>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'FormNotebookCover',
props: {
action: {
type: String,
default: '',
},
width: {
type: String,
default: 'auto',
},
height: {
type: String,
default: 'auto',
},
disabled: {
type: Boolean,
default: false,
},
image: {
type: String,
default: '',
},
errorImage: {
type: String,
default: '/static/images/blank.png',
},
},
data() {
return {
profile: {},
}
},
computed: {
...mapGetters('user', ['token']),
},
methods: {
success(res) {
this.$emit('success', res)
},
},
}
</script>

@ -53,7 +53,7 @@ export default {
changeOrigin: true,
},
'/uploads': {
target: 'http://127.0.0.1:8080', // 目标服务器
target: 'http://127.0.0.1:8880', // 目标服务器
changeOrigin: true,
},
},

@ -198,15 +198,15 @@ export default {
width: 80,
type: 'bool',
},
{ prop: 'path', label: '存储路径', minWidth: 150 },
{ prop: 'hash', label: 'HASH', width: 150 },
{ prop: 'hash', label: 'HASH', width: 280 },
{ prop: 'username', label: '上传者', width: 120 },
{ prop: 'size', label: '大小', width: 80, type: 'bytes' },
{ prop: 'width', label: '宽', width: 80 },
{ prop: 'height', label: '高', width: 80 },
{ prop: 'ext', label: '扩展', width: 80 },
{ prop: 'size', label: '大小', width: 90, type: 'bytes' },
{ prop: 'width', label: '宽', width: 90 },
{ prop: 'height', label: '高', width: 90 },
{ prop: 'ext', label: '扩展', width: 90 },
{ prop: 'ip', label: 'IP', width: 120 },
{ prop: 'description', label: '备注', width: 200 },
{ prop: 'path', label: '存储路径', minWidth: 300 },
{ prop: 'created_at', label: '创建时间', width: 160, type: 'datetime' },
{ prop: 'updated_at', label: '更新时间', width: 160, type: 'datetime' },
]

@ -1,10 +1,236 @@
<template>
<div>{{ $route.name }}</div>
<div>
<el-card shadow="never" class="search-card">
<FormSearch
:fields="searchFormFields"
:loading="loading"
:show-create="true"
:show-delete="true"
:disabled-delete="selectedRow.length === 0"
@onCreate="onCreate"
@onSearch="onSearch"
@onDelete="batchDelete"
/>
</el-card>
<el-card shadow="never" class="mgt-20px">
<TableList
:table-data="listData"
:fields="tableListFields"
:show-actions="true"
:show-view="false"
:show-edit="true"
:show-delete="true"
:show-select="true"
@selectRow="selectRow"
@editRow="editRow"
@deleteRow="deleteRow"
/>
</el-card>
<el-card shadow="never" class="mgt-20px">
<div class="text-right">
<el-pagination
background
:current-page="search.page"
:page-sizes="[10, 20, 50, 100]"
:page-size="search.size"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handlePageChange"
>
</el-pagination>
</div>
</el-card>
<el-dialog
width="640px"
:title="banner.id > 0 ? '编辑横幅' : '新增横幅'"
:visible.sync="formVisible"
>
<FormBanner :init-banner="banner" @success="formSuccess" />
</el-dialog>
</div>
</template>
<script>
import { listBanner, deleteBanner } from '~/api/banner'
import TableList from '~/components/TableList.vue'
import FormSearch from '~/components/FormSearch.vue'
import FormBanner from '~/components/FormBanner.vue'
import { bannerTypeOptions } from '~/utils/enum'
export default {
components: { TableList, FormSearch, FormBanner },
layout: 'admin',
created() {},
data() {
return {
loading: false,
formVisible: false,
search: {
wd: '',
page: 1,
status: [],
size: 10,
},
listData: [],
total: 0,
searchFormFields: [],
tableListFields: [],
selectedRow: [],
banner: {},
bannerTypeOptions,
}
},
async created() {
this.initSearchForm()
this.initTableListFields()
await this.listBanner()
},
methods: {
async listBanner() {
this.loading = true
const res = await listBanner(this.search)
if (res.status === 200) {
this.listData = res.data.banner
this.total = res.data.total
} else {
this.$message.error(res.data.message)
}
this.loading = false
},
handleSizeChange(val) {
this.search.size = val
this.listBanner()
},
handlePageChange(val) {
this.search.page = val
this.listBanner()
},
onSearch(search) {
this.search = { ...this.search, page: 1, ...search }
this.listBanner()
},
onCreate() {
this.banner = {}
this.formVisible = true
},
editRow(row) {
this.formVisible = true
this.banner = row
},
formSuccess() {
this.formVisible = false
this.listBanner()
},
batchDelete() {
this.$confirm(
`您确定要删除选中的【${this.selectedRow.length}个】横幅吗?删除之后不可恢复!`,
'温馨提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(async () => {
const ids = this.selectedRow.map((item) => item.id)
const res = await deleteBanner({ id: ids })
if (res.status === 200) {
this.$message.success('删除成功')
this.listBanner()
} else {
this.$message.error(res.data.message)
}
})
.catch(() => {})
},
deleteRow(row) {
this.$confirm(
`您确定要删除横幅【${row.title}】吗?删除之后不可恢复!`,
'温馨提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(async () => {
const res = await deleteBanner({ id: row.id })
if (res.status === 200) {
this.$message.success('删除成功')
this.listBanner()
} else {
this.$message.error(res.data.message)
}
})
.catch(() => {})
},
selectRow(rows) {
this.selectedRow = rows
},
initSearchForm() {
this.searchFormFields = [
{
type: 'text',
label: '关键字',
name: 'wd',
placeholder: '请输入关键字',
},
{
type: 'select',
label: '类型',
name: 'type',
placeholder: '请选择横幅类型',
multiple: true,
options: this.bannerTypeOptions,
},
{
type: 'select',
label: '状态',
name: 'status',
placeholder: '是否启用',
multiple: true,
options: [
{ label: '启用', value: 0 },
{ label: '禁用', value: 1 },
],
},
]
},
initTableListFields() {
const typeMap = {}
this.bannerTypeOptions.forEach((item) => {
typeMap[item.value] = item
})
const statusMap = {
0: { label: '启用', value: 0, type: 'success' },
1: { label: '禁用', value: 1, type: 'danger' },
}
this.tableListFields = [
{ prop: 'id', label: 'ID', width: 80, type: 'number' },
{ prop: 'path', label: '横幅', width: 360, type: 'banner' },
{
prop: 'type',
label: '类型',
width: 120,
type: 'enum',
enum: typeMap,
},
{
prop: 'status',
label: '状态',
width: 80,
type: 'enum',
enum: statusMap,
},
{ prop: 'title', label: '名称', minWidth: 150 },
{ prop: 'url', label: '链接', minWidth: 150, type: 'link' },
{ prop: 'sort', label: '排序', width: 80, type: 'number' },
{ prop: 'description', label: '备注', width: 200 },
{ prop: 'created_at', label: '创建时间', width: 160, type: 'datetime' },
{ prop: 'updated_at', label: '更新时间', width: 160, type: 'datetime' },
]
},
},
}
</script>
<style></style>

@ -14,3 +14,10 @@ export const attachmentTypeOptions = [
{ label: '横幅', value: 4 },
{ label: '分类封面', value: 5 },
]
// 0 网站横幅1 小程序横幅2 APP横幅
export const bannerTypeOptions = [
{ label: '网站横幅', value: 0, type: 'primary' },
{ label: '小程序横幅', value: 1, type: 'success' },
{ label: 'APP横幅', value: 2, type: 'warning' },
]

Loading…
Cancel
Save