根据一些调用资料和尝试,OHIF 的底层用的是Cornerstonejs ,这个是基于web端写的,如果说写在微信小程序里,确实有很多报错,
第一个问题就是 npm下载的依赖,
一、运行环境差异
微信小程序的运行环境与传统的 Node.js 环境有很大不同。小程序在微信客户端中运行,有严格的安全限制和性能要求。而 npm 包通常是为 Node.js 环境设计的,其中可能包含一些在小程序环境中不被支持的代码或依赖项。
二、构建机制不同
- 小程序有自己特定的构建体系。微信小程序使用自己的开发工具进行构建和打包,这个过程与基于 npm 和 Webpack 等工具的传统前端构建流程不同。小程序的构建工具主要针对小程序的特定结构和需求进行优化,不一定能直接处理 npm 包的复杂依赖关系。
- 小程序的代码结构通常是由多个页面和组件组成,每个页面和组件都有自己独立的代码文件。这种结构与传统的基于模块的前端项目有所不同,也使得直接引入 npm 包变得更加困难。
第二个 修改的话很考验技术,得修改js文件,而且不保证是否能运行起来
第三个 ohif 最新版用的react+ hooks 框架写的,很多组件都是已经封装好,要是另外写的话 ,也很考研技术
现在是用手机模式写的适配移动端
第一个:
platform\app\src\utils\isMobile.ts
创建一个ts文件 用来判断现在是否处于移动端模式
export default function isMobile(): boolean {
const pattern: RegExp = new RegExp(
'Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini'
);
return pattern.test(navigator.userAgent);
}
第二个:
extensions\default\src\ViewerLayout\index.tsx
在这个里面是基础查看器的布局位置,我们先通过这个文件来找到 导航栏、左侧面板、右侧面板、·中间影像的查看器、以及工具栏的工具们的组件位置
导航栏 的移动端改造
原来的导航栏的组件组件 是
extensions\default\src\ViewerLayout\ViewerHeader.tsx
这个里面的 Header
platform\ui\src\components\Header\Header.tsx
里面有个NavBar
platform\ui\src\components\NavBar\NavBar.tsx
这个是控制外面那个容器的
我把它改成 没有下拉菜单的 ,都横着展示出来,所以要自己写一套组件组件出来或者直接判断 写两个return也可以
组件们都在
platform\ui\src\components 这个下面,如果想要自己新建一个组件的话,就来这里,记得导出,建完文件夹以后,还得导出
有两个index.js里面 写上新建的组件
platform\ui\src\components\index.js
platform\ui\src\index.js
这两个里面都得加上,不然你的组件没有办法使用
里面图标的具体 控制
extensions\default\src\Toolbar\ToolbarSplitButtonWithServices.tsx
这个里面的
<SplitButton
primary={primary}
secondary={secondary}
items={getSplitButtonItems(items)}
groupId={groupId}
renderer={listItemRenderer}
onInteraction={onInteraction}
Component={props => (
<PrimaryButtonComponent
{...props}
servicesManager={servicesManager}
/>
)}
/>
我先建了一个SplitButtonAPP的组件,在新的组件里面的把按钮拆成一长条,具体代码
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import OutsideClickHandler from 'react-outside-click-handler';
import { useTranslation } from 'react-i18next';
import Icon from '../Icon';
import Tooltip from '../Tooltip';
import ListMenu from '../ListMenu';
const baseClasses = {
Button: 'flex flex-col items-center rounded-md border-transparent group/button',
Primary: 'flex flex-col items-center text-center',
Content: 'flex flex-row space-x-4',
};
const classes = {
Button: () => classNames(baseClasses.Button, 'hover:!bg-primary-dark hover:border-primary-dark'),
Primary: ({ isActive }) =>
classNames(
baseClasses.Primary,
isActive
? 'border-primary-light bg-primary-light rounded-md'
: 'border-secondary-dark bg-secondary-dark group-hover/button:border-primary-dark group-hover/button:text-primary-light hover:!bg-primary-dark hover:border-primary-dark'
),
Content: () => classNames(baseClasses.Content),
};
const DefaultListItemRenderer = props => {
const { t, icon, label, className, isActive } = props;
return (
<div
className={classNames(
'flex h-8 w-full select-none flex-row items-center p-3',
'whitespace-pre text-base',
className,
`${isActive ? 'hover:opacity-80' : 'hover:bg-primary-dark'}`
)}
>
{icon && (
<span className="mr-4">
<Icon
name={icon}
className="h-[28px] w-[28px]"
/>
</span>
)}
<span className="mr-5">{t?.(label)}</span>
</div>
);
};
/**
* SplitButton 组件是一个更通用的拆分按钮实现,没有isActive和其他交互属性
* 它提供了一种在主按钮和次按钮之间切换,并展示相关选项列表的功能
*
* @param {object} props - 组件的属性对象
* @param {string} props.groupId - 按钮组的ID,用于数据追踪
* @param {object} props.primary - 主按钮的配置对象
* @param {object} props.secondary - 次按钮的配置对象
* @param {array} props.items - 列表菜单项的数组
* @param {function} props.renderer - 渲染列表项的自定义函数
* @param {function} props.onInteraction - 交互时触发的回调函数
* @param {React.Component} props.Component - 渲染图标使用的组件,默认为Icon
* @returns {JSX.Element} - 拆分按钮的JSX实现
*/
const SplitButtonAPP = ({
groupId,
primary,
secondary,
items,
renderer = null,
onInteraction,
Component = Icon,
}) => {
// 使用useTranslation钩子管理翻译
const { t } = useTranslation('Buttons');
// 渲染函数,如果未提供renderer则使用默认渲染函数
const listItemRenderer = renderer || DefaultListItemRenderer;
return (
<div
id="SplitButtonAPP"
className={classes.Content()}
>
{items.map((item, index) => {
const primaryClassNames = classNames(
classes.Primary({
isActive: item.isActive,
}),
item.className
);
return (
<div
key={item.id}
className="flex flex-col items-center"
>
<Tooltip
content={item.label}
className="h-full"
>
<Component
key={item.id}
{...item}
onInteraction={onInteraction}
rounded="none"
className={primaryClassNames}
data-tool={item.id}
data-cy={`${groupId}-split-button-item-${index}`}
/>
</Tooltip>
{/* <div className="mt-2 text-center text-xs text-white">{t?.(item.label)}</div>{' '} */}
{/* 添加按钮下方的名称 */}
</div>
);
})}
</div>
);
};
SplitButtonAPP.propTypes = {
groupId: PropTypes.string.isRequired,
// primary: PropTypes.object.isRequired,
// secondary: PropTypes.object.isRequired,
items: PropTypes.array.isRequired,
renderer: PropTypes.func,
// isActive: PropTypes.bool,
onInteraction: PropTypes.func.isRequired,
Component: PropTypes.elementType,
// interactionType: PropTypes.oneOf(['action', 'tool', 'toggle']),
};
export default SplitButtonAPP;
在这个文件里面进行判断当前模式是否是移动端
extensions\default\src\Toolbar\ToolbarSplitButtonWithServices.tsx
引入import isMobile from '../../../../platform/app/src/utils/isMobile';
//判断当前是否是移动设备
let isMob = isMobile();
localStorage.setItem('isMobile', isMob);
if (isMob) {
return (
<SplitButtonAPP
primary={primary}
secondary={secondary}
items={getSplitButtonItems(items)}
groupId={groupId}
renderer={listItemRenderer}
onInteraction={onInteraction}
Component={props => (
<PrimaryButtonComponent
{...props}
servicesManager={servicesManager}
/>
)}
/>
);
} else {
return (
<SplitButton
primary={primary}
secondary={secondary}
items={getSplitButtonItems(items)}
groupId={groupId}
renderer={listItemRenderer}
onInteraction={onInteraction}
Component={props => (
<PrimaryButtonComponent
{...props}
servicesManager={servicesManager}
/>
)}
/>
);
}
这个标题栏还是有很多英文的占的地方比较大,
modes\longitudinal\src\moreTools.ts
这里可以把英文的 修改成中文
modes\longitudinal\src\toolbarButtons.ts
这个里面也有
换成了中文
缩略图的改造
通过extensions\default\src\ViewerLayout\index.tsx
找这个组件
extensions\default\src\Components\SidePanelWithServices.tsx
// 渲染侧边面板组件
return (
<SidePanel
{...props}
side={side}
tabs={tabs}
activeTabIndex={activeTabIndex}
onOpen={handleSidePanelOpen}
onActiveTabIndexChange={handleActiveTabIndexChange}
expandedWidth={expandedWidth}
></SidePanel>
);
跳到这个组件里面
我忘记是怎么找的了,应该是根据类名找的
找到的几个有关于缩略图的
然后自己写了几个组件
ThumbnailAPP
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { useDrag } from 'react-dnd';
import Icon from '../Icon';
import { StringNumber } from '../../types';
import DisplaySetMessageListTooltip from '../DisplaySetMessageListTooltip';
/**
* 显示一个展示集的缩略图。
*
* 此组件用于显示指定展示集的缩略图,包括图片、文字描述及交互反馈。它支持单击、双击与拖拽操作。
*
* @param {string} displaySetInstanceUID - 展示集的唯一标识符。
* @param {string} [className] - 缩略图容器的额外 CSS 类名。
* @param {string} imageSrc - 图片资源的 URL 地址。
* @param {string} imageAltText - 图片的替代文本。
* @param {string} description - 缩略图下方的描述文字。
* @param {number} seriesNumber - 序列号。
* @param {number} numInstances - 实例数量。
* @param {number} loadingProgress - 加载进度,范围从 0 到 1。
* @param {object} [dragData={}] - 用于拖拽的数据对象。
* @param {boolean} isActive - 是否处于激活状态。
* @param {function} onClick - 单击事件处理函数。
* @param {function} onDoubleClick - 双击事件处理函数。
* @returns {React.ReactNode} - 返回缩略图组件。
*/
const ThumbnailAPP = ({
displaySetInstanceUID,
className,
imageSrc,
imageAltText,
description,
seriesNumber,
numInstances,
loadingProgress,
countIcon,
messages,
dragData = {},
isActive,
onClick,
onDoubleClick,
}): React.ReactNode => {
// 使用 useDrag Hook 来实现拖拽功能
const [collectedProps, drag, dragPreview] = useDrag({
type: 'displayset',
item: { ...dragData },
canDrag: function (monitor) {
return Object.keys(dragData).length !== 0;
},
});
// 状态管理:记录最后一次触摸结束的时间戳
const [lastTap, setLastTap] = useState(0);
// 触摸开始事件处理器,用于记录触摸开始时间
const handleTouchStart = e => {
setLastTap(new Date().getTime());
};
// 触摸结束事件处理器,用于判断是否触发双击事件
const handleTouchEnd = e => {
const currentTime = new Date().getTime();
const tapLength = currentTime - lastTap;
// 判断是否为双击
if (tapLength < 300 && tapLength > 0) {
onDoubleClick(e);
} else {
onClick(e);
}1
setLastTap(currentTime);
};
// 渲染缩略图组件
return (
<div
className={classnames(
className,
'group mb-8 flex flex-1 cursor-pointer select-none flex-col px-3 outline-none'
)}
id={`thumbnail-${displaySetInstanceUID}`}
data-cy={`study-browser-thumbnail`}
data-series={seriesNumber}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
role="button"
tabIndex="0"
>
<div ref={drag}>
<div
className={classnames(
'flex h-32 w-32 flex-1 items-center justify-center overflow-hidden rounded-md bg-black text-base text-white',
isActive
? 'border-primary-light border-2'
: 'border-secondary-light border hover:border-blue-300'
)}
>
{imageSrc ? (
<img
src={imageSrc}
alt={imageAltText}
className="h-full w-full object-contain"
crossOrigin="anonymous"
/>
) : (
<div>{imageAltText}</div>
)}
</div>
<div className="flex flex-1 flex-row items-center pt-2 text-base text-blue-300">
<div className="mr-4">
<span className="text-primary-main font-bold">{'S: '}</span>
{seriesNumber}
</div>
<div className="flex flex-1 flex-row items-center">
<Icon
name={countIcon || 'group-layers'}
className="mr-2 w-3"
/>
{` ${numInstances}`}
</div>
<div className="mr-2 flex last:mr-0">
{loadingProgress && loadingProgress < 1 && <>{Math.round(loadingProgress * 100)}%</>}
{loadingProgress && loadingProgress === 1 && (
<Icon
name={'database'}
className="w-3"
/>
)}
</div>
</div>
<DisplaySetMessageListTooltip
messages={messages}
id={`display-set-tooltip-${displaySetInstanceUID}`}
/>
</div>
{/* <div className="break-all text-base text-white">{description}</div> */}
</div>
);
};
ThumbnailAPP.propTypes = {
displaySetInstanceUID: PropTypes.string.isRequired,
className: PropTypes.string,
imageSrc: PropTypes.string,
dragData: PropTypes.shape({
type: PropTypes.string.isRequired,
}),
imageAltText: PropTypes.string,
description: PropTypes.string.isRequired,
seriesNumber: StringNumber.isRequired,
numInstances: PropTypes.number.isRequired,
loadingProgress: PropTypes.number,
messages: PropTypes.object,
isActive: PropTypes.bool.isRequired,
onClick: PropTypes.func.isRequired,
onDoubleClick: PropTypes.func.isRequired,
};
export default ThumbnailAPP;
ThumbnailListAPP
import React from 'react';
import PropTypes from 'prop-types';
import ThumbnailAPP from '../ThumbnailAPP';
import ThumbnailNoImage from '../ThumbnailNoImage';
import ThumbnailTracked from '../ThumbnailTracked';
import * as Types from '../../types';
const ThumbnailListAPP = ({
thumbnails,
onThumbnailClick,
onThumbnailDoubleClick,
onClickUntrack,
activeDisplaySetInstanceUIDs = [],
}) => {
return (
<div
id="ohif-thumbnail-list"
className="flex flex-row overflow-x-auto overflow-y-hidden bg-black"
style={
{
position: 'fixed',
bottom: 0,
width: '100%',
whiteSpace: 'nowrap',
padding: '0 10px', // 添加填充以便在移动设备上获得更好的间距
}}
>
{thumbnails.map(
({
displaySetInstanceUID,
description,
dragData,
seriesNumber,
numInstances,
loadingProgress,
modality,
componentType,
seriesDate,
countIcon,
isTracked,
canReject,
onReject,
imageSrc,
messages,
imageAltText,
isHydratedForDerivedDisplaySet,
}) => {
const isActive = activeDisplaySetInstanceUIDs.includes(displaySetInstanceUID);
const commonProps = {
key: displaySetInstanceUID,
displaySetInstanceUID,
dragData,
description,
seriesNumber,
numInstances,
loadingProgress,
countIcon,
imageSrc,
imageAltText,
messages,
isActive,
onClick: () => onThumbnailClick(displaySetInstanceUID),
onDoubleClick: () => onThumbnailDoubleClick(displaySetInstanceUID),
style: {
display: 'inline-block',
marginRight: '10px',
width: '75px', // 调整宽度以保持移动设备上的方块样式
height: '75px', // 保持高度与宽度相同,以确保方块形状
},
};
switch (componentType) {
case 'thumbnail':
return <ThumbnailAPP {...commonProps} />;
case 'thumbnailTracked':
return (
<ThumbnailTracked
{...commonProps}
isTracked={isTracked}
onClickUntrack={() => onClickUntrack(displaySetInstanceUID)}
/>
);
case 'thumbnailNoImage':
return (
<ThumbnailNoImage
{...commonProps}
modality={modality}
modalityTooltip={_getModalityTooltip(modality)}
seriesDate={seriesDate}
canReject={canReject}
onReject={onReject}
isHydratedForDerivedDisplaySet={isHydratedForDerivedDisplaySet}
/>
);
default:
return <></>;
}
}
)}
</div>
);
};
ThumbnailListAPP.propTypes = {
thumbnails: PropTypes.arrayOf(
PropTypes.shape({
displaySetInstanceUID: PropTypes.string.isRequired,
imageSrc: PropTypes.string,
imageAltText: PropTypes.string,
seriesDate: PropTypes.string,
seriesNumber: Types.StringNumber,
numInstances: PropTypes.number,
description: PropTypes.string,
componentType: Types.ThumbnailType.isRequired,
isTracked: PropTypes.bool,
dragData: PropTypes.shape({
type: PropTypes.string.isRequired,
}),
})
),
activeDisplaySetInstanceUIDs: PropTypes.arrayOf(PropTypes.string),
onThumbnailClick: PropTypes.func.isRequired,
onThumbnailDoubleClick: PropTypes.func.isRequired,
onClickUntrack: PropTypes.func.isRequired,
};
function _getModalityTooltip(modality) {
if (_modalityTooltips.hasOwnProperty(modality)) {
return _modalityTooltips[modality];
}
return 'Unknown';
}
const _modalityTooltips = {
SR: 'Structured Report',
SEG: 'Segmentation',
OT: 'Other',
RTSTRUCT: 'RT Structure Set',
};
export default ThumbnailListAPP;
期间还会有有其他几个组件的调整
一般是样式跳转,根据isMobile()判断当前是否是移动端设备
ThumbnailNoImage
//判断当前是否是移动设备
let isMob = isMobile();
localStorage.setItem('isMobile', isMob);
if (isMob) {
return (
<div
className={classnames(
'flex flex-1 cursor-pointer select-none flex-row rounded outline-none hover:border-blue-300 focus:border-blue-300',
isActive ? 'border-primary-light border-2' : 'border border-transparent'
)}
style={
{
padding: isActive ? '11px' : '12px',
}}
id={`thumbnail-${displaySetInstanceUID}`}
onClick={onClick}
onDoubleClick={onDoubleClick}
onTouchEnd={handleTouchEnd}
role="button"
tabIndex="0"
data-cy={`study-browser-thumbnail-no-image`}
>
<div ref={drag}>
<div className="flex flex-1 flex-col">
<div className="mb-2 flex flex-1 flex-row items-center">
<Icon
name="list-bullets"
className={classnames(
'w-12',
isHydratedForDerivedDisplaySet ? 'text-primary-light' : 'text-secondary-light'
)}
/>
<Tooltip
position="bottom"
content={<Typography>{modalityTooltip}</Typography>}
>
<div
className={classnames(
'rounded-sm px-3 text-lg',
isHydratedForDerivedDisplaySet
? 'bg-primary-light text-black'
: 'bg-primary-main text-white'
)}
>
{modality}
</div>
</Tooltip>
<span className="ml-4 text-base text-blue-300">{seriesDate}</span>
<DisplaySetMessageListTooltip
messages={messages}
id={`display-set-tooltip-${displaySetInstanceUID}`}
/>
</div>
<div className="flex flex-row">
{canReject && (
<Icon
name="old-trash"
style={
{ minWidth: '12px' }}
className="ml-4 w-3 text-red-500"
onClick={onReject}
/>
)}
{/* <div className="ml-4 break-all text-base text-white">{description}</div> */}
</div>
</div>
</div>
</div>
);
} else {
return (
<div
className={classnames(
'flex flex-1 cursor-pointer select-none flex-row rounded outline-none hover:border-blue-300 focus:border-blue-300',
isActive ? 'border-primary-light border-2' : 'border border-transparent'
)}
style={
{
padding: isActive ? '11px' : '12px',
}}
id={`thumbnail-${displaySetInstanceUID}`}
onClick={onClick}
onDoubleClick={onDoubleClick}
onTouchEnd={handleTouchEnd}
role="button"
tabIndex="0"
data-cy={`study-browser-thumbnail-no-image`}
>
<div ref={drag}>
<div className="flex flex-1 flex-col">
<div className="mb-2 flex flex-1 flex-row items-center">
<Icon
name="list-bullets"
className={classnames(
'w-12',
isHydratedForDerivedDisplaySet ? 'text-primary-light' : 'text-secondary-light'
)}
/>
<Tooltip
position="bottom"
content={<Typography>{modalityTooltip}</Typography>}
>
<div
className={classnames(
'rounded-sm px-3 text-lg',
isHydratedForDerivedDisplaySet
? 'bg-primary-light text-black'
: 'bg-primary-main text-white'
)}
>
{modality}
</div>
</Tooltip>
<span className="ml-4 text-base text-blue-300">{seriesDate}</span>
<DisplaySetMessageListTooltip
messages={messages}
id={`display-set-tooltip-${displaySetInstanceUID}`}
/>
</div>
<div className="flex flex-row">
{canReject && (
<Icon
name="old-trash"
style={
{ minWidth: '12px' }}
className="ml-4 w-3 text-red-500"
onClick={onReject}
/>
)}
<div className="ml-4 break-all text-base text-white">{description}</div>
</div>
</div>
</div>
</div>
);
}
};
ThumbnailTracked
//判断当前是否是移动设备
let isMob = isMobile();
localStorage.setItem('isMobile', isMob);
if (isMob) {
return (
<div
className={classnames('flex flex-1 cursor-pointer flex-row px-3 outline-none', className)}
id={`thumbnail-${displaySetInstanceUID}`}
>
{/* <div className="flex-2 flex flex-col items-center">
<div
className={classnames(
'relative mb-2 flex cursor-pointer flex-col items-center justify-start p-2',
isTracked && 'rounded-sm hover:bg-gray-900'
)}
>
<Tooltip
position="right"
content={
<div className="flex flex-1 flex-row">
<div className="flex-2 flex items-center justify-center pr-4">
<Icon
name="info-link"
className="text-primary-active"
/>
</div>
<div className="flex flex-1 flex-col">
<span>
<span className="text-white">
{isTracked ? t('Series is tracked') : t('Series is untracked')}
</span>
</span>
</div>
</div>
}
>
<Icon
name={trackedIcon}
className="text-primary-light mb-2 w-4"
/>
</Tooltip>
</div>
{isTracked && (
<div onClick={onClickUntrack}>
<Icon
name="cancel"
className="text-primary-active w-4"
/>
</div>
)}
</div> */}
<ThumbnailAPP
displaySetInstanceUID={displaySetInstanceUID}
imageSrc={imageSrc}
imageAltText={imageAltText}
dragData={dragData}
description={description}
seriesNumber={seriesNumber}
messages={messages}
numInstances={numInstances}
countIcon={countIcon}
loadingProgress={loadingProgress}
isActive={isActive}
onClick={onClick}
onDoubleClick={onDoubleClick}
/>
</div>
);
} else {
return (
<div
className={classnames('flex flex-1 cursor-pointer flex-row px-3 outline-none', className)}
id={`thumbnail-${displaySetInstanceUID}`}
>
<div className="flex-2 flex flex-col items-center">
<div
className={classnames(
'relative mb-2 flex cursor-pointer flex-col items-center justify-start p-2',
isTracked && 'rounded-sm hover:bg-gray-900'
)}
>
<Tooltip
position="right"
content={
<div className="flex flex-1 flex-row">
<div className="flex-2 flex items-center justify-center pr-4">
<Icon
name="info-link"
className="text-primary-active"
/>
</div>
<div className="flex flex-1 flex-col">
<span>
<span className="text-white">
{isTracked ? t('Series is tracked') : t('Series is untracked')}
</span>
</span>
</div>
</div>
}
>
<Icon
name={trackedIcon}
className="text-primary-light mb-2 w-4"
/>
</Tooltip>
</div>
{isTracked && (
<div onClick={onClickUntrack}>
<Icon
name="cancel"
className="text-primary-active w-4"
/>
</div>
)}
</div>
<Thumbnail
displaySetInstanceUID={displaySetInstanceUID}
imageSrc={imageSrc}
imageAltText={imageAltText}
dragData={dragData}
description={description}
seriesNumber={seriesNumber}
messages={messages}
numInstances={numInstances}
countIcon={countIcon}
loadingProgress={loadingProgress}
isActive={isActive}
onClick={onClick}
onDoubleClick={onDoubleClick}
/>
</div>
);
}
样式自己调整吧,我也是自己慢慢研究的
具体效果是这样的,可以触屏滑动
中间的 图片改造
import isMobile from '../../../../platform/app/src/utils/isMobile';
因为有滚动条,而且 还有左右结构 ,挺难改的,下面那几个样式 我修改了, 我把flex 给删掉了,加了一个判断,判断是否是移动端模式
额 剩下的忘记了
现在就差让中间的图片 放大点有点太小了
我发现了 是照片本身就小,其他的有大的