Playwright测试数据模拟:Mock Service Worker使用指南

news/2026/1/18 12:29:24/文章来源:https://www.cnblogs.com/hogwarts/p/19498212

关注 霍格沃兹测试学院公众号,回复「资料」, 领取人工智能测试开发技术合集

  1. 那个让我加班到凌晨两点的测试场景
    去年我们团队接到一个紧急需求:测试一个预约挂号系统。一切都挺顺利,直到遇到这个场景——“当号源被抢光时,显示候补排队功能”。问题来了:我们怎么在自动化测试里模拟“号源瞬间被抢光”的状态?

最初我们尝试了各种歪门邪道:手动修改数据库、写脚本清空号源、甚至想用两个测试账号同时操作……直到周五晚上11点,第6次尝试失败后,我盯着控制台里那些真实的HTTP请求,突然意识到:我们一直在解决错误的问题。

真正的解决方案不是去操纵真实系统状态,而是拦截请求,直接返回我们想要的响应。这就是Mock Service Worker(MSW)进入我们技术栈的开始。

  1. 为什么传统的Mock方法让我们痛苦不堪?
    先看看我们曾经尝试过的几种方案:

方案A:直接修改业务代码

// ❌ 测试代码侵入业务逻辑
if (process.env.NODE_ENV === 'test') {
mockData = require('./test-data.json');
return res.json(mockData);
}
// 生产环境代码...
方案B:在测试中覆写fetch

// ❌ 混乱不堪,难以维护
beforeEach(() => {
window.fetch = jest.fn().mockImplementation(() => {
return Promise.resolve({
json: () => Promise.resolve({ tickets: 0 })
});
});
});
方案C:搭建一个假的测试服务器

❌ 开发、维护成本太高

$ npm run start-mock-server
$ npm run start-test-server
$ npm run start-dev-server

到底该启动哪个?!

这些方案要么污染生产代码,要么难以维护,要么需要复杂的本地环境。直到我们发现MSW,才真正解决了这些问题。

  1. MSW的核心优势:像真实服务器一样工作
    Mock Service Worker(MSW)是一个基于Service Worker的API mocking库。它的工作原理很巧妙:

在浏览器中注册Service Worker,拦截所有网络请求
匹配请求模式,决定是否要拦截
返回模拟的响应,而不是发送到真实服务器
// 这是MSW的基本工作原理示意图
// [你的应用] --> [fetch('/api/tickets')]
// ↓
// [Service Worker 拦截]
// ↓
// [匹配路由 handlers]
// ↓
// [返回模拟响应 {tickets: 0}]
// ↓
// [应用收到响应]
关键优势在于:你的应用完全不知道自己在被mock。它发送真实的HTTP请求,收到真实的HTTP响应,只是中间的过程被我们“偷梁换柱”了。

  1. 一步步搭建Playwright + MSW环境
    4.1 安装必要的包

安装MSW核心库

npm install msw --save-dev

Playwright测试工具

npm install @playwright/test --save-dev

类型定义(TypeScript项目需要)

npm install @types/msw --save-dev
4.2 创建模拟处理器
mocks/handlers.js

// 模拟处理器 - 定义各种API的mock响应
import { http, HttpResponse } from'msw';

exportconst handlers = [
// 1. 模拟获取号源列表
http.get('/api/tickets', ({ request }) => {
const url = new URL(request.url);
const date = url.searchParams.get('date');
const department = url.searchParams.get('department');

console.log(`[MSW] 拦截请求: /api/tickets?date=${date}&department=${department}`);// 根据日期和科室返回不同数据
if (date === '2024-06-15' && department === 'cardiovascular') {// 模拟心内科号源已抢光return HttpResponse.json({success: true,data: {available: false,tickets: 0,waitingCount: 42,nextAvailableDate: '2024-06-20'},message: '号源已满,可加入候补'});
}// 默认返回有号源的情况
return HttpResponse.json({success: true,data: {available: true,tickets: 12,waitingCount: 0,nextAvailableDate: null}
});

}),

// 2. 模拟提交预约
http.post('/api/appointments', async ({ request }) => {
const body = await request.json();
console.log('[MSW] 创建预约:', body);

// 模拟10%的失败率,测试异常流程
const shouldFail = Math.random() < 0.1;if (shouldFail) {return HttpResponse.json({success: false,error: 'SYSTEM_BUSY',message: '系统繁忙,请稍后重试'},{ status: 503 });
}// 成功响应
return HttpResponse.json({success: true,data: {appointmentId: `APT${Date.now()}`,status: 'PENDING',queuePosition: body.waitList ? 15 : null,estimatedTime: body.waitList ? '2-3工作日' : '立即确认'}
});

}),

// 3. 模拟取消预约
http.delete('/api/appointments/:id', ({ params }) => {
const { id } = params;

// 模拟特定的预约ID不能取消
if (id === 'APT_NO_CANCEL') {return HttpResponse.json({success: false,error: 'CANCELLATION_NOT_ALLOWED',message: '该预约已过取消截止时间'},{ status: 400 });
}return HttpResponse.json({success: true,message: '预约已取消'
});

}),

// 4. 模拟GraphQL请求(如果项目使用)
http.post('/graphql', async ({ request }) => {
const { query, variables } = await request.json();

if (query.includes('GetPatientInfo')) {return HttpResponse.json({data: {patient: {id: variables.id,name: '测试用户',idCard: '110101199001011234',phone: '13800138000'}}});
}return HttpResponse.json({ data: {} });

}),

// 5. 模拟文件上传
http.post('/api/upload', async () => {
// 模拟上传进度
awaitnewPromise(resolve => setTimeout(resolve, 500));

return HttpResponse.json({success: true,url: 'https://mock-cdn.com/uploads/test-image.jpg',size: 204800,filename: 'test-upload.jpg'
});

})
];
4.3 配置Service Worker
mocks/browser.js

// 浏览器环境使用的MSW设置
import { setupWorker } from'msw/browser';
import { handlers } from'./handlers';

// 创建worker实例
exportconst worker = setupWorker(...handlers);

// 开发工具:在控制台暴露一些工具函数
if (typeofwindow !== 'undefined') {
window.__MSW = {
// 动态修改mock响应
overrideHandler: (method, path, newResponse) => {
// 这里可以实现动态修改handlers的逻辑
console.log([MSW Debug] 覆盖 ${method} ${path});
},

// 查看当前拦截的请求
getRequestLog: () => {returnwindow.__mswRequests || [];
},// 模拟网络错误
simulateNetworkError: (shouldFail = true) => {window.__mswNetworkError = shouldFail;
}

};
}
4.4 为Playwright创建专用配置
tests/msw-setup.js

// Playwright专用的MSW配置
import { createServer } from'http';
import { setupServer } from'msw/node';
import { handlers } from'../mocks/handlers';

// 创建Node.js环境下的mock server
exportconst server = setupServer(...handlers);

// 扩展handlers,添加一些测试专用的mock
exportconst testHandlers = {
// 强制让某个接口失败
forceFail: (method, url) => {
server.use(
http[method.toLowerCase()](url, () => {
returnnew Response(null, { status: 500 });
})
);
},

// 延迟响应,测试loading状态
delayResponse: (method, url, delayMs) => {
server.use(
http[method.toLowerCase()](url, async () => {
awaitnewPromise(resolve => setTimeout(resolve, delayMs));
return HttpResponse.json({ delayed: true });
})
);
},

// 验证请求参数
captureRequests: (method, url) => {
const requests = [];
server.use(
http[method.toLowerCase()](url, async ({ request }) => {
const body = await request.text();
requests.push({
url: request.url,
method: request.method,
body: body ? JSON.parse(body) : null,
headers: Object.fromEntries(request.headers.entries()),
timestamp: newDate().toISOString()
});
return HttpResponse.json({ captured: true });
})
);
return requests;
}
};

// 启动和停止server的实用函数
exportconst startMSW = async (page) => {
// 在页面中注入Service Worker
await page.addInitScript(() => {
// 这里可以注入一些全局的mock配置
window.__TEST_MODE = true;
window.__MOCK_API = true;
});

// 启动mock server
server.listen({
onUnhandledRequest: (request) => {
// 对于未处理的请求,根据情况决定是否报错
const url = request.url.toString();

  // 忽略静态资源请求if (url.includes('.css') || url.includes('.js') || url.includes('.ico')) {return;}// 忽略某些特定的API(如果有的话)if (url.includes('/api/health-check')) {return;}// 其他未处理的请求打印警告console.warn(`[MSW] 未处理的请求: ${request.method} ${url}`);
}

});
};

exportconst stopMSW = () => {
server.close();
};
Playwright mcp技术学习交流群
伙伴们,对AI测试、大模型评测、质量保障感兴趣吗?我们建了一个 「Playwright mcp技术学习交流群」,专门用来探讨相关技术、分享资料、互通有无。无论你是正在实践还是好奇探索,都欢迎扫码加入,一起抱团成长!期待与你交流!👇

image

  1. 在Playwright测试中使用MSW
    5.1 基础测试示例
    tests/appointment.spec.js

import { test, expect } from'@playwright/test';
import { startMSW, stopMSW, testHandlers } from'./msw-setup';

// 在每个测试文件开始时启动MSW
test.beforeAll(async () => {
// 这里可以初始化一些全局的mock数据
console.log('[Test Setup] 启动MSW Mock Server');
});

// 在每个测试用例前设置
test.beforeEach(async ({ page }) => {
// 启动MSW
await startMSW(page);

// 跳转到测试页面
await page.goto('/appointment');

// 等待必要的元素加载
await page.waitForSelector('[data-testid="appointment-container"]');
});

// 测试用例1:正常预约流程
test('用户成功预约挂号', async ({ page }) => {
// 页面已经加载了默认的mock数据(有号源)

// 1. 选择日期
await page.click('[data-testid="date-2024-06-10"]');

// 2. 选择科室
await page.selectOption('[data-testid="department-select"]', 'internal');

// 3. 验证号源显示正确
const ticketCount = await page.textContent('[data-testid="ticket-count"]');
expect(parseInt(ticketCount)).toBeGreaterThan(0);

// 4. 选择医生
await page.click('[data-testid="doctor-1001"]');

// 5. 填写患者信息
await page.fill('[data-testid="patient-name"]', '张三');
await page.fill('[data-testid="patient-id"]', '110101199001011234');

// 6. 提交预约
await page.click('[data-testid="submit-appointment"]');

// 7. 验证成功提示
await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
await expect(page.locator('[data-testid="appointment-id"]')).toContainText('APT');
});

// 测试用例2:号源已抢光的情况
test('当号源被抢光时显示候补排队', async ({ page }) => {
// 动态修改mock:让心内科2024-06-15的号源为0
// 这里我们需要用另一种方式,因为MSW在Node环境下运行
// 我们可以通过query参数来触发特定的mock场景

// 1. 直接访问特定日期和科室的组合
await page.goto('/appointment?date=2024-06-15&department=cardiovascular');

// 2. 验证显示"号源已满"
await expect(page.locator('[data-testid="no-tickets-alert"]')).toBeVisible();

// 3. 验证候补排队按钮显示
await expect(page.locator('[data-testid="waitlist-button"]')).toBeVisible();

// 4. 点击加入候补
await page.click('[data-testid="waitlist-button"]');

// 5. 填写候补信息
await page.fill('[data-testid="waitlist-phone"]', '13800138000');
await page.click('[data-testid="confirm-waitlist"]');

// 6. 验证候补成功
await expect(page.locator('[data-testid="waitlist-success"]')).toBeVisible();
const position = await page.textContent('[data-testid="queue-position"]');
expect(position).toMatch(/第\d+位/);
});

// 测试用例3:网络异常处理
test('当API请求失败时显示错误信息', async ({ page }) => {
// 使用testHandlers强制让预约接口失败
// 注意:这里需要MSW支持动态修改handlers
// 简化方案:通过特定参数触发错误

await page.goto('/appointment?forceError=true');

// 尝试提交预约
await page.fill('[data-testid="patient-name"]', '李四');
await page.click('[data-testid="submit-appointment"]');

// 验证错误提示
await expect(page.locator('[data-testid="error-toast"]')).toBeVisible();
await expect(page.locator('[data-testid="error-message"]')).toContainText('系统繁忙');

// 验证重试按钮可用
await expect(page.locator('[data-testid="retry-button"]')).toBeEnabled();
});

// 测试用例4:取消预约的限制条件
test('处理不能取消的预约', async ({ page }) => {
// 查看一个特殊的预约(不能取消的)
await page.goto('/appointment/detail/APT_NO_CANCEL');

// 验证取消按钮不可用或有特殊提示
const cancelButton = page.locator('[data-testid="cancel-button"]');
await expect(cancelButton).toBeDisabled();

// 或者验证有提示信息
await expect(page.locator('[data-testid="cancel-notice"]')).toContainText('已过取消时间');
});

// 清理
test.afterEach(async () => {
// 重置MSW handlers,避免测试间相互影响
server.resetHandlers();
});

test.afterAll(() => {
stopMSW();
});
5.2 高级用法:动态Mock场景
tests/msw-dynamic.spec.js

import { test, expect } from'@playwright/test';
import { server } from'./msw-setup';
import { http, HttpResponse } from'msw';

// 动态修改mock响应的测试
test.describe('动态Mock场景', () => {
let capturedRequests = [];

test.beforeEach(async ({ page }) => {
// 清空之前捕获的请求
capturedRequests = [];

// 动态添加一个handler来捕获请求
server.use(http.post('/api/appointments', async ({ request }) => {const body = await request.json();capturedRequests.push({url: request.url,body,timestamp: newDate().toISOString()});// 根据不同的测试数据返回不同的响应if (body.patientName === '特殊用户') {return HttpResponse.json({success: true,special: true,priority: true});}return HttpResponse.json({ success: true });})
);await page.goto('/appointment');

});

test('验证请求参数是否正确发送', async ({ page }) => {
// 填写表单
await page.fill('[data-testid="patient-name"]', '测试用户');
await page.fill('[data-testid="symptoms"]', '头痛发热');

// 提交
await page.click('[data-testid="submit-appointment"]');// 验证捕获的请求
expect(capturedRequests).toHaveLength(1);
expect(capturedRequests[0].body).toMatchObject({patientName: '测试用户',symptoms: '头痛发热'
});

});

test('模拟慢速网络', async ({ page }) => {
// 添加一个延迟响应的handler
server.use(
http.get('/api/tickets', async () => {
awaitnewPromise(resolve => setTimeout(resolve, 2000)); // 2秒延迟

    return HttpResponse.json({success: true,data: { tickets: 5 }});})
);// 验证loading状态显示
await page.click('[data-testid="refresh-tickets"]');// 应该显示loading
await expect(page.locator('[data-testid="loading-spinner"]')).toBeVisible();// 2秒后loading应该消失
await expect(page.locator('[data-testid="loading-spinner"]')).not.toBeVisible({timeout: 3000
});

});
});
6. 实际项目中的最佳实践
6.1 目录结构建议
project/
├── mocks/
│ ├── handlers/ # 按功能分组的handlers
│ │ ├── appointment.js
│ │ ├── user.js
│ │ └── payment.js
│ ├── fixtures/ # mock数据文件
│ │ ├── users.json
│ │ └── tickets.json
│ ├── utils.js # 工具函数
│ └── browser.js # 浏览器配置
├── tests/
│ ├── msw-setup.js # Playwright MSW配置
│ ├── appointment.spec.js
│ └── user.spec.js
└── playwright.config.js
6.2 在团队中推广的经验
建立Mock数据契约:与后端团队约定API响应格式,确保mock数据与真实API一致

创建Mock数据生成器:

// mocks/factories/appointment.js
exportconst createMockAppointment = (overrides = {}) => ({
id: APT${Date.now()},
patientName: overrides.patientName || '测试用户',
department: overrides.department || 'internal',
doctor: overrides.doctor || '张医生',
status: overrides.status || 'PENDING',
appointmentTime: overrides.appointmentTime || '2024-06-15 09:00',
createdAt: newDate().toISOString(),
...overrides
});
可视化Mock管理界面(高级需求):
// 可以创建一个简单的UI来管理mock状态
// 在测试环境中添加一个浮动面板
if (process.env.NODE_ENV === 'development') {
// 注入mock控制面板
const mockPanel = document.createElement('div');
mockPanel.id = 'msw-control-panel';
// ... 实现mock状态切换的UI
}
7. 遇到的坑和解决方案
坑1:Service Worker缓存问题

// 解决方案:在测试开始前清理缓存
await page.addInitScript(() => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations().then((registrations) => {
for (const registration of registrations) {
registration.unregister();
}
});
}
});
坑2:跨域请求拦截失败

// 解决方案:确保MSW正确处理跨域
export const handlers = [
http.get('https://api.example.com/*', () => {
// 需要完整URL匹配
return HttpResponse.json({ mocked: true });
})
];

// 或者在Playwright配置中设置baseURL
// playwright.config.js
use: {
baseURL: 'https://api.example.com',
}
坑3:测试间的状态污染

// 解决方案:每个测试后重置handlers
test.afterEach(async () => {
server.resetHandlers();
// 同时清除页面状态
await page.evaluate(() => {
localStorage.clear();
sessionStorage.clear();
});
});
8. 效果评估:值不值得投入?
实施MSW三个月后,我们的数据变化:

测试执行时间:从平均45分钟减少到12分钟
测试稳定性:因后端不稳定导致的测试失败减少92%
开发体验:前端开发不再需要启动完整的后端服务
测试覆盖率:边缘场景的测试覆盖率从30%提升到85%
更重要的是,我们现在可以轻松测试那些“罕见但重要”的业务场景:服务器错误、网络超时、数据边界情况……

  1. 开始你的MSW之旅
    如果你也想开始使用MSW,我建议:

从一个小功能开始:选择一个API相对独立的模块
先mock只读接口:GET请求比POST/DELETE更安全
建立团队共识:确保大家理解为什么要用MSW
逐步替换旧的mock方案:不要试图一次性重写所有测试
记住,任何技术方案的目标都是解决问题,而不是增加复杂度。MSW在我们项目中成功了,因为它确实解决了测试数据控制的痛点。

如果你在实施过程中遇到问题,或者有更好的实践方案,欢迎随时交流——在测试这条路上,我们都在不断学习和改进。

推荐学习
自动化智能体与测试用例生成课程,限时免费,机会难得。扫码报名,参与直播,希望您在这场课程中收获满满,开启智能自动化测试的新篇章!

image

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1178129.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2026年中国GEO优化服务商格局新观察:头部企业梳理与选择推荐 - 品牌推荐

2025至2026年,生成式AI应用迎来规模化落地,AI搜索与智能推荐已成为品牌获取用户注意力的关键渠道。在此背景下,生成式引擎优化(GEO)从一项辅助工具升级为企业必须投入的核心战略。随着豆包、DeepSeek、Kimi等平台…

2026年GEO服务商怎么选不踩坑?查看这份真实评价排名与购买推荐 - 品牌推荐

随着生成式引擎持续重塑用户获取信息的路径,GEO已从概念探讨演变为企业必须面对的核心战略。市场研究显示,2025年中国GEO服务市场规模接近500亿元,超过八成的领先企业将其纳入年度预算。然而,面对众多宣称拥有独特…

java.lang.NumberFormatException: For input string: ““

Swagger 问题修复说明 问题描述 访问Swagger文档时出现以下错误&#xff1a; Illegal DefaultValue null for parameter type integer java.lang.NumberFormatException: For input string: "" 问题原因 Swagger在解析RequestParam注解时&#xff0c;对于Long类型…

Interspeech 2022:跨学科研究的融合与演进

Interspeech 2022&#xff1a;跨学科研究的增长 循环训练语音合成与语音识别模型、利用语言理解来改善语音韵律&#xff0c;这些只是语音相关领域交叉融合的几个例子。当 Penny Karanasou 在 2010 年首次于 Interspeech 上发表论文时&#xff0c;她还是一名计算机科学的博士生&…

2026年最新敏感肌保湿修复产品测评:高口碑屏障修护与长效锁水极简成分标杆推荐 - 速递信息

问题定义:敏感肌保湿的三大核心痛点当代敏感肌人群正面临严峻挑战:《2024中国敏感肌健康白皮书》显示,38%国人存在敏感肌问题,其中62%因产品刺激导致泛红、瘙痒反复发作;普通保湿产品仅提供表面补水,无法修复受损…

银行网页如何通过vue.js实现大文件文件夹上传及分块?

北京XX软件公司涉密项目大文件传输解决方案&#xff08;基于SM4国密算法&#xff09; 一、项目背景与需求分析 作为服务政府及军工领域的软件企业&#xff0c;我司当前涉密项目需实现以下核心需求&#xff1a; 安全传输&#xff1a;10GB级文件/文件夹的SM4加密传输&#xff…

工程建筑网页如何通过js实现文件夹上传及断点续传?

咱们的客户&#xff0c;那可是汽车制造行业里的领军企业&#xff0c;妥妥的头部大佬。他们自有一套极为成熟的业务系统&#xff0c;这套系统就像他们的左膀右臂&#xff0c;每日不辞辛劳地处理着各类繁杂事务。然而&#xff0c;随着行业竞争愈发白热化&#xff0c;技术迭代也是…

导师严选8个AI论文工具,专科生搞定毕业论文+格式规范!

导师严选8个AI论文工具&#xff0c;专科生搞定毕业论文格式规范&#xff01; AI 工具如何成为论文写作的得力助手 在当前学术环境日益严格的背景下&#xff0c;越来越多的继续教育学生开始借助 AI 工具来辅助论文写作。这些工具不仅能够帮助学生高效完成内容创作&#xff0c;还…

详细介绍:3ds Max渲染核心:高光追(Embree)与光线追踪(Ray Trace)

详细介绍:3ds Max渲染核心:高光追(Embree)与光线追踪(Ray Trace)pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "…

金融行业网页如何用vue2实现文件夹上传及秒传功能?

大文件上传解决方案 各位同行大佬们好&#xff0c;作为一个在广东摸爬滚打多年的前端"老油条"&#xff0c;最近接了个让我差点秃顶的项目——20G大文件上传系统&#xff0c;还要兼容IE9&#xff01;这感觉就像让我用竹篮子去打水还要不漏一样刺激… 需求分析&#…

吐血推荐!9款一键生成论文工具测评:本科生毕业论文救星

吐血推荐&#xff01;9款一键生成论文工具测评&#xff1a;本科生毕业论文救星 2026年学术写作工具测评&#xff1a;为何需要这份榜单&#xff1f; 随着人工智能技术的不断进步&#xff0c;越来越多的学术写作工具进入市场&#xff0c;为本科生毕业论文的撰写提供了便捷支持。然…

