api/data_api/growth_data.go
Go
package data_api
import (
"blog_server/common/res"
"blog_server/global"
"blog_server/middlware"
"blog_server/models"
"blog_server/models/enum"
"github.com/gin-gonic/gin"
"strings"
"time"
)
type GrowthDataRequest struct {
Type int8 `form:"type",binding:"required,oneof=1 2 3"`
}
type GrowthDataResponse struct {
GrowthRate int `json:"growth_rate"` //增长率
GrowthNum int `json:"growth_num"` //增长数
CountList []int `json:"count_list"`
DateList []string `json:"date_list"`
}
type Table struct {
Date string `gorm:"column:date"`
Count int `gorm:"column:count"`
}
func (DataApi) GrowthDataView(c *gin.Context) {
cr := middlware.GetBind[GrowthDataRequest](c)
now := time.Now()
before7 := now.AddDate(0, 0, -7)
var dataList []Table
switch cr.Type {
case 1:
global.DB.Debug().Model(models.SiteFlowModel{}).Where("created_at >=? and created_at <?", before7.Format("2006-01-02")+":00:00:00", now.Format("2006-01-02 15:04:05")).
Select("date(created_at) as date", "sum(count) as count").Group("date").Scan(&dataList)
case 2:
global.DB.Model(models.ArticleModel{}).Where("created_at >=? and created_at <? and status = ?", before7.Format("2006-01-02")+":00:00:00", now.Format("2006-01-02 15:04:05"), enum.ArticleStatusPublishes).
Select("date(created_at) as date", "count(id) as count").Group("date").Scan(&dataList)
case 3:
global.DB.Debug().Model(models.UserModel{}).Where("created_at >=? and created_at <?", before7.Format("2006-01-02")+":00:00:00", now.Format("2006-01-02 15:04:05")).
Select("date(created_at) as date", "count(id) as count").Group("date").Scan(&dataList)
}
var dateMap = map[string]int{}
for _, model := range dataList {
date := strings.Split(model.Date, "T")[0]
dateMap[date] = model.Count
}
response := RegisterDataResponse{}
for i := 0; i < 7; i++ {
date := before7.AddDate(0, 0, i+1)
dateF := date.Format("2006-01-02")
count, _ := dateMap[dateF]
response.CountList = append(response.CountList, count)
response.DateList = append(response.DateList, dateF)
}
//算增长,找最后一个的前一个
response.GrowthNum = response.CountList[6] - response.CountList[5]
if response.CountList[5] == 0 {
response.GrowthRate = 100
} else {
response.GrowthRate = int((float64(response.GrowthNum) / float64(response.CountList[5])) * 100)
}
res.OkWithData(response, c)
}
api/data_api/register_data.go
Go
package data_api
import (
"blog_server/common/res"
"blog_server/global"
"blog_server/models"
"github.com/gin-gonic/gin"
"time"
)
type RegisterDataResponse struct {
GrowthRate int `json:"growth_rate"` //增长率
GrowthNum int `json:"growth_num"` //增长数
CountList []int `json:"count_list"`
DateList []string `json:"date_list"`
}
// 新增用户统计
func (DataApi) RegisterDataView(c *gin.Context) {
now := time.Now()
before7 := now.AddDate(0, 0, -7)
var userList []models.UserModel
global.DB.Debug().Find(&userList, "created_at >=? and created_at <?", before7.Format("2006-01-02")+":00:00:00", now.Format("2006-01-02 15:04:05"))
var dateMap = map[string]int{}
for _, model := range userList {
date := model.CreatedAt.Format("2006-01-02")
count, ok := dateMap[date]
if !ok {
dateMap[date] = 1
continue
}
dateMap[date] = count + 1
}
response := RegisterDataResponse{}
for i := 0; i < 7; i++ {
date := before7.AddDate(0, 0, i+1)
dateF := date.Format("2006-01-02")
count, _ := dateMap[dateF]
response.CountList = append(response.CountList, count)
response.DateList = append(response.DateList, dateF)
}
//算增长,找最后一个的前一个
response.GrowthNum = response.CountList[6] - response.CountList[5]
response.GrowthRate = int((float64(response.GrowthNum) / float64(response.CountList[5])) * 100)
res.OkWithData(response, c)
}
api/site_api/enter.go
Go
package site_api
import (
"blog_server/common/res"
"blog_server/conf"
"blog_server/core"
"blog_server/global"
"blog_server/middlware"
"blog_server/services/redis_service/redis_site"
"fmt"
"github.com/PuerkitoBio/goquery"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"os"
)
type SiteApi struct {
}
type SiteInfoRequest struct {
Name string `uri:"name" binding:"required"`
}
type QiNiu struct {
Enable bool `json:"enable"`
}
type AI struct {
Enable bool `json:"enable"`
}
type SiteInfoResponse struct {
QiNiu QiNiu `json:"qi_niu"`
AI AI `json:"ai"`
conf.Site
}
func (SiteApi) SiteInfoView(c *gin.Context) {
var cr SiteInfoRequest
if err := c.ShouldBindUri(&cr); err != nil {
res.FailWithError(err, c)
return
}
if cr.Name == "site" {
global.Config.Site.About.Version = global.Version
redis_site.SetFlow()
res.OkWithData(SiteInfoResponse{
Site: global.Config.Site,
QiNiu: QiNiu{
Enable: global.Config.Qiniu.Enable,
},
AI: AI{
Enable: global.Config.AI.Enable,
},
}, c)
return
}
//判断是否是管理员
middlware.AdminMiddleware(c)
_, ok := c.Get("claims")
if !ok {
return
}
var data any
switch cr.Name {
case "email":
rep := global.Config.Email
rep.AuthCode = "******"
data = rep
case "qq":
rep := global.Config.QQ
rep.AppId = "******"
data = rep
case "qiniu":
rep := global.Config.Qiniu
rep.SecretKey = "******"
data = rep
case "ai":
rep := global.Config.AI
rep.SecretKey = "******"
data = rep
default:
res.FailWithMsg("不存在的配置", c)
return
}
res.OkWithData(data, c)
return
}
func (SiteApi) SiteUpdateView(c *gin.Context) {
var cr SiteInfoRequest
err := c.ShouldBindUri(&cr)
if err != nil {
res.FailWithError(err, c)
return
}
var rep any
switch cr.Name {
case "site":
var data conf.Site
err = c.ShouldBindJSON(&data)
rep = data
case "email":
var data conf.Email
err = c.ShouldBindJSON(&data)
rep = data
case "qq":
var data conf.QQ
err = c.ShouldBindJSON(&data)
rep = data
case "qiniu":
var data conf.QiNiu
err = c.ShouldBindJSON(&data)
rep = data
case "ai":
var data conf.AI
err = c.ShouldBindJSON(&data)
rep = data
default:
res.FailWithMsg("不存在的配置", c)
return
}
if err != nil {
res.FailWithError(err, c)
return
}
switch s := rep.(type) {
case conf.Site:
//判断站点信息更新前端文件部分
err = UploadSite(s) //暂时没有处理函数,先设置为空,之后补充
if err != nil {
res.FailWithError(err, c)
return
}
global.Config.Site = s
case conf.Email:
if s.AuthCode == "******" {
s.AuthCode = global.Config.Email.AuthCode
}
global.Config.Email = s
case conf.QQ:
if s.AppId == "******" {
s.AppId = global.Config.QQ.AppId
}
global.Config.QQ = s
case conf.QiNiu:
if s.SecretKey == "******" {
s.SecretKey = global.Config.Qiniu.SecretKey
}
global.Config.Qiniu = s
case conf.AI:
if s.SecretKey == "******" {
s.SecretKey = global.Config.Qiniu.SecretKey
}
global.Config.AI = s
}
core.SetConf()
res.FailWithMsg("更新站成功", c)
return
}
func (SiteApi) SiteInfoQQView(c *gin.Context) {
res.OkWithData(global.Config.QQ.Url(), c)
}
func UploadSite(site conf.Site) error {
if site.Project.Icon == "" && site.Project.Title == "" && site.Project.WebPath == "" && site.Seo.KeyWords == "" && site.Seo.Description == "" {
return nil
}
if site.Project.WebPath == "" {
return errors.New("请配置前端地址")
}
file, err := os.Open(site.Project.WebPath)
if err != nil {
return errors.New(fmt.Sprintf("%s 文件不存在", site.Project.WebPath))
}
doc, err := goquery.NewDocumentFromReader(file)
if err != nil {
logrus.Errorf("goquery 解析失败 %s", err)
return errors.New("goquery 解析失败")
}
if site.Project.Title != "" {
doc.Find("title").SetText(site.Project.Title)
}
if site.Project.Icon != "" {
selection := doc.Find("link[rel='icon']").Length()
if selection == 0 {
//没有-》创建
doc.Find("head").AppendHtml(fmt.Sprintf("<link rel=\"icon\" href=\"%s\">", site.Project.Icon))
} else {
//有修改
doc.Find("link[rel='icon']").SetAttr("href", site.Project.Icon)
}
}
if site.Seo.KeyWords != "" {
selection := doc.Find("meta[name='keywords']").Length()
if selection == 0 {
//没有-》创建
doc.Find("head").AppendHtml(fmt.Sprintf(" <meta name=\"keywords\" content=\"%s\">", site.Seo.KeyWords))
} else {
//有修改
doc.Find("meta[name='keywords']").SetAttr("content", site.Seo.KeyWords)
}
}
if site.Seo.Description != "" {
selection := doc.Find("meta[name='description']").Length()
if selection == 0 {
//没有-》创建
doc.Find("head").AppendHtml(fmt.Sprintf(" <meta name=\"description\" content=\"%s\">", site.Seo.Description))
} else {
//有修改
doc.Find("meta[name='description']").SetAttr("content", site.Seo.Description)
}
}
html, err := doc.Html()
if err != nil {
logrus.Errorf("生成html失败:%s", err)
return errors.New("生成html失败")
}
err = os.WriteFile(site.Project.WebPath, []byte(html), 0666)
if err != nil {
logrus.Errorf("写入html失败:%s", err)
return errors.New("写入html失败")
}
return nil
}
admin
src\components\common\g_card.vue
javascript
<script setup lang="ts">
interface Props {
title: string
}
const props = defineProps<Props>()
</script>
<template>
<div class="g_card">
<div class="title">
{{ props.title }}
</div>
<div class="body">
<slot></slot>
</div>
</div>
</template>
<style lang="less">
.g_card{
background-color: var(--color-bg-1);
border-radius: 5px;
width: 100%;
.title{
padding: 20px;
font-size: 18px;
font-weight: 600;
border-bottom: @g_border;
}
.body{
padding: 20px;
}
}
</style>
src\api\index.ts
javascript
import axios from "axios";
import {Message} from "@arco-design/web-vue";
import {userStorei} from "@/stores/user_store";
import type {Ref} from "vue";
// 基础的响应类型,一般和后端项目的响应类型一致
export interface baseResponse<T> {
code: number
msg: string
data: T
}
// 列表类型的响应类型
export interface listResponse<T> {
list: T[]
count: number
}
// URL的Query Parameters参数
export interface paramsType {
key?: string
limit?: number
page?: number
sort?: string
[key: string]: any
}
export const useAxios = axios.create({
timeout: 6000,
baseURL: "", // 在使用前端代理的情况下,这里必须留空,不然会跨域
})
// axios请求拦截
useAxios.interceptors.request.use((config) => {
const userStore = userStorei() //先去这个全局的store中拿到这个用户的token
config.headers.set("Authorization", userStore.userInfo.token) //将token配置到请求头中
return config
})
// axios响应拦截
useAxios.interceptors.response.use((res) => {
// 响应拦截时先做一个判断,如果状态是200才是响应成功,将数据返回出去
if (res.status === 200) {
return res.data
}
return res
}, (res) => {
// 如果有错误,将错误展示出来
Message.error(res.message)
})
// 默认删除的接口,接受一个url,一个id_list列表,返回基础的响应数据(因为后端的删除接口的请求参数都是是id_list,所以可以放在前端作为一个默认的接口方便调用)
export function defaultDeleteApi(url: string, id_list: number[]): Promise<baseResponse<string>> {
return useAxios.delete(url, {data: {id_list}})
}
// 默认post的接口,接受一个url,一个data,返回基础的响应数据
export function defaultPostApi(url: string, data: any): Promise<baseResponse<string>> {
return useAxios.post(url, data)
}
export function defaultPutApi(url: string, data: any): Promise<baseResponse<string>> {
return useAxios.put(url, data)
}
export interface optionsType {
label: string
value: number | string
}
export type optionsFunc = (params?: paramsType) => Promise<baseResponse<optionsType[]>>
export function getOptions(ref: Ref<optionsType[]>, func: optionsFunc, params?: paramsType){
func(params).then((res)=>{
ref.value = res.data
})
}
// ========================================
export function articleCategoryOptionsApi(): Promise<baseResponse<optionsType[]>> {
return useAxios.get("/api/category/options")
}
src\components\admin\g_list.vue
javascript
<script setup lang='ts'>
import type { listResponse, paramsType, baseResponse,optionsType,optionsFunc } from "@/api/index";
// import {defaultDeleteApi} from "@/api/index";
import { dateCurrentFormat, dateTemFormat } from "@/utils/date";
import { Message, type TableColumnData,type TableRowSelection,type TableData } from "@arco-design/web-vue";
import { reactive, ref } from "vue";
const loading = ref(false)
// 父组件传递给子组件的参数类型;
interface Props {
url: (params?: paramsType) => Promise<baseResponse<listResponse<any>>>, //请求路由,返回的是一个函数,和接口的函数类型保持一致
columns: TableColumnData[], //字段数据
rowKey?:string //(主要使用为默认删除的字段名称)
noDefaultDelete?: boolean // 不启用默认删除
searchPlaceholder?: string //搜索框提示,
addLabel?: string //添加按钮名称,
editLabel?: string //编辑按钮名称
deleteLabel?: string
limit?: number
noAdd?: boolean
noEdit?: boolean
noDelete?: boolean
noActionGroup?:boolean
noCheck?: boolean //是否打开多选
actionGroup?: actionGroupType[] //操作组
noBatchDelete?: boolean // 是否没有批量删除
filterGroup?: filterGroupType[] //过滤组
defaultParams?: Object
noPage?:boolean
// 表单组件相关
formList?:formListType[]
addFormLabel?:string
editFormLabel?:string
}
// 定义父组件传递给子组件的参数
const props = defineProps<Props>()
const {
rowKey ='id',//如果rowkey没有传,就给默认值id,
noDefaultDelete = false,
searchPlaceholder = "搜索",//搜索框提示,默认是搜索
addLabel = "添加",
editLabel = "编辑",
deleteLabel = "删除",
limit = 10,
} = props
// 表格组件的数据类型
const data = reactive<listResponse<any>>({
list: [],
count: 0,
})
// 请求参数类型
const params = reactive<paramsType>({
limit: props.noPage ? -1 : limit,
})
// 获取数据的函数
async function getlist(newParams?: paramsType) {
// 判断有没有默认的params
if (props.defaultParams) {
Object.assign(params, props.defaultParams)
}
loading.value = true
if (newParams) {
Object.assign(params, newParams)
}
const res = await props.url(params)
loading.value = false
if (res.code) {
Message.error(res.msg)
return
}
data.list = res.data.list || []
data.count = res.data.count
}
// 打开多选的配置
const selectedKeys = ref([]);
const rowSelection = reactive<TableRowSelection>({
type: 'checkbox',
showCheckedAll: true,
onlyCurrent: false,
});
const actionValue = ref()
export interface actionGroupType {
label: string
value?: number
callback: (keyList: number[]) => void
}
const actionGroupOptions = ref<actionGroupType[]>([])
// 操作组函数:找到这个 actionGroupOptions 值,然后如果有的话就执行相对应的回调函数
function actionGroupAction() {
const action = actionGroupOptions.value.find((value) => value.value === actionValue.value)
if (action) {
action.callback(selectedKeys.value)
}
}
// 初始化操作组,在开启默认批量删除时,添加一个批量删除的action,并执行回调函数删除对应的keyList的数据
function initActionGroup() {
let index = 0
if (!props.noBatchDelete) {
actionGroupOptions.value.push({
label: "批量删除",
value: 1,
callback: (keyList: number[]) => {
baseDelete(keyList)
selectedKeys.value = []
}
})
index = 1
}
index++
const actionGroup = props.actionGroup || []
for (const action of actionGroup) {
actionGroupOptions.value.push({
label: action.label,
value: index,
callback: action.callback,
})
}
}
initActionGroup()
// 过滤组类型
export interface filterGroupType {
label: string
source: optionsType[] | optionsFunc // 既可以处理options类型,也可以处理函数类型
options?: optionsType[]
column?: string
params?: paramsType
callback?: (value: number | string) => void
width?: number
}
const filterGroups = ref<filterGroupType[]>([])
async function initFilterGroup() {
filterGroups.value = []
// 判断是否是函数类型,是的话,获取到数据,进行赋值
for (const f of props.filterGroup || []) {
if (typeof f.source === 'function') {
const res = await f.source(f.params)
if (res.code) {
Message.error(res.msg)
continue
}
f.options = res.data
} else {
f.options = f.source
}
if (!f.callback) {
// 如果没有callback,那就走默认行为
f.callback = (value) => {
if (f.column) {
const p: { [key: string]: any } = {}
p[f.column] = value
console.log(p)
getlist(p)
}
}
}
filterGroups.value.push(f)
}
}
initFilterGroup()
function edit(record: any) {
if (props.formList?.length) {
modalFormRef.value.setForm(record)
visible.value = true
return
}
emits("edit", record)
}
function add() {
if (props.formList?.length) {
visible.value = true
return
}
emits("add")
}
function rowClick(record: TableData, ev: Event) {
emits("row-click", record)
}
// 抛给父组件的时间
const emits = defineEmits<{
(e: 'delete', keyList: number[] | string[]): void
(e: 'add'): void
(e: 'edit', record: any): void
(e: "row-click", record: any): void //这个是table组件默认自带的点击行的触发函数,将其保留传到子组件中
}>()
// 删除逻辑进行封装
async function baseDelete(keyList: number[]) {
if (noDefaultDelete) {
// 不启用默认删除,将删除时间抛给父组件
emits("delete", keyList)
return
}
// 拿到传递接口的路由,拿到路由路径
const array = /\"(.*?)\"/.exec(props.url.toString())
if (array?.length !== 2) {
return
}
const url = array[1]
const res = await defaultDeleteApi(url, keyList)
if (res.code) {
Message.error(res.msg)
return
}
getlist()
Message.success(res.msg)
}
// 刷新
function refresh() {
getlist()
Message.success("刷新成功")
}
async function remove(record: any) {
const key = record[rowKey]
baseDelete([key])
}
// 页数发生变化时
function pageChange(){
getlist()
}
// 搜索
function search(){
getlist()
}
getlist()
// 将父组件的数据函数抛出
defineExpose({
getlist,
data,
})
// ======================表单组件相关=====================
import G_modal_form from "./g_modal_form.vue";
import {defaultDeleteApi, defaultPostApi, defaultPutApi} from "@/api";
import type { formListType,emitFnType } from "./g_modal_form.vue";
const modalFormRef = ref()
const visible = ref(false)
// 默认删除和更新
async function create(form: any, fn?: emitFnType) {
const array = /\"(.*?)\"/.exec(props.url.toString())
if (array?.length !== 2) {
return
}
const url = array[1]
const res = await defaultPostApi(url, form)
if (res.code) {
Message.error(res.msg)
return
}
getlist()
if (fn) {
fn(true)
}
}
async function update(form: any, fn?: emitFnType) {
const array = /\"(.*?)\"/.exec(props.url.toString())
if (array?.length !== 2) {
return
}
const url = array[1]
const res = await defaultPutApi(url, form)
if (res.code) {
Message.error(res.msg)
return
}
getlist()
if (fn) {
fn(true)
}
}
</script>
<template>
<div class="g_list_com">
<!-- 将表格组件嵌套进来 -->
<g_modal_form
ref="modalFormRef"
v-if="props.formList?.length"
:add-label="props.addFormLabel"
:edit-label="props.editFormLabel"
@create="create"
@update="update"
v-model:visible="visible"
:form-list="props.formList"
></g_modal_form>
<div class="g_list_head">
<div class="action_create" v-if="!noAdd">
<a-button type="primary" @click="add">{{ addLabel }}</a-button>
</div>
<div class="action_group" v-if="!noActionGroup">
<a-select style="width: 140px;" placeholder="操作" v-model="actionValue"
:options="actionGroupOptions"></a-select>
<a-button type="primary" status="danger" @click="actionGroupAction" v-if="actionValue">执行</a-button>
</div>
<div class="actions_search">
<a-input-search :placeholder="searchPlaceholder" v-model="params.key" @search="search"></a-input-search>
</div>
<div class="action_search_slot">
<slot name="search_other"></slot>
</div>
<div class="action_filter">
<a-select v-for="item in filterGroups" :style="{width: item.width + 'px'}" allow-clear @change="item.callback" :placeholder="item.label" :options="item.options" ></a-select>
</div>
<div class="action_refresh" @click="refresh">
<icon-refresh></icon-refresh>
</div>
</div>
<div class="g_list_body">
<a-spin :loading="loading" tip="加载中...">
<div class="g_list_table">
<a-table @row-click="rowClick" :data="data.list" :row-key="rowKey" v-model:selectedKeys="selectedKeys"
:row-selection="props.noCheck ? undefined : rowSelection " >
<!-- 通过 #columns 插槽和 <a-table-column> 组件可以使用模板的方法自定义列渲染 -->
<template #columns>
<template v-for="col in props.columns">
<a-table-column v-if="col.dataIndex" v-bind="col"> </a-table-column>
<a-table-column v-else-if="col.slotName" v-bind="col">
<template #cell="data">
<!-- 如果是slotName是actin的话,显示操作按钮 -->
<div class="col_actions" v-if="col.slotName ==='action'">
<!-- action_left 预留在左边的按钮-->
<slot v-bind="data" name="action_left"></slot>
<!-- 编辑按钮 -->
<a-button type="primary" @click="edit(data.record)" v-if="!noEdit">{{ editLabel }}</a-button>
<!-- 删除按钮 -->
<a-popconfirm @ok="remove(data.record)" content="你确定要删除该记录吗?">
<a-button type="primary" status="danger" v-if="!noDelete">{{ deleteLabel }}</a-button>
</a-popconfirm>
<!-- action_left 预留在右边边的按钮-->
<slot v-bind="data" name="action_rigth"></slot>
</div>
<!-- 如果字段中有dateFormat就使用工具中的函数处理时间格式 -->
<div v-if="col.dateFormat">
{{ dateTemFormat(data.record[col.slotName],col.dateFormat) }}
</div>
<!-- 使用卡槽将传递给子组件,实现自定义 -->
<slot v-else :name="col.slotName" v-bind="data"></slot>
</template>
</a-table-column>
</template>
</template>
</a-table>
</div>
<div class="g_list_page" v-if="!noPage">
<!-- 分页 -->
<a-pagination show-total @change="pageChange" :total="data.count" v-model:current="params.page" :page-size="params.limit"></a-pagination>
</div>
</a-spin>
</div>
</div>
</template>
<style lang='less'>
.g_list_com {
.g_list_head {
padding: 20px 20px 10px 20px;
border-bottom: @g_border;
display: flex;
align-items: center;
position: relative;
.action_create,
.action_group,
.action_search,
.action_search_slot {
margin-right: 10px;
}
.action_group {
display: flex;
align-items: center;
button {
margin-left: 10px;
}
}
.action_filter {
.arco-select {
margin-right: 10px;
&:last-child {
margin-right: 0;
}
}
}
.action_refresh {
position: absolute;
right: 20px;
width: 30px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
background-color: var(--color-fill-2);
border-radius: 5px;
cursor: pointer;
}
}
.g_list_body {
padding: 10px 20px 20px 20px;
>.arco-spin {
width: 100%;
}
.g_list_page {
display: flex;
justify-content: center;
margin-top: 10px;
}
.col_actions {
button {
margin-right: 10px;
&:last-child {
margin-right: 0;
}
}
}
}
}
</style>
src\assets\publick.less
javascript
/* WebKit内核浏览器(Chrome/Safari/Edge)专用语法 滚动条美化 只在scrollbar类id下生效,普通滚动条不受影响*/
.scrollbar::-webkit-scrollbar {
width: 12px; /* 垂直滚动条宽度 */
height: 12px; /* 水平滚动条高度 */
}
.scrollbar::-webkit-scrollbar-track {
background:var(--color-fill-2); /* 滚动条轨道背景 */
border-radius: 6px; /* 轨道圆角 */
}
.scrollbar::-webkit-scrollbar-thumb {
background:var(--color-fill-1); /* 滑块颜色 */
border: 2px solid var(--color-fill-1);/* 白色边框营造悬浮感 */
border-radius: 8px; /* 滑块圆角 */
}
.scrollbar::-webkit-scrollbar-thumb:hover {
background:var(--color-fill-4); /* 悬停状态颜色 */
}
src\main.ts
javascript
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import "@/assets/base.css"//引入初始化样式(放在最前面)
import "nprogress/nprogress.css";
import App from './App.vue'
import router from './router'
import "@/assets/iconfont.css" //引入iconfont图标
import ArcoVue from '@arco-design/web-vue'; //引入arco_design
import '@arco-design/web-vue/dist/arco.css';//引入arco_design 样式
import "@/assets/publick.less" //自定义样式
import ArcoVueIcon from '@arco-design/web-vue/es/icon'; //引入arco_design 图标
import {apiMock} from "@/mock"; //引入mock
import "@/assets/theme.css"
const app = createApp(App)
apiMock() //启动mock的启动函数
app.use(createPinia())
app.use(router)
app.use(ArcoVue); //注册arco_design
app.use(ArcoVueIcon);//注册arco_design图标
app.mount('#app')
管理員修改用户信息接口
api/user_api/admin_user_info_update.go
Go
package user_api
import (
"blog_server/common/res"
"blog_server/global"
"blog_server/models"
"blog_server/models/enum"
"blog_server/utils/mps"
"github.com/gin-gonic/gin"
)
//1.可以更改新的用户名(30天内修改一次)不能那重复
//2.可以更新简介
//3.可以更改兴趣标签
//4.可以更新头像,昵称,(qq登录的不能修改)
//5.可以更新隐私设置
//6.可以更改主页样式
type AdminUserInfoUpdateRequest struct {
UserID uint ` json:"user_id" binging:"required"`
Username *string ` json:"username" s-u:"username"`
Nickname *string ` json:"nickname" s-u:"nickname"`
Avatar *string ` json:"avatar" s-u:"avatar"`
Abstract *string ` json:"abstract" s-u:"abstract"`
Role *enum.RoleType `json:"role" s-u:"role"`
}
func (UserApi) AdminUserInfoUpdateView(c *gin.Context) {
var cr AdminUserInfoUpdateRequest
err := c.ShouldBindJSON(&cr)
if err != nil {
res.FailWithError(err, c)
return
}
userMap := mps.StructToMap(cr, "s-u")
var user models.UserModel
err = global.DB.Debug().Take(&user, cr.UserID).Error
if err != nil {
res.FailWithMsg("用户不存在", c)
return
}
err = global.DB.Model(&user).Updates(userMap).Error
if err != nil {
res.FailWithMsg("用户配置信息修改失败", c)
return
}
res.OkWithMsg("用户信息修改成功", c)
}
前端
==============banner=================================
1.修改index.ts中的idList为id_list 并关掉七牛enable
2.banner enter 修改添加type
3.src\components\web\f_cover_cutter.vue -->if (!store.siteInfo.qi_niu.enable)
4.banner enter 添加 Likes: []string{"cover", "href"},
====================================================
==========article===============
· 审核接口-》res.OkWithMsg("审核成功", c)
·// 用户置顶文章表
type UserTopArticleModel struct {
Model
UserID uint `gorm:"uniqueIndex:idx_name" json:"user_id"`
ArticleID uint `gorm:"uniqueIndex:idx_name" json:"article_id"`
UserModel UserModel `gorm:"foreignKey:user_id" json:"-"`
ArticleModel ArticleModel `gorm:"foreignKey:article_id" json:"-"`
}
· article列表接口添加admin置顶逻辑
·article_list.vue ->{title: "最后更新时间", dataIndex: 'updated_at', type: "date", dateFormat: "current"},form.user_id = record.id 修改userID为user_id
·user_list.vue ->{title: "发文数", dataIndex: 'article_count'},
=========================================
==========log============================
·log_api.ts将字段变为小写
·log接口-》res.FailWithMsg("日志读取成功", c)
·log_list.vue ->:class="{is_read: record.is_read}"
·将logType全部改为log_type
·src\views\admin\settings_manage\log_list.vue-><f_user v-if="record.user_id" :nickname="record.user_name" :avatar="record.avatar"></f_user>
=========================================