feat(admin): 新增转码设置页面
This commit is contained in:
1 parent
723d056fc7
commit
218196ee9f
16 files changed
+1014
-3
No files matched your search
@@ -0,0 +1,350 @@
|
||||
<!-- Author: fruit.com.cn -->
|
||||
<!-- 后台转码档位与缩略图雪碧图配置 -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { AppIcons } from '@/constants/icons'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import PageHeading from '@/views/components/PageHeading.vue'
|
||||
import { useTranscodeSettings } from './composables/useTranscodeSettings'
|
||||
|
||||
const { t } = useI18n()
|
||||
const {
|
||||
form,
|
||||
formRef,
|
||||
rules,
|
||||
loading,
|
||||
saving,
|
||||
loadError,
|
||||
canManage,
|
||||
qualityRules,
|
||||
positiveEvenRule,
|
||||
positiveIntegerRule,
|
||||
enabledRules,
|
||||
loadProfile,
|
||||
addRung,
|
||||
deleteRung,
|
||||
saveProfile
|
||||
} = useTranscodeSettings()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageHeading
|
||||
:eyebrow="t('transcodeSettings.eyebrow')"
|
||||
:title="t('transcodeSettings.title')"
|
||||
:description="t('transcodeSettings.description')"
|
||||
>
|
||||
<el-button :disabled="loading" @click="loadProfile">
|
||||
<AppIcon :icon="AppIcons.refresh" />
|
||||
{{ t('transcodeSettings.reload') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
class="primary-button"
|
||||
:loading="saving"
|
||||
:disabled="!canManage || loading"
|
||||
@click="saveProfile"
|
||||
>
|
||||
<AppIcon :icon="AppIcons.check" />
|
||||
{{ t('transcodeSettings.save') }}
|
||||
</el-button>
|
||||
</PageHeading>
|
||||
|
||||
<el-alert
|
||||
v-if="loadError"
|
||||
class="transcode-alert"
|
||||
:title="loadError"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
<el-alert
|
||||
v-if="!canManage"
|
||||
class="transcode-alert"
|
||||
:title="t('transcodeSettings.noPermission')"
|
||||
type="warning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
|
||||
<el-form
|
||||
ref="formRef"
|
||||
v-loading="loading"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
class="transcode-form"
|
||||
>
|
||||
<section class="panel transcode-panel">
|
||||
<div class="transcode-section-heading">
|
||||
<div>
|
||||
<h2>{{ t('transcodeSettings.enabled') }}</h2>
|
||||
<p>{{ t('transcodeSettings.enabledDescription') }}</p>
|
||||
</div>
|
||||
<el-switch v-model="form.enabled" size="large" />
|
||||
</div>
|
||||
<div class="transcode-grid transcode-grid--compact">
|
||||
<el-form-item :label="t('transcodeSettings.segmentSeconds')" prop="segmentSeconds">
|
||||
<el-input-number
|
||||
v-model="form.segmentSeconds"
|
||||
controls-position="right"
|
||||
:min="2"
|
||||
:max="10"
|
||||
:precision="0"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel transcode-panel">
|
||||
<div class="transcode-section-heading">
|
||||
<div>
|
||||
<h2>{{ t('transcodeSettings.ladderTitle') }}</h2>
|
||||
<p>{{ t('transcodeSettings.ladderDescription') }}</p>
|
||||
</div>
|
||||
<el-button type="primary" plain @click="addRung">
|
||||
<AppIcon :icon="AppIcons.add" />
|
||||
{{ t('transcodeSettings.addRung') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="form.ladder"
|
||||
border
|
||||
class="transcode-table"
|
||||
:empty-text="t('transcodeSettings.emptyLadder')"
|
||||
>
|
||||
<el-table-column type="index" width="60" label="#" />
|
||||
<el-table-column :label="t('transcodeSettings.quality')" min-width="150">
|
||||
<template #default="{ row, $index }">
|
||||
<el-form-item
|
||||
:prop="`ladder.${$index}.quality`"
|
||||
:rules="qualityRules($index)"
|
||||
class="table-form-item"
|
||||
>
|
||||
<el-input v-model="row.quality" maxlength="16" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('transcodeSettings.width')" min-width="120">
|
||||
<template #default="{ row, $index }">
|
||||
<el-form-item
|
||||
:prop="`ladder.${$index}.width`"
|
||||
:rules="positiveEvenRule()"
|
||||
class="table-form-item"
|
||||
>
|
||||
<el-input-number
|
||||
v-model="row.width"
|
||||
controls-position="right"
|
||||
:min="2"
|
||||
:precision="0"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('transcodeSettings.height')" min-width="120">
|
||||
<template #default="{ row, $index }">
|
||||
<el-form-item
|
||||
:prop="`ladder.${$index}.height`"
|
||||
:rules="positiveEvenRule()"
|
||||
class="table-form-item"
|
||||
>
|
||||
<el-input-number
|
||||
v-model="row.height"
|
||||
controls-position="right"
|
||||
:min="2"
|
||||
:precision="0"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('transcodeSettings.videoBitrateKbps')" min-width="150">
|
||||
<template #default="{ row, $index }">
|
||||
<el-form-item
|
||||
:prop="`ladder.${$index}.videoBitrateKbps`"
|
||||
:rules="positiveIntegerRule()"
|
||||
class="table-form-item"
|
||||
>
|
||||
<el-input-number
|
||||
v-model="row.videoBitrateKbps"
|
||||
controls-position="right"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('transcodeSettings.audioBitrateKbps')" min-width="150">
|
||||
<template #default="{ row, $index }">
|
||||
<el-form-item
|
||||
:prop="`ladder.${$index}.audioBitrateKbps`"
|
||||
:rules="positiveIntegerRule()"
|
||||
class="table-form-item"
|
||||
>
|
||||
<el-input-number
|
||||
v-model="row.audioBitrateKbps"
|
||||
controls-position="right"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('transcodeSettings.rowEnabled')" width="100" align="center">
|
||||
<template #default="{ row, $index }">
|
||||
<el-form-item
|
||||
:prop="`ladder.${$index}.enabled`"
|
||||
:rules="enabledRules"
|
||||
class="table-form-item table-form-item--center"
|
||||
>
|
||||
<el-switch v-model="row.enabled" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="" width="72" align="center" fixed="right">
|
||||
<template #default="{ $index }">
|
||||
<el-tooltip :content="t('transcodeSettings.deleteRung')" placement="top">
|
||||
<el-button circle type="danger" plain @click="deleteRung($index)">
|
||||
<AppIcon :icon="AppIcons.delete" />
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
|
||||
<section class="panel transcode-panel">
|
||||
<div class="transcode-section-heading">
|
||||
<div>
|
||||
<h2>{{ t('transcodeSettings.spriteTitle') }}</h2>
|
||||
<p>{{ t('transcodeSettings.spriteDescription') }}</p>
|
||||
</div>
|
||||
<el-switch v-model="form.sprite.enabled" size="large" />
|
||||
</div>
|
||||
<div class="transcode-grid">
|
||||
<el-form-item :label="t('transcodeSettings.intervalSeconds')" prop="sprite.intervalSeconds">
|
||||
<el-input-number
|
||||
v-model="form.sprite.intervalSeconds"
|
||||
controls-position="right"
|
||||
:min="1"
|
||||
:max="10"
|
||||
:precision="0"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('transcodeSettings.columns')" prop="sprite.columns">
|
||||
<el-input-number
|
||||
v-model="form.sprite.columns"
|
||||
controls-position="right"
|
||||
:min="5"
|
||||
:max="20"
|
||||
:precision="0"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('transcodeSettings.thumbWidth')" prop="sprite.thumbWidth">
|
||||
<el-input-number
|
||||
v-model="form.sprite.thumbWidth"
|
||||
controls-position="right"
|
||||
:min="120"
|
||||
:max="240"
|
||||
:precision="0"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="transcode-footer">
|
||||
<el-button
|
||||
type="primary"
|
||||
class="primary-button"
|
||||
:loading="saving"
|
||||
:disabled="!canManage || loading"
|
||||
@click="saveProfile"
|
||||
>
|
||||
<AppIcon :icon="AppIcons.check" />
|
||||
{{ t('transcodeSettings.save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</section>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.transcode-alert {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.transcode-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.transcode-panel {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.transcode-section-heading {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 18px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 6px 0 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.transcode-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(180px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.transcode-grid--compact {
|
||||
grid-template-columns: minmax(180px, 320px);
|
||||
}
|
||||
|
||||
.transcode-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.table-form-item {
|
||||
margin-bottom: 0;
|
||||
|
||||
:deep(.el-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__error) {
|
||||
position: static;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-form-item--center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.transcode-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.transcode-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.transcode-section-heading {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,74 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import type {
|
||||
MediaTranscodeLadderRung,
|
||||
MediaTranscodeProfile,
|
||||
MediaTranscodeSprite
|
||||
} from '@/api/admin/types'
|
||||
|
||||
export const MIN_SEGMENT_SECONDS = 2
|
||||
export const MAX_SEGMENT_SECONDS = 10
|
||||
export const MIN_SPRITE_INTERVAL = 1
|
||||
export const MAX_SPRITE_INTERVAL = 10
|
||||
export const MIN_SPRITE_COLUMNS = 5
|
||||
export const MAX_SPRITE_COLUMNS = 20
|
||||
export const MIN_THUMB_WIDTH = 120
|
||||
export const MAX_THUMB_WIDTH = 240
|
||||
export const MAX_QUALITY_LENGTH = 16
|
||||
|
||||
export const defaultSprite = (): MediaTranscodeSprite => ({
|
||||
enabled: true,
|
||||
intervalSeconds: 1,
|
||||
columns: 10,
|
||||
thumbWidth: 160
|
||||
})
|
||||
|
||||
export const defaultRung = (): MediaTranscodeLadderRung => ({
|
||||
quality: '',
|
||||
width: 640,
|
||||
height: 360,
|
||||
videoBitrateKbps: 400,
|
||||
audioBitrateKbps: 64,
|
||||
enabled: true
|
||||
})
|
||||
|
||||
/** 与后端缺省值一致的四档配置。 */
|
||||
export const defaultProfile = (): MediaTranscodeProfile => ({
|
||||
enabled: true,
|
||||
segmentSeconds: 6,
|
||||
ladder: [
|
||||
{
|
||||
quality: '360p',
|
||||
width: 640,
|
||||
height: 360,
|
||||
videoBitrateKbps: 400,
|
||||
audioBitrateKbps: 64,
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
quality: '480p',
|
||||
width: 854,
|
||||
height: 480,
|
||||
videoBitrateKbps: 800,
|
||||
audioBitrateKbps: 64,
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
quality: '720p',
|
||||
width: 1280,
|
||||
height: 720,
|
||||
videoBitrateKbps: 1600,
|
||||
audioBitrateKbps: 96,
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
quality: '1080p',
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
videoBitrateKbps: 3200,
|
||||
audioBitrateKbps: 128,
|
||||
enabled: true
|
||||
}
|
||||
],
|
||||
sprite: defaultSprite()
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import type { MediaTranscodeProfile } from '@/api/admin/types'
|
||||
import { defaultProfile, defaultRung, defaultSprite } from './transcodeProfileDefaults'
|
||||
|
||||
/** 用服务端响应回填表单,同时保持字段名与后端完全一致。 */
|
||||
export const applyProfileToForm = (
|
||||
form: MediaTranscodeProfile,
|
||||
profile?: MediaTranscodeProfile | null
|
||||
): void => {
|
||||
const source = profile || defaultProfile()
|
||||
form.enabled = Boolean(source.enabled)
|
||||
form.segmentSeconds = Number(source.segmentSeconds)
|
||||
form.sprite = { ...defaultSprite(), ...(source.sprite || {}) }
|
||||
const ladder =
|
||||
Array.isArray(source.ladder) && source.ladder.length ? source.ladder : [defaultRung()]
|
||||
form.ladder.splice(
|
||||
0,
|
||||
form.ladder.length,
|
||||
...ladder.map((row) => ({
|
||||
quality: row.quality || '',
|
||||
width: Number(row.width),
|
||||
height: Number(row.height),
|
||||
videoBitrateKbps: Number(row.videoBitrateKbps),
|
||||
audioBitrateKbps: Number(row.audioBitrateKbps),
|
||||
enabled: Boolean(row.enabled)
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
/** 生成只包含后端约定字段的 PUT 请求体。 */
|
||||
export const buildProfilePayload = (form: MediaTranscodeProfile): MediaTranscodeProfile => ({
|
||||
enabled: form.enabled,
|
||||
segmentSeconds: Number(form.segmentSeconds),
|
||||
ladder: form.ladder.map((row) => ({
|
||||
quality: row.quality.trim(),
|
||||
width: Number(row.width),
|
||||
height: Number(row.height),
|
||||
videoBitrateKbps: Number(row.videoBitrateKbps),
|
||||
audioBitrateKbps: Number(row.audioBitrateKbps),
|
||||
enabled: Boolean(row.enabled)
|
||||
})),
|
||||
sprite: {
|
||||
enabled: Boolean(form.sprite.enabled),
|
||||
intervalSeconds: Number(form.sprite.intervalSeconds),
|
||||
columns: Number(form.sprite.columns),
|
||||
thumbWidth: Number(form.sprite.thumbWidth)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,98 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import type { FormItemRule, FormRules } from 'element-plus'
|
||||
import type { MediaTranscodeProfile } from '@/api/admin/types'
|
||||
import {
|
||||
MAX_QUALITY_LENGTH,
|
||||
MAX_SEGMENT_SECONDS,
|
||||
MAX_SPRITE_COLUMNS,
|
||||
MAX_SPRITE_INTERVAL,
|
||||
MAX_THUMB_WIDTH,
|
||||
MIN_SPRITE_COLUMNS,
|
||||
MIN_SEGMENT_SECONDS,
|
||||
MIN_SPRITE_INTERVAL,
|
||||
MIN_THUMB_WIDTH
|
||||
} from './transcodeProfileDefaults'
|
||||
|
||||
type Translate = (key: string, named?: Record<string, unknown>) => string
|
||||
|
||||
const rangeRule = (min: number, max: number, t: Translate): FormItemRule => ({
|
||||
trigger: 'change',
|
||||
validator: (_rule, value, callback) => {
|
||||
const parsed = Number(value)
|
||||
callback(
|
||||
Number.isInteger(parsed) && parsed >= min && parsed <= max
|
||||
? undefined
|
||||
: new Error(t('transcodeSettings.validation.range', { min, max }))
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export const positiveIntegerRule = (t: Translate): FormItemRule => ({
|
||||
trigger: 'change',
|
||||
validator: (_rule, value, callback) => {
|
||||
const parsed = Number(value)
|
||||
callback(
|
||||
Number.isInteger(parsed) && parsed > 0
|
||||
? undefined
|
||||
: new Error(t('transcodeSettings.validation.positiveInteger'))
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export const positiveEvenRule = (t: Translate): FormItemRule => ({
|
||||
trigger: 'change',
|
||||
validator: (_rule, value, callback) => {
|
||||
const parsed = Number(value)
|
||||
callback(
|
||||
Number.isInteger(parsed) && parsed > 0 && parsed % 2 === 0
|
||||
? undefined
|
||||
: new Error(t('transcodeSettings.validation.positiveEven'))
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export const createProfileRules = (t: Translate): FormRules => ({
|
||||
segmentSeconds: [rangeRule(MIN_SEGMENT_SECONDS, MAX_SEGMENT_SECONDS, t)],
|
||||
'sprite.intervalSeconds': [rangeRule(MIN_SPRITE_INTERVAL, MAX_SPRITE_INTERVAL, t)],
|
||||
'sprite.columns': [rangeRule(MIN_SPRITE_COLUMNS, MAX_SPRITE_COLUMNS, t)],
|
||||
'sprite.thumbWidth': [rangeRule(MIN_THUMB_WIDTH, MAX_THUMB_WIDTH, t)]
|
||||
})
|
||||
|
||||
export const createQualityRules = (
|
||||
form: MediaTranscodeProfile,
|
||||
index: number,
|
||||
t: Translate
|
||||
): FormItemRule[] => [
|
||||
{
|
||||
trigger: 'blur',
|
||||
validator: (_rule, value, callback) => {
|
||||
const quality = String(value ?? '').trim()
|
||||
if (!quality) {
|
||||
callback(new Error(t('transcodeSettings.validation.required')))
|
||||
return
|
||||
}
|
||||
if (quality.length > MAX_QUALITY_LENGTH) {
|
||||
callback(new Error(t('transcodeSettings.validation.qualityTooLong')))
|
||||
return
|
||||
}
|
||||
const duplicate = form.ladder.some(
|
||||
(row, rowIndex) => rowIndex !== index && row.quality.trim() === quality
|
||||
)
|
||||
callback(duplicate ? new Error(t('transcodeSettings.validation.qualityUnique')) : undefined)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
export const createEnabledRules = (form: MediaTranscodeProfile, t: Translate): FormItemRule[] => [
|
||||
{
|
||||
trigger: 'change',
|
||||
validator: (_rule, _value, callback) => {
|
||||
callback(
|
||||
form.ladder.some((row) => row.enabled)
|
||||
? undefined
|
||||
: new Error(t('transcodeSettings.validation.atLeastOneEnabled'))
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,79 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import { nextTick } from 'vue'
|
||||
import type { ComputedRef, Ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { getMediaTranscodeProfileApi, saveMediaTranscodeProfileApi } from '@/api/admin/media'
|
||||
import type { MediaTranscodeProfile } from '@/api/admin/types'
|
||||
import { applyProfileToForm, buildProfilePayload } from './transcodeProfileModel'
|
||||
|
||||
type Translate = (key: string, named?: Record<string, unknown>) => string
|
||||
|
||||
interface SettingsContext {
|
||||
form: MediaTranscodeProfile
|
||||
formRef: Ref<FormInstance | undefined>
|
||||
loading: Ref<boolean>
|
||||
saving: Ref<boolean>
|
||||
loadError: Ref<string>
|
||||
canManage: ComputedRef<boolean>
|
||||
t: Translate
|
||||
}
|
||||
|
||||
/** 读取服务端配置并回填表单。 */
|
||||
export const loadTranscodeProfile = async (context: SettingsContext): Promise<void> => {
|
||||
context.loading.value = true
|
||||
context.loadError.value = ''
|
||||
try {
|
||||
const response = await getMediaTranscodeProfileApi()
|
||||
if (response.code !== 0) {
|
||||
context.loadError.value = response.message || context.t('transcodeSettings.loadFailed')
|
||||
return
|
||||
}
|
||||
applyProfileToForm(context.form, response.data)
|
||||
} catch {
|
||||
context.loadError.value = context.t('transcodeSettings.loadFailed')
|
||||
} finally {
|
||||
context.loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const validateProfile = async (context: SettingsContext): Promise<boolean> => {
|
||||
if (!context.form.ladder.length) {
|
||||
ElMessage.warning(context.t('transcodeSettings.validation.atLeastOneEnabled'))
|
||||
return false
|
||||
}
|
||||
try {
|
||||
await context.formRef.value?.validate()
|
||||
return true
|
||||
} catch (fields) {
|
||||
const firstField = Object.keys((fields || {}) as Record<string, unknown>)[0]
|
||||
if (firstField) {
|
||||
await nextTick()
|
||||
context.formRef.value?.scrollToField(firstField)
|
||||
}
|
||||
ElMessage.warning(context.t('transcodeSettings.validation.fixErrors'))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** 校验并保存,成功后用服务端返回值回填。 */
|
||||
export const saveTranscodeProfile = async (context: SettingsContext): Promise<void> => {
|
||||
if (!context.canManage.value || context.saving.value || !(await validateProfile(context))) {
|
||||
return
|
||||
}
|
||||
context.saving.value = true
|
||||
try {
|
||||
const response = await saveMediaTranscodeProfileApi(buildProfilePayload(context.form))
|
||||
if (response.code !== 0) {
|
||||
ElMessage.error(response.message || context.t('transcodeSettings.saveFailed'))
|
||||
return
|
||||
}
|
||||
applyProfileToForm(context.form, response.data)
|
||||
ElMessage.success(context.t('transcodeSettings.saved'))
|
||||
} catch {
|
||||
ElMessage.error(context.t('transcodeSettings.saveFailed'))
|
||||
} finally {
|
||||
context.saving.value = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import type { MediaTranscodeProfile } from '@/api/admin/types'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { useRbac } from '@/stores/rbac'
|
||||
import { defaultProfile, defaultRung } from './transcodeProfileDefaults'
|
||||
import {
|
||||
createEnabledRules,
|
||||
createProfileRules,
|
||||
createQualityRules,
|
||||
positiveEvenRule,
|
||||
positiveIntegerRule
|
||||
} from './transcodeProfileRules'
|
||||
import { loadTranscodeProfile, saveTranscodeProfile } from './transcodeSettingsActions'
|
||||
|
||||
/** 转码设置页的响应式状态与动作装配。 */
|
||||
export function useTranscodeSettings() {
|
||||
const { t } = useI18n()
|
||||
const { can } = useRbac()
|
||||
const formRef = ref<FormInstance>()
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const loadError = ref('')
|
||||
const form = reactive<MediaTranscodeProfile>(defaultProfile())
|
||||
const canManage = computed(() => can('episode:upload'))
|
||||
const context = { form, formRef, loading, saving, loadError, canManage, t }
|
||||
const rules = computed(() => createProfileRules(t))
|
||||
const enabledRules = computed(() => createEnabledRules(form, t))
|
||||
const qualityRules = (index: number) => createQualityRules(form, index, t)
|
||||
|
||||
const addRung = (): void => {
|
||||
form.ladder.push(defaultRung())
|
||||
nextTick(() => formRef.value?.clearValidate())
|
||||
}
|
||||
|
||||
const deleteRung = (index: number): void => {
|
||||
form.ladder.splice(index, 1)
|
||||
nextTick(() => formRef.value?.clearValidate())
|
||||
}
|
||||
|
||||
const loadProfile = () => loadTranscodeProfile(context)
|
||||
const saveProfile = () => saveTranscodeProfile(context)
|
||||
onMounted(loadProfile)
|
||||
|
||||
return {
|
||||
form,
|
||||
formRef,
|
||||
rules,
|
||||
loading,
|
||||
saving,
|
||||
loadError,
|
||||
canManage,
|
||||
qualityRules,
|
||||
positiveEvenRule: () => positiveEvenRule(t),
|
||||
positiveIntegerRule: () => positiveIntegerRule(t),
|
||||
enabledRules,
|
||||
loadProfile,
|
||||
addRung,
|
||||
deleteRung,
|
||||
saveProfile
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user