优化构架
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
import { CircleUserRound } from '@lucide/vue'
|
||||
import type { ClientModuleManifest } from '../types.ts'
|
||||
|
||||
export const accountModule = {
|
||||
key: 'account',
|
||||
routes: [
|
||||
{
|
||||
path: 'account/profile',
|
||||
name: 'account-profile',
|
||||
component: () => import('./views/AccountProfileView.vue'),
|
||||
release: 'active',
|
||||
meta: { title: '我的信息', description: '维护个人联系方式和登录密码', permission: 'account:profile:view' },
|
||||
navigation: { key: 'account-profile', icon: CircleUserRound },
|
||||
},
|
||||
],
|
||||
} satisfies ClientModuleManifest
|
||||
@@ -2,16 +2,16 @@ import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const routerSource = fs.readFileSync(new URL('../router/index.ts', import.meta.url), 'utf8')
|
||||
const navigationSource = fs.readFileSync(new URL('../navigation/clientNavigation.ts', import.meta.url), 'utf8')
|
||||
const routerSource = fs.readFileSync(new URL('../routes.ts', import.meta.url), 'utf8')
|
||||
const viewSource = fs.readFileSync(new URL('./AccountProfileView.vue', import.meta.url), 'utf8')
|
||||
const authSource = fs.readFileSync(new URL('../stores/auth.ts', import.meta.url), 'utf8')
|
||||
const authApiSource = fs.readFileSync(new URL('../api/auth.ts', import.meta.url), 'utf8')
|
||||
const authSource = fs.readFileSync(new URL('../../auth/store/auth.ts', import.meta.url), 'utf8')
|
||||
const authApiSource = fs.readFileSync(new URL('../../auth/api/auth.ts', import.meta.url), 'utf8')
|
||||
|
||||
test('registers the account profile route and navigation entry', () => {
|
||||
assert.match(routerSource, /account\/profile'.*AccountProfileView\.vue/)
|
||||
assert.match(routerSource, /path: 'account\/profile'[\s\S]*?component: \(\) => import\('\.\/views\/AccountProfileView\.vue'\)/)
|
||||
assert.match(routerSource, /release: 'active'/)
|
||||
assert.match(routerSource, /permission: 'account:profile:view'/)
|
||||
assert.match(navigationSource, /title: '我的信息'.*path: '\/account\/profile'/)
|
||||
assert.match(routerSource, /navigation: \{ key: 'account-profile'/)
|
||||
})
|
||||
|
||||
test('implements profile view, edit, save and cancel states', () => {
|
||||
|
||||
@@ -12,14 +12,13 @@ import {
|
||||
UserRound,
|
||||
X,
|
||||
} from '@lucide/vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { changeCurrentPassword } from '@/api/auth'
|
||||
import { changeCurrentPassword, useAuthStore } from '@/modules/auth/public'
|
||||
import {
|
||||
getPasswordStrength,
|
||||
validateEditableAccountProfile,
|
||||
validatePasswordChange,
|
||||
type PasswordChangePayload,
|
||||
} from '@/utils/accountProfile'
|
||||
} from '@/modules/account/utils/accountProfile'
|
||||
|
||||
type AccountTab = 'profile' | 'password'
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { changeCurrentPassword } from './api/auth'
|
||||
export { useAuthStore } from './store/auth'
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
export const authRoutes = [
|
||||
{ path: '/login', name: 'login', component: () => import('./views/LoginView.vue'), meta: { title: '登录' } },
|
||||
{ path: '/register', name: 'register', component: () => import('./views/RegisterView.vue'), meta: { title: '企业注册' } },
|
||||
] satisfies RouteRecordRaw[]
|
||||
@@ -1,12 +1,19 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import type { CurrentUser } from '@chongchong/shared'
|
||||
import { loginMerchant, logoutMerchant } from '@/api/auth'
|
||||
import { loginMerchant, logoutMerchant } from '@/modules/auth/api/auth'
|
||||
import { ApiError } from '@/api/http'
|
||||
import { getMerchantPermissionResources, getMerchantPersonalProfile, getTenantProfile, updateMerchantPersonalProfile, type MerchantPermissionResource } from '@/api/merchantIam'
|
||||
import {
|
||||
FRONTEND_TO_MERCHANT_PERMISSION_CODES,
|
||||
TENANT_PERMISSION_GROUPS,
|
||||
getMerchantPermissionResources,
|
||||
getMerchantPersonalProfile,
|
||||
getTenantProfile,
|
||||
updateMerchantPersonalProfile,
|
||||
type MerchantPermissionResource,
|
||||
} from '@/modules/tenant-management/public'
|
||||
import { clearSessionTokens, getAccessToken, getRefreshToken, persistTokenPair } from '@/api/session'
|
||||
import { FRONTEND_TO_MERCHANT_PERMISSION_CODES, TENANT_PERMISSION_GROUPS } from '@/stores/tenantAdministration'
|
||||
import { decodeMerchantToken, type MerchantJwtPayload } from '@/utils/jwt'
|
||||
import { decodeMerchantToken, type MerchantJwtPayload } from '@/modules/auth/utils/jwt'
|
||||
|
||||
const frontendPermissionCodes = new Set(TENANT_PERMISSION_GROUPS.flatMap((group) => group.items.map((item) => item.code)))
|
||||
const backendPermissionMapping = Object.entries(FRONTEND_TO_MERCHANT_PERMISSION_CODES)
|
||||
|
||||
@@ -4,11 +4,11 @@ import test from 'node:test'
|
||||
|
||||
const loginSource = readFileSync(new URL('./LoginView.vue', import.meta.url), 'utf8')
|
||||
const registerSource = readFileSync(new URL('./RegisterView.vue', import.meta.url), 'utf8')
|
||||
const authStoreSource = readFileSync(new URL('../stores/auth.ts', import.meta.url), 'utf8')
|
||||
const httpSource = readFileSync(new URL('../api/http.ts', import.meta.url), 'utf8')
|
||||
const sessionSource = readFileSync(new URL('../api/session.ts', import.meta.url), 'utf8')
|
||||
const authStoreSource = readFileSync(new URL('../store/auth.ts', import.meta.url), 'utf8')
|
||||
const httpSource = readFileSync(new URL('../../../api/http.ts', import.meta.url), 'utf8')
|
||||
const sessionSource = readFileSync(new URL('../../../api/session.ts', import.meta.url), 'utf8')
|
||||
const authApiSource = readFileSync(new URL('../api/auth.ts', import.meta.url), 'utf8')
|
||||
const mainSource = readFileSync(new URL('../main.ts', import.meta.url), 'utf8')
|
||||
const mainSource = readFileSync(new URL('../../../main.ts', import.meta.url), 'utf8')
|
||||
|
||||
test('matches the backend username login contract', () => {
|
||||
assert.match(loginSource, /label="用户名"/)
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { ArrowRight, LockKeyhole, UserRound } from '@lucide/vue'
|
||||
import baiyueLogoDark from '@/assets/brand/baiyue-logo-dark.png'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useAuthStore } from '@/modules/auth/store/auth'
|
||||
|
||||
interface LoginForm {
|
||||
account: string
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useRouter } from 'vue-router'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { ArrowLeft, Building2, CheckCircle2, Mail, Phone } from '@lucide/vue'
|
||||
import baiyueLogoDark from '@/assets/brand/baiyue-logo-dark.png'
|
||||
import { registerMerchant } from '@/api/auth'
|
||||
import { registerMerchant } from '@/modules/auth/api/auth'
|
||||
|
||||
interface RegisterForm {
|
||||
username: string
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Package, PlaneTakeoff, Ship, Truck } from '@lucide/vue'
|
||||
import type { ClientModuleManifest } from '../types.ts'
|
||||
|
||||
export const crossBorderModule = {
|
||||
key: 'cross-border',
|
||||
navigationGroup: { title: '出海下单', icon: PlaneTakeoff },
|
||||
routes: [
|
||||
{
|
||||
path: 'cross-border/procurement',
|
||||
name: 'cross-border-procurement',
|
||||
component: () => import('./views/ProcurementView.vue'),
|
||||
release: 'prototype',
|
||||
meta: { title: '海外采购', description: '管理海外采购订单', permission: 'merchant:cross-border:procurement' },
|
||||
navigation: { key: 'cross-border-procurement', icon: Package },
|
||||
},
|
||||
{
|
||||
path: 'cross-border/domestic-trucking',
|
||||
name: 'cross-border-domestic-trucking',
|
||||
component: () => import('./views/DomesticTruckingView.vue'),
|
||||
release: 'prototype',
|
||||
meta: { title: '美国本土卡', description: '创建美国本土卡车运输订单', permission: 'merchant:cross-border:domestic-trucking' },
|
||||
navigation: { key: 'cross-border-domestic-trucking', icon: Truck },
|
||||
},
|
||||
{
|
||||
path: 'cross-border/pallet-shipping',
|
||||
name: 'cross-border-pallet-shipping',
|
||||
component: () => import('./views/PalletShippingView.vue'),
|
||||
release: 'prototype',
|
||||
meta: { title: '美本土散板', description: '创建美国本土散板运输订单', permission: 'merchant:cross-border:pallet-shipping' },
|
||||
navigation: { key: 'cross-border-pallet-shipping', icon: Ship },
|
||||
},
|
||||
],
|
||||
} satisfies ClientModuleManifest
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeFeatureView from '@/views/prototype/PrototypeFeatureView.vue'
|
||||
import PrototypeFeatureView from '@/components/common/PrototypeFeatureView.vue'
|
||||
</script>
|
||||
|
||||
<template><PrototypeFeatureView title="美国本土卡" description="创建美国本土卡车运输订单" /></template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeFeatureView from '@/views/prototype/PrototypeFeatureView.vue'
|
||||
import PrototypeFeatureView from '@/components/common/PrototypeFeatureView.vue'
|
||||
</script>
|
||||
|
||||
<template><PrototypeFeatureView title="美本土散板" description="创建美国本土散板运输订单" /></template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeFeatureView from '@/views/prototype/PrototypeFeatureView.vue'
|
||||
import PrototypeFeatureView from '@/components/common/PrototypeFeatureView.vue'
|
||||
</script>
|
||||
|
||||
<template><PrototypeFeatureView title="海外采购" description="管理海外采购订单" /></template>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Gauge } from '@lucide/vue'
|
||||
import type { ClientModuleManifest } from '../types.ts'
|
||||
|
||||
export const dashboardModule = {
|
||||
key: 'dashboard',
|
||||
routes: [
|
||||
{
|
||||
path: '',
|
||||
name: 'dashboard',
|
||||
component: () => import('./views/DashboardView.vue'),
|
||||
release: 'active',
|
||||
meta: { title: '业务总览', description: '企业账户与头程业务概览', permission: 'dashboard:view' },
|
||||
navigation: { key: 'dashboard', icon: Gauge },
|
||||
},
|
||||
],
|
||||
} satisfies ClientModuleManifest
|
||||
@@ -1,10 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { AlertTriangle, ArrowRight, ClipboardCheck, Clock3, FilePlus2, Users } from '@lucide/vue'
|
||||
import { useFirstLegOrdersStore } from '@/stores/firstLegOrders'
|
||||
import { useTenantAdministrationStore } from '@/stores/tenantAdministration'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { getFirstLegStatusLabel, getFirstLegStatusTone } from '@/utils/firstLegOrder'
|
||||
import { getFirstLegStatusLabel, getFirstLegStatusTone, useFirstLegOrdersStore } from '@/modules/first-leg/public'
|
||||
import { useTenantAdministrationStore } from '@/modules/tenant-management/public'
|
||||
import { useAuthStore } from '@/modules/auth/public'
|
||||
|
||||
const orders = useFirstLegOrdersStore()
|
||||
const tenant = useTenantAdministrationStore()
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ReceiptText } from '@lucide/vue'
|
||||
import type { ClientModuleManifest } from '../types.ts'
|
||||
|
||||
export const financeModule = {
|
||||
key: 'finance',
|
||||
navigationGroup: { title: '财务结算', icon: ReceiptText },
|
||||
routes: [
|
||||
{
|
||||
path: 'finance/payables',
|
||||
name: 'finance-payables',
|
||||
component: () => import('./views/PayableBillsView.vue'),
|
||||
release: 'prototype',
|
||||
meta: { title: '应付账单', description: '查询企业应付账单', permission: 'merchant:finance:payables:list' },
|
||||
navigation: { key: 'finance-payables', icon: ReceiptText },
|
||||
},
|
||||
],
|
||||
} satisfies ClientModuleManifest
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeFeatureView from '@/views/prototype/PrototypeFeatureView.vue'
|
||||
import PrototypeFeatureView from '@/components/common/PrototypeFeatureView.vue'
|
||||
</script>
|
||||
|
||||
<template><PrototypeFeatureView title="应付账单" description="查询企业应付账单" /></template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getMerchantPartnerCandidates, type MerchantPartner } from '@/api/partners'
|
||||
import type { FirstLegParty, FirstLegPartyRole } from '@/data/firstLegParties'
|
||||
import { getMerchantPartnerCandidates, type MerchantPartner } from '@/modules/first-leg/api/partners'
|
||||
import type { FirstLegParty, FirstLegPartyRole } from '@/modules/first-leg/data/firstLegParties'
|
||||
|
||||
export interface FirstLegPartySource {
|
||||
listAvailable(role: FirstLegPartyRole): readonly FirstLegParty[]
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { useFirstLegOrdersStore } from './store/firstLegOrders'
|
||||
export { getFirstLegStatusLabel, getFirstLegStatusTone } from './utils/firstLegOrder'
|
||||
@@ -0,0 +1,40 @@
|
||||
import { FileText, Package, Ship, Zap } from '@lucide/vue'
|
||||
import type { ClientModuleManifest } from '../types.ts'
|
||||
|
||||
export const firstLegModule = {
|
||||
key: 'first-leg',
|
||||
navigationGroup: { title: '头程物流', icon: Ship },
|
||||
routes: [
|
||||
{
|
||||
path: 'orders/first-leg',
|
||||
name: 'first-leg-orders',
|
||||
component: () => import('./views/FirstLegOrdersView.vue'),
|
||||
release: 'active',
|
||||
meta: { title: '头程订单', description: '创建、查询并跟进本企业头程订单', permission: 'order:first-leg:list' },
|
||||
navigation: { key: 'first-leg-orders', title: '头程下单', icon: FileText },
|
||||
},
|
||||
{
|
||||
path: 'orders/first-leg/create',
|
||||
name: 'first-leg-order-create',
|
||||
component: () => import('./views/FirstLegOrdersView.vue'),
|
||||
release: 'active',
|
||||
meta: { title: '创建头程订单', description: '填写发货、目的地和货物信息', permission: 'order:first-leg:create' },
|
||||
},
|
||||
{
|
||||
path: 'first-leg/import',
|
||||
name: 'first-leg-import',
|
||||
component: () => import('./views/BatchImportView.vue'),
|
||||
release: 'prototype',
|
||||
meta: { title: '一键导入', description: '批量导入头程订单', permission: 'merchant:first-leg:import' },
|
||||
navigation: { key: 'first-leg-import', icon: Package },
|
||||
},
|
||||
{
|
||||
path: 'first-leg/quick-order',
|
||||
name: 'first-leg-quick-order',
|
||||
component: () => import('./views/QuickOrderView.vue'),
|
||||
release: 'prototype',
|
||||
meta: { title: '快速下单', description: '快速创建头程订单', permission: 'merchant:first-leg:quick-create' },
|
||||
navigation: { key: 'first-leg-quick-order', icon: Zap },
|
||||
},
|
||||
],
|
||||
} satisfies ClientModuleManifest
|
||||
@@ -14,12 +14,12 @@ import {
|
||||
type MerchantFirstLegOrder,
|
||||
type MerchantFirstLegOrderDetail,
|
||||
type MerchantWarehouse,
|
||||
} from '@/api/firstLeg'
|
||||
import type { MerchantTenantStatus } from '@/api/merchantIam'
|
||||
} from '@/modules/first-leg/api/firstLeg'
|
||||
import type { MerchantTenantStatus } from '@/modules/tenant-management/public'
|
||||
import type { PlatformReceivingWarehouse, PlatformReceivingWarehouseSnapshot } from '@chongchong/shared'
|
||||
import type { FirstLegPartySnapshot } from '@/data/firstLegParties'
|
||||
import type { FirstLegOrder, FirstLegOrderDraft, FirstLegOrderTimelineItem } from '@/types/firstLegOrder'
|
||||
import { canClientEditFirstLegOrder } from '@/utils/firstLegOrder'
|
||||
import type { FirstLegPartySnapshot } from '@/modules/first-leg/data/firstLegParties'
|
||||
import type { FirstLegOrder, FirstLegOrderDraft, FirstLegOrderTimelineItem } from '@/modules/first-leg/types/firstLegOrder'
|
||||
import { canClientEditFirstLegOrder } from '@/modules/first-leg/utils/firstLegOrder'
|
||||
|
||||
const countryCodes: Readonly<Record<string, string>> = {
|
||||
中国: 'CN', 美国: 'US', 德国: 'DE', 英国: 'GB', 加拿大: 'CA', 日本: 'JP', 澳大利亚: 'AU',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeFeatureView from '@/views/prototype/PrototypeFeatureView.vue'
|
||||
import PrototypeFeatureView from '@/components/common/PrototypeFeatureView.vue'
|
||||
</script>
|
||||
|
||||
<template><PrototypeFeatureView title="一键导入" description="批量导入头程订单" /></template>
|
||||
|
||||
@@ -3,15 +3,15 @@ import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const source = readFileSync(new URL('./FirstLegOrdersView.vue', import.meta.url), 'utf8')
|
||||
const routerSource = readFileSync(new URL('../router/index.ts', import.meta.url), 'utf8')
|
||||
const editorShellSource = readFileSync(new URL('../components/first-leg/FirstLegOrderEditorShell.vue', import.meta.url), 'utf8')
|
||||
const routerSource = readFileSync(new URL('../routes.ts', import.meta.url), 'utf8')
|
||||
const editorShellSource = readFileSync(new URL('../components/FirstLegOrderEditorShell.vue', import.meta.url), 'utf8')
|
||||
|
||||
test('routes the confirmed first-leg path to the implemented order view', () => {
|
||||
assert.match(routerSource, /path: 'orders\/first-leg'.*FirstLegOrdersView\.vue/)
|
||||
assert.match(routerSource, /path: 'orders\/first-leg'[\s\S]*?component: \(\) => import\('\.\/views\/FirstLegOrdersView\.vue'\)[\s\S]*?release: 'active'/)
|
||||
})
|
||||
|
||||
test('opens order creation as a dedicated protected page while keeping draft editing in a drawer', () => {
|
||||
assert.match(routerSource, /path: 'orders\/first-leg\/create'.*name: 'first-leg-order-create'.*permission: 'order:first-leg:create'/)
|
||||
assert.match(routerSource, /path: 'orders\/first-leg\/create'[\s\S]*?name: 'first-leg-order-create'[\s\S]*?release: 'active'[\s\S]*?permission: 'order:first-leg:create'/)
|
||||
assert.match(source, /router\.push\(\{ name: 'first-leg-order-create' \}\)/)
|
||||
assert.match(source, /const isCreatePage = computed\(\(\) => route\.name === 'first-leg-order-create'\)/)
|
||||
assert.match(source, /<FirstLegOrderEditorShell[^>]*:page="isCreatePage"/)
|
||||
|
||||
@@ -6,13 +6,13 @@ import { AlertTriangle, Box, Eye, FilePlus2, MapPin, PackageCheck, Pencil, Plus,
|
||||
import { FIRST_LEG_ORDER_STATUSES, FIRST_LEG_TRANSPORT_MODES, type FirstLegOrderStatus } from '@chongchong/shared'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
||||
import FirstLegOrderEditorShell from '@/components/first-leg/FirstLegOrderEditorShell.vue'
|
||||
import { useFirstLegOrdersStore } from '@/stores/firstLegOrders'
|
||||
import { useTenantAdministrationStore } from '@/stores/tenantAdministration'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { createFirstLegPartySnapshot, type FirstLegPartyRole } from '@/data/firstLegParties'
|
||||
import { firstLegPartySource } from '@/data/firstLegPartySource'
|
||||
import type { FirstLegOrder, FirstLegOrderDraft } from '@/types/firstLegOrder'
|
||||
import FirstLegOrderEditorShell from '@/modules/first-leg/components/FirstLegOrderEditorShell.vue'
|
||||
import { useFirstLegOrdersStore } from '@/modules/first-leg/store/firstLegOrders'
|
||||
import { useTenantAdministrationStore } from '@/modules/tenant-management/public'
|
||||
import { useAuthStore } from '@/modules/auth/public'
|
||||
import { createFirstLegPartySnapshot, type FirstLegPartyRole } from '@/modules/first-leg/data/firstLegParties'
|
||||
import { firstLegPartySource } from '@/modules/first-leg/data/firstLegPartySource'
|
||||
import type { FirstLegOrder, FirstLegOrderDraft } from '@/modules/first-leg/types/firstLegOrder'
|
||||
import {
|
||||
canClientEditFirstLegOrder,
|
||||
createEmptyCargoItem,
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
getFirstLegTransportModeLabel,
|
||||
summarizeFirstLegCargo,
|
||||
validateFirstLegOrderDraft,
|
||||
} from '@/utils/firstLegOrder'
|
||||
} from '@/modules/first-leg/utils/firstLegOrder'
|
||||
|
||||
const store = useFirstLegOrdersStore()
|
||||
const tenantStore = useTenantAdministrationStore()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeFeatureView from '@/views/prototype/PrototypeFeatureView.vue'
|
||||
import PrototypeFeatureView from '@/components/common/PrototypeFeatureView.vue'
|
||||
</script>
|
||||
|
||||
<template><PrototypeFeatureView title="快速下单" description="快速创建头程订单" /></template>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ClipboardList, Package, ReceiptText, Truck } from '@lucide/vue'
|
||||
import type { ClientModuleManifest } from '../types.ts'
|
||||
|
||||
export const orderManagementModule = {
|
||||
key: 'order-management',
|
||||
navigationGroup: { title: '订单管理', icon: ClipboardList },
|
||||
routes: [
|
||||
{
|
||||
path: 'orders/my',
|
||||
name: 'orders-my',
|
||||
component: () => import('./views/MyOrdersView.vue'),
|
||||
release: 'prototype',
|
||||
meta: { title: '我的订单', description: '统一查询各类业务订单', permission: 'merchant:order:list' },
|
||||
navigation: { key: 'orders-my', icon: ReceiptText },
|
||||
},
|
||||
{
|
||||
path: 'orders/inbound',
|
||||
name: 'orders-inbound',
|
||||
component: () => import('./views/InboundOrdersView.vue'),
|
||||
release: 'prototype',
|
||||
meta: { title: '入库订单列表', description: '查询海外仓入库订单', permission: 'merchant:warehouse:inbound:list' },
|
||||
navigation: { key: 'orders-inbound', icon: Package },
|
||||
},
|
||||
{
|
||||
path: 'orders/delivery',
|
||||
name: 'orders-delivery',
|
||||
component: () => import('./views/DeliveryOrdersView.vue'),
|
||||
release: 'prototype',
|
||||
meta: { title: '派送订单列表', description: '查询海外仓派送订单', permission: 'merchant:warehouse:delivery:list' },
|
||||
navigation: { key: 'orders-delivery', icon: Truck },
|
||||
},
|
||||
],
|
||||
} satisfies ClientModuleManifest
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeFeatureView from '@/views/prototype/PrototypeFeatureView.vue'
|
||||
import PrototypeFeatureView from '@/components/common/PrototypeFeatureView.vue'
|
||||
</script>
|
||||
|
||||
<template><PrototypeFeatureView title="派送订单列表" description="查询海外仓派送订单" /></template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeFeatureView from '@/views/prototype/PrototypeFeatureView.vue'
|
||||
import PrototypeFeatureView from '@/components/common/PrototypeFeatureView.vue'
|
||||
</script>
|
||||
|
||||
<template><PrototypeFeatureView title="入库订单列表" description="查询海外仓入库订单" /></template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeFeatureView from '@/views/prototype/PrototypeFeatureView.vue'
|
||||
import PrototypeFeatureView from '@/components/common/PrototypeFeatureView.vue'
|
||||
</script>
|
||||
|
||||
<template><PrototypeFeatureView title="我的订单" description="统一查询各类业务订单" /></template>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Package, Tags, Truck, Warehouse } from '@lucide/vue'
|
||||
import type { ClientModuleManifest } from '../types.ts'
|
||||
|
||||
export const overseasWarehouseModule = {
|
||||
key: 'overseas-warehouse',
|
||||
navigationGroup: { title: '海外仓', icon: Warehouse },
|
||||
routes: [
|
||||
{
|
||||
path: 'overseas-warehouse/labeling',
|
||||
name: 'overseas-labeling',
|
||||
component: () => import('./views/LabelingView.vue'),
|
||||
release: 'prototype',
|
||||
meta: { title: '贴标申请', description: '提交海外仓贴标申请', permission: 'merchant:overseas-warehouse:labeling' },
|
||||
navigation: { key: 'overseas-labeling', icon: Tags },
|
||||
},
|
||||
{
|
||||
path: 'overseas-warehouse/return-inbound',
|
||||
name: 'overseas-return-inbound',
|
||||
component: () => import('./views/ReturnInboundView.vue'),
|
||||
release: 'prototype',
|
||||
meta: { title: '退货入库', description: '登记海外仓退货入库', permission: 'merchant:overseas-warehouse:return-inbound' },
|
||||
navigation: { key: 'overseas-return-inbound', icon: Package },
|
||||
},
|
||||
{
|
||||
path: 'overseas-warehouse/return-relabel',
|
||||
name: 'overseas-return-relabel',
|
||||
component: () => import('./views/ReturnRelabelView.vue'),
|
||||
release: 'prototype',
|
||||
meta: { title: '退货换标', description: '提交退货换标申请', permission: 'merchant:overseas-warehouse:return-relabel' },
|
||||
navigation: { key: 'overseas-return-relabel', icon: Tags },
|
||||
},
|
||||
{
|
||||
path: 'overseas-warehouse/return-outbound',
|
||||
name: 'overseas-return-outbound',
|
||||
component: () => import('./views/ReturnOutboundView.vue'),
|
||||
release: 'prototype',
|
||||
meta: { title: '退货出库', description: '创建海外仓退货出库单', permission: 'merchant:overseas-warehouse:return-outbound' },
|
||||
navigation: { key: 'overseas-return-outbound', icon: Truck },
|
||||
},
|
||||
],
|
||||
} satisfies ClientModuleManifest
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeFeatureView from '@/views/prototype/PrototypeFeatureView.vue'
|
||||
import PrototypeFeatureView from '@/components/common/PrototypeFeatureView.vue'
|
||||
</script>
|
||||
|
||||
<template><PrototypeFeatureView title="贴标申请" description="提交海外仓贴标申请" /></template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeFeatureView from '@/views/prototype/PrototypeFeatureView.vue'
|
||||
import PrototypeFeatureView from '@/components/common/PrototypeFeatureView.vue'
|
||||
</script>
|
||||
|
||||
<template><PrototypeFeatureView title="退货入库" description="登记海外仓退货入库" /></template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeFeatureView from '@/views/prototype/PrototypeFeatureView.vue'
|
||||
import PrototypeFeatureView from '@/components/common/PrototypeFeatureView.vue'
|
||||
</script>
|
||||
|
||||
<template><PrototypeFeatureView title="退货出库" description="创建海外仓退货出库单" /></template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeFeatureView from '@/views/prototype/PrototypeFeatureView.vue'
|
||||
import PrototypeFeatureView from '@/components/common/PrototypeFeatureView.vue'
|
||||
</script>
|
||||
|
||||
<template><PrototypeFeatureView title="退货换标" description="提交退货换标申请" /></template>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { accountModule } from './account/routes.ts'
|
||||
import { crossBorderModule } from './cross-border/routes.ts'
|
||||
import { dashboardModule } from './dashboard/routes.ts'
|
||||
import { financeModule } from './finance/routes.ts'
|
||||
import { firstLegModule } from './first-leg/routes.ts'
|
||||
import { orderManagementModule } from './order-management/routes.ts'
|
||||
import { overseasWarehouseModule } from './overseas-warehouse/routes.ts'
|
||||
import { systemModule } from './system/routes.ts'
|
||||
import { tenantManagementModule } from './tenant-management/routes.ts'
|
||||
import { getActiveWorkspaceRoutes, type ClientModuleManifest } from './types.ts'
|
||||
|
||||
export const clientModuleRegistry = [
|
||||
dashboardModule,
|
||||
firstLegModule,
|
||||
crossBorderModule,
|
||||
overseasWarehouseModule,
|
||||
orderManagementModule,
|
||||
financeModule,
|
||||
tenantManagementModule,
|
||||
accountModule,
|
||||
systemModule,
|
||||
] satisfies readonly ClientModuleManifest[]
|
||||
|
||||
export const activeWorkspaceRoutes = getActiveWorkspaceRoutes(clientModuleRegistry)
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { ClientModuleManifest } from '../types.ts'
|
||||
|
||||
export const systemModule = {
|
||||
key: 'system',
|
||||
routes: [
|
||||
{
|
||||
path: 'forbidden',
|
||||
name: 'forbidden',
|
||||
component: () => import('./views/ForbiddenView.vue'),
|
||||
release: 'active',
|
||||
meta: { title: '无权访问', description: '当前账号没有访问该页面的权限' },
|
||||
},
|
||||
],
|
||||
} satisfies ClientModuleManifest
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ArrowLeft, ShieldX } from '@lucide/vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useAuthStore } from '@/modules/auth/public'
|
||||
import { getFirstAccessiblePath } from '@/navigation/clientNavigation'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -3,8 +3,9 @@ import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const source = readFileSync(new URL('./merchantIam.ts', import.meta.url), 'utf8')
|
||||
const storeSource = readFileSync(new URL('../stores/tenantAdministration.ts', import.meta.url), 'utf8')
|
||||
const authSource = readFileSync(new URL('../stores/auth.ts', import.meta.url), 'utf8')
|
||||
const storeSource = readFileSync(new URL('../store/tenantAdministration.ts', import.meta.url), 'utf8')
|
||||
const contractsSource = readFileSync(new URL('../contracts.ts', import.meta.url), 'utf8')
|
||||
const authSource = readFileSync(new URL('../../auth/store/auth.ts', import.meta.url), 'utf8')
|
||||
|
||||
test('declares the documented merchant role and member endpoints', () => {
|
||||
assert.match(source, /url: '\/merchant\/v1\/iam\/roles'/)
|
||||
@@ -50,9 +51,11 @@ test('starts empty and only loads server role and member records', () => {
|
||||
})
|
||||
|
||||
test('shares the merchant permission map and preserves resources not rendered by the role editor', () => {
|
||||
assert.match(storeSource, /export const FRONTEND_TO_MERCHANT_PERMISSION_CODES/)
|
||||
assert.match(storeSource, /'order:first-leg:cancel': \['merchant:order:cancel'\]/)
|
||||
assert.match(contractsSource, /export const FRONTEND_TO_MERCHANT_PERMISSION_CODES/)
|
||||
assert.match(contractsSource, /'order:first-leg:cancel': \['merchant:order:cancel'\]/)
|
||||
assert.match(storeSource, /from '@\/modules\/tenant-management\/contracts'/)
|
||||
assert.match(authSource, /FRONTEND_TO_MERCHANT_PERMISSION_CODES/)
|
||||
assert.match(authSource, /from '@\/modules\/tenant-management\/public'/)
|
||||
assert.match(authSource, /Object\.entries\(FRONTEND_TO_MERCHANT_PERMISSION_CODES\)/)
|
||||
assert.match(storeSource, /const preservedPermissionIds = role\.permissionIds\.filter/)
|
||||
assert.match(storeSource, /const nextPermissionIds = \[\.\.\.new Set\(\[\.\.\.preservedPermissionIds, \.\.\.selectedPermissionIds\]\)\]/)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { apiBlobRequest, apiRequest } from '@/api/http'
|
||||
|
||||
export type MerchantTenantStatus = 'PROFILE_REQUIRED' | 'PENDING' | 'ACTIVE' | 'REJECTED' | 'DISABLED'
|
||||
import type { MerchantTenantStatus } from '../contracts'
|
||||
export type QualificationStatus = 'PENDING' | 'APPROVED' | 'REJECTED'
|
||||
|
||||
export interface MerchantTenantProfile {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
export type MerchantTenantStatus = 'PROFILE_REQUIRED' | 'PENDING' | 'ACTIVE' | 'REJECTED' | 'DISABLED'
|
||||
|
||||
export interface TenantPermissionItem {
|
||||
code: string
|
||||
label: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface TenantPermissionGroup {
|
||||
key: string
|
||||
label: string
|
||||
items: TenantPermissionItem[]
|
||||
}
|
||||
|
||||
export const TENANT_PERMISSION_GROUPS: readonly TenantPermissionGroup[] = [
|
||||
{
|
||||
key: 'account',
|
||||
label: '个人账号',
|
||||
items: [{ code: 'account:profile:view', label: '维护我的信息', description: '查看并维护当前成员自己的资料和登录密码' }],
|
||||
},
|
||||
{
|
||||
key: 'dashboard',
|
||||
label: '业务总览',
|
||||
items: [{ code: 'dashboard:view', label: '查看总览', description: '查看企业统计和最近订单' }],
|
||||
},
|
||||
{
|
||||
key: 'orders',
|
||||
label: '头程订单',
|
||||
items: [
|
||||
{ code: 'order:first-leg:list', label: '查看订单', description: '查看本企业头程订单列表和详情' },
|
||||
{ code: 'order:first-leg:create', label: '创建订单', description: '新建并保存订单草稿' },
|
||||
{ code: 'order:first-leg:edit', label: '编辑订单', description: '编辑本企业尚未提交的订单草稿' },
|
||||
{ code: 'order:first-leg:submit', label: '提交订单', description: '提交草稿进入平台审核' },
|
||||
{ code: 'order:first-leg:cancel', label: '取消订单', description: '在允许的状态下取消订单' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'company',
|
||||
label: '企业资料',
|
||||
items: [
|
||||
{ code: 'company:profile:view', label: '查看资料', description: '查看企业、联系人和资质资料' },
|
||||
{ code: 'company:profile:edit', label: '编辑资料', description: '维护企业联系信息和业务地址' },
|
||||
{ code: 'company:qualification:upload', label: '更新资质', description: '上传或更新企业资质文件' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'members',
|
||||
label: '企业成员',
|
||||
items: [
|
||||
{ code: 'company:member:list', label: '查看成员', description: '查看本企业成员列表' },
|
||||
{ code: 'company:member:write', label: '维护成员', description: '新增、编辑、启停企业成员' },
|
||||
{ code: 'company:member:reset-password', label: '重置密码', description: '重置企业成员登录密码' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'roles',
|
||||
label: '角色与权限',
|
||||
items: [
|
||||
{ code: 'company:role:list', label: '查看角色', description: '查看本企业角色列表' },
|
||||
{ code: 'company:role:write', label: '维护角色', description: '新增、编辑、启停企业角色' },
|
||||
{ code: 'company:permission:list', label: '查看权限', description: '查看角色拥有的菜单和操作权限' },
|
||||
{ code: 'company:permission:assign', label: '分配权限', description: '为本企业角色分配菜单及操作权限' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const FRONTEND_TO_MERCHANT_PERMISSION_CODES: Readonly<Record<string, readonly string[]>> = {
|
||||
'order:first-leg:list': ['merchant:order:list', 'merchant:order:view'],
|
||||
'order:first-leg:create': ['merchant:order:create'],
|
||||
'order:first-leg:edit': ['merchant:order:edit'],
|
||||
'order:first-leg:submit': ['merchant:order:submit'],
|
||||
'order:first-leg:cancel': ['merchant:order:cancel'],
|
||||
'company:profile:view': ['merchant:profile:view'],
|
||||
'company:profile:edit': ['merchant:profile:edit'],
|
||||
'company:qualification:upload': ['merchant:qualification:upload'],
|
||||
'company:member:list': ['merchant:member:list'],
|
||||
'company:member:write': ['merchant:member:create', 'merchant:member:edit', 'merchant:member:grant-role', 'merchant:member:status'],
|
||||
'company:member:reset-password': ['merchant:member:reset-password'],
|
||||
'company:role:list': ['merchant:role:list'],
|
||||
'company:role:write': ['merchant:role:create', 'merchant:role:edit', 'merchant:role:status', 'merchant:role:delete'],
|
||||
'company:permission:list': ['merchant:role:list'],
|
||||
'company:permission:assign': ['merchant:role:grant'],
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export {
|
||||
getMerchantPermissionResources,
|
||||
getMerchantPersonalProfile,
|
||||
getTenantProfile,
|
||||
updateMerchantPersonalProfile,
|
||||
type MerchantPermissionResource,
|
||||
} from './api/merchantIam'
|
||||
export {
|
||||
FRONTEND_TO_MERCHANT_PERMISSION_CODES,
|
||||
TENANT_PERMISSION_GROUPS,
|
||||
type MerchantTenantStatus,
|
||||
} from './contracts'
|
||||
export { useTenantAdministrationStore } from './store/tenantAdministration'
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Building2, KeyRound, Settings, ShieldCheck, Users } from '@lucide/vue'
|
||||
import type { ClientModuleManifest } from '../types.ts'
|
||||
|
||||
export const tenantManagementModule = {
|
||||
key: 'company',
|
||||
navigationGroup: { title: '企业管理', icon: Settings, dividerBefore: true },
|
||||
routes: [
|
||||
{
|
||||
path: 'company/profile',
|
||||
name: 'company-profile',
|
||||
component: () => import('./views/CompanyProfileView.vue'),
|
||||
release: 'active',
|
||||
meta: { title: '企业资料', description: '维护企业基本信息、联系人和资质文件', permission: 'company:profile:view' },
|
||||
navigation: { key: 'company-profile', icon: Building2 },
|
||||
},
|
||||
{
|
||||
path: 'organization/members',
|
||||
name: 'members',
|
||||
component: () => import('./views/MembersView.vue'),
|
||||
release: 'active',
|
||||
meta: { title: '企业成员', description: '管理本企业成员账号、状态和角色', permission: 'company:member:list' },
|
||||
navigation: { key: 'company-members', icon: Users },
|
||||
},
|
||||
{
|
||||
path: 'organization/roles',
|
||||
name: 'roles',
|
||||
component: () => import('./views/RolesView.vue'),
|
||||
release: 'active',
|
||||
meta: { title: '角色管理', description: '建立符合企业分工的成员角色', permission: 'company:role:list' },
|
||||
navigation: { key: 'company-roles', icon: ShieldCheck },
|
||||
},
|
||||
{
|
||||
path: 'organization/permissions',
|
||||
name: 'permissions',
|
||||
component: () => import('./views/PermissionsView.vue'),
|
||||
release: 'active',
|
||||
meta: { title: '菜单权限', description: '为本企业角色分配菜单及操作权限', permission: 'company:permission:list' },
|
||||
navigation: { key: 'company-permissions', icon: KeyRound },
|
||||
},
|
||||
],
|
||||
} satisfies ClientModuleManifest
|
||||
@@ -19,8 +19,12 @@ import {
|
||||
type MerchantRole,
|
||||
type MerchantMember,
|
||||
type MerchantPermissionResource,
|
||||
} from '@/modules/tenant-management/api/merchantIam'
|
||||
import {
|
||||
FRONTEND_TO_MERCHANT_PERMISSION_CODES,
|
||||
TENANT_PERMISSION_GROUPS,
|
||||
type MerchantTenantStatus,
|
||||
} from '@/api/merchantIam'
|
||||
} from '@/modules/tenant-management/contracts'
|
||||
|
||||
export type TenantRecordStatus = 'ACTIVE' | 'INACTIVE'
|
||||
|
||||
@@ -67,90 +71,7 @@ export interface TenantRole {
|
||||
permissionCodes: string[]
|
||||
}
|
||||
|
||||
export interface TenantPermissionItem {
|
||||
code: string
|
||||
label: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface TenantPermissionGroup {
|
||||
key: string
|
||||
label: string
|
||||
items: TenantPermissionItem[]
|
||||
}
|
||||
|
||||
export const TENANT_PERMISSION_GROUPS: readonly TenantPermissionGroup[] = [
|
||||
{
|
||||
key: 'account',
|
||||
label: '个人账号',
|
||||
items: [{ code: 'account:profile:view', label: '维护我的信息', description: '查看并维护当前成员自己的资料和登录密码' }],
|
||||
},
|
||||
{
|
||||
key: 'dashboard',
|
||||
label: '业务总览',
|
||||
items: [{ code: 'dashboard:view', label: '查看总览', description: '查看企业统计和最近订单' }],
|
||||
},
|
||||
{
|
||||
key: 'orders',
|
||||
label: '头程订单',
|
||||
items: [
|
||||
{ code: 'order:first-leg:list', label: '查看订单', description: '查看本企业头程订单列表和详情' },
|
||||
{ code: 'order:first-leg:create', label: '创建订单', description: '新建并保存订单草稿' },
|
||||
{ code: 'order:first-leg:edit', label: '编辑订单', description: '编辑本企业尚未提交的订单草稿' },
|
||||
{ code: 'order:first-leg:submit', label: '提交订单', description: '提交草稿进入平台审核' },
|
||||
{ code: 'order:first-leg:cancel', label: '取消订单', description: '在允许的状态下取消订单' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'company',
|
||||
label: '企业资料',
|
||||
items: [
|
||||
{ code: 'company:profile:view', label: '查看资料', description: '查看企业、联系人和资质资料' },
|
||||
{ code: 'company:profile:edit', label: '编辑资料', description: '维护企业联系信息和业务地址' },
|
||||
{ code: 'company:qualification:upload', label: '更新资质', description: '上传或更新企业资质文件' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'members',
|
||||
label: '企业成员',
|
||||
items: [
|
||||
{ code: 'company:member:list', label: '查看成员', description: '查看本企业成员列表' },
|
||||
{ code: 'company:member:write', label: '维护成员', description: '新增、编辑、启停企业成员' },
|
||||
{ code: 'company:member:reset-password', label: '重置密码', description: '重置企业成员登录密码' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'roles',
|
||||
label: '角色与权限',
|
||||
items: [
|
||||
{ code: 'company:role:list', label: '查看角色', description: '查看本企业角色列表' },
|
||||
{ code: 'company:role:write', label: '维护角色', description: '新增、编辑、启停企业角色' },
|
||||
{ code: 'company:permission:list', label: '查看权限', description: '查看角色拥有的菜单和操作权限' },
|
||||
{ code: 'company:permission:assign', label: '分配权限', description: '为本企业角色分配菜单及操作权限' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const allPermissionCodes = TENANT_PERMISSION_GROUPS.flatMap((group) => group.items.map((item) => item.code))
|
||||
|
||||
export const FRONTEND_TO_MERCHANT_PERMISSION_CODES: Readonly<Record<string, readonly string[]>> = {
|
||||
'order:first-leg:list': ['merchant:order:list', 'merchant:order:view'],
|
||||
'order:first-leg:create': ['merchant:order:create'],
|
||||
'order:first-leg:edit': ['merchant:order:edit'],
|
||||
'order:first-leg:submit': ['merchant:order:submit'],
|
||||
'order:first-leg:cancel': ['merchant:order:cancel'],
|
||||
'company:profile:view': ['merchant:profile:view'],
|
||||
'company:profile:edit': ['merchant:profile:edit'],
|
||||
'company:qualification:upload': ['merchant:qualification:upload'],
|
||||
'company:member:list': ['merchant:member:list'],
|
||||
'company:member:write': ['merchant:member:create', 'merchant:member:edit', 'merchant:member:grant-role', 'merchant:member:status'],
|
||||
'company:member:reset-password': ['merchant:member:reset-password'],
|
||||
'company:role:list': ['merchant:role:list'],
|
||||
'company:role:write': ['merchant:role:create', 'merchant:role:edit', 'merchant:role:status', 'merchant:role:delete'],
|
||||
'company:permission:list': ['merchant:role:list'],
|
||||
'company:permission:assign': ['merchant:role:grant'],
|
||||
}
|
||||
|
||||
const mappedMerchantPermissionCodes = new Set(Object.values(FRONTEND_TO_MERCHANT_PERMISSION_CODES).flat())
|
||||
|
||||
const emptyCompany: TenantCompanyProfile = {
|
||||
|
||||
@@ -4,10 +4,10 @@ import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules, UploadFile, UploadInstance } from 'element-plus'
|
||||
import { Building2, CheckCircle2, Clock3, FileCheck2, FileText, Pencil, Trash2, Upload, UserRound } from '@lucide/vue'
|
||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
||||
import { useTenantAdministrationStore, type TenantCompanyProfile, type TenantQualification } from '@/stores/tenantAdministration'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { formatQualificationFileSize, validateQualificationFile } from '@/utils/qualificationFile'
|
||||
import { downloadQualificationImage, uploadBusinessLicense } from '@/api/merchantIam'
|
||||
import { useTenantAdministrationStore, type TenantCompanyProfile, type TenantQualification } from '@/modules/tenant-management/store/tenantAdministration'
|
||||
import { useAuthStore } from '@/modules/auth/public'
|
||||
import { formatQualificationFileSize, validateQualificationFile } from '@/modules/tenant-management/utils/qualificationFile'
|
||||
import { downloadQualificationImage, uploadBusinessLicense } from '@/modules/tenant-management/api/merchantIam'
|
||||
import { ApiError } from '@/api/http'
|
||||
|
||||
const store = useTenantAdministrationStore()
|
||||
|
||||
@@ -4,8 +4,8 @@ import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'elem
|
||||
import { KeyRound, Pencil, Plus } from '@lucide/vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
||||
import { useTenantAdministrationStore, type TenantMember, type TenantRecordStatus } from '@/stores/tenantAdministration'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useTenantAdministrationStore, type TenantMember, type TenantRecordStatus } from '@/modules/tenant-management/store/tenantAdministration'
|
||||
import { useAuthStore } from '@/modules/auth/public'
|
||||
|
||||
const store = useTenantAdministrationStore()
|
||||
const auth = useAuthStore()
|
||||
|
||||
@@ -3,8 +3,9 @@ import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { LockKeyhole, RotateCcw, Save, ShieldCheck } from '@lucide/vue'
|
||||
import { TENANT_PERMISSION_GROUPS, useTenantAdministrationStore } from '@/stores/tenantAdministration'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useTenantAdministrationStore } from '@/modules/tenant-management/store/tenantAdministration'
|
||||
import { TENANT_PERMISSION_GROUPS } from '@/modules/tenant-management/contracts'
|
||||
import { useAuthStore } from '@/modules/auth/public'
|
||||
|
||||
const store = useTenantAdministrationStore()
|
||||
const auth = useAuthStore()
|
||||
|
||||
@@ -5,8 +5,8 @@ import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'elem
|
||||
import { LockKeyhole, Pencil, Plus, ShieldCheck, Trash2 } from '@lucide/vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
||||
import { useTenantAdministrationStore, type TenantRole } from '@/stores/tenantAdministration'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useTenantAdministrationStore, type TenantRole } from '@/modules/tenant-management/store/tenantAdministration'
|
||||
import { useAuthStore } from '@/modules/auth/public'
|
||||
|
||||
const store = useTenantAdministrationStore()
|
||||
const auth = useAuthStore()
|
||||
|
||||
@@ -2,18 +2,19 @@ import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const routerSource = fs.readFileSync(new URL('../router/index.ts', import.meta.url), 'utf8')
|
||||
const routerSource = fs.readFileSync(new URL('../routes.ts', import.meta.url), 'utf8')
|
||||
const profileSource = fs.readFileSync(new URL('./CompanyProfileView.vue', import.meta.url), 'utf8')
|
||||
const membersSource = fs.readFileSync(new URL('./MembersView.vue', import.meta.url), 'utf8')
|
||||
const rolesSource = fs.readFileSync(new URL('./RolesView.vue', import.meta.url), 'utf8')
|
||||
const permissionsSource = fs.readFileSync(new URL('./PermissionsView.vue', import.meta.url), 'utf8')
|
||||
const storeSource = fs.readFileSync(new URL('../stores/tenantAdministration.ts', import.meta.url), 'utf8')
|
||||
const storeSource = fs.readFileSync(new URL('../store/tenantAdministration.ts', import.meta.url), 'utf8')
|
||||
|
||||
test('routes tenant administration modules to implemented views instead of the placeholder', () => {
|
||||
assert.match(routerSource, /company\/profile'.*CompanyProfileView\.vue/)
|
||||
assert.match(routerSource, /organization\/members'.*MembersView\.vue/)
|
||||
assert.match(routerSource, /organization\/roles'.*RolesView\.vue/)
|
||||
assert.match(routerSource, /organization\/permissions'.*PermissionsView\.vue/)
|
||||
assert.match(routerSource, /path: 'company\/profile'[\s\S]*?CompanyProfileView\.vue/)
|
||||
assert.match(routerSource, /path: 'organization\/members'[\s\S]*?MembersView\.vue/)
|
||||
assert.match(routerSource, /path: 'organization\/roles'[\s\S]*?RolesView\.vue/)
|
||||
assert.match(routerSource, /path: 'organization\/permissions'[\s\S]*?PermissionsView\.vue/)
|
||||
assert.equal((routerSource.match(/release: 'active'/g) ?? []).length, 4)
|
||||
assert.doesNotMatch(routerSource, /ModuleView\.vue/)
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { Component } from 'vue'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
export type ClientRouteRelease = 'active' | 'prototype'
|
||||
|
||||
export interface ClientRouteNavigation {
|
||||
key: string
|
||||
icon: Component
|
||||
title?: string
|
||||
}
|
||||
|
||||
export interface ClientModuleNavigationGroup {
|
||||
title: string
|
||||
icon: Component
|
||||
dividerBefore?: boolean
|
||||
}
|
||||
|
||||
interface ClientRouteBase {
|
||||
path: string
|
||||
name: string
|
||||
component: Component | (() => Promise<unknown>)
|
||||
release: ClientRouteRelease
|
||||
meta: {
|
||||
title: string
|
||||
description?: string
|
||||
permission?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface NavigableClientRoute extends ClientRouteBase {
|
||||
navigation: ClientRouteNavigation
|
||||
meta: ClientRouteBase['meta'] & { permission: string }
|
||||
}
|
||||
|
||||
export interface HiddenClientRoute extends ClientRouteBase {
|
||||
navigation?: never
|
||||
}
|
||||
|
||||
export type ClientRouteDefinition = NavigableClientRoute | HiddenClientRoute
|
||||
|
||||
export interface ClientModuleManifest {
|
||||
key: string
|
||||
navigationGroup?: ClientModuleNavigationGroup
|
||||
routes: readonly ClientRouteDefinition[]
|
||||
}
|
||||
|
||||
export function getActiveWorkspaceRoutes(modules: readonly ClientModuleManifest[]): RouteRecordRaw[] {
|
||||
return modules.flatMap((module) => module.routes
|
||||
.filter((route) => route.release === 'active')
|
||||
.map(({ release: _release, navigation: _navigation, ...route }) => route as RouteRecordRaw))
|
||||
}
|
||||
Reference in New Issue
Block a user