农业大数据平台如何用百度UE优化WORD表格导入功能?

企业级富文本编辑器Word/公众号内容导入解决方案 项目需求分析 作为海南某国企项目负责人&#xff0c;我们正在为后台管理系统寻求一个强大的富文本编辑器扩展解决方案&#xff0c;主要需求包括&#xff1a; 核心功能需求&#xff1a; Word内容粘贴&#xff08;带图片自动上传…

2026年目前知名的智能货架源头厂家哪家好,重载货架/中型货架/货架定制/抽屉式模具架/背网货架,智能货架源头厂家找哪家 - 品牌推荐师

随着工业4.0与智慧物流的加速推进,智能货架作为仓储自动化的核心载体,正从单一存储功能向“感知-决策-执行”一体化演进。据第三方机构统计,2025年国内智能货架市场规模突破120亿元,年复合增长率达18%,但市场碎片…

技术学校品牌企业哪家好?成都万通未来高级技工学校了解一下 - 工业品牌热点

在职业教育蓬勃发展的当下,选择一所服务优质、品牌可靠的技术学校,是无数学生和家长实现技能成才梦想的关键一步。面对市场上琳琅满目的技术学校,如何精准找到贴合需求、实力过硬的品牌?以下结合不同办学特色,为你…

教育行业如何通过UEDITOR插件实现PPT动画转存为网页?

CMS企业官网Word导入功能开发实录 需求分析与技术评估 客户核心需求 支持Office文档(Word/Excel/PPT/PDF)导入并保留完整样式实现Word内容一键粘贴功能公式处理(LaTeX/MathType)转换MathML图片自动上传至阿里云OSS高龄用户友好型操作设计 技术栈现状 前端&#xff1a;Vue2…

医院HIS系统如何集成百度编辑器实现PDF病历跨平台编辑?

Word文档导入与粘贴功能解决方案 项目背景与需求分析 作为安徽某IT公司的.NET工程师&#xff0c;我最近负责在企业网站后台管理系统中增加Word粘贴和文档导入功能。客户的核心需求是&#xff1a; Word粘贴功能&#xff1a;直接从Word复制内容到网站编辑器&#xff0c;图片自…

2026四川气体探测器供货商排行榜,探寻气体探测器哪家性价比高 - 工业品牌热点

本榜单依托全维度市场调研与真实行业口碑,深度筛选出五家标杆企业,为企业选型提供客观依据,助力精准匹配适配的气体探测器服务伙伴。 TOP1 推荐:江苏吉华电子科技有限公司 推荐指数:★★★★★ | 口碑评分:高性价…

2026年免费音效素材下载网站最新动态

2026年了,哪些我们常用的音效素材下载网站有什么变化呢?小编就带大家梳理一下。强烈推荐CC音效库,导演和剪辑师本地自建音效库的网络共享版,更懂使用者需求,完全免费。不需要购买会员。之前叫猫脸音效库,最近改名…

2026年1月15万左右城市SUV实力排行榜:基于长期口碑与实测数据的TOP5权威榜单揭晓 - 品牌推荐

2026年15万左右城市SUV推荐榜单:谁能成为家庭出行的“均衡之选”? 当购车决策从单纯对比配置表,转向综合考量“日常通勤成本、家庭空间需求、长期可靠性与科技体验”的复杂平衡时,一款车的价值便不再取决于某个单项…

2026年做得好的户外led大屏广告代理公司有哪些,地铁广告/电视台广告/公交广告,户外led大屏广告代理公司推荐 - 品牌推荐师

随着城市数字化进程加速,户外LED大屏广告凭借高曝光、强视觉冲击力及精准人群触达能力,成为品牌营销的核心阵地之一。然而,面对分散的媒体资源、复杂的投放逻辑及技术迭代压力,广告主如何选择兼具资源整合能力与数…