从零到精通:用go+vue语言打造高效多语言博客系统的完整指南

后端部分 (Go)

1. 首先创建文章相关的数据模型

go:app/model/article.go 复制代码
package model

import (
	"gorm.io/gorm"
)

// Article 文章主表
type Article struct {
	gorm.Model
	Status     uint8  `json:"status" gorm:"default:1"` // 状态:0-禁用 1-启用
	Sort       int    `json:"sort" gorm:"default:0"`   // 排序
	AuthorId   uint   `json:"authorId"`                // 作者ID
	CategoryId uint   `json:"categoryId"`              // 分类ID
	Thumbnail  string `json:"thumbnail"`               // 缩略图
	
	// 关联
	Translations []ArticleTranslation `json:"translations"`
	Category     ArticleCategory      `json:"category"`
}

// ArticleTranslation 文章翻译表
type ArticleTranslation struct {
	gorm.Model
	ArticleId   uint   `json:"articleId"`
	Lang        string `json:"lang" gorm:"size:5"`        // 语言代码 如:zh-CN, en-US
	Title       string `json:"title"`                     // 标题
	Description string `json:"description"`               // 描述
	Content     string `json:"content" gorm:"type:text"`  // 内容
	Keywords    string `json:"keywords"`                  // SEO关键词
	Slug        string `json:"slug" gorm:"uniqueIndex"`   // URL友好的标题
}

// ArticleCategory 文章分类
type ArticleCategory struct {
	gorm.Model
	ParentId     uint   `json:"parentId"`
	Status       uint8  `json:"status" gorm:"default:1"`
	Sort         int    `json:"sort" gorm:"default:0"`
	Translations []ArticleCategoryTranslation `json:"translations"`
}

// ArticleCategoryTranslation 分类翻译表
type ArticleCategoryTranslation struct {
	gorm.Model
	CategoryId  uint   `json:"categoryId"`
	Lang        string `json:"lang" gorm:"size:5"`
	Name        string `json:"name"`
	Description string `json:"description"`
	Slug        string `json:"slug" gorm:"uniqueIndex"`
}

2. 创建服务层

go:app/service/article_service.go 复制代码
package service

import (
	"dagisku-server/app/model"
	"dagisku-server/global"
)

type ArticleService struct{}

type ArticleListRequest struct {
	Page     int    `json:"page" form:"page"`
	PageSize int    `json:"pageSize" form:"pageSize"`
	Lang     string `json:"lang" form:"lang"`
	Status   *uint8 `json:"status" form:"status"`
	CategoryId *uint `json:"categoryId" form:"categoryId"`
}

func (s *ArticleService) GetList(req ArticleListRequest) (list []model.Article, total int64, err error) {
	limit := req.PageSize
	offset := req.PageSize * (req.Page - 1)
	
	db := global.DB.Model(&model.Article{})
	
	// 构建查询条件
	if req.Status != nil {
		db = db.Where("status = ?", *req.Status)
	}
	if req.CategoryId != nil {
		db = db.Where("category_id = ?", *req.CategoryId)
	}
	
	// 预加载翻译数据
	db = db.Preload("Translations", "lang = ?", req.Lang)
	db = db.Preload("Category").Preload("Category.Translations", "lang = ?", req.Lang)
	
	err = db.Count(&total).Error
	if err != nil {
		return
	}
	
	err = db.Order("sort desc, id desc").Limit(limit).Offset(offset).Find(&list).Error
	return
}

func (s *ArticleService) Create(article *model.Article) error {
	return global.DB.Create(article).Error
}

func (s *ArticleService) Update(article *model.Article) error {
	return global.DB.Save(article).Error
}

func (s *ArticleService) Delete(id uint) error {
	return global.DB.Delete(&model.Article{}, id).Error
}

3. 创建控制器

go:app/controller/admin/article/article.go 复制代码
package article

import (
	"dagisku-server/app/model"
	"dagisku-server/app/service"
	"dagisku-server/utils/response"
	"github.com/gin-gonic/gin"
)

type ArticleApi struct{}

