feat(任务判死): 心跳正常但 180 分钟无结果上报的二次判死线(13 模块)+ 同期待发改动
判死线(治 28131 型「主线程卡死、心跳线程照发」): - 判据改看 biz_task_scope_state.last_chunk_at(HTTP 心跳不刷新它);从未上报跳过不判 - 中央线覆盖 DELETE_BRAND/PRODUCT_RISK_RESOLVE/PRICE_TRACK/SHOP_MATCH/PATROL_DELETE/QUERY_ASIN/WITHDRAW - 自带线接入 COLLECT_DATA/SIMILAR_ASIN/APPEARANCE_PATENT/SHOP_DATA_CRAWL/PUBLISH/BRAND - 客户端心跳带处理位置 progressText,判死文案含最后位置;no-result-upload-timeout-minutes 默认 180(0 关闭) 同期带上另一工作流的待发改动:跟价换 IP 重试、品牌检测重试上限与 LLM 并发下调、 教程包后台管理页与 V126 迁移、admin-vue 教程记录页。
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import { expect, test, type Page } from '@playwright/test'
|
||||
|
||||
// 教程管理页验收(module 记录与版本):列表新增「版本号」列 + 上传时间列,
|
||||
// 默认按上传时间降序;版本号/上传时间表头可点击切换升降序;空版本历史行沉底。
|
||||
// 依赖 scripts/mock-admin-server.mjs 的教程包夹具(4 条,其中 1 条无版本号、1 条 09-01 历史行)。
|
||||
|
||||
async function openTutorial(page: Page) {
|
||||
await page.goto('/admin-vue/records/tutorial')
|
||||
await expect(page.locator('.admin-topbar h1')).toHaveText('教程管理')
|
||||
await expect(page.locator('.panel-box tbody tr').first()).toBeVisible()
|
||||
}
|
||||
|
||||
const versionTexts = (page: Page) =>
|
||||
page.locator('.panel-box tbody tr td:nth-child(3)').allTextContents()
|
||||
|
||||
const fileTexts = (page: Page) =>
|
||||
page.locator('.panel-box tbody tr td:nth-child(2) .file-name').allTextContents()
|
||||
|
||||
test('test_tutorial_list_default_desc_by_upload_time', async ({ page }) => {
|
||||
await openTutorial(page)
|
||||
|
||||
// 表头:版本号、上传时间(均带排序标记)
|
||||
await expect(page.locator('.sort-version')).toHaveText(/版本号/)
|
||||
await expect(page.locator('.sort-time')).toHaveText(/上传时间/)
|
||||
// 默认排序状态:上传时间降序(▼),版本号未激活(▲▼)
|
||||
await expect(page.locator('.sort-time .sort-mark')).toHaveText('▼')
|
||||
await expect(page.locator('.sort-version .sort-mark')).toHaveText('▲▼')
|
||||
|
||||
// 默认按上传时间降序:09-14 → 09-12 → 09-10 → 09-01;空版本行沉底
|
||||
expect(await versionTexts(page)).toEqual(['v3.10.0', 'v3.2.0', 'v3.1.0', '—'])
|
||||
expect(await fileTexts(page)).toEqual([
|
||||
'数富AI-教学客户端-v4.zip',
|
||||
'数富AI-教学客户端-v3.zip',
|
||||
'数富AI-教学客户端-v2.zip',
|
||||
'数富AI-教学客户端.zip',
|
||||
])
|
||||
// 当前生效 = 最新上传(夹具里 09-14 那条),不随列表排序变化
|
||||
await expect(page.locator('.tag-active')).toHaveCount(1)
|
||||
await expect(page.locator('.tag-active').locator('xpath=..')).toHaveText(/数富AI-教学客户端-v4\.zip/)
|
||||
})
|
||||
|
||||
test('test_tutorial_sort_toggle_by_version_and_time', async ({ page }) => {
|
||||
await openTutorial(page)
|
||||
|
||||
// 点「版本号」:首次为降序(v3.10.0 按自然序大于 v3.2.0),空版本行仍在末尾
|
||||
await page.locator('.sort-version').click()
|
||||
await expect(page.locator('.sort-version .sort-mark')).toHaveText('▼')
|
||||
expect(await versionTexts(page)).toEqual(['v3.10.0', 'v3.2.0', 'v3.1.0', '—'])
|
||||
|
||||
// 再点一次切升序
|
||||
await page.locator('.sort-version').click()
|
||||
await expect(page.locator('.sort-version .sort-mark')).toHaveText('▲')
|
||||
expect(await versionTexts(page)).toEqual(['v3.1.0', 'v3.2.0', 'v3.10.0', '—'])
|
||||
|
||||
// 点回「上传时间」:切换字段时回到降序默认
|
||||
await page.locator('.sort-time').click()
|
||||
await expect(page.locator('.sort-time .sort-mark')).toHaveText('▼')
|
||||
await expect(page.locator('.sort-version .sort-mark')).toHaveText('▲▼')
|
||||
expect(await fileTexts(page)).toEqual([
|
||||
'数富AI-教学客户端-v4.zip',
|
||||
'数富AI-教学客户端-v3.zip',
|
||||
'数富AI-教学客户端-v2.zip',
|
||||
'数富AI-教学客户端.zip',
|
||||
])
|
||||
})
|
||||
|
||||
test('test_tutorial_upload_dialog_has_version_field', async ({ page }) => {
|
||||
await openTutorial(page)
|
||||
|
||||
await page.getByRole('button', { name: '上传教程包' }).click()
|
||||
const dialog = page.locator('.el-dialog')
|
||||
await expect(dialog).toBeVisible()
|
||||
await expect(dialog.locator('.el-form-item').first()).toContainText('版本号')
|
||||
await expect(dialog.locator('input').first()).toHaveAttribute('maxlength', '64')
|
||||
|
||||
// 不选文件直接提交:提示仍以 zip 为必填(版本号可空)
|
||||
await dialog.getByRole('button', { name: '上传教程包' }).click()
|
||||
await expect(dialog.locator('.el-alert')).toContainText('请选择 zip 压缩包')
|
||||
})
|
||||
@@ -142,6 +142,15 @@ function json(res, payload, status = 200) {
|
||||
res.end(body)
|
||||
}
|
||||
|
||||
/* ===================== 教程包夹具(教程管理页:版本号列 + 上传时间排序验收用) ===================== */
|
||||
// 故意乱序给出,且含一条无版本号的历史行:页面默认应按上传时间降序、空版本行沉底。
|
||||
const TUTORIAL_PACKAGES = [
|
||||
{ id: 3, file_name: '数富AI-教学客户端-v3.zip', version: 'v3.2.0', object_key: 'tutorial/20260912090000-数富AI-教学客户端-v3.zip', file_size: 1048576, file_url: 'https://oss.aishufu.top/client/tutorial/t3.zip', created_at: '2026-09-12 09:00' },
|
||||
{ id: 1, file_name: '数富AI-教学客户端.zip', version: '', object_key: 'tutorial/数富AI-教学客户端.zip', file_size: 0, file_url: 'https://oss.aishufu.top/client/tutorial/legacy.zip', created_at: '2026-09-01 08:00' },
|
||||
{ id: 4, file_name: '数富AI-教学客户端-v4.zip', version: 'v3.10.0', object_key: 'tutorial/20260914103000-数富AI-教学客户端-v4.zip', file_size: 2097152, file_url: 'https://oss.aishufu.top/client/tutorial/t4.zip', created_at: '2026-09-14 10:30' },
|
||||
{ id: 2, file_name: '数富AI-教学客户端-v2.zip', version: 'v3.1.0', object_key: 'tutorial/20260910120000-数富AI-教学客户端-v2.zip', file_size: 524288, file_url: 'https://oss.aishufu.top/client/tutorial/t2.zip', created_at: '2026-09-10 12:00' },
|
||||
]
|
||||
|
||||
/* ===================== 站内通知夹具(铃铛面板:搜索/按天分组/分页验收用) ===================== */
|
||||
const NOTIFICATIONS = [
|
||||
...[6, 5, 4, 3, 2, 1].map((seq) => notif(seq, '2026-09-13', 22, seq, seq <= 3)),
|
||||
@@ -243,6 +252,11 @@ const server = createServer((req, res) => {
|
||||
if (url === '/api/admin/notifications') {
|
||||
return json(res, notificationPage(new URL(req.url || '/', 'http://127.0.0.1').searchParams))
|
||||
}
|
||||
if (url === '/api/admin/tutorials') {
|
||||
// 与 Java 侧同契约:created_at 降序返回(页面"当前生效"取第一条)。
|
||||
const items = [...TUTORIAL_PACKAGES].sort((a, b) => (a.created_at < b.created_at ? 1 : -1))
|
||||
return json(res, { success: true, data: { items } })
|
||||
}
|
||||
if (url.startsWith('/api/')) {
|
||||
return json(res, { success: true, data: { items: [], total: 0 } })
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import { formatDateTime } from '@/utils/datetime'
|
||||
/** 教程管理页:工具台首页「立即下载教程」的包体管理(上传新包 / 列表 / 下载 / 删除)。
|
||||
* 上传走浏览器直传 MinIO(presign → PUT 带进度 → confirm 落库),与软件版本管理页同一套链路;
|
||||
* 工具台始终下载"最新上传"的包(列表第一条即当前生效)。 */
|
||||
* 上传时可填版本号(仅展示与排序用);工具台始终下载"最新上传"的包(不随列表排序变化)。
|
||||
* 列表默认按上传时间降序,「版本号」「上传时间」表头可点击切换升降序。 */
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import OldPagination from '@/components/OldPagination.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
@@ -22,12 +23,64 @@ const filteredItems = computed(() => {
|
||||
/** 全站分页统一:客户端分页(10/20/50/100)。 */
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const pagedItems = computed(() => filteredItems.value.slice((page.value - 1) * pageSize.value, page.value * pageSize.value))
|
||||
|
||||
/** 排序:默认按上传时间降序(最新上传在最前,与后端返回顺序一致);点表头切换升降序。 */
|
||||
type SortKey = 'version' | 'createdAt'
|
||||
const sortKey = ref<SortKey>('createdAt')
|
||||
const sortAsc = ref(false)
|
||||
|
||||
function toggleSort(key: SortKey) {
|
||||
if (sortKey.value === key) {
|
||||
sortAsc.value = !sortAsc.value
|
||||
} else {
|
||||
sortKey.value = key
|
||||
sortAsc.value = false
|
||||
}
|
||||
page.value = 1
|
||||
}
|
||||
|
||||
/** 排序标记:未激活 ▲▼,激活时只留方向箭头(比 ⇅/↕ 字形支持好,避免 Windows 缺字形显示方框)。 */
|
||||
function sortMark(key: SortKey): string {
|
||||
if (sortKey.value !== key) return '▲▼'
|
||||
return sortAsc.value ? '▲' : '▼'
|
||||
}
|
||||
|
||||
function compareText(a: string, b: string): number {
|
||||
return a.localeCompare(b, 'zh-Hans-CN', { numeric: true, sensitivity: 'base' })
|
||||
}
|
||||
|
||||
const sortedItems = computed(() => {
|
||||
const rows = [...filteredItems.value]
|
||||
rows.sort((a, b) => {
|
||||
// 版本号为空的历史行始终排在末尾,避免切换排序时"无版本"占满首页。
|
||||
if (sortKey.value === 'version') {
|
||||
const av = a.version
|
||||
const bv = b.version
|
||||
if (!av || !bv) {
|
||||
if (!av && !bv) return 0
|
||||
return av ? -1 : 1
|
||||
}
|
||||
const diff = compareText(av, bv)
|
||||
return sortAsc.value ? diff : -diff
|
||||
}
|
||||
const at = a.createdAt
|
||||
const bt = b.createdAt
|
||||
if (!at || !bt) {
|
||||
if (!at && !bt) return 0
|
||||
return at ? -1 : 1
|
||||
}
|
||||
const diff = compareText(at, bt)
|
||||
return sortAsc.value ? diff : -diff
|
||||
})
|
||||
return rows
|
||||
})
|
||||
|
||||
const pagedItems = computed(() => sortedItems.value.slice((page.value - 1) * pageSize.value, page.value * pageSize.value))
|
||||
function changePage(p: number) { page.value = p }
|
||||
function changeSize(size: number) { pageSize.value = size; page.value = 1 }
|
||||
// 搜索导致数据收缩时回钳页码,避免停在空页。
|
||||
watch(filteredItems, () => {
|
||||
page.value = Math.min(page.value, Math.max(1, Math.ceil(filteredItems.value.length / pageSize.value)))
|
||||
// 搜索/排序导致数据收缩时回钳页码,避免停在空页。
|
||||
watch(sortedItems, () => {
|
||||
page.value = Math.min(page.value, Math.max(1, Math.ceil(sortedItems.value.length / pageSize.value)))
|
||||
})
|
||||
|
||||
/** 当前生效包 = 列表第一条(工具台下发的就是它)。 */
|
||||
@@ -80,6 +133,7 @@ async function removeOne(row: TutorialPackageItem) {
|
||||
const uploadVisible = ref(false)
|
||||
const uploading = ref(false)
|
||||
const uploadPercent = ref(0)
|
||||
const newVersion = ref('')
|
||||
const pickedFile = ref<File | null>(null)
|
||||
/** 弹窗内成功/失败文案留驻(对齐软件版本页交互)。 */
|
||||
const uploadMsg = ref('')
|
||||
@@ -103,6 +157,7 @@ function onFileChange(file: File) {
|
||||
function openUpload() {
|
||||
uploadMsg.value = ''
|
||||
uploadMsgOk.value = false
|
||||
newVersion.value = ''
|
||||
pickedFile.value = null
|
||||
uploadVisible.value = true
|
||||
}
|
||||
@@ -126,12 +181,13 @@ async function submitUpload() {
|
||||
uploading.value = true
|
||||
try {
|
||||
// 浏览器直传 MinIO:presign → PUT(进度条)→ confirm 落库。
|
||||
await uploadTutorialPackage(pickedFile.value, pickedFile.value.name, (p) => {
|
||||
await uploadTutorialPackage(pickedFile.value, pickedFile.value.name, newVersion.value.trim(), (p) => {
|
||||
uploadPercent.value = p
|
||||
uploadMsg.value = p >= 100 ? '上传完成,正在登记教程包...' : `正在上传:${p}%`
|
||||
})
|
||||
uploadMsg.value = '上传成功,工具台「立即下载教程」已切换为该包'
|
||||
uploadMsgOk.value = true
|
||||
newVersion.value = ''
|
||||
pickedFile.value = null
|
||||
uploadPercent.value = 0
|
||||
load()
|
||||
@@ -165,8 +221,13 @@ onMounted(load)
|
||||
<input type="checkbox" :checked="pagedItems.length > 0 && pagedItems.every((r) => selectedIds.has(r.id))" @change="toggleSelectAllPage" :disabled="!pagedItems.length" />
|
||||
</th>
|
||||
<th style="width: 260px">文件名</th>
|
||||
<th style="width: 110px">
|
||||
<button class="sort-th sort-version" type="button" @click="toggleSort('version')">版本号<span class="sort-mark">{{ sortMark('version') }}</span></button>
|
||||
</th>
|
||||
<th style="width: 110px">大小</th>
|
||||
<th style="width: 150px">上传时间</th>
|
||||
<th style="width: 150px">
|
||||
<button class="sort-th sort-time" type="button" @click="toggleSort('createdAt')">上传时间<span class="sort-mark">{{ sortMark('createdAt') }}</span></button>
|
||||
</th>
|
||||
<th>下载链接</th>
|
||||
<th style="width: 170px">操作</th>
|
||||
</tr>
|
||||
@@ -181,6 +242,10 @@ onMounted(load)
|
||||
<span class="file-name" :title="row.fileName">{{ row.fileName }}</span>
|
||||
<span v-if="row.id === activeId" class="tag-active">当前生效</span>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="row.version" class="version-cell" :title="row.version">{{ row.version }}</span>
|
||||
<span v-else class="dim">—</span>
|
||||
</td>
|
||||
<td>{{ formatFileSize(row.fileSize) }}</td>
|
||||
<td>{{ formatDateTime(row.createdAt) }}</td>
|
||||
<td>
|
||||
@@ -195,10 +260,10 @@ onMounted(load)
|
||||
</tr>
|
||||
</template>
|
||||
<tr v-else-if="loading">
|
||||
<td colspan="6" class="empty-tip">加载中...</td>
|
||||
<td colspan="7" class="empty-tip">加载中...</td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<td colspan="6" class="empty-tip">{{ keyword ? '暂无匹配教程包' : '暂无教程包记录' }}</td>
|
||||
<td colspan="7" class="empty-tip">{{ keyword ? '暂无匹配教程包' : '暂无教程包记录' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -209,6 +274,10 @@ onMounted(load)
|
||||
<el-dialog v-model="uploadVisible" title="上传教程包" width="520px">
|
||||
<p class="upload-desc">上传教程 ZIP 包后,工具台首页「立即下载教程」将以下载该包为准(以最新上传的为主)。</p>
|
||||
<el-form label-width="110px">
|
||||
<el-form-item label="版本号">
|
||||
<el-input v-model="newVersion" placeholder="例如:v2026.09 或留空" maxlength="64" />
|
||||
<p class="zip-hint">仅作展示与排序用,可留空;不影响工具台按最新上传下载</p>
|
||||
</el-form-item>
|
||||
<el-form-item label="ZIP 压缩包" required>
|
||||
<el-upload :auto-upload="false" :limit="1" accept=".zip" :on-change="(file: any) => onFileChange(file.raw)" :on-remove="() => (pickedFile = null)">
|
||||
<el-button>选择文件</el-button>
|
||||
@@ -368,6 +437,46 @@ h3 {
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
}
|
||||
/* 表头排序:按钮铺满单元格,仅 hover 时加深字色,保持旧版表头观感。 */
|
||||
.sort-th {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
letter-spacing: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.sort-th:hover {
|
||||
color: #2f5d8b;
|
||||
}
|
||||
.sort-mark {
|
||||
color: #8293a5;
|
||||
font-size: 12px;
|
||||
}
|
||||
.sort-th:hover .sort-mark {
|
||||
color: #5f85ad;
|
||||
}
|
||||
.sort-version {
|
||||
width: 110px;
|
||||
}
|
||||
.sort-time {
|
||||
width: 150px;
|
||||
}
|
||||
.version-cell {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.tag-active {
|
||||
display: inline-block;
|
||||
margin-left: 6px;
|
||||
|
||||
@@ -22,11 +22,12 @@ export interface TutorialUploadTarget {
|
||||
objectKey: string
|
||||
uploadUrl: string
|
||||
fileUrl: string
|
||||
version: string
|
||||
}
|
||||
|
||||
/** 第一步:向后端申请直传 PUT 预签名地址(登录会话签发,3 分钟有效)。 */
|
||||
export async function requestTutorialPresign(fileName: string): Promise<TutorialUploadTarget> {
|
||||
const { data } = await http.post<unknown>(TUTORIAL_PRESIGN_ENDPOINT, null, { params: { file_name: fileName } })
|
||||
export async function requestTutorialPresign(fileName: string, version = ''): Promise<TutorialUploadTarget> {
|
||||
const { data } = await http.post<unknown>(TUTORIAL_PRESIGN_ENDPOINT, null, { params: { file_name: fileName, version } })
|
||||
const core = (unwrap(data) ?? {}) as Record<string, unknown>
|
||||
const uploadUrl = typeof core.upload_url === 'string' ? core.upload_url : ''
|
||||
const objectKey = typeof core.object_key === 'string' ? core.object_key : ''
|
||||
@@ -37,13 +38,14 @@ export async function requestTutorialPresign(fileName: string): Promise<Tutorial
|
||||
objectKey,
|
||||
uploadUrl,
|
||||
fileUrl: typeof core.file_url === 'string' ? core.file_url : '',
|
||||
version: typeof core.version === 'string' ? core.version : version.trim(),
|
||||
}
|
||||
}
|
||||
|
||||
/** 第三步:直传完成后通知后端校验对象并写入记录(data.item 为新记录行)。 */
|
||||
export async function confirmTutorialPackage(objectKey: string, fileName: string): Promise<TutorialPackageItem | null> {
|
||||
export async function confirmTutorialPackage(objectKey: string, fileName: string, version = ''): Promise<TutorialPackageItem | null> {
|
||||
const { data } = await http.post<unknown>(TUTORIAL_CONFIRM_ENDPOINT, null, {
|
||||
params: { object_key: objectKey, file_name: fileName },
|
||||
params: { object_key: objectKey, file_name: fileName, version },
|
||||
})
|
||||
return parseTutorialPackageUpload(data)
|
||||
}
|
||||
@@ -63,9 +65,10 @@ export async function deleteTutorialPackages(ids: number[]): Promise<number> {
|
||||
export async function uploadTutorialPackage(
|
||||
file: Blob,
|
||||
fileName: string,
|
||||
version = '',
|
||||
onProgress?: (percent: number) => void,
|
||||
): Promise<TutorialPackageItem | null> {
|
||||
const target = await requestTutorialPresign(fileName)
|
||||
const target = await requestTutorialPresign(fileName, version)
|
||||
await directPut.put(target.uploadUrl, file, {
|
||||
headers: { 'Content-Type': 'application/octet-stream' },
|
||||
onUploadProgress: (event) => {
|
||||
@@ -74,5 +77,5 @@ export async function uploadTutorialPackage(
|
||||
onProgress(total > 0 ? Math.min(Math.round((event.loaded / total) * 100), 100) : 0)
|
||||
},
|
||||
})
|
||||
return confirmTutorialPackage(target.objectKey, fileName)
|
||||
return confirmTutorialPackage(target.objectKey, fileName, target.version)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
export interface TutorialPackageItem {
|
||||
id: number
|
||||
fileName: string
|
||||
/** 版本号(上传时填写,V126 之前的历史行为空串) */
|
||||
version: string
|
||||
objectKey: string
|
||||
fileSize: number
|
||||
fileUrl: string
|
||||
|
||||
@@ -19,6 +19,7 @@ export function toTutorialPackageItem(raw: unknown): TutorialPackageItem | null
|
||||
return {
|
||||
id,
|
||||
fileName: text(r.file_name ?? r.fileName),
|
||||
version: text(r.version),
|
||||
objectKey: text(r.object_key ?? r.objectKey),
|
||||
fileSize: numberOrNull(r.file_size ?? r.fileSize) ?? 0,
|
||||
fileUrl: text(r.file_url ?? r.fileUrl),
|
||||
|
||||
@@ -14,16 +14,23 @@ test('align_tutorial_page_registered', () => {
|
||||
|
||||
test('align_tutorial_page_wiring', () => {
|
||||
const page = readSource('src/pages/records/RecordsTutorialPage.vue')
|
||||
// 上传入口:选择 zip → 直传(进度条)→ 成功提示留驻弹窗。
|
||||
// 上传入口:版本号(可空)→ 选择 zip → 直传(进度条)→ 成功提示留驻弹窗。
|
||||
assert.match(page, /上传教程包/, '存在上传入口按钮')
|
||||
assert.match(page, /uploadTutorialPackage/, '上传走教程包直传链路')
|
||||
assert.match(page, /label="版本号"/, '上传弹窗提供版本号输入')
|
||||
assert.match(page, /newVersion\.value\.trim\(\)/, '版本号去空白后随上传提交')
|
||||
assert.match(page, /请选择 zip 压缩包/, '未选文件时提示')
|
||||
assert.match(page, /仅支持 \.zip 格式/, '格式校验提示')
|
||||
assert.match(page, /上传成功,工具台「立即下载教程」已切换为该包/, '成功提示说明生效位置')
|
||||
assert.match(page, /上传完成,正在登记教程包/, '进度文案')
|
||||
// 列表:当前生效标记 + 下载 + 删除 + 空态。
|
||||
// 列表:版本号列 + 上传时间列 + 当前生效标记 + 下载 + 删除 + 空态。
|
||||
assert.match(page, /当前生效/, '最新上传的包标记当前生效')
|
||||
assert.match(page, /formatFileSize/, '展示包体大小')
|
||||
assert.match(page, /row\.version/, '展示版本号列')
|
||||
assert.match(page, /toggleSort\('version'\)/, '版本号表头可点击排序')
|
||||
assert.match(page, /toggleSort\('createdAt'\)/, '上传时间表头可点击排序')
|
||||
assert.match(page, /const sortKey = ref<SortKey>\('createdAt'\)/, '默认排序字段为上传时间')
|
||||
assert.match(page, /const sortAsc = ref\(false\)/, '默认降序')
|
||||
assert.match(page, /下载/, '操作列提供下载')
|
||||
assert.match(page, /<a v-if="row\.fileUrl" class="btn btn-sm dl-btn"[^>]*download/, '下载链接带 download 强制下载')
|
||||
assert.match(page, /确认删除选中的/, '批量删除二次确认')
|
||||
@@ -37,7 +44,8 @@ test('align_tutorial_api_contract', () => {
|
||||
assert.match(api, /\$\{TUTORIAL_UPLOAD_ENDPOINT\}\/presign/, '直传预签名端点')
|
||||
assert.match(api, /\$\{TUTORIAL_UPLOAD_ENDPOINT\}\/confirm/, '确认端点')
|
||||
assert.match(api, /\$\{TUTORIAL_UPLOAD_ENDPOINT\}\/delete/, '删除端点')
|
||||
assert.match(api, /params: \{ object_key: objectKey, file_name: fileName \}/, '确认回传对象 key 与文件名')
|
||||
assert.match(api, /params: \{ file_name: fileName, version \}/, '预签名回传文件名与版本号')
|
||||
assert.match(api, /params: \{ object_key: objectKey, file_name: fileName, version \}/, '确认回传对象 key、文件名与版本号')
|
||||
assert.match(api, /withCredentials: false/, '直传实例不挂会话拦截器')
|
||||
})
|
||||
|
||||
@@ -46,4 +54,5 @@ test('align_tutorial_model_parsers', () => {
|
||||
assert.match(model, /r\.file_name \?\? r\.fileName/, '兼容 snake/camel 文件名')
|
||||
assert.match(model, /r\.file_url \?\? r\.fileUrl/, '兼容 snake/camel 下载链接')
|
||||
assert.match(model, /r\.file_size \?\? r\.fileSize/, '兼容 snake/camel 文件大小')
|
||||
assert.match(model, /version: text\(r\.version\)/, '解析版本号(缺失为空串,兼容历史行)')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user