chore(web): 收口前几轮后台改动(品牌 / 媒资上传 / 轮播图 / 类型管理 / 操作日志 / App 用户)
为什么:这些改动在前几轮就已完成并在 :3005 验证过,但一直留在工作区没有入库。 下一步 3-4 的四个页面依赖其中的公共样式入口(src/styles/business/index.css)与 api/admin 的公共类型(PageResult 等),如果不先收口,新页面的提交就会夹带旧改动, 既说不清 diff 边界,也让 index.css 这类多轮共用的文件长期处于半提交状态。 本提交只做归档,不改行为: - 品牌与接口基址改成 FruitShorts / 短剧服务(.env.*、README、vite 代理) - 去掉前端 mock 数据目录(src/data、src/permissions、stores/auth),改为走真实接口 - 新增 api/admin 下的接口封装与 src/utils/uploader.ts(媒资分片直传) - 新增/改造:轮播图管理、类型管理、操作日志、App 用户管理、后台布局与公共样式
This commit is contained in:
1 parent
40546bfd01
commit
44c2c46a68
59 files changed
+4082
-982
No files matched your search
@@ -0,0 +1,23 @@
|
||||
/** 播放量等大数值的展示格式:12345 → 1.2万,3100000 → 310.0万 */
|
||||
export function formatCount(value) {
|
||||
const num = Number(value || 0)
|
||||
if (!Number.isFinite(num)) return '0'
|
||||
if (num >= 100000000) return `${(num / 100000000).toFixed(2)}亿`
|
||||
if (num >= 10000) return `${(num / 10000).toFixed(1)}万`
|
||||
return String(num)
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间展示:直接返回实体的接口给的是 ISO 时间(2026-10-15T13:22:03),
|
||||
* 和后台其它页面「yyyy-MM-dd HH:mm:ss」的观感不一致,这里统一成后者。
|
||||
*/
|
||||
export function formatDateTime(value) {
|
||||
if (!value) return '-'
|
||||
const text = String(value).replace('T', ' ')
|
||||
return text.length > 19 ? text.slice(0, 19) : text
|
||||
}
|
||||
|
||||
/** el-table-column 的 formatter 适配(签名是 row, column, cellValue) */
|
||||
export function dateCell(_row, _column, cellValue) {
|
||||
return formatDateTime(cellValue)
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
const statusClasses = {
|
||||
published: 'is-online',
|
||||
reviewing: 'is-reviewing',
|
||||
draft: 'is-draft',
|
||||
approved: 'is-pending',
|
||||
offline: 'is-offline',
|
||||
已上线: 'is-online',
|
||||
审核中: 'is-reviewing',
|
||||
草稿: 'is-draft',
|
||||
待审核: 'is-reviewing',
|
||||
待发布: 'is-pending',
|
||||
已下线: 'is-offline'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { completeUploadApi, initUploadApi, uploadChunkApi } from '@/api/admin/media'
|
||||
import type { MediaAsset } from '@/api/admin/types'
|
||||
|
||||
export interface UploadFileOptions {
|
||||
/** DRAMA_COVER / EPISODE / AVATAR */
|
||||
bizType?: string
|
||||
/** 并发分片数,默认 3 */
|
||||
concurrency?: number
|
||||
/** 视频时长(秒),由调用方探测后传入 */
|
||||
durationSec?: number
|
||||
onProgress?: (percent: number) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 分片上传:init 建会话 → 并发传分片 → complete 合并。
|
||||
* 服务端返回合并后的媒资(含可访问 URL)。
|
||||
*/
|
||||
export async function uploadFile(file: File, options: UploadFileOptions = {}): Promise<MediaAsset> {
|
||||
const { bizType = 'EPISODE', concurrency = 3, onProgress } = options
|
||||
const initRes = await initUploadApi({
|
||||
fileName: file.name,
|
||||
fileSize: file.size,
|
||||
bizType
|
||||
})
|
||||
const info = initRes.data
|
||||
const total = Math.max(info.totalChunks, 1)
|
||||
const pending: number[] = Array.from({ length: total }, (_, index) => index)
|
||||
let finished = 0
|
||||
|
||||
const worker = async () => {
|
||||
for (;;) {
|
||||
const index = pending.shift()
|
||||
if (index === undefined) return
|
||||
const start = index * info.chunkSize
|
||||
const chunk = file.slice(start, Math.min(start + info.chunkSize, file.size))
|
||||
await uploadChunkApi(info.uploadId, index, chunk, file.name)
|
||||
finished += 1
|
||||
onProgress?.(Math.min(95, Math.round((finished / total) * 95)))
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(Array.from({ length: Math.min(concurrency, total) }, worker))
|
||||
const doneRes = await completeUploadApi(info.uploadId, options.durationSec)
|
||||
onProgress?.(100)
|
||||
return doneRes.data
|
||||
}
|
||||
|
||||
/** 读取本地视频时长,取不到时返回 0。 */
|
||||
export function probeVideoDuration(file: File): Promise<number> {
|
||||
return new Promise((resolve) => {
|
||||
const url = URL.createObjectURL(file)
|
||||
const video = document.createElement('video')
|
||||
video.preload = 'metadata'
|
||||
const cleanup = (value: number) => {
|
||||
URL.revokeObjectURL(url)
|
||||
resolve(value)
|
||||
}
|
||||
video.onloadedmetadata = () => {
|
||||
cleanup(Number.isFinite(video.duration) ? Math.round(video.duration) : 0)
|
||||
}
|
||||
video.onerror = () => cleanup(0)
|
||||
video.src = url
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user