更新结构
@@ -1,6 +1,4 @@
|
||||
<template>
|
||||
|
||||
|
||||
<div class="article-publish-page">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
@@ -26,6 +24,35 @@
|
||||
</p>
|
||||
</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. 封面图上传 -->
|
||||
<div class="form-group cover-group">
|
||||
<label class="form-label">文章封面</label>
|
||||
@@ -117,13 +144,15 @@ import { onMounted, ref, computed } from 'vue';
|
||||
import Quill from 'quill';
|
||||
import 'quill/dist/quill.snow.css';
|
||||
// Element Plus 组件与图标
|
||||
import { ElUpload, ElButton, ElIcon, ElMessage } from 'element-plus';
|
||||
import { Upload, Close, Loading } from '@element-plus/icons-vue';
|
||||
import { ElUpload, ElButton, ElIcon, ElMessage, ElRadioGroup, ElRadio } from 'element-plus';
|
||||
import { Upload, Close, Loading, Document, UserFilled, Trophy, Files } from '@element-plus/icons-vue';
|
||||
import type { UploadProps } from 'element-plus';
|
||||
|
||||
// -------------------------- 状态管理 --------------------------
|
||||
// 文章标题
|
||||
const articleTitle = ref('');
|
||||
// 选中的专题
|
||||
const selectedTopic = ref('news'); // 默认选中"新闻"
|
||||
// 封面图URL
|
||||
const coverUrl = ref('');
|
||||
// 富文本编辑器容器引用
|
||||
@@ -135,8 +164,6 @@ let quillInstance: Quill | null = null;
|
||||
const isSubmitting = ref(false); // 发布中
|
||||
const isSavingDraft = ref(false); // 保存草稿中
|
||||
|
||||
//抽屉状态
|
||||
|
||||
// -------------------------- 表单校验 --------------------------
|
||||
// 标题校验
|
||||
const titleIsValid = ref(false);
|
||||
@@ -301,6 +328,10 @@ onMounted(() => {
|
||||
const draft = JSON.parse(savedDraft);
|
||||
articleTitle.value = draft.title;
|
||||
coverUrl.value = draft.coverUrl;
|
||||
// 加载保存的专题选择
|
||||
if (draft.topic) {
|
||||
selectedTopic.value = draft.topic;
|
||||
}
|
||||
if (quillInstance && draft.contentHtml) {
|
||||
quillInstance.root.innerHTML = draft.contentHtml;
|
||||
// 草稿加载后触发校验
|
||||
@@ -429,6 +460,7 @@ const saveDraft = async () => {
|
||||
|
||||
const draftData = {
|
||||
title: articleTitle.value.trim() || '未命名草稿',
|
||||
topic: selectedTopic.value, // 保存选中的专题
|
||||
coverUrl: coverUrl.value,
|
||||
contentHtml: quillInstance?.root.innerHTML || '',
|
||||
updatedAt: new Date().toISOString()
|
||||
@@ -452,6 +484,7 @@ const submitArticle = async () => {
|
||||
try {
|
||||
const articleData = {
|
||||
title: articleTitle.value.trim(),
|
||||
topic: selectedTopic.value, // 提交选中的专题
|
||||
coverUrl: coverUrl.value,
|
||||
contentHtml: quillInstance?.root.innerHTML || '',
|
||||
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-uploader {
|
||||
width: 100%;
|
||||
border-radius: $radius;
|
||||
overflow: hidden;
|
||||
padding: 0; // 移除父容器内边距,确保子元素贴边
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// 已上传封面预览框:紧贴左侧
|
||||
.cover-preview {
|
||||
width: 100%;
|
||||
max-width: 600px; // 保留最大宽度限制
|
||||
margin: 0; // 移除水平居中,改为靠左
|
||||
max-width: 600px;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: $radius;
|
||||
@@ -620,7 +698,7 @@ $transition: all 0.3s ease;
|
||||
min-height: 150px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start; // 图片靠左显示
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.cover-img {
|
||||
@@ -658,14 +736,12 @@ $transition: all 0.3s ease;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// 未上传封面的上传框:紧贴左侧
|
||||
.cover-upload-area {
|
||||
width: 100%;
|
||||
max-width: 600px; // 与预览框宽度一致
|
||||
margin: 0; // 移除水平居中,改为靠左
|
||||
max-width: 600px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// 上传占位框:内部内容靠左
|
||||
.upload-placeholder {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
@@ -673,12 +749,12 @@ $transition: all 0.3s ease;
|
||||
border-radius: $radius;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start; // 内容靠左对齐
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
background-color: #F9FAFB;
|
||||
transition: $transition;
|
||||
cursor: pointer;
|
||||
padding-left: 20px; // 左侧内边距,避免内容贴边框
|
||||
padding-left: 20px;
|
||||
|
||||
&:hover {
|
||||
border-color: $primary-color;
|
||||
@@ -848,6 +924,17 @@ $transition: all 0.3s ease;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
// 专题选择在移动端的适配
|
||||
.topic-radio-group {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.topic-radio {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.title-input {
|
||||
height: 44px;
|
||||
font-size: 16px;
|
||||
@@ -859,7 +946,7 @@ $transition: all 0.3s ease;
|
||||
|
||||
.upload-placeholder {
|
||||
height: 150px;
|
||||
padding-left: 16px; // 移动端减少左内边距
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.quill-editor {
|
||||
|
||||
29
server/internal/articlemodel/articlemodel.go
Normal 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))
|
||||
}
|
||||
93
server/internal/articlemodel/articlemodel_gen.go
Normal 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
|
||||
}
|
||||
5
server/internal/articlemodel/vars.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package articlemodel
|
||||
|
||||
import "github.com/zeromicro/go-zero/core/stores/sqlx"
|
||||
|
||||
var ErrNotFound = sqlx.ErrNotFound
|
||||
@@ -2,7 +2,7 @@ package router
|
||||
|
||||
import (
|
||||
"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/gin-gonic/gin"
|
||||
)
|
||||
|
||||
16
server/sql/article.sql
Normal 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;
|
||||
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB |