Support custom component action

This commit is contained in:
the1812 2022-05-12 23:03:23 +08:00
parent 010d2243ba
commit 46ee30204d
5 changed files with 186 additions and 99 deletions

View File

@ -0,0 +1,160 @@
<template>
<div>
<div
v-if="canStartDebug"
class="component-action dev-client-action"
@click="startDebug"
>
<VIcon v-if="busy" icon="mdi-network-outline" :size="16" />
<VIcon v-else icon="mdi-play-network-outline" :size="16" />
{{ busy ? '启动中' : '开始调试' }}
</div>
<div
v-if="canStopDebug"
class="component-action dev-client-action"
@click="stopDebug"
>
<VIcon v-if="busy" icon="mdi-network-outline" :size="16" />
<VIcon v-else icon="mdi-minus-network-outline" :size="16" />
{{ busy ? '停止中' : '停止调试' }}
</div>
</div>
</template>
<script lang="ts">
import { OptionsOfMetadata } from '@/components/define'
import { ComponentMetadata } from '@/components/types'
import { getComponentSettings } from '@/core/settings'
import { Toast } from '@/core/toast'
import { VIcon } from '@/ui'
import { autoUpdateOptions, devClientOptionsMetadata } from './options'
const { options } = getComponentSettings<OptionsOfMetadata<typeof devClientOptionsMetadata>>('devClient')
const converter = {
toDevUrl: (url: string) => {
if (!url) {
return null
}
const devUrlMatch = url.match(new RegExp(`localhost:${options.port}\\/registry\\/components\\/(.+)$`))
if (devUrlMatch) {
return url
}
const localhostMatch = url.match(/localhost:(\d+?)\/components\/(.+)$/)
if (localhostMatch) {
return `http://localhost:${options.port}/registry/dist/components/${localhostMatch[2]}`
}
const onlineMatch = url.match(/\/registry\/dist\/components\/(.+)$/)
if (onlineMatch) {
return `http://localhost:${options.port}/registry/dist/components/${onlineMatch[1]}`
}
return null
},
}
export default Vue.extend({
components: {
VIcon,
},
props: {
item: {
type: Object,
required: true,
},
component: {
type: Object,
required: true,
},
},
data() {
return {
busy: false,
autoUpdateComponents: autoUpdateOptions.urls.components,
sessions: [],
}
},
computed: {
autoUpdateRecord() {
const metadata = this.component as ComponentMetadata
return this.autoUpdateComponents[metadata.name]
},
componentUpdateUrl() {
return this.autoUpdateRecord?.url
},
isDebugging() {
return this.componentUpdateUrl && this.sessions.some((path: string) => {
const { pathname } = new URL(this.componentUpdateUrl)
return path === pathname
})
},
canStartDebug() {
return !this.isDebugging && converter.toDevUrl(this.componentUpdateUrl) !== null
},
canStopDebug() {
return Boolean(this.isDebugging && this.componentUpdateUrl)
},
},
async created() {
const { devClient } = await import('./client')
this.sessions = devClient.sessions
devClient.addEventListener('sessionsUpdate', this.handleSessionsUpdate)
},
async beforeDestroy() {
const { devClient } = await import('./client')
devClient.removeEventListener('sessionsUpdate', this.handleSessionsUpdate)
},
methods: {
handleSessionsUpdate(e: CustomEvent<string[]>) {
this.sessions = e.detail
},
async handleClick(action: () => Promise<void>) {
if (this.busy) {
return
}
try {
this.busy = true
await action()
} finally {
this.busy = false
}
},
async startDebug() {
await this.handleClick(async () => {
const { devClient } = await import('./client')
const metadata = this.component as ComponentMetadata
const devUrl = converter.toDevUrl(this.componentUpdateUrl)
console.log('devUrl:', devUrl, 'autoUpdateRecord.url:', this.autoUpdateRecord.url)
if (this.autoUpdateRecord.url !== devUrl) {
options.devRecords[metadata.name] = {
name: metadata.name,
originalUrl: this.componentUpdateUrl,
}
this.autoUpdateRecord.url = devUrl
}
const toast = Toast.info('启动调试中...', 'DevClient')
try {
await devClient.startDebug(this.autoUpdateRecord.url)
} catch (error) {
console.error(error)
} finally {
toast.close()
}
})
},
async stopDebug() {
await this.handleClick(async () => {
const { devClient } = await import('./client')
const metadata = this.component as ComponentMetadata
const { pathname } = new URL(this.componentUpdateUrl)
await devClient.stopDebug(pathname)
if (options.devRecords[metadata.name]) {
this.autoUpdateRecord.url = options.devRecords[metadata.name].originalUrl
delete options.devRecords[metadata.name]
}
})
},
},
})
</script>
<style lang="scss">
.dev-client-action {
}
</style>

View File

