更新结构

This commit is contained in:
2025-10-04 22:35:53 +08:00
parent f5c3b26479
commit 48461b9c49
34 changed files with 251 additions and 21 deletions

View File

@@ -1,6 +1,4 @@
<template> <template>
<div class="article-publish-page"> <div class="article-publish-page">
<!-- 页面标题 --> <!-- 页面标题 -->
<div class="page-header"> <div class="page-header">
@@ -26,6 +24,35 @@
</p> </p>
</div> </div>
<!-- 新增专题选择 -->
<div class="form-group topic-group">
<label class="form-label">发表到专题</label>
<p class="form-desc">选择文章所属的专题分类</p>
<el-radio-group v-model="selectedTopic" class="topic-radio-group">
<el-radio label="news" class="topic-radio">
<el-icon><News /></el-icon>
<span>新闻</span>
</el-radio>
<el-radio label="cases" class="topic-radio">
<el-icon><Document /></el-icon>
<span>案例资源</span>
</el-radio>
<el-radio label="community" class="topic-radio">
<el-icon><UserFilled /></el-icon>
<span>社区服务</span>
</el-radio>
<el-radio label="awards" class="topic-radio">
<el-icon><Trophy /></el-icon>
<span>学生获奖</span>
</el-radio>
<el-radio label="papers" class="topic-radio">
<el-icon><Files /></el-icon>
<span>论文发表</span>
</el-radio>
</el-radio-group>
</div>
<!-- 2. 封面图上传 --> <!-- 2. 封面图上传 -->
<div class="form-group cover-group"> <div class="form-group cover-group">
<label class="form-label">文章封面</label> <label class="form-label">文章封面</label>
@@ -117,13 +144,15 @@ import { onMounted, ref, computed } from 'vue';
import Quill from 'quill'; import Quill from 'quill';
import 'quill/dist/quill.snow.css'; import 'quill/dist/quill.snow.css';
// Element Plus 组件与图标 // Element Plus 组件与图标
import { ElUpload, ElButton, ElIcon, ElMessage } from 'element-plus'; import { ElUpload, ElButton, ElIcon, ElMessage, ElRadioGroup, ElRadio } from 'element-plus';
import { Upload, Close, Loading } from '@element-plus/icons-vue'; import { Upload, Close, Loading, Document, UserFilled, Trophy, Files } from '@element-plus/icons-vue';
import type { UploadProps } from 'element-plus'; import type { UploadProps } from 'element-plus';
// -------------------------- 状态管理 -------------------------- // -------------------------- 状态管理 --------------------------
// 文章标题 // 文章标题
const articleTitle = ref(''); const articleTitle = ref('');
// 选中的专题
const selectedTopic = ref('news'); // 默认选中"新闻"
// 封面图URL // 封面图URL
const coverUrl = ref(''); const coverUrl = ref('');
// 富文本编辑器容器引用 // 富文本编辑器容器引用
@@ -135,8 +164,6 @@ let quillInstance: Quill | null = null;
const isSubmitting = ref(false); // 发布中 const isSubmitting = ref(false); // 发布中
const isSavingDraft = ref(false); // 保存草稿中 const isSavingDraft = ref(false); // 保存草稿中
//抽屉状态
// -------------------------- 表单校验 -------------------------- // -------------------------- 表单校验 --------------------------
// 标题校验 // 标题校验
const titleIsValid = ref(false); const titleIsValid = ref(false);
@@ -301,6 +328,10 @@ onMounted(() => {
const draft = JSON.parse(savedDraft); const draft = JSON.parse(savedDraft);
articleTitle.value = draft.title; articleTitle.value = draft.title;
coverUrl.value = draft.coverUrl; coverUrl.value = draft.coverUrl;
// 加载保存的专题选择
if (draft.topic) {
selectedTopic.value = draft.topic;
}
if (quillInstance && draft.contentHtml) { if (quillInstance && draft.contentHtml) {
quillInstance.root.innerHTML = draft.contentHtml; quillInstance.root.innerHTML = draft.contentHtml;
// 草稿加载后触发校验 // 草稿加载后触发校验
@@ -429,6 +460,7 @@ const saveDraft = async () => {
const draftData = { const draftData = {
title: articleTitle.value.trim() || '未命名草稿', title: articleTitle.value.trim() || '未命名草稿',
topic: selectedTopic.value, // 保存选中的专题
coverUrl: coverUrl.value, coverUrl: coverUrl.value,
contentHtml: quillInstance?.root.innerHTML || '', contentHtml: quillInstance?.root.innerHTML || '',
updatedAt: new Date().toISOString() updatedAt: new Date().toISOString()
@@ -452,6 +484,7 @@ const submitArticle = async () => {
try { try {
const articleData = { const articleData = {
title: articleTitle.value.trim(), title: articleTitle.value.trim(),
topic: selectedTopic.value, // 提交选中的专题
coverUrl: coverUrl.value, coverUrl: coverUrl.value,
contentHtml: quillInstance?.root.innerHTML || '', contentHtml: quillInstance?.root.innerHTML || '',
contentText: quillInstance?.getText().trim() || '', contentText: quillInstance?.getText().trim() || '',
@@ -597,21 +630,66 @@ $transition: all 0.3s ease;
} }
} }
// 封面上传样式(核心修改:紧贴左侧) // 专题选择样式
.topic-group {
.topic-radio-group {
display: flex;
flex-wrap: wrap;
gap: 20px;
padding: 10px 0;
}
.topic-radio {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
border-radius: $radius;
border: 1px solid $border-color;
cursor: pointer;
transition: $transition;
&:hover {
border-color: $primary-color;
background-color: $primary-light;
}
.el-icon {
font-size: 18px;
color: $text-secondary;
}
span {
font-size: 16px;
color: $text-primary;
}
}
.el-radio__input.is-checked + .topic-radio {
border-color: $primary-color;
background-color: $primary-light;
color: $primary-color;
.el-icon, span {
color: $primary-color;
}
}
}
// 封面上传样式
.cover-group { .cover-group {
.cover-uploader { .cover-uploader {
width: 100%; width: 100%;
border-radius: $radius; border-radius: $radius;
overflow: hidden; overflow: hidden;
padding: 0; // 移除父容器内边距,确保子元素贴边 padding: 0;
margin: 0; margin: 0;
} }
// 已上传封面预览框:紧贴左侧
.cover-preview { .cover-preview {
width: 100%; width: 100%;
max-width: 600px; // 保留最大宽度限制 max-width: 600px;
margin: 0; // 移除水平居中,改为靠左 margin: 0;
position: relative; position: relative;
border: 1px solid $border-color; border: 1px solid $border-color;
border-radius: $radius; border-radius: $radius;
@@ -620,7 +698,7 @@ $transition: all 0.3s ease;
min-height: 150px; min-height: 150px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-start; // 图片靠左显示 justify-content: flex-start;
} }
.cover-img { .cover-img {
@@ -658,14 +736,12 @@ $transition: all 0.3s ease;
opacity: 1; opacity: 1;
} }
// 未上传封面的上传框:紧贴左侧
.cover-upload-area { .cover-upload-area {
width: 100%; width: 100%;
max-width: 600px; // 与预览框宽度一致 max-width: 600px;
margin: 0; // 移除水平居中,改为靠左 margin: 0;
} }
// 上传占位框:内部内容靠左
.upload-placeholder { .upload-placeholder {
width: 100%; width: 100%;
height: 180px; height: 180px;
@@ -673,12 +749,12 @@ $transition: all 0.3s ease;
border-radius: $radius; border-radius: $radius;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; // 内容靠左对齐 align-items: flex-start;
justify-content: center; justify-content: center;
background-color: #F9FAFB; background-color: #F9FAFB;
transition: $transition; transition: $transition;
cursor: pointer; cursor: pointer;
padding-left: 20px; // 左侧内边距,避免内容贴边框 padding-left: 20px;
&:hover { &:hover {
border-color: $primary-color; border-color: $primary-color;
@@ -848,6 +924,17 @@ $transition: all 0.3s ease;
margin-bottom: 24px; margin-bottom: 24px;
} }
// 专题选择在移动端的适配
.topic-radio-group {
flex-direction: column;
gap: 12px;
}
.topic-radio {
width: 100%;
justify-content: center;
}
.title-input { .title-input {
height: 44px; height: 44px;
font-size: 16px; font-size: 16px;
@@ -859,7 +946,7 @@ $transition: all 0.3s ease;
.upload-placeholder { .upload-placeholder {
height: 150px; height: 150px;
padding-left: 16px; // 移动端减少左内边距 padding-left: 16px;
} }
.quill-editor { .quill-editor {

View File

@@ -0,0 +1,29 @@
package articlemodel
import "github.com/zeromicro/go-zero/core/stores/sqlx"
var _ ArticleModel = (*customArticleModel)(nil)
type (
// ArticleModel is an interface to be customized, add more methods here,
// and implement the added methods in customArticleModel.
ArticleModel interface {
articleModel
withSession(session sqlx.Session) ArticleModel
}
customArticleModel struct {
*defaultArticleModel
}
)
// NewArticleModel returns a model for the database table.
func NewArticleModel(conn sqlx.SqlConn) ArticleModel {
return &customArticleModel{
defaultArticleModel: newArticleModel(conn),
}
}
func (m *customArticleModel) withSession(session sqlx.Session) ArticleModel {
return NewArticleModel(sqlx.NewSqlConnFromSession(session))
}

View File

@@ -0,0 +1,93 @@
// Code generated by goctl. DO NOT EDIT.
// versions:
// goctl version: 1.9.1
package articlemodel
import (
"context"
"database/sql"
"fmt"
"strings"
"time"
"github.com/zeromicro/go-zero/core/stores/builder"
"github.com/zeromicro/go-zero/core/stores/sqlx"
"github.com/zeromicro/go-zero/core/stringx"
)
var (
articleFieldNames = builder.RawFieldNames(&Article{})
articleRows = strings.Join(articleFieldNames, ",")
articleRowsExpectAutoSet = strings.Join(stringx.Remove(articleFieldNames, "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), ",")
articleRowsWithPlaceHolder = strings.Join(stringx.Remove(articleFieldNames, "`id`", "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), "=?,") + "=?"
)
type (
articleModel interface {
Insert(ctx context.Context, data *Article) (sql.Result, error)
FindOne(ctx context.Context, id int64) (*Article, error)
Update(ctx context.Context, data *Article) error
Delete(ctx context.Context, id int64) error
}
defaultArticleModel struct {
conn sqlx.SqlConn
table string
}
Article struct {
Title string `db:"title"`
Content string `db:"content"`
Cover string `db:"cover"`
CreateAt time.Time `db:"create_at"`
UpdateAt time.Time `db:"update_at"`
IsDelete int64 `db:"is_delete"`
Topic string `db:"topic"`
Excerpt string `db:"excerpt"`
Id int64 `db:"id"`
}
)
func newArticleModel(conn sqlx.SqlConn) *defaultArticleModel {
return &defaultArticleModel{
conn: conn,
table: "`article`",
}
}
func (m *defaultArticleModel) Delete(ctx context.Context, id int64) error {
query := fmt.Sprintf("delete from %s where `id` = ?", m.table)
_, err := m.conn.ExecCtx(ctx, query, id)
return err
}
func (m *defaultArticleModel) FindOne(ctx context.Context, id int64) (*Article, error) {
query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", articleRows, m.table)
var resp Article
err := m.conn.QueryRowCtx(ctx, &resp, query, id)
switch err {
case nil:
return &resp, nil
case sqlx.ErrNotFound:
return nil, ErrNotFound
default:
return nil, err
}
}
func (m *defaultArticleModel) Insert(ctx context.Context, data *Article) (sql.Result, error) {
query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?)", m.table, articleRowsExpectAutoSet)
ret, err := m.conn.ExecCtx(ctx, query, data.Title, data.Content, data.Cover, data.IsDelete, data.Topic, data.Excerpt, data.Id)
return ret, err
}
func (m *defaultArticleModel) Update(ctx context.Context, data *Article) error {
query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, articleRowsWithPlaceHolder)
_, err := m.conn.ExecCtx(ctx, query, data.Title, data.Content, data.Cover, data.IsDelete, data.Topic, data.Excerpt, data.Id)
return err
}
func (m *defaultArticleModel) tableName() string {
return m.table
}

View File

@@ -0,0 +1,5 @@
package articlemodel
import "github.com/zeromicro/go-zero/core/stores/sqlx"
var ErrNotFound = sqlx.ErrNotFound

View File

@@ -2,7 +2,7 @@ package router
import ( import (
"github.com/JACKYMYPERSON/hldrCenter/config" "github.com/JACKYMYPERSON/hldrCenter/config"
handler "github.com/JACKYMYPERSON/hldrCenter/handler/uploadimg" handler "github.com/JACKYMYPERSON/hldrCenter/internal/handler/uploadimg"
"github.com/JACKYMYPERSON/hldrCenter/middleware" "github.com/JACKYMYPERSON/hldrCenter/middleware"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )

16
server/sql/article.sql Normal file
View File

@@ -0,0 +1,16 @@
DROP TABLE IF EXISTS `article`;
CREATE TABLE `article` (
`title` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
`content` text CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
`cover` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
`create_at` datetime NOT NULL,
`update_at` datetime NOT NULL,
`is_delete` tinyint NOT NULL,
`topic` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
`excerpt` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
`id` bigint NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = Dynamic;
-- @table:article -- 声明表名为article
SET FOREIGN_KEY_CHECKS = 1;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB