feat(教程管理): 后台可上传教程包,工具台按最新上传下载
- admin-vue 新增「记录与版本 → 教程管理」页(menuKey admin_tutorial):
zip 浏览器直传 MinIO(presign → PUT 带进度 → confirm 落库)+ 列表/下载/删除,
最新一条标「当前生效」;补操作指引与 align-tutorial 守卫测试。
- frontend-vue 工具台「立即下载教程」进页面取 /api/tutorial/latest(最新上传优先),
接口异常或无记录回退历史固定直链,按钮不失效。
- Java 侧 tutorial 模块与 V119 迁移已在 dc6e8924 提交并上线(2026-09-13)。
vue-tsc + 两端单测(工具台 695 / 后台 1617)通过;生产已实测
presign→PUT→confirm→latest 切换→删除回落全链路。
This commit is contained in:
@@ -126,7 +126,7 @@ import { useRouter } from 'vue-router'
|
||||
import AmazonTopBar from '@/pages/amazon/components/AmazonTopBar.vue'
|
||||
import AmazonFooterBar from '@/pages/amazon/components/AmazonFooterBar.vue'
|
||||
import ToolIcon from '@/pages/amazon/components/ToolIcon.vue'
|
||||
import { TUTORIAL_DOWNLOAD_URL } from '@/pages/amazon/download-btn'
|
||||
import { TUTORIAL_DOWNLOAD_URL, resolveTutorialDownloadUrl } from '@/pages/amazon/download-btn'
|
||||
import { filterGroupsByPermission, filterWorkflowByPermission, TOOL_GROUPS, TOOL_MAP } from '@/pages/amazon/tool-catalog'
|
||||
import type { ToolGroup, ToolInfo, VisibleGroup, WorkflowStep } from '@/pages/amazon/tool-catalog'
|
||||
import { resolvePageHref } from '@/shared/page-prefix'
|
||||
@@ -217,13 +217,20 @@ function toast(msg: string, type: 'normal' | 'error' = 'normal') {
|
||||
}, 2500)
|
||||
}
|
||||
|
||||
/** 教程包直链:进页面时解析一次(后台「教程管理」最新上传优先),失败自动回退固定直链 */
|
||||
const tutorialUrl = ref(TUTORIAL_DOWNLOAD_URL)
|
||||
|
||||
async function refreshTutorialUrl() {
|
||||
tutorialUrl.value = await resolveTutorialDownloadUrl()
|
||||
}
|
||||
|
||||
async function onDownloadClick() {
|
||||
// 教程客户端压缩包取自 MinIO 公开直链(client/tutorial/,见 download-btn.ts)
|
||||
// 教程客户端压缩包:最新上传的包优先(/api/tutorial/latest),兜底走固定直链(见 download-btn.ts)
|
||||
const api = getPywebviewApi()
|
||||
if (api?.open_external_url) {
|
||||
// 桌面端:交系统默认浏览器下载,可看到下载进度
|
||||
try {
|
||||
const res = await api.open_external_url(TUTORIAL_DOWNLOAD_URL)
|
||||
const res = await api.open_external_url(tutorialUrl.value)
|
||||
if (!res?.success) toast(`下载失败:${res?.error ?? '请重试'}`, 'error')
|
||||
} catch (_error) {
|
||||
toast('下载失败,请重试', 'error')
|
||||
@@ -231,7 +238,7 @@ async function onDownloadClick() {
|
||||
return
|
||||
}
|
||||
// 网页端:新窗口打开直链下载
|
||||
window.open(TUTORIAL_DOWNLOAD_URL, '_blank', 'noopener')
|
||||
window.open(tutorialUrl.value, '_blank', 'noopener')
|
||||
}
|
||||
|
||||
function onCatChange(key: 'front' | 'ops' | 'logi') {
|
||||
@@ -246,6 +253,8 @@ function parseHashGroup() {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 教程包直链异步解析(不阻塞页面渲染;失败自动回退固定直链)
|
||||
void refreshTutorialUrl()
|
||||
try {
|
||||
allowedKeys.value = await getCurrentUserAppColumnKeys()
|
||||
} catch (_error) {
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
/**
|
||||
* 工具台首页「立即下载教程」:数富AI 教学客户端压缩包公开直链。
|
||||
* 工具台首页「立即下载教程」:优先取后台「教程管理」最新上传的包(GET /api/tutorial/latest,
|
||||
* 见 shared/api/tutorial.ts);接口异常或无记录时回退到历史固定直链。
|
||||
*
|
||||
* 对象位于 MinIO 公开 client 桶(host A minio 容器,openresty 反代 oss.aishufu.top),
|
||||
* 由 scripts/upload_tutorial_zip.py 上传(同 key 可覆盖更新)。
|
||||
* 固定直链对象位于 MinIO 公开 client 桶(host A minio 容器,openresty 反代 oss.aishufu.top),
|
||||
* 由 scripts/upload_tutorial_zip.py 上传(同 key 覆盖更新),保留作为兜底。
|
||||
*/
|
||||
import { fetchLatestTutorialPackage } from '@/shared/api/tutorial'
|
||||
|
||||
export const TUTORIAL_DOWNLOAD_URL = 'https://oss.aishufu.top/client/tutorial/数富AI-教学客户端.zip'
|
||||
|
||||
/** 解析当前生效的教程包直链:最新上传优先;接口异常/无记录时静默回退固定直链,保证按钮始终可下载。 */
|
||||
export async function resolveTutorialDownloadUrl(): Promise<string> {
|
||||
try {
|
||||
const latest = await fetchLatestTutorialPackage()
|
||||
return latest?.fileUrl || TUTORIAL_DOWNLOAD_URL
|
||||
} catch {
|
||||
return TUTORIAL_DOWNLOAD_URL
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,6 +262,9 @@ export const API_ENDPOINTS = {
|
||||
delete: '/api/digital-human/versions/{version}',
|
||||
downloadUrl: '/api/digital-human/versions/{version}/download-url',
|
||||
},
|
||||
tutorial: {
|
||||
latest: '/api/tutorial/latest',
|
||||
},
|
||||
} as const
|
||||
|
||||
export type ApiEndpointGroup = {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './types/modules/tutorial'
|
||||
@@ -0,0 +1,31 @@
|
||||
import { get, type JavaApiResponse, unwrapJavaResponse } from '../../http.ts'
|
||||
import { buildJavaUrl } from '../../url.ts'
|
||||
import { API_ENDPOINTS } from '../../endpoints.ts'
|
||||
|
||||
/** 工具台首页「立即下载教程」的最新教程包(后台「教程管理」上传,以最新上传为主)。 */
|
||||
export interface LatestTutorialPackage {
|
||||
fileName: string
|
||||
fileUrl: string
|
||||
fileSize: number
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
/** 读取最新教程包;后端无记录时返回 null(调用方回退到历史固定直链)。 */
|
||||
export async function fetchLatestTutorialPackage(): Promise<LatestTutorialPackage | null> {
|
||||
const payload = await unwrapJavaResponse(
|
||||
get<JavaApiResponse<Record<string, unknown>>>(buildJavaUrl(API_ENDPOINTS.tutorial.latest)),
|
||||
)
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return null
|
||||
}
|
||||
const fileUrl = typeof payload.file_url === 'string' ? payload.file_url.trim() : ''
|
||||
if (!fileUrl) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
fileName: typeof payload.file_name === 'string' ? payload.file_name.trim() : '',
|
||||
fileUrl,
|
||||
fileSize: typeof payload.file_size === 'number' ? payload.file_size : 0,
|
||||
createdAt: typeof payload.created_at === 'string' ? payload.created_at : '',
|
||||
}
|
||||
}
|
||||
@@ -369,6 +369,9 @@ test('test_endpoints_frozen_snapshot', () => {
|
||||
"setLatest": "/api/digital-human/versions/{version}/set-latest",
|
||||
"delete": "/api/digital-human/versions/{version}",
|
||||
"downloadUrl": "/api/digital-human/versions/{version}/download-url"
|
||||
},
|
||||
"tutorial": {
|
||||
"latest": "/api/tutorial/latest"
|
||||
}
|
||||
}, '常量表快照不应漂移')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user