@axewo/fin-micro 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -3
- package/cli/index.mjs +22 -6
- package/dist/core.cjs +1 -1
- package/dist/core.mjs +131 -127
- package/dist/layout.cjs +1 -0
- package/dist/layout.css +1 -0
- package/dist/layout.mjs +719 -0
- package/dist/types/layout/HostShell.vue.d.ts +528 -0
- package/dist/types/layout/ShellHeader.vue.d.ts +46 -0
- package/dist/types/layout/ShellMenu.vue.d.ts +12 -0
- package/dist/types/layout/ShellTabs.vue.d.ts +23 -0
- package/dist/types/layout/StandaloneMenuItem.vue.d.ts +11 -0
- package/dist/types/layout/StandaloneShell.vue.d.ts +40 -0
- package/dist/types/layout/index.d.ts +5 -0
- package/dist/types/layout/types.d.ts +65 -0
- package/dist/types/sdk/index.d.ts +2 -0
- package/dist/types/watermark/FinWatermark.vue.d.ts +16 -0
- package/dist/types/watermark/config.d.ts +36 -0
- package/dist/types/watermark/index.d.ts +4 -0
- package/dist/watermark.cjs +2 -0
- package/dist/watermark.mjs +119 -0
- package/package.json +28 -3
- package/scaffold/base/index.html +2 -1
- package/scaffold/base/pnpm-workspace.yaml +6 -0
- package/scaffold/base/src/router/demo.ts +1 -1
- package/scaffold/base/src/utils/errorUtils.ts +19 -2
- package/scaffold/base/src/utils/watermarkConfig.ts +18 -0
- package/scaffold/base/src/views/{DemoView.vue → demo/index.vue} +1 -1
- package/scaffold/host/src/layout/HostMenu.vue +10 -2
- package/scaffold/host/src/layout/HostTabs.vue +55 -5
- package/scaffold/host/src/layout/HostWorkspace.vue +12 -2
- package/scaffold/host/src/localPages.ts +2 -2
- package/scaffold/host/src/platform.ts +12 -6
- package/scaffold/host/src/router/index.ts +3 -3
- package/scaffold/host/src/styles/shell.css +16 -1
- package/scaffold/host/src/views/{LoginView.vue → login/components/LoginForm.vue} +7 -12
- package/scaffold/host/src/views/{LoginRoute.vue → login/index.vue} +5 -5
- package/scaffold/host/src/views/{ProfileView.vue → profile/index.vue} +1 -1
- package/scaffold/host/src/views/{WorkspaceRoute.vue → workspace/index.vue} +3 -3
- package/scaffold/layouts/standard/host/src/layout/HostLayout.vue +116 -0
- package/scaffold/layouts/standard/host/src/styles/shell.css +168 -0
- package/scaffold/layouts/standard/vue/src/layout/StandaloneLayout.vue +26 -0
- package/scaffold/vue/src/App.vue +13 -7
- package/scaffold/vue/src/bootstrap.ts +7 -2
- package/scaffold/vue/src/layout/Header.vue +15 -0
- package/scaffold/vue/src/layout/Sidebar.vue +21 -0
- package/scaffold/vue/src/layout/SidebarItem.vue +21 -0
- package/scaffold/vue/src/layout/StandaloneLayout.vue +34 -0
- package/scaffold/vue/src/router/index.ts +8 -5
- package/scaffold/vue/src/router/menu.ts +38 -0
- package/scaffold/vue/src/views/demo/configs/table.ts +24 -0
- package/scaffold/vue/src/views/demo/hooks/useRecordNavigation.ts +32 -0
- package/scaffold/vue/src/views/demo/index.vue +17 -0
- package/scaffold/vue/src/views/{DetailView.vue → demo/modules/detail/index.vue} +2 -2
- package/scaffold/vue/src/views/demo/types.ts +5 -0
- package/scaffold/vue/src/views/ListView.vue +0 -55
- /package/scaffold/host/src/views/{HomeView.vue → home/index.vue} +0 -0
- /package/scaffold/host/src/views/{NotFoundView.vue → not-found/index.vue} +0 -0
- /package/scaffold/vue/src/views/{NotFoundView.vue → not-found/index.vue} +0 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { FinWatermark } from '@axewo/fin-micro/watermark'
|
|
4
|
+
import { watermarkConfig } from '../utils/watermarkConfig'
|
|
5
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
6
|
+
import { createStandaloneMenus, standaloneActiveMenu } from '../router/menu'
|
|
7
|
+
import Header from './Header.vue'
|
|
8
|
+
import Sidebar from './Sidebar.vue'
|
|
9
|
+
|
|
10
|
+
// 使用 router 实际声明树保留父子关系,不用 getRoutes 的扁平记录重复生成菜单。
|
|
11
|
+
const router = useRouter()
|
|
12
|
+
const route = useRoute()
|
|
13
|
+
const menus = computed(() => createStandaloneMenus(router.options.routes))
|
|
14
|
+
const activeMenu = computed(() => standaloneActiveMenu(route))
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<div class="standalone-layout">
|
|
19
|
+
<Header :title="route.meta.title ?? '业务应用'" />
|
|
20
|
+
<Sidebar :menus="menus" :active-menu="activeMenu" />
|
|
21
|
+
<!-- App 仅在独立运行时挂载此布局;嵌入时使用基座水印,避免重复。 -->
|
|
22
|
+
<div class="standalone-viewport">
|
|
23
|
+
<main class="standalone-content"><slot /></main>
|
|
24
|
+
<FinWatermark :config="watermarkConfig" />
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<style scoped>
|
|
30
|
+
.standalone-layout { display: grid; grid-template: 64px minmax(0, 1fr) / 220px minmax(0, 1fr); height: 100vh; height: 100dvh; overflow: hidden; }
|
|
31
|
+
.standalone-viewport { position: relative; isolation: isolate; display: flex; min-width: 0; min-height: 0; overflow: hidden; }
|
|
32
|
+
.standalone-content { flex: 1 1 auto; min-width: 0; min-height: 0; overflow: auto; }
|
|
33
|
+
@media (max-width: 760px) { .standalone-layout { grid-template-columns: 160px minmax(0, 1fr); } }
|
|
34
|
+
</style>
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { createMemoryHistory, createRouter, createWebHashHistory } from 'vue-router'
|
|
2
2
|
import { demoRoutes } from './demo'
|
|
3
|
+
import { List, Tickets } from '@element-plus/icons-vue'
|
|
4
|
+
import './menu'
|
|
3
5
|
|
|
4
6
|
/** 嵌入模式使用 memory history 不争用宿主 URL;独立模式使用 hash 路由便于静态部署。 */
|
|
5
7
|
export function createMicroRouter(embedded: boolean) {
|
|
6
8
|
return createRouter({
|
|
7
9
|
history: embedded ? createMemoryHistory() : createWebHashHistory(),
|
|
8
10
|
routes: [
|
|
9
|
-
{ path: '/', redirect: '/list' },
|
|
10
|
-
{ path: '/list', component: () => import('../views/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
{
|
|
11
|
+
{ path: '/', redirect: '/list', meta: { hidden: true } },
|
|
12
|
+
{ path: '/list', component: () => import('../views/demo/index.vue'), meta: { title: '业务列表', icon: List, order: 10 } },
|
|
13
|
+
// 详情保留同一业务页面与 SDK,只隐藏菜单入口并高亮所属列表。
|
|
14
|
+
{ path: '/detail', component: () => import('../views/demo/modules/detail/index.vue'), meta: { title: '业务详情', hidden: true, activeMenu: '/list' } },
|
|
15
|
+
...demoRoutes.map(route => ({ ...route, meta: { ...route.meta, title: '基础示例', icon: Tickets, order: 20 } })),
|
|
16
|
+
{ path: '/:pathMatch(.*)*', component: () => import('../views/not-found/index.vue'), meta: { title: '页面不存在', hidden: true } },
|
|
14
17
|
],
|
|
15
18
|
})
|
|
16
19
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Component } from 'vue'
|
|
2
|
+
import type { RouteLocationNormalizedLoaded, RouteRecordRaw } from 'vue-router'
|
|
3
|
+
|
|
4
|
+
/** 独立导航只消费本地路由元信息,不表示宿主授权或后台接口权限。 */
|
|
5
|
+
declare module 'vue-router' {
|
|
6
|
+
interface RouteMeta {
|
|
7
|
+
title?: string
|
|
8
|
+
icon?: Component
|
|
9
|
+
order?: number
|
|
10
|
+
hidden?: boolean
|
|
11
|
+
activeMenu?: string
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface StandaloneMenu {
|
|
16
|
+
path: string
|
|
17
|
+
title: string
|
|
18
|
+
icon?: Component
|
|
19
|
+
children: StandaloneMenu[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 按声明层级生成菜单;隐藏父级隐藏整棵子树,无标题分组提升子项。 */
|
|
23
|
+
export function createStandaloneMenus(routes: readonly RouteRecordRaw[], parent = ''): StandaloneMenu[] {
|
|
24
|
+
return [...routes].sort((left, right) => (left.meta?.order ?? 0) - (right.meta?.order ?? 0)).flatMap(route => {
|
|
25
|
+
if (route.meta?.hidden || route.path.includes(':')) return []
|
|
26
|
+
const path = route.path.startsWith('/') ? route.path : `${parent}/${route.path}`.replace(/\/+/g, '/')
|
|
27
|
+
const children = createStandaloneMenus(route.children ?? [], path)
|
|
28
|
+
const title = route.meta?.title
|
|
29
|
+
// 重定向仅在有可见子菜单时充当分组,默认跳转和别名不额外生成入口。
|
|
30
|
+
if (!title || (route.redirect && children.length === 0)) return children
|
|
31
|
+
return [{ path, title, icon: route.meta?.icon, children }]
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 详情可通过 activeMenu 指向列表;查询和片段不参与菜单身份。 */
|
|
36
|
+
export function standaloneActiveMenu(route: Pick<RouteLocationNormalizedLoaded, 'path' | 'meta'>): string {
|
|
37
|
+
return route.meta.activeMenu ?? route.path
|
|
38
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { TableConfig } from '@axewo/fin-ui'
|
|
2
|
+
import type { RecordRow } from '../types'
|
|
3
|
+
|
|
4
|
+
/** 业务列表的 fin-ui 列、表单和 CRUD 配置;不复制弹窗、删除确认或分页状态机。 */
|
|
5
|
+
export function createRecordTableConfig(onSuccess: () => void): TableConfig {
|
|
6
|
+
// 内存适配仅用于展示组件 API;每次进入页面独立实例,刷新重置,不能充当业务接口。
|
|
7
|
+
let rows: RecordRow[] = [{ id: 1, name: '示例记录' }]
|
|
8
|
+
let nextId = 2
|
|
9
|
+
return {
|
|
10
|
+
rowKey: 'id', title: '示例记录', pagination: false,
|
|
11
|
+
columns: [
|
|
12
|
+
{ field: 'id', label: '编号', width: 100 },
|
|
13
|
+
{ field: 'name', label: '名称', formItem: { component: 'FInput', required: true, componentProps: { maxlength: 80 } } },
|
|
14
|
+
],
|
|
15
|
+
requestConfig: { api: async () => ({ list: rows.map(row => ({ ...row })), total: rows.length }) },
|
|
16
|
+
crudConfig: {
|
|
17
|
+
// FCrudTable 统一生成新增/编辑弹框、校验、删除确认及刷新,模板不重复实现。
|
|
18
|
+
createApi: async data => { rows.push({ id: nextId++, name: String(data.name) }) },
|
|
19
|
+
updateApi: async data => { rows = rows.map(row => row.id === data.id ? { ...row, name: String(data.name) } : row) },
|
|
20
|
+
deleteApi: async data => { rows = rows.filter(row => row.id !== data.id) },
|
|
21
|
+
onSuccess,
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
import { useRouter } from 'vue-router'
|
|
3
|
+
import { useMicroPage, useMicroTab, usePermission } from '@axewo/fin-micro'
|
|
4
|
+
import { useBridgeSession } from '../../../bridge/session'
|
|
5
|
+
import { toSafeError } from '../../../utils/errorUtils'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 统一列表权限、宿主页签 SDK 与独立路由导航,页面只装配业务动作。
|
|
9
|
+
* 必须在列表组件 setup 内调用,SDK 绑定当前 tab.key;异步返回不能误操作另一页。
|
|
10
|
+
*/
|
|
11
|
+
export function useRecordNavigation() {
|
|
12
|
+
const { bridge, identity } = useBridgeSession()
|
|
13
|
+
const canRead = usePermission(identity).can('demo:read')
|
|
14
|
+
const router = useRouter()
|
|
15
|
+
const tab = bridge ? useMicroTab() : undefined
|
|
16
|
+
const sdk = bridge && tab ? useMicroPage(bridge, tab) : undefined
|
|
17
|
+
const message = ref('')
|
|
18
|
+
|
|
19
|
+
/** 错误只展示安全分类文案,不泄漏异常对象、请求体或 Token。 */
|
|
20
|
+
function reportError(error: unknown): void { message.value = toSafeError(error).message }
|
|
21
|
+
|
|
22
|
+
/** 嵌入模式经授权打开同应用详情页签;独立模式由本应用 router 打开。 */
|
|
23
|
+
async function openDetail(): Promise<void> {
|
|
24
|
+
try {
|
|
25
|
+
if (bridge && !canRead.value) { message.value = '403:无权访问详情'; return }
|
|
26
|
+
if (sdk && bridge) await sdk.openTab({ appId: bridge.appId, tabId: `${bridge.appId}:detail:1`, title: '示例详情 1', route: '/detail?id=1' })
|
|
27
|
+
else await router.push('/detail?id=1')
|
|
28
|
+
} catch (error) { reportError(error) }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return { bridge, canRead, message, openDetail, reportError }
|
|
32
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { FCrudTable } from '@axewo/fin-ui'
|
|
3
|
+
import { createRecordTableConfig } from './configs/table'
|
|
4
|
+
import { useRecordNavigation } from './hooks/useRecordNavigation'
|
|
5
|
+
|
|
6
|
+
const { bridge, canRead, message, openDetail, reportError } = useRecordNavigation()
|
|
7
|
+
const tableConfig = createRecordTableConfig(() => { message.value = '演示内存数据已更新(刷新后重置)' })
|
|
8
|
+
</script>
|
|
9
|
+
<template>
|
|
10
|
+
<section class="panel">
|
|
11
|
+
<h1>__PROJECT_NAME__</h1>
|
|
12
|
+
<p>以下 CRUD 为内存示例,非生产业务接口;新增、编辑和删除均使用 fin-ui 内置流程。</p>
|
|
13
|
+
<div class="actions"><el-button :disabled="Boolean(bridge) && !canRead" @click="openDetail">打开详情页</el-button></div>
|
|
14
|
+
<p v-if="message" role="status">{{ message }}</p>
|
|
15
|
+
<FCrudTable :table-config="tableConfig" @error="reportError" />
|
|
16
|
+
</section>
|
|
17
|
+
</template>
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { ref } from 'vue'
|
|
3
3
|
import { useRoute, useRouter } from 'vue-router'
|
|
4
4
|
import { useMicroPage, useMicroTab, usePermission } from '@axewo/fin-micro'
|
|
5
|
-
import { useBridgeSession } from '
|
|
6
|
-
import { toSafeError } from '
|
|
5
|
+
import { useBridgeSession } from '../../../../bridge/session'
|
|
6
|
+
import { toSafeError } from '../../../../utils/errorUtils'
|
|
7
7
|
const { bridge, identity } = useBridgeSession()
|
|
8
8
|
const permission = usePermission(identity)
|
|
9
9
|
const canRead = permission.can('demo:read')
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { ref } from 'vue'
|
|
3
|
-
import { useRouter } from 'vue-router'
|
|
4
|
-
import { FCrudTable, type TableConfig } from '@axewo/fin-ui'
|
|
5
|
-
import { useMicroPage, useMicroTab, usePermission } from '@axewo/fin-micro'
|
|
6
|
-
import { useBridgeSession } from '../bridge/session'
|
|
7
|
-
import { toSafeError } from '../utils/errorUtils'
|
|
8
|
-
const { bridge, identity } = useBridgeSession()
|
|
9
|
-
const permission = usePermission(identity)
|
|
10
|
-
const canRead = permission.can('demo:read')
|
|
11
|
-
const router = useRouter()
|
|
12
|
-
// SDK 在 setup 中绑定缓存边界的固定 tab.key;后续异步操作不会误操作另一个活动页。
|
|
13
|
-
const tab = bridge ? useMicroTab() : undefined
|
|
14
|
-
const sdk = bridge && tab ? useMicroPage(bridge, tab) : undefined
|
|
15
|
-
const message = ref('')
|
|
16
|
-
/** 组件异常只展示文本,兼容公开版本未提供强类型事件声明的情况。 */
|
|
17
|
-
function reportError(error: unknown): void { message.value = toSafeError(error).message }
|
|
18
|
-
interface RecordRow { id: number; name: string }
|
|
19
|
-
// 最小内存适配器仅演示组件 API;每个页面独立数据,刷新会重置,生产请替换为真实业务请求。
|
|
20
|
-
let rows: RecordRow[] = [{ id: 1, name: '示例记录' }]
|
|
21
|
-
let nextId = 2
|
|
22
|
-
const tableConfig: TableConfig = {
|
|
23
|
-
rowKey: 'id', title: '示例记录', pagination: false,
|
|
24
|
-
columns: [
|
|
25
|
-
{ field: 'id', label: '编号', width: 100 },
|
|
26
|
-
{ field: 'name', label: '名称', formItem: { component: 'FInput', required: true, componentProps: { maxlength: 80 } } },
|
|
27
|
-
],
|
|
28
|
-
requestConfig: { api: async () => ({ list: rows.map(row => ({ ...row })), total: rows.length }) },
|
|
29
|
-
crudConfig: {
|
|
30
|
-
// 由 FCrudTable 统一生成新增/编辑弹框及删除确认,不另造重复表单。
|
|
31
|
-
createApi: async data => { rows.push({ id: nextId++, name: String(data.name) }) },
|
|
32
|
-
updateApi: async data => { rows = rows.map(row => row.id === data.id ? { ...row, name: String(data.name) } : row) },
|
|
33
|
-
deleteApi: async data => { rows = rows.filter(row => row.id !== data.id) },
|
|
34
|
-
onSuccess: () => { message.value = '演示内存数据已更新(刷新后重置)' },
|
|
35
|
-
},
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** 同应用详情用页面 SDK 打开宿主页签;独立模式才使用普通 router,错误留在原页面。 */
|
|
39
|
-
async function openDetail(): Promise<void> {
|
|
40
|
-
try {
|
|
41
|
-
if (bridge && !canRead.value) { message.value = '403:无权访问详情'; return }
|
|
42
|
-
if (sdk && bridge) await sdk.openTab({ appId: bridge.appId, tabId: `${bridge.appId}:detail:1`, title: '示例详情 1', route: '/detail?id=1' })
|
|
43
|
-
else await router.push('/detail?id=1')
|
|
44
|
-
} catch (error) { message.value = toSafeError(error).message }
|
|
45
|
-
}
|
|
46
|
-
</script>
|
|
47
|
-
<template>
|
|
48
|
-
<section class="panel">
|
|
49
|
-
<h1>__PROJECT_NAME__</h1>
|
|
50
|
-
<p>以下 CRUD 为内存示例,非生产业务接口;新增、编辑和删除均使用 fin-ui 内置流程。</p>
|
|
51
|
-
<div class="actions"><el-button :disabled="Boolean(bridge) && !canRead" @click="openDetail">打开详情页</el-button></div>
|
|
52
|
-
<p v-if="message" role="status">{{ message }}</p>
|
|
53
|
-
<FCrudTable :table-config="tableConfig" @error="reportError" />
|
|
54
|
-
</section>
|
|
55
|
-
</template>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|