@axewo/fin-micro 1.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 +181 -4
- package/cli/index.mjs +22 -6
- package/dist/browser.cjs +1 -1
- package/dist/browser.mjs +68 -66
- package/dist/core.cjs +1 -1
- package/dist/core.mjs +459 -441
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +31 -24
- package/dist/layout.cjs +1 -0
- package/dist/layout.css +1 -0
- package/dist/layout.mjs +719 -0
- package/dist/{router-view-impl-C1KG6mxu.mjs → router-view-impl-B1NzCGKV.mjs} +18 -18
- package/dist/router-view-impl-BvOToV_Z.cjs +1 -0
- package/dist/runtime.cjs +1 -1
- package/dist/runtime.mjs +40 -39
- package/dist/sdk.cjs +1 -1
- package/dist/sdk.mjs +92 -56
- package/dist/types/browser/index.d.ts +4 -0
- package/dist/types/core/index.d.ts +1 -1
- package/dist/types/index.d.ts +2 -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/runtime/lifecycle.d.ts +1 -1
- package/dist/types/sdk/index.d.ts +65 -4
- package/dist/types/vue/index.d.ts +13 -2
- package/dist/types/vue/router-view-impl.d.ts +1 -1
- 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/vue.cjs +1 -1
- package/dist/vue.mjs +49 -35
- package/dist/watermark.cjs +2 -0
- package/dist/watermark.mjs +119 -0
- package/package.json +29 -4
- package/scaffold/base/index.html +2 -1
- package/scaffold/base/package.json.template +5 -1
- package/scaffold/base/pnpm-workspace.yaml +6 -0
- package/scaffold/base/scripts/check-runtime.mjs +43 -0
- package/scaffold/base/src/locales/en.ts +12 -0
- package/scaffold/base/src/locales/index.ts +7 -1
- package/scaffold/base/src/locales/zh-CN.ts +12 -0
- package/scaffold/base/src/main.ts +13 -5
- package/scaffold/base/src/router/demo.ts +6 -0
- package/scaffold/base/src/router/index.ts +13 -0
- package/scaffold/base/src/stores/demo.ts +12 -0
- package/scaffold/base/src/stores/index.ts +11 -0
- package/scaffold/base/src/utils/appConfig.ts +49 -0
- package/scaffold/base/src/utils/errorUtils.ts +109 -0
- package/scaffold/base/src/utils/requestUtils.ts +32 -11
- package/scaffold/base/src/utils/watermarkConfig.ts +18 -0
- package/scaffold/base/src/views/demo/index.vue +26 -0
- package/scaffold/host/.env.example +11 -3
- package/scaffold/host/src/App.vue +1 -13
- package/scaffold/host/src/bootstrap.ts +40 -8
- package/scaffold/host/src/hooks/usePlatformSession.ts +40 -8
- package/scaffold/host/src/layout/HostHeader.vue +65 -5
- package/scaffold/host/src/layout/HostLayout.vue +65 -21
- package/scaffold/host/src/layout/HostMenu.vue +11 -2
- package/scaffold/host/src/layout/HostTabs.vue +91 -8
- package/scaffold/host/src/layout/HostWorkspace.vue +29 -7
- package/scaffold/host/src/localPages.ts +45 -0
- package/scaffold/host/src/platform.ts +51 -14
- package/scaffold/host/src/router/guards.ts +128 -0
- package/scaffold/host/src/router/index.ts +25 -0
- package/scaffold/host/src/router/targets.ts +30 -0
- package/scaffold/host/src/router/workspace.ts +63 -0
- package/scaffold/host/src/styles/shell.css +776 -0
- package/scaffold/host/src/utils/request.ts +30 -3
- package/scaffold/host/src/views/home/index.vue +19 -0
- package/scaffold/host/src/views/login/components/LoginForm.vue +83 -0
- package/scaffold/host/src/views/login/index.vue +16 -0
- package/scaffold/host/src/views/not-found/index.vue +1 -0
- package/scaffold/host/src/views/profile/index.vue +24 -0
- package/scaffold/host/src/views/workspace/index.vue +11 -0
- 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 +25 -24
- package/scaffold/vue/src/bridge/auth.ts +23 -0
- package/scaffold/vue/src/bridge/session.ts +2 -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/main.ts +5 -2
- package/scaffold/vue/src/router/index.ts +9 -4
- 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} +15 -6
- package/scaffold/vue/src/views/demo/types.ts +5 -0
- package/dist/router-view-impl-CgLEalx4.cjs +0 -1
- package/scaffold/host/src/views/LoginView.vue +0 -38
- package/scaffold/vue/src/views/ListView.vue +0 -51
- /package/scaffold/vue/src/views/{NotFoundView.vue → not-found/index.vue} +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { getToken, TokenError, type AppBridge, type AuthCapabilities } from '@axewo/fin-micro/sdk'
|
|
2
|
+
import { installRequestTokenProvider } from '../utils/requestUtils'
|
|
3
|
+
import { appConfig } from '../utils/appConfig'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 为本次嵌入生命周期安装正式认证能力;卸载调用返回函数,独立演示不安装。
|
|
7
|
+
* 请求票据先绑定宿主私有版本,再读取 Token,前后复查公开会话;版本变化直接拒绝发送。
|
|
8
|
+
* 401/Token 未登录上报原票据,宿主独立核验对象身份、来源及私有版本;不传任意 revision。
|
|
9
|
+
* 不自动刷新凭据、不重放写请求;可信应用白名单不是 XSS 隔离沙箱。
|
|
10
|
+
*/
|
|
11
|
+
export function installBridgeAuth(bridge: AppBridge<AuthCapabilities>): () => void {
|
|
12
|
+
return installRequestTokenProvider(async signal => {
|
|
13
|
+
const before = bridge.getSession()
|
|
14
|
+
const token = await getToken({ bridge, signal })
|
|
15
|
+
if (!before.authenticated || bridge.getSession().revision !== before.revision) throw new TokenError('SESSION_CHANGED')
|
|
16
|
+
return token
|
|
17
|
+
}, appConfig.trustedApiBase, async signal => {
|
|
18
|
+
const before = bridge.getSession()
|
|
19
|
+
const ticket = await bridge.callHostWithOptions('beginAuthRequest', { signal })
|
|
20
|
+
if (bridge.getSession().revision !== before.revision) throw new TokenError('SESSION_CHANGED')
|
|
21
|
+
return () => bridge.callHost('reportAuthFailure', ticket)
|
|
22
|
+
})
|
|
23
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { inject, ref, shallowRef, type InjectionKey } from 'vue'
|
|
2
|
-
import { getMicroBridge, type SessionSnapshot } from '@axewo/fin-micro'
|
|
2
|
+
import { getMicroBridge, type SessionSnapshot, type AuthCapabilities } from '@axewo/fin-micro'
|
|
3
3
|
|
|
4
4
|
/** 每次挂载获取独立会话;仅保存公开身份,不读取宿主 Token,不构造虚假的平台能力。 */
|
|
5
5
|
export function createBridgeSession() {
|
|
6
|
-
return { bridge: getMicroBridge(), identity: shallowRef<SessionSnapshot>(), status: ref('正在连接宿主') }
|
|
6
|
+
return { bridge: getMicroBridge<AuthCapabilities>(), identity: shallowRef<SessionSnapshot>(), status: ref('正在连接宿主') }
|
|
7
7
|
}
|
|
8
8
|
export type BridgeSession = ReturnType<typeof createBridgeSession>
|
|
9
9
|
export const sessionKey: InjectionKey<BridgeSession> = Symbol('micro-session')
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/** 独立应用顶栏只展示本地标题,不创建登录状态或宿主能力。 */
|
|
3
|
+
defineProps<{ title: string }>()
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<header class="standalone-header">
|
|
8
|
+
<strong>__PROJECT_NAME__</strong>
|
|
9
|
+
<span>{{ title }} · 独立运行</span>
|
|
10
|
+
</header>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<style scoped>
|
|
14
|
+
.standalone-header { grid-column: 1 / -1; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 0 24px; background: #fff; border-bottom: 1px solid #e5e7eb; }
|
|
15
|
+
</style>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ElMenu } from 'element-plus'
|
|
3
|
+
import type { StandaloneMenu } from '../router/menu'
|
|
4
|
+
import SidebarItem from './SidebarItem.vue'
|
|
5
|
+
|
|
6
|
+
/** 菜单只负责本地导航;活动项随路由更新,详情页可显式关联列表。 */
|
|
7
|
+
defineProps<{ menus: StandaloneMenu[]; activeMenu: string }>()
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<template>
|
|
11
|
+
<aside class="standalone-sidebar" aria-label="独立应用导航">
|
|
12
|
+
<ElMenu router :default-active="activeMenu" :collapse-transition="false">
|
|
13
|
+
<SidebarItem v-for="item in menus" :key="item.path" :item="item" />
|
|
14
|
+
</ElMenu>
|
|
15
|
+
</aside>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<style scoped>
|
|
19
|
+
.standalone-sidebar { overflow: auto; background: #fff; border-right: 1px solid #e5e7eb; }
|
|
20
|
+
.standalone-sidebar :deep(.el-menu) { border-right: 0; }
|
|
21
|
+
</style>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ElIcon, ElMenuItem, ElSubMenu } from 'element-plus'
|
|
3
|
+
import type { StandaloneMenu } from '../router/menu'
|
|
4
|
+
|
|
5
|
+
/** 递归保留实际路由分组;叶子使用完整路径交给同一个 Vue Router。 */
|
|
6
|
+
defineProps<{ item: StandaloneMenu }>()
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<ElSubMenu v-if="item.children.length" :index="item.path">
|
|
11
|
+
<template #title>
|
|
12
|
+
<ElIcon v-if="item.icon"><component :is="item.icon" /></ElIcon>
|
|
13
|
+
<span>{{ item.title }}</span>
|
|
14
|
+
</template>
|
|
15
|
+
<SidebarItem v-for="child in item.children" :key="child.path" :item="child" />
|
|
16
|
+
</ElSubMenu>
|
|
17
|
+
<ElMenuItem v-else :index="item.path">
|
|
18
|
+
<ElIcon v-if="item.icon"><component :is="item.icon" /></ElIcon>
|
|
19
|
+
<span>{{ item.title }}</span>
|
|
20
|
+
</ElMenuItem>
|
|
21
|
+
</template>
|
|
@@ -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>
|
package/scaffold/vue/src/main.ts
CHANGED
|
@@ -2,9 +2,12 @@ import 'element-plus/dist/index.css'
|
|
|
2
2
|
import '@axewo/fin-ui/style.css'
|
|
3
3
|
import './styles/index.css'
|
|
4
4
|
import { setupWujieLifecycle } from '@axewo/fin-micro/runtime'
|
|
5
|
-
import {
|
|
5
|
+
import { createStartupLifecycle } from './utils/errorUtils'
|
|
6
6
|
|
|
7
|
-
//
|
|
7
|
+
// 配置及 bootstrap 静态依赖加载失败也显示安全兜底;卸载取消尚未完成的导入。
|
|
8
|
+
const { mount, unmount } = createStartupLifecycle(() => import('./bootstrap'))
|
|
9
|
+
|
|
10
|
+
// 环境判断、Wujie 回调注册、独立启动和 pagehide 统一由库管理。
|
|
8
11
|
// 业务替换点在 bootstrap:创建/销毁应用,并取消本轮尚未完成的异步初始化。
|
|
9
12
|
const dispose = setupWujieLifecycle({ mount, unmount })
|
|
10
13
|
if (import.meta.hot) import.meta.hot.dispose(dispose)
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { createMemoryHistory, createRouter, createWebHashHistory } from 'vue-router'
|
|
2
|
+
import { demoRoutes } from './demo'
|
|
3
|
+
import { List, Tickets } from '@element-plus/icons-vue'
|
|
4
|
+
import './menu'
|
|
2
5
|
|
|
3
6
|
/** 嵌入模式使用 memory history 不争用宿主 URL;独立模式使用 hash 路由便于静态部署。 */
|
|
4
7
|
export function createMicroRouter(embedded: boolean) {
|
|
5
8
|
return createRouter({
|
|
6
9
|
history: embedded ? createMemoryHistory() : createWebHashHistory(),
|
|
7
10
|
routes: [
|
|
8
|
-
{ path: '/', redirect: '/list' },
|
|
9
|
-
{ path: '/list', component: () => import('../views/
|
|
10
|
-
|
|
11
|
-
{ path: '
|
|
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 } },
|
|
12
17
|
],
|
|
13
18
|
})
|
|
14
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>
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref } from 'vue'
|
|
3
3
|
import { useRoute, useRouter } from 'vue-router'
|
|
4
|
-
import { useMicroPage, useMicroTab } from '@axewo/fin-micro'
|
|
5
|
-
import { useBridgeSession } from '
|
|
6
|
-
|
|
4
|
+
import { useMicroPage, useMicroTab, usePermission } from '@axewo/fin-micro'
|
|
5
|
+
import { useBridgeSession } from '../../../../bridge/session'
|
|
6
|
+
import { toSafeError } from '../../../../utils/errorUtils'
|
|
7
|
+
const { bridge, identity } = useBridgeSession()
|
|
8
|
+
const permission = usePermission(identity)
|
|
9
|
+
const canRead = permission.can('demo:read')
|
|
7
10
|
const tab = bridge ? useMicroTab() : undefined
|
|
8
11
|
const sdk = bridge && tab ? useMicroPage(bridge, tab) : undefined
|
|
9
12
|
const route = useRoute()
|
|
@@ -16,7 +19,11 @@ const message = ref('')
|
|
|
16
19
|
sdk?.beforeLeave(() => !dirty.value || window.confirm('存在未保存的演示草稿,确认离开?'))
|
|
17
20
|
|
|
18
21
|
/** 仅演示标记已保存,不宣称写入后台;真实业务必须 await 保存成功后才清除 dirty。 */
|
|
19
|
-
function save(): void {
|
|
22
|
+
function save(): void {
|
|
23
|
+
if (bridge && !canRead.value) { message.value = '403:无权执行此操作'; return }
|
|
24
|
+
dirty.value = false
|
|
25
|
+
message.value = '演示草稿已标记保存,未写入服务器'
|
|
26
|
+
}
|
|
20
27
|
|
|
21
28
|
/** SDK 固定绑定创建时的原页面,即使等待期间切换页签,也不关闭或刷新其他页面。 */
|
|
22
29
|
async function action(type: 'close' | 'refresh' | 'back'): Promise<void> {
|
|
@@ -29,11 +36,13 @@ async function action(type: 'close' | 'refresh' | 'back'): Promise<void> {
|
|
|
29
36
|
if (type === 'close') await sdk.closeCurrent()
|
|
30
37
|
else if (type === 'refresh') await sdk.refreshCurrent()
|
|
31
38
|
else await sdk.back()
|
|
32
|
-
} catch (error) { message.value =
|
|
39
|
+
} catch (error) { message.value = toSafeError(error).message }
|
|
33
40
|
}
|
|
34
41
|
</script>
|
|
35
42
|
<template>
|
|
36
|
-
<section class="panel">
|
|
43
|
+
<section v-if="bridge && !canRead" class="panel" role="alert"><h2>403</h2><p>没有详情访问权限,请联系管理员。</p></section>
|
|
44
|
+
<section v-else class="panel">
|
|
45
|
+
<p v-if="!bridge">独立演示不创建登录身份,仅允许本地草稿操作,不发送受保护请求。</p>
|
|
37
46
|
<h2>详情 {{ route.query.id }}</h2>
|
|
38
47
|
<p>切换宿主页签保留草稿;刷新/关闭只影响本页生命周期。</p>
|
|
39
48
|
<el-input v-model="draft" type="textarea" placeholder="编辑演示草稿" @input="dirty = true" />
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=require("vue"),n=require("vue-router"),O=require("./vue.cjs");class f extends Error{constructor(a){super(a),this.code=a}}function R(h,a){let e=h.resolve(a,n.START_LOCATION);const p=e,c=new Set;for(let i=0;i<30;i++){if(!e.matched.length)throw new f("NOT_FOUND");const l=e.matched[e.matched.length-1];if(!l.redirect)return e===p?e:{...e,redirectedFrom:p};if(c.has(e.fullPath))throw new f("REDIRECT_LOOP");c.add(e.fullPath);const r=typeof l.redirect=="function"?l.redirect(e,n.START_LOCATION):l.redirect;let u;if(typeof r=="string")if(r.includes("?")||r.includes("#")){const o=h.resolve(r,n.START_LOCATION);u={path:o.path,query:o.query,hash:o.hash}}else u={path:r};else u=r;const d="path"in u&&u.path!=null;if(!d&&!("name"in u))throw new f("RESOLVE_FAILED");e=h.resolve({query:e.query,hash:e.hash,params:d?{}:e.params,...u},n.START_LOCATION)}throw new f("REDIRECT_LOOP")}const T=t.defineComponent({name:"MicroRouteViewImpl",props:{name:{type:String,default:"default"}},slots:Object,setup(h,{slots:a}){const e=O.useMicroTab(),p=n.useRouter(),c=t.shallowRef(),i=t.shallowRef(),l=t.shallowRef(!1);let r=!1;const u={};for(const o of Object.keys(n.START_LOCATION))Object.defineProperty(u,o,{enumerable:!0,get:()=>(c.value??n.START_LOCATION)[o]});t.provide(n.routeLocationKey,t.shallowReactive(u)),t.provide(n.viewDepthKey,0);async function d(){if(r||l.value)return;l.value=!0,i.value=void 0;let o="RESOLVE_FAILED";try{const s=R(p,e.route);o="LOAD_FAILED";const v=await n.loadRouteLocation(s);r||(c.value=v)}catch(s){r||(i.value=s instanceof f?s.code:o)}finally{r||(l.value=!1)}}return t.onBeforeUnmount(()=>{r=!0}),d(),()=>{var o,s;return i.value?((o=a.error)==null?void 0:o.call(a,{tab:e,code:i.value,retry:d}))??t.h("div",{role:"alert","data-micro-route-error":i.value},[t.h("p",`页面加载失败(${i.value})`),t.h("button",{type:"button",onClick:d},"重新加载")]):l.value||!c.value?((s=a.loading)==null?void 0:s.call(a,{tab:e}))??t.h("p",{role:"status"},"页面加载中…"):t.h(n.RouterView,{route:c.value,name:h.name,key:e.key})}}});exports.MicroRouteViewImpl=T;
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { ref } from 'vue'
|
|
3
|
-
import { demoMode } from '../platform'
|
|
4
|
-
|
|
5
|
-
// 表单只保留本地输入;会话恢复、取消与错误状态统一由上层会话组合函数管理。
|
|
6
|
-
const props = defineProps<{
|
|
7
|
-
loading: boolean
|
|
8
|
-
error: string
|
|
9
|
-
signIn: (username: string, password: string) => Promise<void>
|
|
10
|
-
}>()
|
|
11
|
-
const emit = defineEmits<{ restore: [] }>()
|
|
12
|
-
const username = ref('')
|
|
13
|
-
const password = ref('')
|
|
14
|
-
async function submit(): Promise<void> {
|
|
15
|
-
if (props.loading || !username.value.trim() || (!demoMode && !password.value)) return
|
|
16
|
-
const credential = password.value
|
|
17
|
-
// 提交即清空密码输入,不在会话或全局状态中保存凭据。
|
|
18
|
-
password.value = ''
|
|
19
|
-
await props.signIn(username.value.trim(), credential)
|
|
20
|
-
}
|
|
21
|
-
</script>
|
|
22
|
-
|
|
23
|
-
<template>
|
|
24
|
-
<main class="login panel">
|
|
25
|
-
<h1>__PROJECT_NAME__</h1>
|
|
26
|
-
<el-alert v-if="demoMode" title="仅演示登录,不提供生产身份认证" type="warning" :closable="false" />
|
|
27
|
-
<p>请接入 src/platform.ts 的平台接口;默认不启用演示身份。</p>
|
|
28
|
-
<el-form label-position="top" @submit.prevent="submit">
|
|
29
|
-
<el-form-item label="用户名"><el-input v-model="username" autocomplete="username" /></el-form-item>
|
|
30
|
-
<el-form-item v-if="!demoMode" label="密码"><el-input v-model="password" type="password" autocomplete="current-password" /></el-form-item>
|
|
31
|
-
<p v-if="error" role="alert">{{ error }}</p>
|
|
32
|
-
<div class="actions">
|
|
33
|
-
<el-button type="primary" native-type="submit" :loading="loading">登录</el-button>
|
|
34
|
-
<el-button :disabled="loading" @click="emit('restore')">重试恢复会话</el-button>
|
|
35
|
-
</div>
|
|
36
|
-
</el-form>
|
|
37
|
-
</main>
|
|
38
|
-
</template>
|
|
@@ -1,51 +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 } from '@axewo/fin-micro'
|
|
6
|
-
import { useBridgeSession } from '../bridge/session'
|
|
7
|
-
const { bridge } = useBridgeSession()
|
|
8
|
-
const router = useRouter()
|
|
9
|
-
// SDK 在 setup 中绑定缓存边界的固定 tab.key;后续异步操作不会误操作另一个活动页。
|
|
10
|
-
const tab = bridge ? useMicroTab() : undefined
|
|
11
|
-
const sdk = bridge && tab ? useMicroPage(bridge, tab) : undefined
|
|
12
|
-
const message = ref('')
|
|
13
|
-
/** 组件异常只展示文本,兼容公开版本未提供强类型事件声明的情况。 */
|
|
14
|
-
function reportError(error: unknown): void { message.value = error instanceof Error ? error.message : String(error) }
|
|
15
|
-
interface RecordRow { id: number; name: string }
|
|
16
|
-
// 最小内存适配器仅演示组件 API;每个页面独立数据,刷新会重置,生产请替换为真实业务请求。
|
|
17
|
-
let rows: RecordRow[] = [{ id: 1, name: '示例记录' }]
|
|
18
|
-
let nextId = 2
|
|
19
|
-
const tableConfig: TableConfig = {
|
|
20
|
-
rowKey: 'id', title: '示例记录', pagination: false,
|
|
21
|
-
columns: [
|
|
22
|
-
{ field: 'id', label: '编号', width: 100 },
|
|
23
|
-
{ field: 'name', label: '名称', formItem: { component: 'FInput', required: true, componentProps: { maxlength: 80 } } },
|
|
24
|
-
],
|
|
25
|
-
requestConfig: { api: async () => ({ list: rows.map(row => ({ ...row })), total: rows.length }) },
|
|
26
|
-
crudConfig: {
|
|
27
|
-
// 由 FCrudTable 统一生成新增/编辑弹框及删除确认,不另造重复表单。
|
|
28
|
-
createApi: async data => { rows.push({ id: nextId++, name: String(data.name) }) },
|
|
29
|
-
updateApi: async data => { rows = rows.map(row => row.id === data.id ? { ...row, name: String(data.name) } : row) },
|
|
30
|
-
deleteApi: async data => { rows = rows.filter(row => row.id !== data.id) },
|
|
31
|
-
onSuccess: () => { message.value = '演示内存数据已更新(刷新后重置)' },
|
|
32
|
-
},
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** 同应用详情用页面 SDK 打开宿主页签;独立模式才使用普通 router,错误留在原页面。 */
|
|
36
|
-
async function openDetail(): Promise<void> {
|
|
37
|
-
try {
|
|
38
|
-
if (sdk && bridge) await sdk.openTab({ appId: bridge.appId, tabId: `${bridge.appId}:detail:1`, title: '示例详情 1', route: '/detail?id=1' })
|
|
39
|
-
else await router.push('/detail?id=1')
|
|
40
|
-
} catch (error) { message.value = String(error) }
|
|
41
|
-
}
|
|
42
|
-
</script>
|
|
43
|
-
<template>
|
|
44
|
-
<section class="panel">
|
|
45
|
-
<h1>__PROJECT_NAME__</h1>
|
|
46
|
-
<p>以下 CRUD 为内存示例,非生产业务接口;新增、编辑和删除均使用 fin-ui 内置流程。</p>
|
|
47
|
-
<div class="actions"><el-button @click="openDetail">打开详情页</el-button></div>
|
|
48
|
-
<p v-if="message" role="status">{{ message }}</p>
|
|
49
|
-
<FCrudTable :table-config="tableConfig" @error="reportError" />
|
|
50
|
-
</section>
|
|
51
|
-
</template>
|
|
File without changes
|