@qynpm/hooks
v1.0.3
Published
起印公共 hooks 包。
Readme
@qynpm/hooks
@qiyin 公共 hooks 的 npm 拆包目录。
录单拆包的 transform 实际代码盘点见 录单拆包迁移总览。该文档记录了 packages/erp/src/views/transform 当前缺少的公共 UI、hooks、pickers、product-preview 和 table-schema 能力。
当前状态
目前这个包还在迁移初期,不是把老的 @qiyin/hooks 一次性整体搬过来,而是按“先公共、再解耦、最后替换”的方式逐步迁移。
当前已经迁入的能力:
useCreateInstanceuseInstanceViewuseKeyuseExpirecreateUseConfigcreateUseRefreshuseBottomBarfeedback runtime
当前职责
这个包主要承接适合做成 npm 公共能力的 hooks。
当前这批能力的边界是:
useCreateInstance- 负责动态创建实例
- 支持普通挂载
- 支持
asComponent模式
useInstanceView- 负责渲染
useCreateInstance创建出来的实例视图
- 负责渲染
useKey- 负责提供组件刷新 key 和递增方法
- 不依赖业务 store、router、request
useExpire- 负责通过
HEAD请求检查入口文件Last-Modified - 支持传入
system保持旧调用兼容,也支持传入完整url / fetcher
- 负责通过
createUseConfig- 负责生成可注入的配置读取 hook
- 只承接
loading / error / configs / mapping / getConfig这些公共逻辑 - API、缓存和响应结构由宿主项目注入
录单 hooks 迁移清单
录单基础组件拆分基线 中提到的 hooks 处理状态如下:
| hook | 当前状态 | 处理建议 |
| --- | --- | --- |
| useKey | 已迁入 @qynpm/hooks | 可作为公共 hook 使用 |
| useExpire | 已迁入 @qynpm/hooks | 保留旧 useExpire(system) 调用,同时支持对象参数 |
| useCreateInstance | 已迁入 @qynpm/hooks | 已被 @qynpm/form、@qynpm/product-preview 使用 |
| useInstanceView | 已迁入 @qynpm/hooks | 宿主应用按需接入实例视图 |
| useConfig | 已提供公共工厂 createUseConfig | 各项目继续保留薄包装,注入本项目 API 和缓存 |
| useRefresh | 已提供公共工厂 createUseRefresh | ERP 已补本地 adapter,页面 import 可保持不变 |
| useBottomBar | 已提供宿主协议 useBottomBar | ERP 平张录单已做最小宿主验证,正式接入仍建议走项目 adapter |
| useCol | 暂不进入 @qynpm/hooks | 详见 @qynpm/table-schema,建议单独做 table/schema 包 |
| usePopperMessage | 已收敛为 feedback.popperMessage 协议 | ERP 已接 setupPopperMessageRuntime() 宿主实现,旧 hook 不原样迁 |
当前原则:
- 纯公共、无宿主依赖的 hook 先迁入。
- 依赖 store、router、request、页面容器的 hook 先做 adapter 方案,不原样搬迁。
- 只服务录单流程的 hook 暂不进入公共 hooks 包。
createUseConfig
createUseConfig 用来收口各项目重复的配置读取逻辑,但不绑定任何项目的 API、store 或 alias。
推荐使用方式:
import { createUseConfig } from '@qynpm/hooks'
import { GlobalAPI } from '@api/index'
import useStateStore from '@/store/modules/state'
export default createUseConfig({
fetchConfig: GlobalAPI.getConfig,
getCache(id) {
return useStateStore().config[id]
},
setCache(id, value) {
useStateStore().setConfig(id, value)
}
})如果宿主接口返回结构不是 { data: GlobalConfig[] },可以通过 normalizeResponse 适配:
export default createUseConfig({
fetchConfig: GlobalAPI.getConfig,
normalizeResponse(response) {
return response.rows
}
})生成后的 hook 保持旧调用形态:
const useConfig = createUseConfig({ fetchConfig })
const { mapping, configs, loading, error, getConfig } = useConfig(2)
await getConfig()边界:
@qynpm/hooks不直接 importGlobalAPI。@qynpm/hooks不直接 import Pinia store。- 各项目保留自己的
src/hooks/useConfig.ts作为 adapter。
后续组件迁移方案
公共 hooks 包只提供可复用内核,不直接替换各项目页面。后续迁移按“公共包能力 -> 宿主 adapter -> 组件引用”的顺序推进。
迁移分层
@qynpm/hooks- 放纯公共逻辑。
- 不依赖
@/store、@api、vue-router页面实例或具体业务模块。 - 对需要宿主能力的 hook,只提供工厂函数或运行时协议。
宿主项目
src/hooks- 保留薄包装。
- 负责注入本项目 API、store、缓存、router、页面容器等能力。
- 对旧页面继续暴露原来的 hook 名称,降低替换成本。
业务组件 / 页面
- 优先从宿主
src/hooks引入。 - 新拆出来准备发布 npm 的组件,才直接从
@qynpm/hooks引入纯公共 hook。
- 优先从宿主
useConfig 迁移方式
第一阶段只改各项目 src/hooks/useConfig.ts,不批量改页面调用。
旧页面保持:
import useConfig from '@/hooks/useConfig'宿主 adapter 改成:
import { createUseConfig } from '@qynpm/hooks'
import { GlobalAPI } from '@api/index'
import useStateStore from '@/store/modules/state'
export default createUseConfig({
fetchConfig: GlobalAPI.getConfig,
getCache(id) {
return useStateStore().config[id]
},
setCache(id, value) {
useStateStore().setConfig(id, value)
}
})如果项目没有统一缓存,可以只注入接口:
import { createUseConfig } from '@qynpm/hooks'
import { AppAPI } from '@api/index'
export default createUseConfig({
fetchConfig: AppAPI.getConfig
})如果接口入参不是 { num },在 adapter 内转换:
export default createUseConfig({
fetchConfig({ num }) {
return GlobalAPI.getConfig(num)
}
})如果接口返回结构不同,用 normalizeResponse 转换:
export default createUseConfig({
fetchConfig: GlobalAPI.getConfig,
normalizeResponse(response) {
return response.data?.list || response.rows || []
}
})各项目建议迁移顺序
先迁已经是薄包装的项目:
packages/erp/src/hooks/useConfig.tspackages/foundation/src/hooks/useConfig.tspackages/production/src/hooks/useConfig.tspackages/quotation/src/hooks/useConfig.ts
再迁有自定义实现的项目:
packages/app/src/hooks/useConfig.tspackages/market/src/hooks/useConfig.tspackages/resource/src/hooks/useConfig.tspackages/repository/src/hooks/useConfig.tspackages/quotate-page/src/hooks/useConfig.ts
迁移自定义实现时,先保持页面调用和返回字段不变,只替换内部实现。
useKey / useExpire 迁移方式
这两个属于低耦合公共 hook,可以在新组件中直接使用:
import { useKey, useExpire } from '@qynpm/hooks'旧页面不需要批量替换。后续如果某个包要发布 npm,且内部用到了旧 @qiyin/hooks/useKey 或 @qiyin/hooks/useExpire,再局部改成 @qynpm/hooks。
暂不直接迁移的 hooks
useRefresh:
- 旧实现依赖
vue-router、tagsViewstore 和$tab。 - 已提供
createUseRefresh({ getRoute, getVisitedViews, refreshPage, delIframeView })。 - ERP 侧通过
packages/erp/src/hooks/useRefresh.ts接本地 adapter,页面 import 不变。
useBottomBar:
- 旧实现持有全局底部栏状态和 DOM 高度计算。
- 已提供宿主协议和
getBottomBarEntries()。 - ERP 平张录单已挂最小
BottomBarHost验证,正式接入仍建议按宿主 layout 单独设计。
useCol:
- 依赖 vxe/table 渲染组件、配置弹窗、列配置 schema。
- 更适合单独做 table/schema 方案,不混入普通 hooks 迁移。
- 详细方案见 @qynpm/table-schema。
table-schema 后续替代旧 useCol 时,以下能力应由 hooks runtime 或宿主 adapter 提供,不应反向写入 table-schema 内核:
confirm:菜单危险操作确认,可复用 feedback runtime 的confirm。message / popperMessage:复制、菜单操作提示,可复用 feedback runtime。hasPermission:菜单权限过滤,由宿主注入权限实现。- 用户缓存 adapter:旧
useUserCache只能在 ERP adapter 中转接,不进入公共 table-schema 内核。 copy:复制实现由宿主注入,浏览器项目可用navigator.clipboard,ERP 可接旧@qiyin/utils/copy。
hooks 包只维护通用 runtime 协议和组合式函数,不承接 Element Plus 表格列渲染、列设置 UI 或业务列类型。
usePopperMessage:
- 旧实现依赖鼠标全局监听、TSX、样式和
useCreateInstance。 - 已收敛成统一 feedback runtime 的
popperMessage(target, message, options)。 - ERP 已通过
setupPopperMessageRuntime()注入宿主 DOM 浮层实现,避免重复注册鼠标监听。
useRefresh 宿主协议
useRefresh 用来刷新当前路由对应的标签页视图。旧实现很小,但直接依赖 vue-router、宿主 tagsView store 和全局 $tab,不能原样放进公共 hooks 包。
当前建议拆成两层:
@qynpm/hooks- 提供
createUseRefresh工厂。 - 不直接 import
vue-router。 - 不直接 import
@/store/modules/tagsView。 - 不读取组件实例上的
$tab。 - 只负责“找当前 view -> 调宿主刷新 -> iframe/link 额外清理”的流程编排。
- 提供
宿主项目
src/hooks/useRefresh.ts- 注入当前项目的
route、visitedViews、refreshPage、delIframeView。 - 继续向业务页面暴露旧的
useRefresh()用法。 - ERP、app、market 等不同后台项目可以各自接自己的 tagsView/store。
- 注入当前项目的
目标
让公共包复用刷新逻辑,但不绑定后台 layout。
典型场景:
- 保存成功后刷新当前标签页。
- 删除数据后刷新当前列表页。
- iframe/link 类型页面刷新时,同时清理宿主 iframe 缓存。
- 不同后台项目使用同一套刷新协议,但各自注入自己的路由和标签页实现。
建议 API
第一版建议实现:
createUseRefresh(options)协议:
type RefreshRoute = {
path: string
fullPath?: string
name?: string | symbol | null
meta?: {
link?: string
[key: string]: unknown
}
[key: string]: unknown
}
type RefreshView = {
path: string
fullPath?: string
name?: string | symbol | null
[key: string]: unknown
}
type CreateUseRefreshOptions = {
getRoute: () => RefreshRoute
getVisitedViews: () => RefreshView[]
refreshPage: (view: RefreshView) => void | Promise<void>
delIframeView?: (route: RefreshRoute) => void | Promise<void>
matchView?: (view: RefreshView, route: RefreshRoute) => boolean
onMissingView?: (route: RefreshRoute) => void
}返回:
type UseRefreshReturn = () => Promise<void>默认匹配规则:
view.path === route.path如果某个宿主使用 fullPath 或 name 管理标签页,可以通过 matchView 覆盖。
宿主接入方式
ERP 这类后台项目保留本地薄包装,页面不需要改 import。
src/hooks/useRefresh.ts:
import { useRoute } from 'vue-router'
import useTagsViewStore from '@/store/modules/tagsView'
import { getCurrentInstance } from 'vue'
import { createUseRefresh } from '@qynpm/hooks'
export default function useRefresh() {
const route = useRoute()
const tagsViewStore = useTagsViewStore()
const { proxy } = getCurrentInstance()!
return createUseRefresh({
getRoute: () => route,
getVisitedViews: () => tagsViewStore.visitedViews,
refreshPage(view) {
proxy.$tab.refreshPage(view)
},
delIframeView(route) {
tagsViewStore.delIframeView(route)
}
})()
}如果想减少每次调用时重新创建工厂,也可以在宿主 hook 内先创建 refresh 再返回:
export default function useRefresh() {
const route = useRoute()
const tagsViewStore = useTagsViewStore()
const { proxy } = getCurrentInstance()!
const refresh = createUseRefresh({
getRoute: () => route,
getVisitedViews: () => tagsViewStore.visitedViews,
refreshPage: (view) => proxy.$tab.refreshPage(view),
delIframeView: (route) => tagsViewStore.delIframeView(route)
})
return refresh
}页面使用方式
旧页面继续这样用:
import useRefresh from '@/hooks/useRefresh'
const refreshView = useRefresh()
async function handleSave() {
await saveData()
await refreshView()
}准备独立发布的公共组件,如果确实需要刷新宿主页面,不应该直接 import ERP 的 @/hooks/useRefresh。应由宿主通过 props、事件或 adapter 注入刷新能力。
生命周期约定
- 公共包不要求必须在组件 setup 内调用,但宿主 adapter 如果使用
useRoute()或getCurrentInstance(),仍然必须放在组件 setup 上下文。 - 找不到当前 view 时默认静默返回,避免误刷新其他标签页。
route.meta.link存在时,刷新后调用delIframeView(route)。refreshPage和delIframeView都允许返回 Promise,方便宿主串接异步 store action。
当前不做
- 不在
@qynpm/hooks里直接 importvue-router。 - 不在
@qynpm/hooks里直接 import 任意项目的tagsViewstore。 - 不在
@qynpm/hooks里读取proxy.$tab。 - 不替业务页面批量改 import。
- 不处理多标签页关闭、固定标签、右键菜单等 TagsView 行为,这些仍归宿主 layout/store 管。
ERP 验证建议
第一阶段只改 ERP 的本地 adapter:
packages/erp/src/hooks/useRefresh.ts验证点:
- 页面原有
import useRefresh from '@/hooks/useRefresh'不变。 - 普通页面调用
refreshView()后,当前标签页重新加载。 route.meta.link页面调用后,会触发tagsViewStore.delIframeView(route)。- 当前路由不在
visitedViews时,不抛错、不刷新其他页面。 - 不把
vue-router、tagsView、$tab引入@qynpm/hooks。
useBottomBar 宿主协议
useBottomBar 第一版已按宿主协议迁入 @qynpm/hooks。旧实现同时承担了全局状态、内容渲染、高度计算、页面生命周期清理和 DOM 查询,不能原样搬进公共包。
当前拆成两层:
@qynpm/uiQyActionBar继续只负责底部操作栏的 UI 布局。- 提供 sticky、左右分区、窄屏换行、底部安全区适配等展示能力。
- 不负责跨页面注册、路由清理或业务按钮编排。
@qynpm/hooks- 只提供底部栏运行时协议。
- 不直接查询
.bottom-bar-content。 - 不直接依赖
vue-router。 - 不直接 import 宿主 store 或页面布局组件。
目标
底部栏 runtime 负责让页面或业务组件把操作区挂到宿主统一容器里,并由宿主决定这个容器如何展示。
典型场景:
- 录单页面底部提交 / 保存 / 取消按钮。
- 详情页底部审核 / 驳回 / 打印按钮。
- 页面切换后自动清理当前页面注册的底部操作区。
- 移动端或窄屏下使用
QyActionBar的换行和安全区样式。
建议 API
当前已实现:
configureBottomBarRuntime(runtime)
getBottomBarRuntime()
getBottomBarEntries()
useBottomBar(render, options)
useBottomBarState(id)协议:
type BottomBarRender = () => VNode | null
type BottomBarRegisterOptions = {
id: string
routeKey?: string
destroyOnUnmount?: boolean
destroyOnRouteChange?: boolean
minHeight?: number
maxHeight?: number
}
type BottomBarEntry = {
id: string
routeKey?: string
content: VNode | null
visible: boolean
height?: number
minHeight?: number
maxHeight?: number
}
type BottomBarRuntime = {
register: (entry: BottomBarEntry) => void
update: (id: string, entry: Partial<BottomBarEntry>) => void
unregister: (id: string) => void
show: (id: string) => void
hide: (id: string) => void
setHeight?: (id: string, height: number) => void
}宿主接入方式
宿主应用提供一个统一底部栏容器,并在初始化阶段注册 runtime。这个容器不要求宿主必须有登录页、左侧菜单或后台 layout。
独立项目最小接入
适用于页面数量不多、只有 App.vue + router-view 的新项目。公共包内部已经维护 getBottomBarEntries(),所以最小宿主可以先注册一组无状态 runtime,让页面注册流程闭环。
src/runtime/setupBottomBarRuntime.ts:
import { configureBottomBarRuntime } from '@qynpm/hooks'
configureBottomBarRuntime({
register() {},
update() {},
unregister() {},
show() {},
hide() {},
setHeight() {}
})src/components/BottomBarHost.vue:
<template>
<div v-if="activeEntry?.visible" class="bottom-bar-host">
<QyActionBar bordered shadow justify="end">
<component :is="activeEntry.content" />
</QyActionBar>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { getBottomBarEntries } from '@qynpm/hooks'
import { QyActionBar } from '@qynpm/ui'
const entries = getBottomBarEntries()
const activeEntry = computed(() => {
const visibleEntries = Array.from(entries.value.values()).filter(
(entry) => entry.visible && entry.content
)
return visibleEntries.at(-1) ?? null
})
</script>
<style scoped>
.bottom-bar-host {
position: fixed;
right: 0;
bottom: 0;
left: 0;
z-index: 50;
}
</style>src/main.ts:
import './runtime/setupBottomBarRuntime'App.vue:
<template>
<router-view />
<BottomBarHost />
</template>
<script setup lang="ts">
import BottomBarHost from '@/components/BottomBarHost.vue'
</script>如果新项目没有统一 layout,也可以把 BottomBarHost 放在某个录单页面根组件里。区别是:放在 App.vue 时跨页面统一承载,放在页面根组件时只服务当前业务域。
页面局部挂载时,不建议继续使用 fixed + left/right 0,否则会脱离当前页面宽度。例如录单页面自身有 max-width 或在后台主内容区内居中展示时,底部栏会铺满宿主主区域。
局部页面推荐写法:
.bottom-bar-host {
position: sticky;
bottom: 40px;
z-index: 50;
width: 100%;
background: #fff;
}其中 bottom 按页面已有底部操作区高度调整;如果新项目没有旧底栏,可以直接用 bottom: 0。
复杂宿主接入
如果宿主已有 Pinia store、后台 layout、路由缓存或多标签页,需要把 runtime 转接到宿主自己的状态管理中:
import { configureBottomBarRuntime } from '@qynpm/hooks'
import { bottomBarStore } from '@/stores/bottomBar'
configureBottomBarRuntime({
register(entry) {
bottomBarStore.register(entry)
},
update(id, entry) {
bottomBarStore.update(id, entry)
},
unregister(id) {
bottomBarStore.unregister(id)
},
show(id) {
bottomBarStore.show(id)
},
hide(id) {
bottomBarStore.hide(id)
},
setHeight(id, height) {
bottomBarStore.setHeight(id, height)
}
})页面使用方式
页面侧只注册内容,不关心容器放在哪里:
import { useBottomBar } from '@qynpm/hooks'
useBottomBar(
() => (
<>
<el-button>取消</el-button>
<el-button type="primary">提交</el-button>
</>
),
{
id: 'create-order-bottom-bar',
destroyOnUnmount: true,
destroyOnRouteChange: true
}
)生命周期约定
id必填,公共包不再递归查找组件名作为隐式 id。- 页面卸载时,如果
destroyOnUnmount不是false,调用runtime.unregister(id)。 - 路由变化清理由宿主 adapter 判断,公共包只接收
routeKey或显式清理调用。 - 内容更新只调用
runtime.update(id, { content }),不在 hook 内查询 DOM 高度。 - 高度计算优先由宿主容器或
QyActionBar自身完成,hook 只保留setHeight协议。
当前不做
- 不把旧
useBottomBar原样搬进@qynpm/hooks。 - 不在公共包内监听
window.resize并直接写全局状态。 - 不在公共包内查询
.bottom-bar-content或任何宿主 DOM class。 - 不在公共包内直接依赖
vue-router。 - 不把底部按钮业务逻辑写进
QyActionBar。
当前验证
@qynpm/hooks 已补充 bottomBar runtime 单测,覆盖:
- 缺少
id时抛出明确错误。 - 缺少宿主 runtime 时抛出明确错误。
configureBottomBarRuntime只接受函数或null。useBottomBar能注册内容并随响应式依赖更新。- 组件卸载时按
destroyOnUnmount清理或保留。 show / hide / setHeight / unregister会调用宿主 runtime。useBottomBarState能读取并操作已有条目。
与 QyActionBar 的关系
QyActionBar 是 UI 壳,解决“怎么展示底部操作区”。
useBottomBar runtime 是状态和注册协议,解决“谁把内容放进宿主底部容器”。
两者可以配合使用,但不互相强依赖。普通页面也可以直接使用 QyActionBar,不一定要接 useBottomBar runtime。
ERP 当前联调示例
ERP 已在平张录单页面做过最小联调:
packages/erp/src/main.js
packages/erp/src/views/transform/index2.vue
packages/erp/src/views/transform/components/BottomBarHost.vue当前验证方式:
main.js注册 no-opconfigureBottomBarRuntime,只用于打通公共协议。index2.vue使用useBottomBar注册bottomBar测试1 / 保存测试1 / 提交测试1。BottomBarHost.vue读取getBottomBarEntries(),并使用QyActionBar展示最后一个可见 entry。- 平张页已有旧 sticky 操作栏,测试宿主使用
position: sticky; width: 100%,避免脱离max-w-[640px]页面容器。 - 旧操作栏暂时保留,新协议接入只用于验证展示、点击和卸载流程。
验证要求
每迁一个宿主 adapter,至少确认:
- 页面原有 import 不变。
mapping / configs / loading / error / getConfig返回字段不变。- 已有缓存命中逻辑不变。
- 接口入参和返回结构经过 adapter 转换后和旧行为一致。
- 不把宿主 API、store、router 引入
@qynpm/hooks。
统一反馈 Runtime
统一反馈 runtime 用来收口组件和业务薄封装里的提示、确认、通知和局部浮层提示能力。
目标是让公共包不直接绑定 ElMessage / ElMessageBox / ElNotification,也不在每个组件里重复写一套反馈实现。
放置位置
源码位置:
@qiyin/hooks/src/feedback发布到:
@qynpm/hooks不建议放到:
@qynpm/ui:反馈 runtime 不是 UI 组件。@qynpm/form:反馈能力不只服务表单。@qynpm/product-preview:商品包当前有feedback注入,但后续应向统一 runtime 收口。
API 形态
当前对外提供:
configureFeedbackRuntime(runtime)
getFeedbackRuntime()
useFeedback()
message(options)
confirm(options)
notify(options)
popperMessage(target, message, options)当前实现状态:
- 已实现
configureFeedbackRuntime - 已实现
getFeedbackRuntime - 已实现
useFeedback - 已实现
message / confirm / notify - 已实现
popperMessage协议和导出方法 - ERP 已接入
setupPopperMessageRuntime(),宿主未注入时仍会抛出明确错误
协议:
type FeedbackMessageOptions = {
message: string
type?: 'success' | 'warning' | 'info' | 'error'
duration?: number
}
type FeedbackConfirmOptions = {
title?: string
message: string
confirmButtonText?: string
cancelButtonText?: string
type?: 'success' | 'warning' | 'info' | 'error'
}
type FeedbackNotifyOptions = FeedbackMessageOptions & {
title?: string
}
type FeedbackPopperTarget =
| HTMLElement
| { x: number; y: number }
| 'mouse'宿主接入方式
宿主项目只需要在初始化层注册一次,推荐放在:
main.ts/main.jssetupFeedbackRuntime.ts- 项目公共初始化入口
以 Element Plus 为例:
import { configureFeedbackRuntime } from '@qynpm/hooks'
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
configureFeedbackRuntime({
message(options) {
return ElMessage(options)
},
confirm(options) {
return ElMessageBox.confirm(options.message, options.title, options)
},
notify(options) {
return ElNotification(options)
}
})如果希望拆成独立初始化文件,可以这样写:
// setupFeedbackRuntime.ts
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
import { configureFeedbackRuntime } from '@qynpm/hooks'
export function setupFeedbackRuntime() {
configureFeedbackRuntime({
message(options) {
return ElMessage(options)
},
confirm(options) {
return ElMessageBox.confirm(options.message, options.title, options)
},
notify(options) {
return ElNotification(options)
}
})
}然后在应用启动入口调用:
import { setupFeedbackRuntime } from './setupFeedbackRuntime'
setupFeedbackRuntime()使用方式
公共包或新拆出来的业务薄封装中,优先从 @qynpm/hooks 调用反馈能力:
import { message, confirm, notify, useFeedback } from '@qynpm/hooks'
message({
type: 'success',
message: '保存成功'
})
await confirm({
title: '提示',
message: '确定删除当前数据吗?',
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
notify({
title: '处理完成',
type: 'success',
message: '数据已同步'
})
const feedback = useFeedback()
feedback.message({
type: 'warning',
message: '请先选择数据'
})也可以读取当前 runtime,用于调试或桥接其它包:
import { getFeedbackRuntime } from '@qynpm/hooks'
const runtime = getFeedbackRuntime()未注册行为
@qynpm/hooks 不内置 Element Plus fallback。宿主未注册对应能力时,调用会抛出明确错误:
@qynpm/hooks 缺少 feedback 运行时配置:请在应用启动前通过 configureFeedbackRuntime 注入 message这样可以避免公共包在内部偷偷绑定某个 UI 框架,也能更早暴露宿主漏接 runtime 的问题。
popperMessage
popperMessage 用来承接旧 usePopperMessage 的“鼠标 / 元素 / 坐标附近轻量提示”能力。公共包只暴露协议,不内置 DOM 浮层实现。
import { popperMessage } from '@qynpm/hooks'
popperMessage('mouse', '提示内容')旧实现不适合原样迁入公共包,原因:
- 依赖
window.addEventListener('mousemove')全局监听。 - 依赖 JSX、CSS、Tailwind class 和
useCreateInstance。 - 每次调用
usePopperMessage()都会新增一次mousemove监听,旧代码没有清理。 - 定位和动画都是宿主 UI 行为,不应该塞进
@qynpm/hooks。
推荐方案是在宿主实现 setupPopperMessageRuntime(),启动时只执行一次:
// src/runtime/setupPopperMessageRuntime.ts
import { h, render } from 'vue'
import { configureFeedbackRuntime, type FeedbackPopperTarget } from '@qynpm/hooks'
type PopperMessageType = 'success' | 'warning' | 'error' | 'info'
type PopperMessageOptions = {
duration?: number
type?: PopperMessageType
}
let installed = false
let lastMousePosition = {
x: 0,
y: 0
}
function resolveTargetPosition(target: FeedbackPopperTarget) {
if (target === 'mouse') return lastMousePosition
if (target instanceof HTMLElement) {
const rect = target.getBoundingClientRect()
return {
x: rect.x,
y: rect.y
}
}
return target
}
function showPopperMessage(
target: FeedbackPopperTarget,
message: string,
options: PopperMessageOptions = {}
) {
const { duration = 1500, type = 'success' } = options
const position = resolveTargetPosition(target)
const container = document.createElement('div')
document.body.appendChild(container)
const destroy = () => {
render(null, container)
container.remove()
}
render(
h(
'div',
{
class: ['qiyin-popper-message', `is-${type}`],
style: {
left: `${position.x}px`,
top: `${position.y}px`
}
},
message
),
container
)
window.setTimeout(destroy, duration)
return {
destroy
}
}
export function setupPopperMessageRuntime() {
if (installed) return
installed = true
window.addEventListener('mousemove', (event) => {
lastMousePosition = {
x: event.clientX,
y: event.clientY
}
})
configureFeedbackRuntime({
popperMessage: showPopperMessage
})
}配套样式由宿主提供,不进入 @qynpm/hooks:
.qiyin-popper-message {
position: fixed;
z-index: 10000;
user-select: none;
pointer-events: none;
padding: 4px 8px;
border-radius: 4px;
color: #fff;
font-size: 12px;
line-height: 1.2;
box-shadow: 0 4px 12px rgb(15 23 42 / 16%);
animation: qiyin-popper-message-slide-in 0.2s ease-out forwards;
}
.qiyin-popper-message.is-success {
background: #22c55e;
}
.qiyin-popper-message.is-warning {
background: #eab308;
}
.qiyin-popper-message.is-error {
background: #ef4444;
}
.qiyin-popper-message.is-info {
background: #64748b;
}
@keyframes qiyin-popper-message-slide-in {
from {
transform: translateY(0);
opacity: 0.95;
}
to {
transform: translateY(-24px);
opacity: 1;
}
}然后在宿主初始化入口调用:
import { setupPopperMessageRuntime } from './runtime/setupPopperMessageRuntime'
setupPopperMessageRuntime()未注入 popperMessage 时调用会抛出明确错误。
迁移边界
- 公共组件不直接 import
ElMessage / ElMessageBox / ElNotification。 - 业务薄封装优先调用统一 runtime,不自己实现确认弹窗。
usePopperMessage不原样迁移,统一收敛为popperMessage能力。@qynpm/product-preview当前已有feedbackadapter,后续可以在宿主层把它转接到统一 runtime。- 第一版允许宿主只实现
message / confirm / notify,需要局部浮层提示时再接popperMessage。
ERP 当前联调示例
ERP 已在启动入口注册 runtime:
packages/erp/src/main.js当前在老录单 transform 页面放了三处临时验证点:
message:覆盖提示显示覆盖成功1confirm:生成新订单号确认框显示是否继续1?notify:点击关联生产单显示通知成功1
验证完成后,这些测试文案应恢复为正常业务文案,后续再按模块逐步替换真实调用点。
当前联调方式
根目录 vite.config.ts 里有统一联调开关:
ENABLE_LOCAL_QIYIN_DEBUG = true@qynpm/hooks走本地 @qiyin/hooks/index.ts
ENABLE_LOCAL_QIYIN_DEBUG = false@qynpm/hooks走包正常入口
注意:
- 当前联调只接管
@qynpm/hooks - 老的
@qiyin/hooks仍然保留原有实现 - 不要把
@qiyin/hooks直接整体 alias 到这个新包,否则会影响现有业务
当前使用建议
如果是新拆出来、准备发布 npm 的能力:
- 优先从
@qynpm/hooks增加和引用
如果是老业务还在跑的历史能力:
- 继续走原来的
@qiyin/hooks
后续迁移原则
后面其他 hooks 迁移时,按下面的顺序处理:
- 先判断是不是公共能力
- 如果依赖业务 store / router / 页面上下文,先解耦
- 解耦完成后再迁到
@qynpm/hooks - 验证稳定后,再逐步把业务引用切到 npm 包
当前不建议原样直接迁移的,通常是这类:
- 强依赖
@/store - 强依赖具体业务模块
- 只在单一业务场景内使用的 hooks
已迁移说明
这次迁移里:
@qynpm/form内部的useSave已改为依赖@qynpm/hooks的useCreateInstanceerp根应用已经接入useInstanceView,可用于验证新实例池链路
构建
pnpm run build