fix(ui): 修复新 UI 侧栏被顶出与平台设置页导航重复 (#1706)
* fix(ui): 去掉 html/body 的 overflow-x: hidden,恢复侧栏与页头吸顶 #1704 引入的 bg-global.css 给 html、body 加了 overflow-x: hidden,任何非 visible 的 overflow 都会把 body 变成滚动容器,侧栏(layout.module.scss .sider)和 PageHeader 的 position: sticky 于是相对 body 而不是视口定位——主内容一长, 整页滚动就把左侧导航和「保存」按钮一起顶出视口。 - 删除 html/body 的 overflow-x: hidden,并在两处样式注明这条约束 - 两个 position: fixed 的背景层改用 inset: 0 铺满视口,不再依赖 100vw, 也不会撑出横向滚动条 问题 1 of #1705(侧边导航被顶出)。 * fix(ui): 空间配置「平台设置」只保留左侧平台列表一套导航 #1704 把平台设置改成「左列平台列表 + 右栏 Collapse」后,右栏的手风琴仍然 为每个平台渲染一条带平台名的折叠栏,与左列重复(#1705 第二点)。 - 新增 app/ui/plugins/PlatformPanel.tsx:各平台插件的外壳,传入 bare 时只 输出字段,否则保持原来的 Collapse.Panel(配置覆写弹窗仍在用) - 10 个平台插件把 Collapse.Panel 换成 PlatformPanel,并透传 bare - dashboard 右栏不再用 Collapse,改为每个平台一个 <section hidden>:所有 字段始终挂载,只切换 hidden,保证 PUT /v1/configuration 整表覆盖时其他 平台的参数与凭据不会因为字段卸载而丢失 - 左列平台列表在页头下方吸顶(position: sticky),右栏面板很长时列表不再 随内容滚出视口;PageHeader 把自身高度写入 --page-header-height 供其取用; Tabs 关闭 tabPaneMotion 并放开 .semi-tabs-pane 的 overflow,否则 pane 会 成为 sticky 的滚动容器。移动端横排 chip 行保持随内容滚动 - 左列按钮补 aria-pressed,nav 补 aria-label Fixes #1705 * feat(ui): 空间配置保存校验失败时自动定位到第一个出错字段 平台设置的字段全部挂载、只切换 hidden,未选中的 Tab / 平台面板里的校验 错误用户看不到,表现为点「保存」没有任何反应。 - Form 加 onSubmitFail:把 Semi 的嵌套 errors 拍平成字段路径,按 DOM 顺序 找到第一个出错字段,切到它所在的 Tab(Tabs 改为受控)与平台面板,再 scrollIntoView 并聚焦,同时 Toast 提示未通过的项数 - TabPane 内容加 data-tab、平台 section 加 data-platform,供定位使用; 不改插件接口 Refs #1705 * refactor(ui): 改用应用壳布局解决侧栏被顶出,移除 sticky 补救与 tabPaneMotion 取舍 按 review 意见(#1706)不再靠 position: sticky 解决问题一,改成简单的应用壳: - layout.module.scss:.app 改为 height: 100vh(100dvh)+ overflow: hidden 的 flex 容器, 页面本身不滚动;.sider 是固定宽度的普通子元素(撑满壳高,去掉 sticky/top/100vh); .main 加 min-height: 0 + overflow: auto,成为唯一的滚动容器。侧栏、页脚服务灯 自然留在视口内,PageHeader 原有的 position: sticky 相对 .main 吸顶,无需改动 - 去掉为让平台列表在 Tab 面板内 sticky 而加的补救:.platformNav 的 sticky / --page-header-height / max-height、PageHeader 的 ResizeObserver、Tabs 的 tabPaneMotion={false} 以及 .semi-tabs-pane 的 overflow 覆盖,Tab 切换动画恢复。 平台列表改回随内容滚动 - bg-global.css:文档不再滚动,html/body 不需要 overflow 规则,注释改为说明现状; 两个 fixed 背景层保持 inset: 0 验证:tsc 0 错误、next build 通过;13 页 × 浅色 / 深色 / 折叠 / 移动端 390 / 平板 760, 主区滚到底后 document.scrollHeight == innerHeight(文档不滚)、aside top 0、页脚 bottom == innerHeight、header top 0、main 无横向溢出;Semi Select 下拉(主区预滚 1.2k px 及打开后继续滚动)、Popconfirm、配置覆写 Modal 及其内部 Select、Toast 位置全部正确; 切遍 10 个平台后保存 PUT 73/73 键(3 次);校验失败自动定位三场景通过(2 轮)。 Refs #1705 * refactor(ui): 平台设置去掉外层嵌套卡片,改两栏独立滚动布局 按 review 第二条意见(#1706):平台列表不用 sticky,也不随内容滚走——去掉包在 「平台列表 + 面板」外面的嵌套卡片,让两栏并排、各自独立滚动。 - dashboard/page.tsx:删掉 dc.content / rootConfigPanel / main / content 四层包裹和 平台设置里的 framePlatformConfig 卡片与重复的「平台设置」标题;页面主体改为 .page(占满页头以下的剩余高度)→ Form → Tabs,每个 Tab 面板在自己内部滚动, 页面本身不滚,页头「保存」始终可见。去掉底部两个 Space 占位 - dashboard.module.scss:重写。删除 #1704 之前遗留的整段 .rootConfigPanel 嵌套规则 (其中 frameInside / group / lineStory 等类早已无人引用);Form / Tabs / semi-tabs-content / semi-tabs-pane(含开启动画时的 motion-overlay)改为 flex: 1 + min-height: 0 的列容器,把高度让给当前面板,并补回 .semi-tabs-pane-inactive 的 display: none(本模块选择器特异性更高) - 平台设置:.platformLayout 两栏,.platformNav 160px、align-self: flex-start、 max-height: 100% + overflow-y: auto(矮视口时列表自己滚),.platformBody flex: 1 + overflow-y: auto。右栏再长列表也不动 - 全局设置 / 开发者选项一致处理:frameDownload / frameUpload / frameDeveloper 不再是 带边框和 50px 内边距的卡片,改为分区标题 + 分隔线的普通分区,面板 .pane 自身滚动 - 移动端(≤680px):两栏改为上下堆叠,平台列表是横向滑动的标签行,固定在面板顶部 不随字段滚走 - 自定义背景开启时给两个滚动面板垫一层 75% 的 bg-1 半透明底色,保证文字可读 验证:tsc 0 错误、next build 通过;空间配置页在 1440 / 760 / 390 × 浅 / 深 / 折叠 / 520px 矮视口下:文档与 <main> 均不滚动,页头与「保存」始终在视口内,右栏滚到底 (哔哩哔哩面板 1223px)时平台列表位置不变,矮视口下列表自身可滚;全局设置面板独立 滚动;切遍 10 个平台后保存 PUT 73/73 键(4 次);校验失败自动定位三场景通过(3 轮, scrollIntoView 在嵌套滚动容器内正常);Select 下拉在面板预滚 1k px 及滚动中跟随触发器, Popconfirm / Modal / Toast 位置正确;其余 12 页布局探针与上一提交一致。 Refs #1705 * fix(ui): 更新日志页内容不再覆盖导航顶栏 应用壳布局下页头真正吸顶后暴露出来的问题(review 第三条,#1706):更新日志页把 PageHeader 又包在一层 position: sticky; z-index: 1 的 Semi Layout.Header 里, 形成一个层级只有 1 的 stacking context,PageHeader 自己的 z-index: 10 只在这层 内部生效;而版本卡片流 .feed 又设了 position: relative; z-index: 1,DOM 顺序在 后,滚动时就画在页头之上。 - changelog/page.tsx:去掉外层 Header,与其它页一样直接渲染 PageHeader - changelog.module.scss:删掉 .feed / .center 的 position: relative; z-index: 1 验证:10 个带页头的页面滚到底后对页头内 6 个点做 elementFromPoint 命中测试, 全部命中页头自身(修前 /changelog 有 4 个点命中版本卡片头);/history 播放器 Modal 居中且在最上层;/logviewer、/status 等无层叠。 Refs #1705
This commit is contained in:
18 files changed
+320
-491
No files matched your search
+4
-12
@@ -1,16 +1,11 @@
|
||||
/* 全局自定义背景:当 body 带 app-has-bg 时生效 */
|
||||
html,
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* 两个背景层用 position: fixed + inset: 0 铺满视口。页面滚动由应用壳的主内容区承担
|
||||
(layout.module.scss .app / .main),文档本身不滚,这里不需要再给 html / body 设 overflow */
|
||||
body.app-has-bg::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
inset: 0;
|
||||
z-index: -2;
|
||||
background-image: var(--app-bg-image);
|
||||
background-repeat: no-repeat;
|
||||
@@ -23,10 +18,7 @@ body.app-has-bg::before {
|
||||
body.app-has-bg::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
inset: 0;
|
||||
background: var(--semi-color-bg-0);
|
||||
opacity: var(--app-bg-mask-opacity, 0.35);
|
||||
z-index: -1;
|
||||
|
||||
@@ -3,12 +3,6 @@
|
||||
min-height: calc(100vh - 69px);
|
||||
}
|
||||
|
||||
.feed,
|
||||
.center {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { formatVersion } from '@/app/lib/status'
|
||||
import { SLOW_REFRESH_MS } from '@/app/lib/use-dashboard'
|
||||
import styles from './changelog.module.scss'
|
||||
|
||||
const { Header, Content } = Layout
|
||||
const { Content } = Layout
|
||||
const { Title, Text } = Typography
|
||||
|
||||
// 数据源:biliup 官方 CHANGELOG(随版本自动更新,零后端改动)
|
||||
@@ -210,20 +210,13 @@ export default function Changelog() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
style={{
|
||||
backgroundColor: 'var(--semi-color-bg-1)',
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<PageHeader
|
||||
icon={<IconBook size="large" />}
|
||||
title="更新日志"
|
||||
description="biliup 版本更新记录"
|
||||
/>
|
||||
</Header>
|
||||
{/* PageHeader 自带 sticky 与 z-index,不要再包一层带 position / z-index 的容器,
|
||||
否则会形成更低层级的 stacking context,滚动时被下方内容盖住 */}
|
||||
<PageHeader
|
||||
icon={<IconBook size="large" />}
|
||||
title="更新日志"
|
||||
description="biliup 版本更新记录"
|
||||
/>
|
||||
<Content>
|
||||
<main className={styles.content}>
|
||||
{isLoading && (
|
||||
|
||||
+125
-94
@@ -1,19 +1,16 @@
|
||||
'use client'
|
||||
import React, { useRef, useState } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Collapse,
|
||||
Avatar,
|
||||
Select,
|
||||
Space,
|
||||
Toast,
|
||||
Notification,
|
||||
Typography,
|
||||
Tabs,
|
||||
TabPane,
|
||||
} from '@douyinfe/semi-ui'
|
||||
import { IconPlusCircle, IconStar, IconGlobe } from '@douyinfe/semi-icons'
|
||||
import { IconPlusCircle, IconStar } from '@douyinfe/semi-icons'
|
||||
import useSWR from 'swr'
|
||||
import { fetcher, put } from '@/app/lib/api-streamer'
|
||||
import useSWRMutation from 'swr/mutation'
|
||||
@@ -21,14 +18,28 @@ import { FormApi } from '@douyinfe/semi-ui/lib/es/form'
|
||||
import { useBiliUsers } from '../../lib/use-streamers'
|
||||
import styles from '../../styles/dashboard.module.scss'
|
||||
import PageHeader from '../components/PageHeader'
|
||||
import SectionTitle from '../components/SectionTitle'
|
||||
import dc from '@/app/ui/data-card.module.scss'
|
||||
|
||||
// 注册各平台组件
|
||||
import { PlatformPanels } from '../../ui/plugins'
|
||||
import Global from '../../ui/plugins/global'
|
||||
import Developer from '../../ui/plugins/developer'
|
||||
|
||||
const TAB_GLOBAL = '1'
|
||||
const TAB_PLATFORM = '2'
|
||||
const TAB_DEVELOPER = '3'
|
||||
|
||||
/** Semi 把校验错误按字段路径存成嵌套对象({ user: { bili_cookie: '…' } }),拍平成 x-field-id 形式 */
|
||||
function errorFieldPaths(errors: unknown, prefix = ''): string[] {
|
||||
if (!errors || typeof errors !== 'object' || Array.isArray(errors) || '$$typeof' in errors) {
|
||||
return prefix ? [prefix] : []
|
||||
}
|
||||
return Object.entries(errors as Record<string, unknown>).flatMap(([k, v]) =>
|
||||
errorFieldPaths(v, prefix ? `${prefix}.${k}` : k),
|
||||
)
|
||||
}
|
||||
|
||||
const fieldElement = (path: string) =>
|
||||
document.querySelector<HTMLElement>(`.semi-form-field[x-field-id="${path}"]`)
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
const { data: entity, error, isLoading } = useSWR('/v1/configuration', fetcher)
|
||||
@@ -61,8 +72,34 @@ const { data: entity, error, isLoading } = useSWR('/v1/configuration', fetcher)
|
||||
|
||||
const { biliUsers } = useBiliUsers()
|
||||
|
||||
// 平台设置:左列平台名 + 右栏仅展开选中平台。列表来自插件注册表 PlatformPanels
|
||||
// 平台设置:左列平台名是唯一的导航,右栏只显示选中平台的字段。列表来自插件注册表 PlatformPanels
|
||||
const [activePlatform, setActivePlatform] = useState(PlatformPanels[0].key)
|
||||
const [activeTab, setActiveTab] = useState(TAB_GLOBAL)
|
||||
|
||||
// 校验失败时出错字段可能藏在未选中的 Tab / 平台面板里(字段全部挂载、只切 hidden),
|
||||
// 用户看不到红字也不知道为什么保存没反应。这里切到第一个出错字段所在的面板并滚过去。
|
||||
const [pendingField, setPendingField] = useState<string | null>(null)
|
||||
const handleSubmitFail = (errors: Record<string, unknown>) => {
|
||||
const fields = errorFieldPaths(errors)
|
||||
.map(path => ({ path, el: fieldElement(path) }))
|
||||
.filter((f): f is { path: string; el: HTMLElement } => !!f.el)
|
||||
.sort((a, b) => (a.el.compareDocumentPosition(b.el) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1))
|
||||
const first = fields[0]
|
||||
if (!first) return
|
||||
const tab = first.el.closest<HTMLElement>('[data-tab]')?.dataset.tab
|
||||
const platform = first.el.closest<HTMLElement>('[data-platform]')?.dataset.platform
|
||||
if (tab) setActiveTab(tab)
|
||||
if (platform) setActivePlatform(platform)
|
||||
setPendingField(first.path)
|
||||
Toast.warning(`有 ${fields.length} 项未通过校验,已定位到第一项`)
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!pendingField) return
|
||||
const el = fieldElement(pendingField)
|
||||
el?.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
||||
el?.querySelector<HTMLElement>('input, textarea')?.focus({ preventScroll: true })
|
||||
setPendingField(null)
|
||||
}, [pendingField])
|
||||
|
||||
if (isLoading) {
|
||||
return <>Loading</>
|
||||
@@ -105,92 +142,86 @@ const { data: entity, error, isLoading } = useSWR('/v1/configuration', fetcher)
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<div className={dc.content}>
|
||||
<main className={styles.rootConfigPanel}>
|
||||
<div className={styles.main}>
|
||||
<div className={styles.content}>
|
||||
<Form
|
||||
className={styles.form}
|
||||
// key={formKey}
|
||||
initValues={entity}
|
||||
onSubmit={async values => {
|
||||
try {
|
||||
const payload = { ...values }
|
||||
if (payload.file_size === undefined || payload.file_size === '') {
|
||||
payload.file_size = null
|
||||
}
|
||||
if (payload.segment_time === undefined || payload.segment_time === '') {
|
||||
payload.segment_time = null
|
||||
}
|
||||
await trigger(payload)
|
||||
Toast.success('保存成功')
|
||||
} catch (e: any) {
|
||||
// error handling
|
||||
Notification.error({
|
||||
title: '保存失败',
|
||||
content: <Typography style={{ maxWidth: 450 }}>{e.message}</Typography>,
|
||||
// theme: 'light',
|
||||
// duration: 0,
|
||||
style: { width: 'min-content' },
|
||||
})
|
||||
throw e
|
||||
}
|
||||
}}
|
||||
getFormApi={formApi => (formRef.current = formApi)}
|
||||
>
|
||||
<Tabs
|
||||
type="line"
|
||||
contentStyle={{
|
||||
margin: '10px 0 0 0',
|
||||
}}
|
||||
>
|
||||
<TabPane tab="全局设置" itemKey="1">
|
||||
{/* 全局设置 */}
|
||||
<Global />
|
||||
</TabPane>
|
||||
<TabPane tab="平台设置" itemKey="2">
|
||||
{/* 平台设置:左列平台名 + 右栏选中表单 */}
|
||||
<div className={styles.framePlatformConfig}>
|
||||
<SectionTitle icon={<IconGlobe size="small" />} title="平台设置" />
|
||||
<div className={styles.platformLayout}>
|
||||
<nav className={styles.platformNav}>
|
||||
{PlatformPanels.map(p => (
|
||||
<button
|
||||
key={p.key}
|
||||
type="button"
|
||||
className={`${styles.platformNavItem} ${
|
||||
activePlatform === p.key ? styles.platformNavItemActive : ''
|
||||
}`}
|
||||
onClick={() => setActivePlatform(p.key)}
|
||||
>
|
||||
{p.name}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
<div className={styles.platformBody}>
|
||||
{/* 所有平台组件保持挂载(keepDOM),仅展开当前平台面板。
|
||||
卸载会注销 Semi Form 字段状态,提交时仅剩挂载字段;后端 PUT /configuration
|
||||
整表覆盖保存,会清空其他平台的参数与凭据 */}
|
||||
<Collapse keepDOM activeKey={[activePlatform]}>
|
||||
{PlatformPanels.map(p => (
|
||||
<p.Component key={p.key} entity={entity} list={list} />
|
||||
))}
|
||||
</Collapse>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane tab="开发者选项" itemKey="3">
|
||||
{/* 开发者选项 */}
|
||||
<Developer />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
<Space />
|
||||
<Space style={{ height: '160px' }} />
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{/* 页头下方占满剩余高度;每个 Tab 面板在内部滚动,页面本身不滚,「保存」始终可见 */}
|
||||
<div className={styles.page}>
|
||||
<Form
|
||||
className={styles.form}
|
||||
initValues={entity}
|
||||
onSubmit={async values => {
|
||||
try {
|
||||
const payload = { ...values }
|
||||
if (payload.file_size === undefined || payload.file_size === '') {
|
||||
payload.file_size = null
|
||||
}
|
||||
if (payload.segment_time === undefined || payload.segment_time === '') {
|
||||
payload.segment_time = null
|
||||
}
|
||||
await trigger(payload)
|
||||
Toast.success('保存成功')
|
||||
} catch (e: any) {
|
||||
// error handling
|
||||
Notification.error({
|
||||
title: '保存失败',
|
||||
content: <Typography style={{ maxWidth: 450 }}>{e.message}</Typography>,
|
||||
// theme: 'light',
|
||||
// duration: 0,
|
||||
style: { width: 'min-content' },
|
||||
})
|
||||
throw e
|
||||
}
|
||||
}}
|
||||
onSubmitFail={handleSubmitFail}
|
||||
getFormApi={formApi => (formRef.current = formApi)}
|
||||
>
|
||||
<Tabs type="line" className={styles.tabs} activeKey={activeTab} onChange={setActiveTab}>
|
||||
<TabPane tab="全局设置" itemKey={TAB_GLOBAL}>
|
||||
<div className={styles.pane} data-tab={TAB_GLOBAL}>
|
||||
<Global />
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane tab="平台设置" itemKey={TAB_PLATFORM}>
|
||||
{/* 左列平台列表 + 右栏选中平台的字段,两栏并排、各自独立滚动 */}
|
||||
<div className={styles.platformLayout} data-tab={TAB_PLATFORM}>
|
||||
<nav className={styles.platformNav} aria-label="平台列表">
|
||||
{PlatformPanels.map(p => (
|
||||
<button
|
||||
key={p.key}
|
||||
type="button"
|
||||
className={`${styles.platformNavItem} ${
|
||||
activePlatform === p.key ? styles.platformNavItemActive : ''
|
||||
}`}
|
||||
aria-pressed={activePlatform === p.key}
|
||||
onClick={() => setActivePlatform(p.key)}
|
||||
>
|
||||
{p.name}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
<div className={styles.platformBody}>
|
||||
{/* 所有平台的字段始终保持挂载,只用 hidden 切换显示。
|
||||
卸载会注销 Semi Form 字段状态,提交时仅剩挂载字段;后端 PUT /configuration
|
||||
整表覆盖保存,会清空其他平台的参数与凭据 */}
|
||||
{PlatformPanels.map(p => (
|
||||
<section
|
||||
key={p.key}
|
||||
className={styles.platformPanel}
|
||||
hidden={activePlatform !== p.key}
|
||||
aria-label={p.name}
|
||||
data-platform={p.key}
|
||||
>
|
||||
<p.Component entity={entity} list={list} bare />
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane tab="开发者选项" itemKey={TAB_DEVELOPER}>
|
||||
<div className={styles.pane} data-tab={TAB_DEVELOPER}>
|
||||
<Developer />
|
||||
</div>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</Form>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
/* ===== 应用骨架:自绘侧边栏 ===== */
|
||||
|
||||
/* 应用壳:整体占满视口且自身不滚动;侧栏是固定宽度的普通子元素,
|
||||
主内容区(.main)独立滚动。页面本身不滚,长内容不会把侧栏顶出视口(#1705) */
|
||||
.app {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
overflow: hidden;
|
||||
background: var(--semi-color-bg-0);
|
||||
}
|
||||
|
||||
@@ -17,14 +21,13 @@
|
||||
}
|
||||
|
||||
/* ===== 侧边栏 ===== */
|
||||
/* 撑满应用壳高度;导航项多于一屏时只有 .nav 内部滚动 */
|
||||
.sider {
|
||||
width: 220px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
height: 100%;
|
||||
background: var(--semi-color-bg-1);
|
||||
border-right: 1px solid var(--semi-color-border);
|
||||
transition: width 0.18s ease, transform 0.2s ease;
|
||||
@@ -265,12 +268,14 @@
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
/* ===== 主内容区 ===== */
|
||||
/* ===== 主内容区:唯一的滚动容器,页头(PageHeader)的 position: sticky 相对它吸顶 ===== */
|
||||
.main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* ===== 移动端抽屉 ===== */
|
||||
|
||||
@@ -1,325 +1,76 @@
|
||||
@media screen and (min-width: 640px) {
|
||||
.rootConfigPanel {
|
||||
padding: 0px 50px 0px 30px;
|
||||
//align-items: center;
|
||||
//background: var(--semi-color-bg-0);
|
||||
//flex-direction: column;
|
||||
//height: auto;
|
||||
//overflow: auto;
|
||||
//row-gap: 0;
|
||||
//min-width: auto;
|
||||
//max-width: auto;
|
||||
//display: flex;
|
||||
//color: var(--semi-color-text-0);
|
||||
//font-family: Inter;
|
||||
//font-size: 20px;
|
||||
//font-weight: 600;
|
||||
//letter-spacing: 0.4px;
|
||||
//line-height: 28px;
|
||||
//text-align: left;
|
||||
|
||||
.main {
|
||||
//align-items: center;
|
||||
display: flex;
|
||||
flex-basis: 0;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
//padding: 40px 0px 0px;
|
||||
row-gap: 32px;
|
||||
align-self: stretch;
|
||||
|
||||
.content {
|
||||
//align-items: flex-start;
|
||||
//align-items: center;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
row-gap: 32px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.item {
|
||||
min-width: 175px;
|
||||
vertical-align: middle;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.form {
|
||||
//align-items: flex-start;
|
||||
//align-items: center;
|
||||
//display: flex;
|
||||
//flex-direction: column;
|
||||
//row-gap: 16px;
|
||||
//min-width: 50px;
|
||||
//max-width: 965px;
|
||||
flex-shrink: 0;
|
||||
//color: var(--semi-color-text-0);
|
||||
//font-family: "PingFang SC";
|
||||
//font-size: 16px;
|
||||
//font-weight: 500;
|
||||
//letter-spacing: 0.32px;
|
||||
//line-height: 22px;
|
||||
//text-align: left;
|
||||
|
||||
.frameDownload {
|
||||
//align-items: flex-start;
|
||||
border-color: rgb(var(--semi-grey-2));
|
||||
border-radius: 6px;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
padding: 25px 50px 50px;
|
||||
row-gap: 10px;
|
||||
align-self: stretch;
|
||||
min-width: 12px;
|
||||
max-width: auto;
|
||||
|
||||
.frameInside {
|
||||
align-items: center;
|
||||
column-gap: 4px;
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
|
||||
.group {
|
||||
height: 20px;
|
||||
position: relative;
|
||||
width: 20px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.buttonOnlyIconSecond {
|
||||
background: rgb(16, 192, 25);
|
||||
border-radius: 8px;
|
||||
height: 20px;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
top: 0;
|
||||
width: 20px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.lineStory {
|
||||
height: 10px;
|
||||
left: 4.5px;
|
||||
overflow: hidden;
|
||||
top: 4.5px;
|
||||
width: 12px;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
.meegoSharedWebWorkIt {
|
||||
min-width: 68px;
|
||||
vertical-align: bottom;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.frameUpload {
|
||||
align-items: flex-start;
|
||||
border-color: rgb(var(--semi-grey-2));
|
||||
border-radius: 6px;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
padding: 25px 50px 50px;
|
||||
row-gap: 10px;
|
||||
align-self: stretch;
|
||||
|
||||
.frameInside {
|
||||
align-items: center;
|
||||
column-gap: 4px;
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
|
||||
.group {
|
||||
height: 20px;
|
||||
position: relative;
|
||||
width: 20px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.buttonOnlyIconSecond {
|
||||
background: rgb(236, 23, 66);
|
||||
border-radius: 8px;
|
||||
height: 20px;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
top: 0;
|
||||
width: 20px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.lineStory {
|
||||
height: 10px;
|
||||
left: 4.5px;
|
||||
overflow: hidden;
|
||||
top: 4.5px;
|
||||
width: 12px;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
.meegoSharedWebWorkIt {
|
||||
min-width: 68px;
|
||||
vertical-align: bottom;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.framePlatformConfig {
|
||||
align-items: flex-start;
|
||||
border-color: rgb(var(--semi-grey-2));
|
||||
border-radius: 6px;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
padding: 25px 50px 50px;
|
||||
row-gap: 10px;
|
||||
align-self: stretch;
|
||||
|
||||
.frameInside {
|
||||
align-items: center;
|
||||
column-gap: 4px;
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
|
||||
.group {
|
||||
height: 20px;
|
||||
position: relative;
|
||||
width: 20px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.outlinedVersion {
|
||||
height: 10px;
|
||||
left: 5px;
|
||||
overflow: hidden;
|
||||
top: 5px;
|
||||
width: 10px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.buttonOnlyIconSecond {
|
||||
background: rgb(47, 76, 206);
|
||||
border-radius: 8px;
|
||||
height: 20px;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
top: 0;
|
||||
width: 20px;
|
||||
position: absolute;
|
||||
}
|
||||
.lineStory {
|
||||
height: 15px;
|
||||
left: 4.5px;
|
||||
overflow: hidden;
|
||||
top: 4.1px;
|
||||
width: 12px;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
.meegoSharedWebSettin {
|
||||
min-width: 68px;
|
||||
vertical-align: bottom;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.frameDeveloper {
|
||||
align-items: flex-start;
|
||||
border-color: rgb(var(--semi-grey-2));
|
||||
border-radius: 6px;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
padding: 25px 50px 50px;
|
||||
row-gap: 10px;
|
||||
align-self: stretch;
|
||||
|
||||
.frameInside {
|
||||
align-items: center;
|
||||
column-gap: 4px;
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
|
||||
.group {
|
||||
height: 20px;
|
||||
position: relative;
|
||||
width: 20px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.outlinedVersion {
|
||||
height: 10px;
|
||||
left: 5px;
|
||||
overflow: hidden;
|
||||
top: 5px;
|
||||
width: 10px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.buttonOnlyIconSecond {
|
||||
background: rgb(200, 109, 5);
|
||||
border-radius: 8px;
|
||||
height: 20px;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
top: 0;
|
||||
width: 20px;
|
||||
position: absolute;
|
||||
}
|
||||
.lineStory {
|
||||
height: 15px;
|
||||
left: 4.5px;
|
||||
overflow: hidden;
|
||||
top: 4.1px;
|
||||
width: 12px;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
.meegoSharedWebSettin {
|
||||
min-width: 68px;
|
||||
vertical-align: bottom;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.line {
|
||||
flex-shrink: 0;
|
||||
height: 0;
|
||||
align-self: stretch;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/* 空间配置页
|
||||
页头下方是 Tabs,每个 Tab 面板占满剩余高度并在自己内部滚动;
|
||||
页面本身(应用壳的 .main)不滚动,页头的「保存」始终可见。 */
|
||||
.page {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
box-sizing: border-box;
|
||||
padding: 12px 28px 20px;
|
||||
}
|
||||
|
||||
/* 表单卡片:必须加实色背景,避免全局背景图穿透导致文字不可读 */
|
||||
/* Semi 的 Form / Tabs 默认按内容撑开,这里让它们把剩余高度让给当前面板
|
||||
(semi-tabs-pane-motion-overlay 是开启切换动画时 Semi 多包的一层) */
|
||||
.form,
|
||||
.tabs,
|
||||
.tabs :global(.semi-tabs-content),
|
||||
.tabs :global(.semi-tabs-pane),
|
||||
.tabs :global(.semi-tabs-pane-motion-overlay) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.tabs :global(.semi-tabs-content) {
|
||||
padding: 0;
|
||||
}
|
||||
/* 上面的 display: flex 特异性高于 Semi 自己的 .semi-tabs-pane-inactive,未选中的面板要补回隐藏 */
|
||||
.tabs :global(.semi-tabs-pane-inactive) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 全局设置 / 开发者选项:面板自身滚动 */
|
||||
.pane {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 16px 4px 32px;
|
||||
}
|
||||
|
||||
/* 表单分区:不再套卡片,只用分区标题 + 分隔线区分 */
|
||||
.frameDownload,
|
||||
.frameUpload,
|
||||
.framePlatformConfig,
|
||||
.frameDeveloper {
|
||||
background: var(--semi-color-bg-1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 10px;
|
||||
}
|
||||
.frameUpload {
|
||||
margin-top: 24px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--semi-color-border);
|
||||
}
|
||||
|
||||
/* 平台设置:左列平台名 + 右栏选中表单 */
|
||||
/* 平台设置:左列平台列表 + 右栏选中平台的字段。两栏并排、各自独立滚动,
|
||||
右栏再长,列表也不会跟着滚走 */
|
||||
.platformLayout {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
}
|
||||
.platformNav {
|
||||
width: 160px;
|
||||
flex-shrink: 0;
|
||||
align-self: flex-start;
|
||||
max-height: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
@@ -351,30 +102,44 @@
|
||||
.platformBody {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
/* 右栏:只显示选中平台的字段;不设 display,以免覆盖 hidden 属性 */
|
||||
.platformPanel {
|
||||
padding: 4px 16px 8px 8px;
|
||||
}
|
||||
|
||||
/* 移动端:卡片内边距收紧,避免左右留白/溢出 */
|
||||
/* 自定义背景开启时表单直接铺在背景图上,给滚动面板垫一层半透明底色保证可读 */
|
||||
:global(body.app-has-bg) .pane,
|
||||
:global(body.app-has-bg) .platformBody {
|
||||
background: color-mix(in srgb, var(--semi-color-bg-1) 75%, transparent);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
/* 移动端:两栏改为上下堆叠,平台列表变成横向滑动的标签行,仍不随内容滚走 */
|
||||
@media (max-width: 680px) {
|
||||
.rootConfigPanel {
|
||||
padding: 0 12px !important;
|
||||
}
|
||||
.frameDownload,
|
||||
.frameUpload,
|
||||
.framePlatformConfig,
|
||||
.frameDeveloper {
|
||||
padding: 16px 14px 24px !important;
|
||||
border-radius: 10px;
|
||||
.page {
|
||||
padding: 8px 12px 12px;
|
||||
}
|
||||
.platformLayout {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
.platformNav {
|
||||
width: 100%;
|
||||
max-height: none;
|
||||
align-self: stretch;
|
||||
flex-direction: row;
|
||||
overflow-x: auto;
|
||||
flex-shrink: 0;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
.platformNavItem {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.platformPanel {
|
||||
padding: 4px 4px 8px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { Collapse } from '@douyinfe/semi-ui'
|
||||
|
||||
type Props = {
|
||||
header: string
|
||||
itemKey: string
|
||||
/**
|
||||
* 只输出字段,不套 Collapse.Panel。
|
||||
* 空间配置页由左侧平台列表负责切换,右侧再出现一排带平台名的折叠栏就是重复导航(#1705);
|
||||
* 直播管理页的「配置覆写」弹窗仍以 Collapse.Panel 形式嵌在 Collapse 里。
|
||||
*/
|
||||
bare?: boolean
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const PlatformPanel: React.FC<Props> = ({ header, itemKey, bare, children }) => {
|
||||
if (bare) {
|
||||
return <>{children}</>
|
||||
}
|
||||
return (
|
||||
<Collapse.Panel header={header} itemKey={itemKey}>
|
||||
{children}
|
||||
</Collapse.Panel>
|
||||
)
|
||||
}
|
||||
|
||||
export default PlatformPanel
|
||||
@@ -1,15 +1,17 @@
|
||||
'use client'
|
||||
import React, { useEffect } from 'react'
|
||||
import { Form, Select, Collapse, useFormApi } from '@douyinfe/semi-ui'
|
||||
import { Form, Select, useFormApi } from '@douyinfe/semi-ui'
|
||||
import PlatformPanel from './PlatformPanel'
|
||||
|
||||
type Props = {
|
||||
entity: any
|
||||
list: any
|
||||
initValues?: Record<string, any>
|
||||
bare?: boolean
|
||||
}
|
||||
|
||||
const Bilibili: React.FC<Props> = props => {
|
||||
const { entity, list, initValues } = props
|
||||
const { entity, list, initValues, bare } = props
|
||||
const formApi = useFormApi()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -22,7 +24,7 @@ const Bilibili: React.FC<Props> = props => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Collapse.Panel header="哔哩哔哩" itemKey="bilibili">
|
||||
<PlatformPanel header="哔哩哔哩" itemKey="bilibili" bare={bare}>
|
||||
<Form.Select
|
||||
allowCreate={true}
|
||||
filter
|
||||
@@ -273,7 +275,7 @@ const Bilibili: React.FC<Props> = props => {
|
||||
}}
|
||||
showClear={true}
|
||||
/>
|
||||
</Collapse.Panel>
|
||||
</PlatformPanel>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
'use client'
|
||||
import React, { useEffect } from 'react'
|
||||
import { Form, Select, Collapse, useFormApi } from '@douyinfe/semi-ui'
|
||||
import { Form, Select, useFormApi } from '@douyinfe/semi-ui'
|
||||
import PlatformPanel from './PlatformPanel'
|
||||
|
||||
type Props = {
|
||||
entity: any
|
||||
list: any
|
||||
initValues?: Record<string, any>
|
||||
bare?: boolean
|
||||
}
|
||||
|
||||
const CC: React.FC<Props> = props => {
|
||||
const { entity, list, initValues } = props
|
||||
const { entity, list, initValues, bare } = props
|
||||
const formApi = useFormApi()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -22,7 +24,7 @@ const CC: React.FC<Props> = props => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Collapse.Panel header="CC" itemKey="cc">
|
||||
<PlatformPanel header="CC" itemKey="cc" bare={bare}>
|
||||
<Form.Select
|
||||
field="cc_protocol"
|
||||
extraText="CC 直播流协议,默认 hls。录制经常异常断开、分段过多时可尝试切换为 flv。"
|
||||
@@ -38,7 +40,7 @@ const CC: React.FC<Props> = props => {
|
||||
<Select.Option value="flv">flv</Select.Option>
|
||||
<Select.Option value="hls">hls(默认)</Select.Option>
|
||||
</Form.Select>
|
||||
</Collapse.Panel>
|
||||
</PlatformPanel>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { Form, Select, Collapse } from '@douyinfe/semi-ui'
|
||||
import { Form, Select } from '@douyinfe/semi-ui'
|
||||
import PlatformPanel from './PlatformPanel'
|
||||
|
||||
type Props = {
|
||||
entity: any
|
||||
list: any
|
||||
bare?: boolean
|
||||
}
|
||||
|
||||
const Cookie: React.FC<Props> = props => {
|
||||
@@ -12,7 +14,7 @@ const Cookie: React.FC<Props> = props => {
|
||||
const list = props.list
|
||||
return (
|
||||
<>
|
||||
<Collapse.Panel header="用户 Cookie" itemKey="user">
|
||||
<PlatformPanel header="用户 Cookie" itemKey="user" bare={props.bare}>
|
||||
<Form.Input
|
||||
field="user.kuaishou_cookie"
|
||||
extraText={
|
||||
@@ -94,7 +96,7 @@ const Cookie: React.FC<Props> = props => {
|
||||
padding: 0,
|
||||
}}
|
||||
/>
|
||||
</Collapse.Panel>
|
||||
</PlatformPanel>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
'use client'
|
||||
import React, { useEffect } from 'react'
|
||||
import { Form, Select, Collapse, useFormApi } from '@douyinfe/semi-ui'
|
||||
import { Form, Select, useFormApi } from '@douyinfe/semi-ui'
|
||||
import PlatformPanel from './PlatformPanel'
|
||||
|
||||
type Props = {
|
||||
entity: any
|
||||
list: any
|
||||
initValues?: Record<string, any>
|
||||
bare?: boolean
|
||||
}
|
||||
|
||||
const Douyin: React.FC<Props> = props => {
|
||||
const { entity, list, initValues } = props
|
||||
const { entity, list, initValues, bare } = props
|
||||
const formApi = useFormApi()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -22,7 +24,7 @@ const Douyin: React.FC<Props> = props => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Collapse.Panel header="抖音" itemKey="douyin">
|
||||
<PlatformPanel header="抖音" itemKey="douyin" bare={bare}>
|
||||
<Form.Select
|
||||
field="douyin_quality"
|
||||
extraText={
|
||||
@@ -122,7 +124,7 @@ const Douyin: React.FC<Props> = props => {
|
||||
padding: 0,
|
||||
}}
|
||||
/>
|
||||
</Collapse.Panel>
|
||||
</PlatformPanel>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
'use client'
|
||||
import React, { useEffect } from 'react'
|
||||
import { Form, Select, Collapse, useFormApi } from '@douyinfe/semi-ui'
|
||||
import { Form, Select, useFormApi } from '@douyinfe/semi-ui'
|
||||
import PlatformPanel from './PlatformPanel'
|
||||
|
||||
type Props = {
|
||||
entity: any
|
||||
list: any
|
||||
initValues?: Record<string, any>
|
||||
bare?: boolean
|
||||
}
|
||||
|
||||
const Douyu: React.FC<Props> = props => {
|
||||
const { entity, list, initValues } = props
|
||||
const { entity, list, initValues, bare } = props
|
||||
const formApi = useFormApi()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -22,7 +24,7 @@ const Douyu: React.FC<Props> = props => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Collapse.Panel header="斗鱼" itemKey="douyu">
|
||||
<PlatformPanel header="斗鱼" itemKey="douyu" bare={bare}>
|
||||
<Form.Select
|
||||
allowCreate={true}
|
||||
filter
|
||||
@@ -102,7 +104,7 @@ const Douyu: React.FC<Props> = props => {
|
||||
padding: 0,
|
||||
}}
|
||||
/>
|
||||
</Collapse.Panel>
|
||||
</PlatformPanel>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
'use client'
|
||||
import React, { useEffect } from 'react'
|
||||
import { Form, Select, Collapse, useFormApi } from '@douyinfe/semi-ui'
|
||||
import { Form, Select, useFormApi } from '@douyinfe/semi-ui'
|
||||
import PlatformPanel from './PlatformPanel'
|
||||
|
||||
type Props = {
|
||||
entity: any
|
||||
list: any
|
||||
initValues?: Record<string, any>
|
||||
bare?: boolean
|
||||
}
|
||||
|
||||
const Huya: React.FC<Props> = props => {
|
||||
const { entity, list, initValues } = props
|
||||
const { entity, list, initValues, bare } = props
|
||||
const formApi = useFormApi()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -22,7 +24,7 @@ const Huya: React.FC<Props> = props => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Collapse.Panel header="虎牙" itemKey="huya">
|
||||
<PlatformPanel header="虎牙" itemKey="huya" bare={bare}>
|
||||
<Form.Select
|
||||
allowCreate={true}
|
||||
filter
|
||||
@@ -144,7 +146,7 @@ const Huya: React.FC<Props> = props => {
|
||||
padding: 0,
|
||||
}}
|
||||
/>
|
||||
</Collapse.Panel>
|
||||
</PlatformPanel>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,8 @@ const plugins = {
|
||||
|
||||
/**
|
||||
* 空间配置「平台设置」左栏的展示顺序与文案。
|
||||
* key 必须与各插件 Collapse.Panel 的 itemKey 一致;新增插件时在这里登记一行即可出现在 UI 里。
|
||||
* 各插件组件接受 `bare` 属性:为 true 时只渲染字段(空间配置页),否则渲染成 Collapse.Panel(配置覆写弹窗)。
|
||||
* 新增插件时在这里登记一行即可出现在 UI 里。
|
||||
*/
|
||||
export const PlatformPanels: { key: string; name: string; Component: React.FC<any> }[] = [
|
||||
{ key: 'bilibili', name: '哔哩哔哩', Component: Bilibili },
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
'use client'
|
||||
import React, { useEffect } from 'react'
|
||||
import { Form, Select, Collapse, useFormApi } from '@douyinfe/semi-ui'
|
||||
import { Form, Select, useFormApi } from '@douyinfe/semi-ui'
|
||||
import PlatformPanel from './PlatformPanel'
|
||||
|
||||
type Props = {
|
||||
entity: any
|
||||
list: any
|
||||
initValues?: Record<string, any>
|
||||
bare?: boolean
|
||||
}
|
||||
|
||||
const Kilakila: React.FC<Props> = props => {
|
||||
const { entity, list, initValues } = props
|
||||
const { entity, list, initValues, bare } = props
|
||||
const formApi = useFormApi()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -22,7 +24,7 @@ const Kilakila: React.FC<Props> = props => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Collapse.Panel header="克拉克拉" itemKey="kilakila">
|
||||
<PlatformPanel header="克拉克拉" itemKey="kilakila" bare={bare}>
|
||||
<Form.Select
|
||||
field="kila_protocol"
|
||||
extraText="直播流协议,默认 hls"
|
||||
@@ -38,7 +40,7 @@ const Kilakila: React.FC<Props> = props => {
|
||||
<Select.Option value="hls">hls</Select.Option>
|
||||
<Select.Option value="flv">flv</Select.Option>
|
||||
</Form.Select>
|
||||
</Collapse.Panel>
|
||||
</PlatformPanel>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
'use client'
|
||||
import React, { useEffect } from 'react'
|
||||
import { Form, Select, Collapse, useFormApi } from '@douyinfe/semi-ui'
|
||||
import { Form, Select, useFormApi } from '@douyinfe/semi-ui'
|
||||
import PlatformPanel from './PlatformPanel'
|
||||
|
||||
type Props = {
|
||||
entity: any
|
||||
list: any
|
||||
initValues?: Record<string, any>
|
||||
bare?: boolean
|
||||
}
|
||||
|
||||
const TwitCasting: React.FC<Props> = props => {
|
||||
const { entity, list, initValues } = props
|
||||
const { entity, list, initValues, bare } = props
|
||||
const formApi = useFormApi()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -22,7 +24,7 @@ const TwitCasting: React.FC<Props> = props => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Collapse.Panel header="TwitCasting" itemKey="twitcasting">
|
||||
<PlatformPanel header="TwitCasting" itemKey="twitcasting" bare={bare}>
|
||||
<Form.Select
|
||||
field="twitcasting_quality"
|
||||
extraText="录制画质,默认取最高可用画质。所选画质不存在时自动降到更低一档;更低的也没有时取最高可用画质。"
|
||||
@@ -72,7 +74,7 @@ const TwitCasting: React.FC<Props> = props => {
|
||||
padding: 0,
|
||||
}}
|
||||
/>
|
||||
</Collapse.Panel>
|
||||
</PlatformPanel>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
'use client'
|
||||
import React, { useEffect } from 'react'
|
||||
import { Form, Select, Collapse, useFormApi } from '@douyinfe/semi-ui'
|
||||
import { Form, Select, useFormApi } from '@douyinfe/semi-ui'
|
||||
import PlatformPanel from './PlatformPanel'
|
||||
|
||||
type Props = {
|
||||
entity: any
|
||||
list: any
|
||||
initValues?: Record<string, any>
|
||||
bare?: boolean
|
||||
}
|
||||
|
||||
const Twitch: React.FC<Props> = props => {
|
||||
const { entity, list, initValues } = props
|
||||
const { entity, list, initValues, bare } = props
|
||||
const formApi = useFormApi()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -22,7 +24,7 @@ const Twitch: React.FC<Props> = props => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Collapse.Panel header="Twitch" itemKey="twitch">
|
||||
<PlatformPanel header="Twitch" itemKey="twitch" bare={bare}>
|
||||
<Form.Switch
|
||||
field="twitch_danmaku"
|
||||
extraText="录制 Twitch 弹幕,默认关闭"
|
||||
@@ -75,7 +77,7 @@ const Twitch: React.FC<Props> = props => {
|
||||
padding: 0,
|
||||
}}
|
||||
/>
|
||||
</Collapse.Panel>
|
||||
</PlatformPanel>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
'use client'
|
||||
import React, { useEffect } from 'react'
|
||||
import { Form, Select, Collapse, useFormApi } from '@douyinfe/semi-ui'
|
||||
import { Form, Select, useFormApi } from '@douyinfe/semi-ui'
|
||||
import PlatformPanel from './PlatformPanel'
|
||||
|
||||
type Props = {
|
||||
entity: any
|
||||
list: any
|
||||
initValues?: Record<string, any>
|
||||
bare?: boolean
|
||||
}
|
||||
|
||||
const YouTube: React.FC<Props> = props => {
|
||||
const { entity, list, initValues } = props
|
||||
const { entity, list, initValues, bare } = props
|
||||
const formApi = useFormApi()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -22,7 +24,7 @@ const YouTube: React.FC<Props> = props => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Collapse.Panel header="YouTube" itemKey="youtube">
|
||||
<PlatformPanel header="YouTube" itemKey="youtube" bare={bare}>
|
||||
<Form.Input
|
||||
field="user.youtube_cookie"
|
||||
extraText={
|
||||
@@ -166,7 +168,7 @@ const YouTube: React.FC<Props> = props => {
|
||||
padding: 0,
|
||||
}}
|
||||
/>
|
||||
</Collapse.Panel>
|
||||
</PlatformPanel>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user