mirror of
https://github.com/insistence/RuoYi-Vue3-FastAPI.git
synced 2026-10-15 05:19:14 +00:00
* feat: 支持 v2 插件、原生扩展与签名发布 * fix: 修复测试 * feat: 增强宿主 SDK、运行观测与制品维护能力 * feat: 增强文件传输、配置观测与本地开发能力 * feat: 支持 SSE 实时事件流并完善开发脚手架 * feat: 完善长连接鉴权复核与关闭回收 * feat: 增强生命周期与任务关闭可靠性 * feat: 收紧健康检查契约并补齐前端与真实网络验收 * feat: 增加SDK溯源与离线更新工具并验证跨版本兼容 * feat: 增加持久化业务示例与受控 Python 交付 * feat: 增加统一运行诊断与多worker状态观测 * feat: 增加固定目标发布等待与自动验收 * fix: 修复多环境插件部署前缀与代理配置不一致 * fix: 修复测试
156 lines
5.5 KiB
JavaScript
156 lines
5.5 KiB
JavaScript
const seedTitles = [
|
||
'确认本周交付范围',
|
||
'整理需求评审记录',
|
||
'补充接口使用示例',
|
||
'核对页面文案',
|
||
'准备演示环境',
|
||
'验证异常处理流程',
|
||
'完善操作说明',
|
||
'检查移动端布局',
|
||
'同步项目里程碑',
|
||
'回顾已完成事项',
|
||
'确认验收清单',
|
||
'整理下一阶段待办',
|
||
]
|
||
|
||
function seedTasks() {
|
||
return seedTitles.map((title, index) => ({
|
||
id: (index + 1).toString(16).padStart(32, '0'),
|
||
title,
|
||
description: index % 3 === 0 ? '与团队核对完成标准,并记录需要继续跟进的事项。' : '',
|
||
status: index % 4 === 0 ? 'done' : 'todo',
|
||
priority: index % 3 === 2 ? 'high' : 'normal',
|
||
createdAt: new Date(Date.UTC(2026, 9, 1, 2, index * 10)).toISOString(),
|
||
updatedAt: new Date(Date.UTC(2026, 9, 2, 2, index * 10)).toISOString(),
|
||
}))
|
||
}
|
||
|
||
function validateTask(data) {
|
||
const title = typeof data?.title === 'string' ? data.title.trim() : ''
|
||
if (!title || title.length > 120) throw new Error('任务标题需为 1–120 个字符')
|
||
if (typeof data?.description !== 'string' || data.description.length > 500)
|
||
throw new Error('任务说明不能超过 500 个字符')
|
||
if (/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/.test(title + data.description))
|
||
throw new Error('文本不能包含不可显示的控制字符')
|
||
if (!['todo', 'done'].includes(data.status)) throw new Error('任务状态无效')
|
||
if (!['normal', 'high'].includes(data.priority)) throw new Error('任务优先级无效')
|
||
return {
|
||
title,
|
||
description: data.description,
|
||
status: data.status,
|
||
priority: data.priority,
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 本地内存 API:使用与生产接口一致的分页、筛选、详情和写入契约。
|
||
* 只供 dev.html 使用;重载整个页面会重置数据,不请求真实服务、不保存凭据。
|
||
*/
|
||
export function createMockTaskApi({
|
||
getDelay = () => 300,
|
||
shouldFail = () => false,
|
||
getCanWrite = () => true,
|
||
} = {}) {
|
||
let tasks = seedTasks()
|
||
let nextId = tasks.length + 1
|
||
const pause = (signal) =>
|
||
new Promise((resolve, reject) => {
|
||
if (signal?.aborted) return reject(new Error('请求已取消'))
|
||
const cancel = () => {
|
||
clearTimeout(timer)
|
||
reject(new Error('请求已取消'))
|
||
}
|
||
const timer = setTimeout(
|
||
() => {
|
||
signal?.removeEventListener('abort', cancel)
|
||
resolve()
|
||
},
|
||
Math.max(0, Math.min(5000, Number(getDelay()) || 0))
|
||
)
|
||
signal?.addEventListener('abort', cancel, { once: true })
|
||
})
|
||
|
||
async function request(config) {
|
||
const prefix = '/apps/task_demo/api/'
|
||
if (!config.url?.startsWith(prefix)) throw new Error('模拟宿主只处理当前插件 API')
|
||
const path = config.url.slice(prefix.length)
|
||
const method = String(config.method || 'get').toUpperCase()
|
||
await pause(config.signal)
|
||
if (config.signal?.aborted) throw new Error('请求已取消')
|
||
if (shouldFail()) throw new Error('模拟请求失败,请关闭“模拟请求失败”后重试。')
|
||
const canWrite = getCanWrite() === true
|
||
if (method === 'GET' && path === 'info')
|
||
return { pluginId: 'task_demo', version: '1.1.0', canWrite }
|
||
if (method === 'GET' && path === 'tasks') {
|
||
const page = Number(config.params?.page ?? 1)
|
||
const pageSize = Number(config.params?.pageSize ?? 10)
|
||
const status = config.params?.status || ''
|
||
if (
|
||
!Number.isInteger(page) ||
|
||
page < 1 ||
|
||
page > 1000000 ||
|
||
!Number.isInteger(pageSize) ||
|
||
pageSize < 1 ||
|
||
pageSize > 20
|
||
)
|
||
throw new Error('分页参数无效')
|
||
if (status && !['todo', 'done'].includes(status)) throw new Error('筛选状态无效')
|
||
const filtered = tasks
|
||
.filter((task) => !status || task.status === status)
|
||
.sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.id.localeCompare(a.id))
|
||
return {
|
||
items: structuredClone(filtered.slice((page - 1) * pageSize, page * pageSize)),
|
||
total: filtered.length,
|
||
page,
|
||
pageSize,
|
||
canWrite,
|
||
}
|
||
}
|
||
const match = /^tasks\/([0-9a-f]{32})$/.exec(path)
|
||
const index = match ? tasks.findIndex((task) => task.id === match[1]) : -1
|
||
if (method === 'GET' && match) {
|
||
if (index === -1) throw new Error('任务不存在或已被删除')
|
||
return structuredClone(tasks[index])
|
||
}
|
||
if (!['POST', 'PUT', 'DELETE'].includes(method)) throw new Error('模拟接口未实现')
|
||
// UI 隐藏按钮之外,再次在请求处理层检查写权限。
|
||
if (!canWrite) throw new Error('当前会话没有写入权限')
|
||
if (method === 'POST' && path === 'tasks') {
|
||
const data = validateTask(config.data)
|
||
const now = new Date().toISOString()
|
||
const task = {
|
||
id: (nextId++).toString(16).padStart(32, '0'),
|
||
...data,
|
||
createdAt: now,
|
||
updatedAt: now,
|
||
}
|
||
tasks.push(task)
|
||
return structuredClone(task)
|
||
}
|
||
if (match && (method === 'PUT' || method === 'DELETE')) {
|
||
if (index === -1) throw new Error('任务不存在或已被删除')
|
||
if (method === 'DELETE') {
|
||
tasks.splice(index, 1)
|
||
return { deleted: true }
|
||
}
|
||
tasks[index] = {
|
||
...tasks[index],
|
||
...validateTask(config.data),
|
||
updatedAt: new Date().toISOString(),
|
||
}
|
||
return structuredClone(tasks[index])
|
||
}
|
||
throw new Error('模拟接口未实现')
|
||
}
|
||
return {
|
||
request,
|
||
reset() {
|
||
tasks = seedTasks()
|
||
nextId = tasks.length + 1
|
||
},
|
||
clear() {
|
||
tasks = []
|
||
},
|
||
}
|
||
}
|