@ -165,6 +165,7 @@ export class DevClient extends EventTarget {
handleSocketMessage(e, payload => {
if (payload.type === 'querySessionsResponse') {
this.sessions = payload.sessions
this.dispatchEvent(new CustomEvent('sessionsUpdate', { detail: payload.sessions }))
resolve(payload.sessions)
}
})

View File

@ -1,106 +1,19 @@
import { PluginSetupParameters } from '@/plugins/plugin'
import { ComponentAction } from '@/components/settings-panel/component-actions/component-actions'
import { isIframe } from '@/core/utils'
import { Toast } from '@/core/toast'
import { getComponentSettings } from '@/core/settings'
import { ComponentMetadata } from '@/components/types'
import { OptionsOfMetadata } from '@/components/define'
import { autoUpdateOptions, devClientOptionsMetadata } from './options'
export const setupPlugin = async ({ addData }: PluginSetupParameters) => {
if (isIframe()) {
return
}
const { options } = getComponentSettings<OptionsOfMetadata<typeof devClientOptionsMetadata>>('devClient')
const { devClient } = await import('./client')
addData('settingsPanel.componentActions', (actions: ComponentAction[]) => {
const converter = {
toDevUrl: (url: string) => {
if (!url) {
return null
}
const devUrlMatch = url.match(new RegExp(`localhost:${options.port}\\/registry\\/components\\/(.+)$`))
if (devUrlMatch) {
return url
}
const localhostMatch = url.match(/localhost:(\d+?)\/components\/(.+)$/)
if (localhostMatch) {
return `http://localhost:${options.port}/registry/dist/components/${localhostMatch[2]}`
}
const onlineMatch = url.match(/\/registry\/dist\/components\/(.+)$/)
if (onlineMatch) {
return `http://localhost:${options.port}/registry/dist/components/${onlineMatch[1]}`
}
return null
},
}
const getDebugInfo = (metadata: ComponentMetadata) => {
const autoUpdateRecord = autoUpdateOptions.urls.components[metadata.name]
const componentUpdateUrl = autoUpdateRecord?.url
const isDebugging = () => componentUpdateUrl && devClient.sessions.some(path => {
const { pathname } = new URL(componentUpdateUrl)
return path === pathname
})
const canStartDebug = () => !isDebugging() && converter.toDevUrl(componentUpdateUrl) !== null
const canStopDebug = () => Boolean(isDebugging() && componentUpdateUrl)
return {
autoUpdateRecord,
componentUpdateUrl,
canStartDebug,
canStopDebug,
}
}
actions.push(
metadata => {
const { componentUpdateUrl, autoUpdateRecord, canStartDebug } = getDebugInfo(metadata)
const result = {
name: 'startDebug',
displayName: '开始调试',
icon: 'mdi-play-network-outline',
visible: canStartDebug(),
action: async () => {
const devUrl = converter.toDevUrl(componentUpdateUrl)
console.log('devUrl:', devUrl, 'autoUpdateRecord.url:', autoUpdateRecord.url)
if (autoUpdateRecord.url !== devUrl) {
options.devRecords[metadata.name] = {
name: metadata.name,
originalUrl: componentUpdateUrl,
}
autoUpdateRecord.url = devUrl
}
const toast = Toast.info('启动调试中...', 'DevClient')
try {
await devClient.startDebug(autoUpdateRecord.url)
} catch (error) {
console.error(error)
} finally {
toast.close()
result.visible = canStartDebug()
console.log('visible', result.visible)
}
},
() => {
const ActionModule = () => import('./Action.vue')
return {
name: 'devClient',
component: ActionModule,
}
return result
},
metadata => {
const { componentUpdateUrl, autoUpdateRecord, canStopDebug } = getDebugInfo(metadata)
const result = {
name: 'stopDebug',
displayName: '停止调试',
icon: 'mdi-minus-network-outline',
visible: canStopDebug(),
action: async () => {
const { pathname } = new URL(componentUpdateUrl)
await devClient.stopDebug(pathname)
if (options.devRecords[metadata.name]) {
autoUpdateRecord.url = options.devRecords[metadata.name].originalUrl
delete options.devRecords[metadata.name]
}
result.visible = canStopDebug()
console.log('visible', result.visible)
},
}
return result
},
)
})

View File

@ -68,14 +68,24 @@
</div>
<template #toast>
<div class="extra-actions-list">
<ComponentAction
<div
v-for="a of componentActions"
v-show="a.visible !== false"
:key="a.name"
class="extra-action-item"
:item="a"
:component="componentData"
/>
>
<component
:is="a.component"
v-if="a.component"
:item="a"
:component="componentData"
/>
<ComponentAction
v-else
v-show="a.visible !== false"
class="extra-action-item"
:item="a"
:component="componentData"
/>
</div>
</div>
</template>
</MiniToast>

View File

@ -1,5 +1,5 @@
import { registerAndGetData } from '@/plugins/data'
import { Executable } from '@/core/common-types'
import { Executable, VueModule } from '@/core/common-types'
import { getHook } from '@/plugins/hook'
import { isUserComponent } from '@/core/settings'
import { ComponentMetadata } from '../../types'
@ -13,6 +13,9 @@ export type ComponentAction = (metadata: ComponentMetadata) => {
visible?: boolean
title?: string
// condition?: () => boolean
} | {
name: string
component: Executable<VueModule>
}
const builtInActions: ComponentAction[] = [