// List 获取文章列表
func (api *ArticleApi) List(c *gin.Context) {
	var req service.ArticleListRequest
	if err := c.ShouldBindQuery(&req); err != nil {
		response.FailWithMessage(err.Error(), c)
		return
	}
	
	if req.Page == 0 {
		req.Page = 1
	}
	if req.PageSize == 0 {
		req.PageSize = 10
	}
	
	service := service.ArticleService{}
	list, total, err := service.GetList(req)
	if err != nil {
		response.FailWithMessage(err.Error(), c)
		return
	}
	
	response.OkWithData(gin.H{
		"list": list,
		"total": total,
	}, c)
}

// Create 创建文章
func (api *ArticleApi) Create(c *gin.Context) {
	var article model.Article
	if err := c.ShouldBindJSON(&article); err != nil {
		response.FailWithMessage(err.Error(), c)
		return
	}
	
	service := service.ArticleService{}
	if err := service.Create(&article); err != nil {
		response.FailWithMessage(err.Error(), c)
		return
	}
	
	response.Ok(c)
}

// Update 更新文章
func (api *ArticleApi) Update(c *gin.Context) {
	var article model.Article
	if err := c.ShouldBindJSON(&article); err != nil {
		response.FailWithMessage(err.Error(), c)
		return
	}
	
	service := service.ArticleService{}
	if err := service.Update(&article); err != nil {
		response.FailWithMessage(err.Error(), c)
		return
	}
	
	response.Ok(c)
}

// Delete 删除文章
func (api *ArticleApi) Delete(c *gin.Context) {
	id := c.Param("id")
	service := service.ArticleService{}
	if err := service.Delete(uint(id)); err != nil {
		response.FailWithMessage(err.Error(), c)
		return
	}
	
	response.Ok(c)
}

4. 注册路由

go:app/controller/admin/enter.go 复制代码
// ... existing code ...

type ApiGroup struct {
	LoginApi
	system.DBApi
	system.JwtApi
	// ... other existing APIs ...
	article.ArticleApi
	article.ArticleCategoryApi
}

// ... existing code ...
go:router/admin.go 复制代码
func InitAdminRouter(Router *gin.RouterGroup) {
	adminRouter := Router.Group("admin")
	
	// ... existing routes ...
	
	// 文章相关路由
	articleRouter := adminRouter.Group("article")
	{
		articleRouter.GET("list", articleApi.List)
		articleRouter.POST("create", articleApi.Create)
		articleRouter.PUT("update", articleApi.Update)
		articleRouter.DELETE("delete/:id", articleApi.Delete)
	}
}

前端部分 (Nuxt 3)

1. 创建API请求

typescript:composables/api/article.ts 复制代码
import { useFetch } from '#app'

export const useArticleApi = () => {
  const config = useRuntimeConfig()
  const baseURL = config.public.apiBase
  
  const getList = async (params: {
    page: number
    pageSize: number
    lang: string
    status?: number
    categoryId?: number
  }) => {
    return await useFetch('/admin/article/list', {
      baseURL,
      method: 'GET',
      params
    })
  }
  
  const create = async (data: any) => {
    return await useFetch('/admin/article/create', {
      baseURL,
      method: 'POST',
      body: data
    })
  }
  
  const update = async (data: any) => {
    return await useFetch('/admin/article/update', {
      baseURL,
      method: 'PUT',
      body: data
    })
  }
  
  const remove = async (id: number) => {
    return await useFetch(`/admin/article/delete/${id}`, {
      baseURL,
      method: 'DELETE'
    })
  }
  
  return {
    getList,
    create,
    update,
    remove
  }
}

2. 创建文章列表页面

