diff --git a/VERSION b/VERSION
index b7c0622b..52980677 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.74.0
+0.74.1
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 6da71cdb..cf4dc4b2 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -8,6 +8,23 @@ This project follows the repository versioning rule:
- `improvement` -> `+0.0.1`(bugfix + 小功能混合)
- `bugfix` -> `+0.0.1`
+## [0.74.1] — 2026-06-30
+
+Released: 2026-06-30
+
+### Highlights
+- 将 `/earth-content` 的品牌标识上传收敛到 `Logo 地址` 与 `标题图地址` 字段内,移除旧的全局“选择资产/上传”工具栏。
+- 新增字段级图片拖拽反馈,拖到对应字段时直接提示将图片复制为 Logo 或标题图。
+- 对齐品牌上传按钮到现有 Tactile UI primary 按钮样式,并同步中英文使用手册、快速开始和控制台上下文文档。
+
+### Added / Fixed / Improved
+- `BrandAssetInput` 支持字段内选择文件、拖拽上传、单字段 loading 和上传后回写草稿 URL。
+- `FieldGrid` 支持按字段注入自定义输入控件,同时复用统一草稿提交路径。
+- 品牌上传拖拽态改为低饱和 tactile 配色,上传按钮保持蓝色轻立体样式,容器内上下/右侧留白对齐为 3px。
+- 补齐品牌上传相关 legacy UI 英文翻译、术语对照和用户文档。
+
+---
+
## [0.74.0] — 2026-06-30
Released: 2026-06-30
diff --git a/docs/technical/en/frontend-admin-frontend-context.md b/docs/technical/en/frontend-admin-frontend-context.md
index eabbe2dc..eb391b52 100644
--- a/docs/technical/en/frontend-admin-frontend-context.md
+++ b/docs/technical/en/frontend-admin-frontend-context.md
@@ -387,6 +387,7 @@ Current boundary:
`/earth-content` reuses the same single-screen tab container from [Settings.tsx](/home/ray/dev/linkong/planet/frontend/src/admin/pages/PlainResourcePages.tsx), but its ownership is separate from System Settings:
- `TV Livestream` owns the Earth media-panel source configuration.
+- `Branding` owns Earth HUD brand assets. `Logo URL` and `Title Image URL` use inline upload controls inside the fields; the upload buttons keep the primary `TactileButton` style, and dropping an image onto the matching field shows a low-saturation drag reaction instead of the old global asset-picker toolbar.
- `Boundary Precision` owns the Earth static boundary asset state: provider, low-precision fallback, high-precision manifest/PMTiles, source JSON, and build action.
- `Base Map`, `Layer Resources`, `3D Assets`, and `News Anchor Strategy` are placeholders only. They show module status and do not invent fake APIs or fake data.
diff --git a/docs/technical/en/manual.md b/docs/technical/en/manual.md
index 3403666c..e3f6a39c 100644
--- a/docs/technical/en/manual.md
+++ b/docs/technical/en/manual.md
@@ -192,7 +192,7 @@ TV livestreams and boundary precision moved to `/earth-content`; collectors and
`/earth-content` is under the console's Operations and Configuration group and owns resources used by the Earth frontend:
-- **Brand Assets**: manages the logo, title image, title text, subtitle, and description used by the Earth HUD. Uploaded images are saved as Earth brand assets and read by the Earth page immediately.
+- **Brand Assets**: manages the logo, title image, title text, subtitle, and description used by the Earth HUD. The `Logo URL` and `Title Image URL` fields each include their own Upload button, and image files can be dropped directly onto the matching field. After upload, the field receives the new asset URL; save the brand configuration to make the Earth page use it.
- **About**: manages the About card shown in Earth settings, including logo, kicker, title, version, description, and metadata.
- **TV Livestream**: manages sources shown in the Earth media panel.
- **News Content**: browses news grouped by RSS source and manual group. RSS items remain read-only; manual groups support create, JSON import, edit, delete, and reprocess.
diff --git a/docs/technical/en/naming-glossary.md b/docs/technical/en/naming-glossary.md
index 734323c5..c27c0227 100644
--- a/docs/technical/en/naming-glossary.md
+++ b/docs/technical/en/naming-glossary.md
@@ -28,6 +28,9 @@ This document standardizes terms used across Intelligent Planet, the console, ba
| AI Provider | AI Provider | Service name |
| tool | 工具 | Web Search, OCR, and similar integrations |
| Playground | Playground | Interactive debugging entry |
+| branding | 品牌标识 | Earth HUD brand configuration section under `/earth-content` |
+| brand assets | 品牌资源 | Logo, title image, and related HUD copy assets |
+| title image | 标题图 | Earth HUD title image |
## Data Types
diff --git a/docs/technical/en/quickstart.md b/docs/technical/en/quickstart.md
index 345cb7ef..849f57e8 100644
--- a/docs/technical/en/quickstart.md
+++ b/docs/technical/en/quickstart.md
@@ -34,9 +34,10 @@ After landing on the `/admin` dashboard, here's a recommended walk-through:
1. `/collection-management?section=collector_credentials`: pick a collector and click the plug icon to test connectivity. Free collectors (e.g. open BGP) usually work right away; credential-bearing ones like `AISStream` or `BarentsWatch` need an API key / client secret first
2. `/ai?section=integrations`: fill an LLM provider (e.g. `minimax` / `openai`), model, base URL, API key, and click the plug at the end of the base URL to test. WebSearch / OCR tools are optional
-3. `/datasources` or `/data`: check whether collectors have produced data. Use `/datasources -> Built-in Sources` for finite collectors: with no rows selected, click `Trigger All`; after selecting rows, the primary button becomes `Trigger Selected N`. The top-right queue button shows progress. Use `/datasources -> Realtime Sources` for AISStream / WebSocket health and counters
-4. `/alerts/system`: verify system alerts look right
-5. `/users` (super_admin only): open accounts for teammates or adjust their groups
+3. `/earth-content?section=brand`: maintain the Earth HUD logo and title image in Branding. The Upload button inside each URL field opens a file picker, and image files can also be dropped directly onto the matching field. Save the brand configuration after upload
+4. `/datasources` or `/data`: check whether collectors have produced data. Use `/datasources -> Built-in Sources` for finite collectors: with no rows selected, click `Trigger All`; after selecting rows, the primary button becomes `Trigger Selected N`. The top-right queue button shows progress. Use `/datasources -> Realtime Sources` for AISStream / WebSocket health and counters
+5. `/alerts/system`: verify system alerts look right
+6. `/users` (super_admin only): open accounts for teammates or adjust their groups
## 4. Open Earth
diff --git a/docs/technical/zh/frontend-admin-frontend-context.md b/docs/technical/zh/frontend-admin-frontend-context.md
index 5092d5c8..fd51d036 100644
--- a/docs/technical/zh/frontend-admin-frontend-context.md
+++ b/docs/technical/zh/frontend-admin-frontend-context.md
@@ -418,6 +418,7 @@ AI 配置不再挂在 `/settings` 下;`/playground` 应跳转到 `/ai?section=
`/earth-content` 复用 [Settings.tsx](/home/ray/dev/linkong/planet/frontend/src/admin/pages/PlainResourcePages.tsx) 的单屏 tab 容器,但页面责任与系统设置分离:
- `电视直播` 迁移原直播源配置,继续管理 Earth 媒体面板内容源。
+- `品牌标识` 管理 Earth HUD 的品牌资源。`Logo 地址` 与 `标题图地址` 使用字段内上传控件,上传按钮保持 `TactileButton` 的 primary 样式;图片拖到对应字段时显示低饱和拖拽反应,避免回到旧的全局“选择资产/上传”工具栏。
- `国界精度` 管理 Earth 静态国界资产:provider 状态、低精 fallback、高精 manifest/PMTiles、源配置 JSON 和构建动作。
- `地球底图`、`图层资源`、`三维素材`、`新闻锚点策略` 是占位页,只显示模块待接入,不造假接口或假数据。
diff --git a/docs/technical/zh/manual.md b/docs/technical/zh/manual.md
index 80e26516..b1f22d31 100644
--- a/docs/technical/zh/manual.md
+++ b/docs/technical/zh/manual.md
@@ -191,7 +191,7 @@ Base URL 输入框尾端的插头图标会触发连接测试。测试通过会
`/earth-content` 位于控制台“运维与配置”下,面向智能星球前端体验资源:
-- **品牌资源**:维护智能星球 HUD 使用的 logo、标题图、标题文本、副标题和描述;上传的图片会保存为智能星球品牌资产并立即供智能星球页面读取。
+- **品牌资源**:维护智能星球 HUD 使用的 logo、标题图、标题文本、副标题和描述。`Logo 地址` 和 `标题图地址` 字段内各有独立的“上传”按钮,也可以把图片直接拖到对应字段;上传成功后字段会写入新的资产地址,保存品牌配置后供智能星球页面读取。
- **关于**:维护智能星球设置面板里的关于卡片,包括 logo、眉标、标题、版本、描述和元信息。
- **电视直播**:维护智能星球媒体面板里的直播源。
- **新闻内容**:按 RSS 来源和手动新闻组查看新闻。RSS 新闻保持只读;手动新闻组可以新增、批量导入 JSON、编辑、删除和重新处理。
diff --git a/docs/technical/zh/naming-glossary.md b/docs/technical/zh/naming-glossary.md
index fe524b4b..9f9be414 100644
--- a/docs/technical/zh/naming-glossary.md
+++ b/docs/technical/zh/naming-glossary.md
@@ -28,6 +28,9 @@
| AI Provider | AI Provider | 服务名,保留英文 |
| tool | 工具 | Web Search、OCR 等工具配置 |
| Playground | Playground | 交互调试入口,保留英文 |
+| branding | 品牌标识 | `/earth-content` 中的 Earth HUD 品牌配置分区 |
+| brand assets | 品牌资源 | Logo、标题图和相关 HUD 文案资源 |
+| title image | 标题图 | Earth HUD 标题图片,不写作“标题图片地址”以外的混合名 |
## 数据类型
diff --git a/docs/technical/zh/quickstart.md b/docs/technical/zh/quickstart.md
index cb4cffa4..f54a28df 100644
--- a/docs/technical/zh/quickstart.md
+++ b/docs/technical/zh/quickstart.md
@@ -34,9 +34,10 @@
1. `/collection-management?section=collector_credentials`:选一个采集器,点插头图标做连接测试。免费 collector(开源 BGP 等)通常直接可用;像 `AISStream`、`BarentsWatch` 这类需要凭证的,需要先填 API Key/Client Secret
2. `/ai?section=integrations`:填一个 LLM provider(例如 `minimax` / `openai`)、模型名、Base URL、API Key,点 Base URL 末端的插头测试连接。WebSearch / OCR 工具可选
-3. `/datasources` 或 `/data`:看采集器是否已经产出数据。有限采集器看 `/datasources -> 内置源`,不勾选时点“触发全部”,勾选后主按钮会变成“触发已选 N”;右上角队列按钮可查看进度。AISStream / WebSocket 长连接看 `/datasources -> 实时源` 的健康状态和计数
-4. `/alerts/system`:看系统告警是否正常
-5. `/users`(仅 `super_admin`):根据需要给同事开账号或调权限组
+3. `/earth-content?section=brand`:在“品牌标识”里维护智能星球的 Logo 和标题图;对应地址字段内的“上传”按钮支持选择文件,也支持把图片直接拖到字段上,保存后会应用到智能星球 HUD
+4. `/datasources` 或 `/data`:看采集器是否已经产出数据。有限采集器看 `/datasources -> 内置源`,不勾选时点“触发全部”,勾选后主按钮会变成“触发已选 N”;右上角队列按钮可查看进度。AISStream / WebSocket 长连接看 `/datasources -> 实时源` 的健康状态和计数
+5. `/alerts/system`:看系统告警是否正常
+6. `/users`(仅 `super_admin`):根据需要给同事开账号或调权限组
## 4. 打开智能星球
diff --git a/docs/version-history.md b/docs/version-history.md
index c92579cb..79c073b2 100644
--- a/docs/version-history.md
+++ b/docs/version-history.md
@@ -16,12 +16,13 @@
## Current Version
- `main` 当前主线历史推导到:`0.16.5`
-- `dev` 当前开发分支历史推导到:`0.74.0`
+- `dev` 当前开发分支历史推导到:`0.74.1`
## Timeline
| Version | Type | Branch | Commit | Summary |
| --- | --- | --- | --- | --- |
+| `0.74.1` | improvement | `dev` | `pending` | 将品牌标识上传收敛到 Logo/标题图字段内,新增字段级拖拽反馈和 Tactile UI primary 上传按钮,并同步中英文使用文档 |
| `0.74.0` | feature | `dev` | `pending` | 扩展统一 i18n 到 Web Earth 动态入口、控制台/API 错误和公开页面,修复 Earth 通知胶囊、语言 switch、品牌栏、legend、tooltip、新闻/TV 英文态裁切与中文残留,并加入 harness 回归覆盖 |
| `0.73.0` | feature | `dev` | `pending` | 新增前端统一 i18n、控制台语言/主题偏好入口、英文态 legacy 过渡翻译和 admin 一屏/状态指示器布局验证 |
| `0.72.0` | feature | `dev` | `pending` | 新增完整 agent harness、单一 AGENTS 入口、Earth News smoke 覆盖和 collector 结构化日志清理,并同步控制台/Earth/Docs 响应式维护文档 |
diff --git a/frontend/package.json b/frontend/package.json
index 5a0b1281..4cb8a158 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "planet-frontend",
- "version": "0.74.0",
+ "version": "0.74.1",
"private": true,
"packageManager": "bun@1",
"dependencies": {
diff --git a/frontend/src/admin/pages/PlainResourcePages.tsx b/frontend/src/admin/pages/PlainResourcePages.tsx
index bcaa4341..7836a821 100644
--- a/frontend/src/admin/pages/PlainResourcePages.tsx
+++ b/frontend/src/admin/pages/PlainResourcePages.tsx
@@ -27,7 +27,7 @@ import {
Trash2,
X,
} from 'lucide-react'
-import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type PointerEvent as ReactPointerEvent, type ReactNode } from 'react'
+import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type CSSProperties, type DragEvent as ReactDragEvent, type PointerEvent as ReactPointerEvent, type ReactNode } from 'react'
import { Link, useLocation, useNavigate } from 'react-router-dom'
import MarkdownRenderer from '../../components/MarkdownRenderer/MarkdownRenderer'
import Scrollbar from '../../components/Scrollbar/Scrollbar'
@@ -132,6 +132,8 @@ type DatasourceMetricBaseline = {
count: number
}
+type BrandAssetTargetKey = 'logo_src' | 'title_src'
+
const DEFAULT_DATASOURCE_FILTERS: DatasourceFilters = {
product: '',
module: '',
@@ -871,6 +873,14 @@ type FieldConfig = {
placeholder?: string
disabled?: boolean
help?: string
+ renderInput?: (props: {
+ disabled?: boolean
+ displayValue: unknown
+ fieldKey: string
+ onChange: (value: unknown) => void
+ placeholder?: string
+ value: unknown
+ }) => ReactNode
inputAction?: {
ariaLabel?: string
disabled?: boolean
@@ -2301,6 +2311,9 @@ function FieldGrid({
const className = field.wide ? 'an-field an-field--wide' : 'an-field'
const searchTarget = searchGroupKey ? `${searchGroupKey}:field:${field.key}` : `field:${field.key}`
const searchText = [label, field.key, text(displayValue, '')].filter(Boolean).join(' ')
+ const commitFieldValue = (nextValue: unknown) => {
+ onDraftChange(setNestedDraftField(draft, record, field.key, nextValue))
+ }
if (field.type === 'boolean') {
return (
)
}
+ if (field.renderInput) {
+ return (
+
+ {label}
+ {field.renderInput({
+ disabled: field.disabled,
+ displayValue,
+ fieldKey: field.key,
+ onChange: commitFieldValue,
+ placeholder,
+ value,
+ })}
+ {help ? {help} : null}
+
+ )
+ }
const inputAction = field.inputAction
? {
...field.inputAction,
@@ -2421,12 +2450,7 @@ function FieldGrid({
value={text(displayValue, '')}
placeholder={placeholder}
disabled={field.disabled}
- onChange={(event) => onDraftChange(setNestedDraftField(
- draft,
- record,
- field.key,
- field.type === 'number' ? Number(event.target.value) : event.target.value,
- ))}
+ onChange={(event) => commitFieldValue(field.type === 'number' ? Number(event.target.value) : event.target.value)}
/>
) : (
@@ -2436,12 +2460,7 @@ function FieldGrid({
value={text(displayValue, '')}
placeholder={placeholder}
disabled={field.disabled}
- onChange={(event) => onDraftChange(setNestedDraftField(
- draft,
- record,
- field.key,
- field.type === 'number' ? Number(event.target.value) : event.target.value,
- ))}
+ onChange={(event) => commitFieldValue(field.type === 'number' ? Number(event.target.value) : event.target.value)}
/>
)}
{help ? {help} : null}
@@ -2545,6 +2564,25 @@ function GroupList({
const PLAYGROUND_SESSION_KEY = 'default'
const BRAND_ASSET_ACCEPT = '.png,.jpg,.jpeg,.webp,.svg'
const BRAND_ASSET_SUFFIXES = ['png', 'jpg', 'jpeg', 'webp', 'svg']
+const BRAND_ASSET_TARGET_META: Record = {
+ logo_src: {
+ copyLabel: '复制为 Logo',
+ dropLabel: '将图片拖到这里',
+ uploadLabel: '上传',
+ uploadTitle: '上传 Logo',
+ },
+ title_src: {
+ copyLabel: '复制为标题图',
+ dropLabel: '将图片拖到这里',
+ uploadLabel: '上传',
+ uploadTitle: '上传标题图片',
+ },
+}
const PLAYGROUND_PRESETS = [
{
key: 'bgp-brief',
@@ -2572,6 +2610,121 @@ const PLAYGROUND_PRESETS = [
},
]
+function isBrandAssetTargetKey(value: string): value is BrandAssetTargetKey {
+ return value === 'logo_src' || value === 'title_src'
+}
+
+function isFileDrag(event: ReactDragEvent) {
+ return Array.from(event.dataTransfer.types).includes('Files')
+}
+
+function BrandAssetInput({
+ disabled,
+ onChange,
+ onFile,
+ placeholder,
+ target,
+ uploading,
+ value,
+}: {
+ disabled?: boolean
+ onChange: (value: string) => void
+ onFile: (file: File) => void
+ placeholder?: string
+ target: BrandAssetTargetKey
+ uploading: boolean
+ value: string
+}) {
+ const { locale } = useLocale()
+ const [dragging, setDragging] = useState(false)
+ const dragCounter = useRef(0)
+ const fileInputRef = useRef(null)
+ const meta = BRAND_ASSET_TARGET_META[target]
+ const suffixHelp = BRAND_ASSET_SUFFIXES.join(' / ')
+
+ const resetDrag = () => {
+ dragCounter.current = 0
+ setDragging(false)
+ }
+
+ const handleDragEnter = (event: ReactDragEvent) => {
+ if (!isFileDrag(event)) return
+ event.preventDefault()
+ dragCounter.current += 1
+ setDragging(true)
+ }
+
+ const handleDragOver = (event: ReactDragEvent) => {
+ if (!isFileDrag(event)) return
+ event.preventDefault()
+ }
+
+ const handleDragLeave = (event: ReactDragEvent) => {
+ if (!isFileDrag(event)) return
+ event.preventDefault()
+ dragCounter.current = Math.max(0, dragCounter.current - 1)
+ if (dragCounter.current === 0) setDragging(false)
+ }
+
+ const handleDrop = (event: ReactDragEvent) => {
+ event.preventDefault()
+ resetDrag()
+ const file = event.dataTransfer.files?.[0]
+ if (file) onFile(file)
+ }
+
+ const handleFileChange = (event: ChangeEvent) => {
+ const file = event.target.files?.[0]
+ event.target.value = ''
+ if (file) onFile(file)
+ }
+
+ return (
+
+
onChange(event.target.value)}
+ placeholder={placeholder}
+ type="text"
+ value={value}
+ />
+
+
+ {dragging ? (
+
+ {localizeAdminText(meta.dropLabel, locale)}
+ {localizeAdminText(meta.copyLabel, locale)}
+
+ ) : null}
+
+ )
+}
+
function PlaygroundLite() {
const { toast } = useToast()
const { locale } = useLocale()
@@ -2848,7 +3001,7 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
const [revealedSecrets, setRevealedSecrets] = useState>({})
const [visibleSecretFields, setVisibleSecretFields] = useState>({})
const [smtpTestEmail, setSmtpTestEmail] = useState('')
- const [brandUploadFile, setBrandUploadFile] = useState(null)
+ const [brandAssetUploadingTarget, setBrandAssetUploadingTarget] = useState(null)
const [newsImportFile, setNewsImportFile] = useState(null)
const newsImportInputRef = useRef(null)
const [resolveTarget, setResolveTarget] = useState(null)
@@ -4002,51 +4155,46 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
setResolveTarget(null)
}
- const uploadBrandAsset = async () => {
- if (!brandUploadFile) {
- toast({ title: '请选择上传文件', tone: 'error' })
- return
- }
- const suffix = brandUploadFile.name.split('.').pop()?.toLowerCase() || ''
- if (!BRAND_ASSET_SUFFIXES.includes(suffix)) {
- toast({ title: '文件类型不支持', description: `仅支持 ${BRAND_ASSET_SUFFIXES.join(', ')}。`, tone: 'error' })
- return
- }
- const formData = new FormData()
- formData.append('file', brandUploadFile)
- setActionLoading(true)
- try {
- const response = await axios.post(apiPath('/earth/brand/assets'), formData, {
- headers: { 'Content-Type': 'multipart/form-data' },
- })
- replaceSelectedWithPayload('brand-upload', '品牌资产上传', [{
- ...response.data,
- __title: '品牌资产上传结果',
- __module: '品牌资产',
- __status: '已上传',
- __metric: brandUploadFile.name,
- }])
- setBrandUploadFile(null)
- toast({ title: '品牌资产已上传', tone: 'success' })
- await load()
- } catch (error) {
- toast({ title: '品牌资产上传失败', description: error instanceof Error ? error.message : '接口请求失败', tone: 'error' })
- } finally {
- setActionLoading(false)
- }
- }
-
- const selectBrandUploadFile = (file: File | null | undefined) => {
+ const uploadBrandAssetFile = async (file: File | null | undefined, target: BrandAssetTargetKey) => {
if (!file) {
- setBrandUploadFile(null)
- return
+ toast({ title: '请拖入图片文件', tone: 'error' })
+ return null
}
const suffix = file.name.split('.').pop()?.toLowerCase() || ''
if (!BRAND_ASSET_SUFFIXES.includes(suffix)) {
toast({ title: '文件类型不支持', description: `仅支持 ${BRAND_ASSET_SUFFIXES.join(', ')}。`, tone: 'error' })
- return
+ return null
+ }
+ const formData = new FormData()
+ formData.append('file', file)
+ setActionLoading(true)
+ setBrandAssetUploadingTarget(target)
+ try {
+ const response = await axios.post(apiPath('/earth/brand/assets'), formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ })
+ const assetUrl = text(response.data?.url, '')
+ if (!assetUrl) throw new Error('上传接口未返回资产 URL')
+ replaceSelectedWithPayload('brand-upload', '品牌资产上传', [{
+ ...response.data,
+ __title: '品牌资产上传结果',
+ __module: target === 'logo_src' ? 'Logo' : '标题图片',
+ __status: '已上传',
+ __metric: file.name,
+ }])
+ toast({
+ title: target === 'logo_src' ? 'Logo 图片已上传' : '标题图片已上传',
+ description: assetUrl,
+ tone: 'success',
+ })
+ return assetUrl
+ } catch (error) {
+ toast({ title: '品牌资产上传失败', description: actionErrorMessage(error), tone: 'error' })
+ return null
+ } finally {
+ setActionLoading(false)
+ setBrandAssetUploadingTarget(null)
}
- setBrandUploadFile(file)
}
const createManualNewsGroup = async () => {
@@ -5413,6 +5561,32 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
{ key: 'location_label', label: '位置标签' },
]
const fields = newsSourceFields.length ? newsSourceFields : newsItemFields.length ? newsItemFields : [...scalarFields, ...objectFields]
+ const uploadBrandAssetToDraft = async (file: File, target: BrandAssetTargetKey) => {
+ if (!activeGroup) return
+ const assetUrl = await uploadBrandAssetFile(file, target)
+ if (!assetUrl) return
+ setHierarchyDraft((current) => setNestedDraftField(current, activeGroup.record, target, assetUrl))
+ }
+ const editableFields = config === configs.earthContent && activeSection.key === 'brand'
+ ? fields.map((field): FieldConfig => {
+ if (!isBrandAssetTargetKey(field.key)) return field
+ const target = field.key
+ return {
+ ...field,
+ renderInput: ({ disabled, displayValue, onChange, placeholder }) => (
+ onChange(nextValue)}
+ onFile={(file) => void uploadBrandAssetToDraft(file, target)}
+ placeholder={placeholder}
+ target={target}
+ uploading={brandAssetUploadingTarget === target}
+ value={text(displayValue, '')}
+ />
+ ),
+ }
+ })
+ : fields
const renderNewsFeedEditor = () => {
if (!activeGroup) return null
const currentSource = draftRecord(hierarchyDraft, activeGroup.record)
@@ -5951,20 +6125,6 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
{config === configs.earthContent && activeSection.key === 'brand' ? (
<>
-
-