feat: add cover image functionality for articles
- Implemented `resolveApiURL` function to handle various URL formats. - Updated `ArticleDetail` and `UpdateArticleRequest` interfaces to include `cover_asset_url`. - Enhanced `ArticleEditorView` to manage cover image uploads, including a new `CoverPickerModal` component. - Added cover image requirements logic in `cover-requirements.ts` to enforce platform-specific cover image rules. - Modified backend services to handle cover image uploads and validations, including checks for required cover images for specific platforms. - Improved error handling for cover image requirements during article publishing.
This commit is contained in:
@@ -263,6 +263,7 @@ type ArticleDetailResponse struct {
|
||||
TemplateName *string `json:"template_name"`
|
||||
GenerationMode *string `json:"generation_mode"`
|
||||
Platforms []string `json:"platforms"`
|
||||
CoverAssetURL *string `json:"cover_asset_url"`
|
||||
GenerateStatus string `json:"generate_status"`
|
||||
PublishStatus string `json:"publish_status"`
|
||||
Title *string `json:"title"`
|
||||
@@ -310,6 +311,7 @@ func (s *ArticleService) Detail(ctx context.Context, id int64) (*ArticleDetailRe
|
||||
}
|
||||
d.GenerationMode = resolveArticleGenerationMode(dbSourceType, inputParamsJSON)
|
||||
d.Platforms = resolveArticlePlatforms(wizardStateJSON, inputParamsJSON)
|
||||
d.CoverAssetURL = resolveArticleCoverAssetURL(wizardStateJSON)
|
||||
d.WordCount = resolveWordCount(d.MarkdownContent, d.WordCount)
|
||||
return &d, nil
|
||||
}
|
||||
@@ -366,6 +368,7 @@ type UpdateArticleRequest struct {
|
||||
Title string `json:"title"`
|
||||
MarkdownContent string `json:"markdown_content"`
|
||||
Platforms []string `json:"platforms"`
|
||||
CoverAssetURL *string `json:"cover_asset_url"`
|
||||
}
|
||||
|
||||
type ArticleImageUploadResponse struct {
|
||||
@@ -418,7 +421,7 @@ func (s *ArticleService) Update(ctx context.Context, id int64, req UpdateArticle
|
||||
|
||||
wordCount := countArticleWords(markdown)
|
||||
sourceLabel := "手动编辑"
|
||||
nextWizardStateJSON, err := mergeArticleWizardState(wizardStateJSON, title, req.Platforms)
|
||||
nextWizardStateJSON, err := mergeArticleWizardState(wizardStateJSON, title, req.Platforms, req.CoverAssetURL)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50012, "update_failed", "failed to update article metadata")
|
||||
}
|
||||
|
||||
@@ -456,6 +456,9 @@ func (s *MediaService) CreatePublishBatch(ctx context.Context, articleID int64,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if publishBatchRequiresCover(accountSeeds) && strings.TrimSpace(pointerStringValue(req.CoverAssetURL)) == "" {
|
||||
return nil, response.ErrBadRequest(40044, "publish_cover_required", "baijiahao publishing requires a cover image")
|
||||
}
|
||||
|
||||
var publishBatchID int64
|
||||
if err := tx.QueryRow(ctx, `
|
||||
@@ -529,6 +532,22 @@ func (s *MediaService) CreatePublishBatch(ctx context.Context, articleID int64,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func publishBatchRequiresCover(accounts []platformAccountSeed) bool {
|
||||
for _, account := range accounts {
|
||||
if strings.TrimSpace(account.PlatformID) == "baijiahao" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func pointerStringValue(value *string) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
func (s *MediaService) ListArticlePublishRecords(ctx context.Context, articleID int64) ([]PublishRecordResponse, error) {
|
||||
actor := auth.MustActor(ctx)
|
||||
return s.listArticlePublishRecords(ctx, actor.TenantID, articleID)
|
||||
|
||||
@@ -181,7 +181,7 @@ func (s *PromptRuleGenerationService) GenerateFromRule(ctx context.Context, req
|
||||
}
|
||||
|
||||
inputJSON, _ := json.Marshal(inputParams)
|
||||
wizardStateJSON, _ := mergeArticleWizardState(nil, promptRuleGeneratingTitlePlaceholder, platformIDs)
|
||||
wizardStateJSON, _ := mergeArticleWizardState(nil, promptRuleGeneratingTitlePlaceholder, platformIDs, nil)
|
||||
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -78,6 +78,31 @@ func extractPlatformsFromJSONPayload(raw []byte) []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveArticleCoverAssetURL(wizardStateJSON []byte) *string {
|
||||
value := strings.TrimSpace(extractStringFromJSONPayload(wizardStateJSON, "cover_asset_url"))
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
return &value
|
||||
}
|
||||
|
||||
func extractStringFromJSONPayload(raw []byte, key string) string {
|
||||
if len(raw) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(raw, &payload); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
value, ok := payload[key].(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
|
||||
func normalizePlatformsFromValue(raw interface{}) []string {
|
||||
switch typed := raw.(type) {
|
||||
case nil:
|
||||
@@ -101,7 +126,7 @@ func normalizePlatformsFromValue(raw interface{}) []string {
|
||||
}
|
||||
}
|
||||
|
||||
func mergeArticleWizardState(raw []byte, title string, platforms []string) ([]byte, error) {
|
||||
func mergeArticleWizardState(raw []byte, title string, platforms []string, coverAssetURL *string) ([]byte, error) {
|
||||
state := make(map[string]interface{})
|
||||
if len(raw) > 0 {
|
||||
_ = json.Unmarshal(raw, &state)
|
||||
@@ -123,5 +148,14 @@ func mergeArticleWizardState(raw []byte, title string, platforms []string) ([]by
|
||||
}
|
||||
}
|
||||
|
||||
if coverAssetURL != nil {
|
||||
trimmedCoverURL := strings.TrimSpace(*coverAssetURL)
|
||||
if trimmedCoverURL == "" {
|
||||
delete(state, "cover_asset_url")
|
||||
} else {
|
||||
state["cover_asset_url"] = trimmedCoverURL
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(state)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user