vue:pages/admin/article/index.vue 复制代码
<template>
  <div>
    <el-card>
      <!-- 搜索栏 -->
      <el-form :inline="true" :model="searchForm">
        <el-form-item>
          <el-select v-model="searchForm.lang" placeholder="选择语言">
            <el-option label="中文" value="zh-CN" />
            <el-option label="English" value="en-US" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="loadData">搜索</el-button>
          <el-button @click="handleAdd">新增</el-button>
        </el-form-item>
      </el-form>
      
      <!-- 数据表格 -->
      <el-table :data="tableData" v-loading="loading">
        <el-table-column prop="id" label="ID" width="80" />
        <el-table-column label="标题">
          <template #default="{ row }">
            {{ row.translations?.[0]?.title }}
          </template>
        </el-table-column>
        <el-table-column label="分类">
          <template #default="{ row }">
            {{ row.category?.translations?.[0]?.name }}
          </template>
        </el-table-column>
        <el-table-column prop="status" label="状态">
          <template #default="{ row }">
            <el-tag :type="row.status === 1 ? 'success' : 'info'">
              {{ row.status === 1 ? '启用' : '禁用' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="200">
          <template #default="{ row }">
            <el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
            <el-button type="danger" link @click="handleDelete(row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
      
      <!-- 分页 -->
      <div class="pagination-container">
        <el-pagination
          v-model:current-page="page"
          v-model:page-size="pageSize"
          :total="total"
          @current-change="loadData"
        />
      </div>
    </el-card>
    
    <!-- 编辑弹窗 -->
    <el-dialog
      :title="dialogTitle"
      v-model="dialogVisible"
      width="800px"
    >
      <article-form
        v-if="dialogVisible"
        :form-data="formData"
        @submit="handleSubmit"
        @cancel="dialogVisible = false"
      />
    </el-dialog>
  </div>
</template>

<script setup lang="ts">
const { getList, remove } = useArticleApi()

// 状态定义
const searchForm = ref({
  lang: 'zh-CN'
})
const loading = ref(false)
const tableData = ref([])
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formData = ref<any>({})

// 加载数据
const loadData = async () => {
  loading.value = true
  try {
    const { data } = await getList({
      page: page.value,
      pageSize: pageSize.value,
      lang: searchForm.value.lang
    })
    tableData.value = data.value.list
    total.value = data.value.total
  } finally {
    loading.value = false
  }
}

// 处理新增
const handleAdd = () => {
  formData.value = {
    status: 1,
    translations: [{ lang: searchForm.value.lang }]
  }
  dialogTitle.value = '新增文章'
  dialogVisible.value = true
}

// 处理编辑
const handleEdit = (row: any) => {
  formData.value = { ...row }
  dialogTitle.value = '编辑文章'
  dialogVisible.value = true
}

// 处理删除
const handleDelete = async (row: any) => {
  try {
    await ElMessageBox.confirm('确认删除该文章吗?')
    await remove(row.id)
    ElMessage.success('删除成功')
    loadData()
  } catch (err) {
    // 取消删除时不显示错误
    if (err !== 'cancel') {
      ElMessage.error('删除失败')
    }
  }
}

// 处理表单提交
const handleSubmit = async (data: any) => {
  try {
    if (data.id) {
      await update(data)
    } else {
      await create(data)
    }
    ElMessage.success('保存成功')
    dialogVisible.value = false
    loadData()
  } catch (err) {
    ElMessage.error('保存失败')
  }
}

// 初始加载
onMounted(() => {
  loadData()
})
</script>

<style scoped>
.pagination-container {
  margin-top: 20px;
  text-align: right;
}
</style>

3. 创建文章表单组件

vue:components/ArticleForm.vue 复制代码
<template>
  <el-form
    ref="formRef"
    :model="form"
    :rules="rules"
    label-width="100px"
  >
    <el-tabs v-model="activeLang">
      <el-tab-pane
        v-for="lang in languages"
        :key="lang.value"
        :label="lang.label"
        :name="lang.value"
      >
        <el-form-item
          :prop="`translations.${getTransIndex(lang.value)}.title`"
          label="标题"
        >
          <el-input
            v-model="getTranslation(lang.value).title"
            placeholder="请输入标题"
          />
        </el-form-item>
        
        <el-form-item
          :prop="`translations.${getTransIndex(lang.value)}.description`"
          label="描述"
        >
          <el-input
            type="textarea"
            v-model="getTranslation(lang.value).description"
            placeholder="请输入描述"
          />
        </el-form-item>
        
        <el-form-item
          :prop="`translations.${getTransIndex(lang.value)}.content`"
          label="内容"
        >
          <editor
            v-model="getTranslation(lang.value).content"
            :height="400"
          />
        </el-form-item>
      </el-tab-pane>
    </el-tabs>
    
    <el-form-item label="分类" prop="categoryId">
      <el-select v-model="form.categoryId">
        <el-option
          v-for="item in categories"
          :key="item.id"
          :label="item.translations[0].name"
          :value="item.id"
        />
      </el-select>
    </el-form-item>
    
    <el-form-item label="缩略图" prop="thumbnail">
      <upload v-model="form.thumbnail" />
    </el-form-item>
    
    <el-form-item label="状态" prop="status">
      <el-switch
        v-model="form.status"
        :active-value="1"
        :inactive-value="0"
      />
    </el-form-item>
    
    <el-form-item>
      <el-button type="primary" @click="handleSubmit">保存</el-button>
      <el-button @click="$emit('cancel')">取消</el-button>
    </el-form-item>
  </el-form>
</template>

<script setup lang="ts">
const props = defineProps<{
  formData: any
}>()

const emit = defineEmits(['submit', 'cancel'])

// 表单数据
const form = ref({
  ...props.formData,
  translations: props.formData.translations || []
})

// 语言配置
const languages = [
  { label: '中文', value: 'zh-CN' },
  { label: 'English', value: 'en-US' }
]
const activeLang = ref(languages[0].value)

// 获取翻译索引
const getTransIndex = (lang: string) => {
  return form.value.translations.findIndex((t: any) => t.lang === lang)
}

// 获取或创建翻译
const getTranslation = (lang: string) => {
  let trans = form.value.translations.find((t: any) => t.lang === lang)
  if (!trans) {
    trans = { lang }
    form.value.translations.push(trans)
  }
  return trans
}

// 表单验证规则
const rules = {
  categoryId: [{ required: true, message: '请选择分类' }],
  translations: {
    title: [{ required: true, message: '请输入标题' }],
    content: [{ required: true, message: '请输入内容' }]
  }
}

// 提交表单
const formRef = ref()
const handleSubmit = async () => {
  if (!formRef.value) return
  await formRef.value.validate()
  emit('submit', form.value)
}
</script>

4. 多语言配置

typescript:plugins/i18n.ts 复制代码
import { createI18n } from 'vue-i18n'

export default defineNuxtPlugin(({ vueApp }) => {
  const i18n = createI18n({
    legacy: false,
    globalInjection: true,
    locale: 'zh-CN',
    messages: {
      'zh-CN': {
        article: {
          title: '文章管理',
          list: '文章列表',
          add: '新增文章',
          edit: '编辑文章',
          // ... 其他翻译
        }
      },
      'en-US': {
        article: {
          title: 'Article Management',
          list: 'Article List',
          add: 'New Article',
          edit: 'Edit Article',
          // ... 其他翻译
        }
      }
    }
  })

  vueApp.use(i18n)
})

5. 前端路由配置

typescript:middleware/auth.ts 复制代码
export default defineNuxtRouteMiddleware((to) => {
  const token = useCookie('token')
  
  if (!token.value && to.path.startsWith('/admin')) {
    return navigateTo('/login')
  }
})

这个实现包含了:

  1. 后端完整的CRUD接口
  2. 多语言支持(中英文)
  3. 富文本编辑器支持
  4. 图片上传功能
  5. 分类管理
  6. 权限控制
相关推荐
喵手1 分钟前
Spring Boot 中的事务管理是如何工作的?
数据库·spring boot·后端
白总Server1 小时前
Golang领域Beego框架的中间件开发实战
服务器·网络·websocket·网络协议·udp·go·ssl
玄武后端技术栈1 小时前
什么是延迟队列?RabbitMQ 如何实现延迟队列?
分布式·后端·rabbitmq
液态不合群2 小时前
rust程序静态编译的两种方法总结
开发语言·后端·rust
bingbingyihao3 小时前
SpringBoot教程(vuepress版)
java·spring boot·后端
一切皆有迹可循4 小时前
Spring Boot 基于 CAS 实现单点登录:原理、实践与优化全解析
java·spring boot·后端
Kookoos4 小时前
从单体到微服务:基于 ABP vNext 模块化设计的演进之路
后端·微服务·云原生·架构·c#·.net
weixin_438335406 小时前
springboot使用阿里云OSS实现文件上传
spring boot·后端·阿里云
咸鱼睡不醒_8 小时前
SpringBoot项目接入DeepSeek
java·spring boot·后端
yi念zhi间9 小时前
如何把ASP.NET Core WebApi打造成Mcp Server
后端·ai·mcp