docs: initialize documentation repository

This commit is contained in:
2026-07-31 10:43:58 +08:00
commit 1ec28f7cfc
72 changed files with 48272 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
.DS_Store
**/.DS_Store
+21
View File
@@ -0,0 +1,21 @@
# 虫虫出海文档索引
项目交付文档按阶段归档,跨阶段原型、调研和过程设计保留在原专题目录。
## 阶段文档
- [一期文档](一期/README.md):一期范围、开发进度和头程订单最小闭环。
- [二期文档](二期/README.md):预约取货、到仓收货、实测差异和指定库位闭环。
- [三期文档](三期/README.md):智能排柜、订舱、拖车、PDA 拣货、报关协同和国内出仓闭环。
## 跨阶段参考
- `悦/`:产品原型、流程图、历史架构资料和调研数据,仅作为需求参考,不直接等同于已确认范围。
- `superpowers/`:具体功能的设计与实施记录,按日期和主题维护。
## 维护规则
- 只影响单个阶段的范围、进度和验收文档放入对应阶段目录。
- 公共架构、跨阶段契约和长期产品资料保留在 `docs/` 根目录或独立专题目录。
- 阶段范围变化时同步更新该阶段的规划、进度表及受影响的公共契约。
- 原型中出现的功能不自动进入开发范围,必须在阶段规划中明确优先级、依赖和验收标准。
@@ -0,0 +1,552 @@
# Admin Grouped Navigation Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Replace the flat admin sidebar with maintainable two-level navigation while preserving every existing static page and interaction.
**Architecture:** Keep Vue Router responsible only for routes and move admin menu metadata into a local navigation module. Render the configuration through a focused Element Plus sidebar component, while all business pages continue to use the current Pinia mock store and make no Java API calls.
**Tech Stack:** Vue 3, TypeScript, Vue Router, Pinia, Element Plus, Lucide Vue, Vitest, Vue Test Utils, jsdom, pnpm
---
The repository is not a Git worktree. Commit steps are intentionally omitted; each green test/build checkpoint is the recovery boundary.
## File map
- Create `apps/admin-web/vitest.config.ts`: admin-only unit-test configuration.
- Create `apps/admin-web/src/navigation/adminNavigation.ts`: admin-local two-level menu types and data.
- Create `apps/admin-web/src/navigation/adminNavigation.test.ts`: navigation structure and uniqueness contract.
- Create `apps/admin-web/src/components/AdminSidebarMenu.vue`: Element Plus menu renderer.
- Create `apps/admin-web/src/components/AdminSidebarMenu.test.ts`: grouped labels, paths and collapsed-state behavior.
- Modify `apps/admin-web/src/layouts/AppLayout.vue`: delegate menu rendering to `AdminSidebarMenu`.
- Modify `apps/admin-web/src/router/index.ts`: stop exporting flat menu metadata.
- Modify `apps/admin-web/src/styles/index.css`: style Element Plus menu within the existing navy sidebar.
- Modify `apps/admin-web/package.json` and `apps/admin-web/pnpm-lock.yaml`: add admin-only test dependencies and script.
- Modify `apps/admin-web/README.md`: document grouped menu and test command.
- Modify `apps/admin-web/AGENTS.md`: add the new admin-local test quality gate.
### Task 1: Install the admin-only test harness
**Files:**
- Modify: `apps/admin-web/package.json`
- Modify: `apps/admin-web/pnpm-lock.yaml`
- Create: `apps/admin-web/vitest.config.ts`
- [ ] **Step 1: Install test dependencies inside admin-web**
Run:
```powershell
cd C:\Users\BY\Desktop\code\chongchong-global\apps\admin-web
pnpm add -D vitest @vue/test-utils jsdom
```
Expected: only `apps/admin-web/package.json`, `apps/admin-web/pnpm-lock.yaml` and `apps/admin-web/node_modules` change.
- [ ] **Step 2: Add the test script**
Add to `scripts` in `apps/admin-web/package.json`:
```json
"test": "vitest run"
```
- [ ] **Step 3: Add the Vitest configuration**
Create `apps/admin-web/vitest.config.ts`:
```ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
test: {
environment: 'jsdom',
include: ['src/**/*.test.ts'],
clearMocks: true,
},
})
```
- [ ] **Step 4: Verify the empty harness**
Run:
```powershell
pnpm test -- --passWithNoTests
```
Expected: PASS with no test files and no dependency-resolution errors.
### Task 2: Define the grouped navigation with TDD
**Files:**
- Create: `apps/admin-web/src/navigation/adminNavigation.test.ts`
- Create: `apps/admin-web/src/navigation/adminNavigation.ts`
- [ ] **Step 1: Write the failing navigation contract**
Create `apps/admin-web/src/navigation/adminNavigation.test.ts`:
```ts
import { describe, expect, it } from 'vitest'
import { adminNavigation, getNavigationLeaves } from './adminNavigation'
describe('adminNavigation', () => {
it('keeps only the dashboard as a direct first-level page', () => {
expect(adminNavigation.map((item) => item.title)).toEqual([
'工作台',
'业务管理',
'系统管理',
])
expect(adminNavigation[0].path).toBe('/')
expect(adminNavigation[1].children?.map((item) => item.title)).toEqual([
'客户管理',
'头程订单',
])
expect(adminNavigation[2].children?.map((item) => item.title)).toEqual([
'内部用户',
'角色管理',
'菜单权限',
])
})
it('assigns every page a unique path and permission code', () => {
const leaves = getNavigationLeaves(adminNavigation)
const paths = leaves.map((item) => item.path)
const permissions = leaves.map((item) => item.permission)
expect(paths).toEqual([
'/',
'/customers',
'/orders/first-leg',
'/system/users',
'/system/roles',
'/system/permissions',
])
expect(new Set(paths).size).toBe(paths.length)
expect(new Set(permissions).size).toBe(permissions.length)
expect(leaves.every((item) => Boolean(item.icon))).toBe(true)
})
})
```
- [ ] **Step 2: Run the test and verify RED**
Run:
```powershell
pnpm test -- src/navigation/adminNavigation.test.ts
```
Expected: FAIL because `src/navigation/adminNavigation.ts` does not exist.
- [ ] **Step 3: Implement the minimal local navigation model**
Create `apps/admin-web/src/navigation/adminNavigation.ts`:
```ts
import type { Component } from 'vue'
import {
BriefcaseBusiness,
Building2,
ClipboardList,
KeyRound,
LayoutDashboard,
Settings,
ShieldCheck,
Users,
} from '@lucide/vue'
export interface AdminNavigationNode {
key: string
title: string
icon: Component
path?: string
permission?: string
children?: AdminNavigationNode[]
}
export interface AdminNavigationLeaf extends AdminNavigationNode {
path: string
permission: string
children?: never
}
export const adminNavigation: AdminNavigationNode[] = [
{
key: 'dashboard',
title: '工作台',
icon: LayoutDashboard,
path: '/',
permission: 'dashboard:view',
},
{
key: 'business',
title: '业务管理',
icon: BriefcaseBusiness,
children: [
{ key: 'customers', title: '客户管理', icon: Building2, path: '/customers', permission: 'customer:list' },
{ key: 'first-leg-orders', title: '头程订单', icon: ClipboardList, path: '/orders/first-leg', permission: 'first-leg:list' },
],
},
{
key: 'system',
title: '系统管理',
icon: Settings,
children: [
{ key: 'internal-users', title: '内部用户', icon: Users, path: '/system/users', permission: 'system:user:list' },
{ key: 'roles', title: '角色管理', icon: ShieldCheck, path: '/system/roles', permission: 'system:role:list' },
{ key: 'permissions', title: '菜单权限', icon: KeyRound, path: '/system/permissions', permission: 'system:permission:list' },
],
},
]
export function getNavigationLeaves(nodes: AdminNavigationNode[]): AdminNavigationLeaf[] {
return nodes.flatMap((node) => {
if (node.children?.length) return getNavigationLeaves(node.children)
if (!node.path || !node.permission) return []
return [node as AdminNavigationLeaf]
})
}
```
- [ ] **Step 4: Run the test and verify GREEN**
Run:
```powershell
pnpm test -- src/navigation/adminNavigation.test.ts
```
Expected: 2 tests PASS.
### Task 3: Render the two-level menu with TDD
**Files:**
- Create: `apps/admin-web/src/components/AdminSidebarMenu.test.ts`
- Create: `apps/admin-web/src/components/AdminSidebarMenu.vue`
- [ ] **Step 1: Write the failing sidebar component test**
Create `apps/admin-web/src/components/AdminSidebarMenu.test.ts`:
```ts
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import AdminSidebarMenu from './AdminSidebarMenu.vue'
const stubs = {
'el-menu': {
props: ['defaultActive', 'collapse'],
template: '<nav :data-active="defaultActive" :data-collapsed="String(collapse)"><slot /></nav>',
},
'el-sub-menu': {
props: ['index'],
template: '<section data-kind="group" :data-index="index"><header><slot name="title" /></header><slot /></section>',
},
'el-menu-item': {
props: ['index'],
template: '<a data-kind="item" :data-index="index"><slot /></a>',
},
}
describe('AdminSidebarMenu', () => {
it('renders grouped page entries and forwards the active route', () => {
const wrapper = mount(AdminSidebarMenu, {
props: { activePath: '/system/users', collapsed: false },
global: { stubs },
})
expect(wrapper.get('nav').attributes('data-active')).toBe('/system/users')
expect(wrapper.findAll('[data-kind="group"] header').map((node) => node.text())).toEqual([
'业务管理',
'系统管理',
])
expect(wrapper.findAll('[data-kind="item"]').map((node) => node.attributes('data-index'))).toEqual([
'/',
'/customers',
'/orders/first-leg',
'/system/users',
'/system/roles',
'/system/permissions',
])
})
it('forwards desktop collapse state to Element Plus', () => {
const wrapper = mount(AdminSidebarMenu, {
props: { activePath: '/', collapsed: true },
global: { stubs },
})
expect(wrapper.get('nav').attributes('data-collapsed')).toBe('true')
})
})
```
- [ ] **Step 2: Run the component test and verify RED**
Run:
```powershell
pnpm test -- src/components/AdminSidebarMenu.test.ts
```
Expected: FAIL because `AdminSidebarMenu.vue` does not exist.
- [ ] **Step 3: Implement the minimal menu component**
Create `apps/admin-web/src/components/AdminSidebarMenu.vue`:
```vue
<script setup lang="ts">
import { adminNavigation } from '@/navigation/adminNavigation'
defineProps<{
activePath: string
collapsed: boolean
}>()
</script>
<template>
<el-menu
class="side-nav-menu"
:default-active="activePath"
:collapse="collapsed"
:default-openeds="['business', 'system']"
router
>
<template v-for="item in adminNavigation" :key="item.key">
<el-sub-menu v-if="item.children?.length" :index="item.key">
<template #title>
<component :is="item.icon" :size="19" :stroke-width="1.8" />
<span>{{ item.title }}</span>
</template>
<el-menu-item v-for="child in item.children" :key="child.key" :index="child.path || child.key">
<component :is="child.icon" :size="17" :stroke-width="1.8" />
<template #title>{{ child.title }}</template>
</el-menu-item>
</el-sub-menu>
<el-menu-item v-else :index="item.path || item.key">
<component :is="item.icon" :size="19" :stroke-width="1.8" />
<template #title>{{ item.title }}</template>
</el-menu-item>
</template>
</el-menu>
</template>
```
- [ ] **Step 4: Run both focused tests and verify GREEN**
Run:
```powershell
pnpm test -- src/navigation/adminNavigation.test.ts src/components/AdminSidebarMenu.test.ts
```
Expected: 4 tests PASS.
### Task 4: Integrate the component into AppLayout
**Files:**
- Modify: `apps/admin-web/src/layouts/AppLayout.vue`
- Modify: `apps/admin-web/src/router/index.ts`
- [ ] **Step 1: Simplify AppLayout imports and state**
Remove the six page icon imports, `adminNavigation`, and `iconMap`. Import the focused component:
```ts
import AdminSidebarMenu from '@/components/AdminSidebarMenu.vue'
```
Add the computed collapse state so the mobile drawer never renders the desktop collapsed menu:
```ts
const menuCollapsed = computed(() => collapsed.value && !mobileMenuOpen.value)
```
- [ ] **Step 2: Replace the flat RouterLink loop**
Replace the contents of `<nav class="side-nav" aria-label="主导航">` with:
```vue
<AdminSidebarMenu :active-path="route.path" :collapsed="menuCollapsed" />
```
Keep the existing route watcher so selecting a menu item closes the mobile drawer.
- [ ] **Step 3: Remove flat navigation metadata from the router**
Delete the `NavigationItem` import and `adminNavigation` array from `apps/admin-web/src/router/index.ts`. Do not change any route path, name, component or permission metadata.
- [ ] **Step 4: Verify all unit tests remain green**
Run:
```powershell
pnpm test
```
Expected: 4 tests PASS with no warnings.
### Task 5: Adapt the existing sidebar styling
**Files:**
- Modify: `apps/admin-web/src/styles/index.css`
- [ ] **Step 1: Replace the flat link styles**
Replace `.side-nav`, `.side-nav-item` and its hover/active rules with:
```css
.side-nav { flex: 1; min-height: 0; overflow-y: auto; padding: 14px 10px; }
.side-nav-menu {
width: 100%;
border-right: 0 !important;
background: transparent !important;
--el-menu-bg-color: transparent;
--el-menu-text-color: #bdc7d7;
--el-menu-active-color: var(--cc-navy);
--el-menu-hover-bg-color: rgba(255,255,255,.08);
}
.side-nav-menu.el-menu--collapse { width: 56px; }
.side-nav-menu .el-menu-item,
.side-nav-menu .el-sub-menu__title {
height: 44px;
margin: 3px 0;
border-radius: 8px;
line-height: 44px;
}
.side-nav-menu .el-menu-item:hover,
.side-nav-menu .el-sub-menu__title:hover { color: #fff; }
.side-nav-menu .el-menu-item.is-active { background: #fff; color: var(--cc-navy); font-weight: 650; }
.side-nav-menu .el-sub-menu .el-menu-item { min-width: 0; padding-left: 48px !important; }
.side-nav-menu .el-menu-item svg,
.side-nav-menu .el-sub-menu__title svg { flex: 0 0 auto; margin-right: 12px; }
```
- [ ] **Step 2: Update the mobile override**
Remove mobile rules that reference `.side-nav-item` and add:
```css
@media (max-width: 720px) {
.side-nav-menu,
.side-nav-menu.el-menu--collapse { width: 100%; }
}
```
Do not alter the existing sidebar transform, scrim, 44px touch targets or reduced-motion behavior.
- [ ] **Step 3: Run static verification**
Run:
```powershell
pnpm test
pnpm typecheck
pnpm build
```
Expected: tests, Vue TypeScript checks and production build all PASS.
### Task 6: Browser and Impeccable verification
**Files:**
- Modify only if a verified defect is found: `apps/admin-web/src/components/AdminSidebarMenu.vue`, `apps/admin-web/src/layouts/AppLayout.vue`, `apps/admin-web/src/styles/index.css`
- [ ] **Step 1: Start the admin development server**
Run:
```powershell
pnpm dev
```
Expected: admin is available at `http://localhost:5173`.
- [ ] **Step 2: Verify desktop expanded and collapsed states in one browser pass**
Check:
- Workbench remains a direct entry.
- Business management contains customers and first-leg orders.
- System management contains users, roles and permissions.
- Active route remains highlighted after direct URL navigation.
- Collapsing the sidebar exposes grouped entries through Element Plus popovers.
- Existing customer, order, user, role and permission interactions still work.
- [ ] **Step 3: Verify the 390px mobile state in the same pass**
Check:
- The menu button opens the drawer.
- Both groups can expand and every leaf is reachable.
- Selecting a leaf closes the drawer.
- Clicking the right-side scrim closes the drawer.
- The page itself has no horizontal viewport overflow.
- [ ] **Step 4: Run the Impeccable detector once**
Run:
```powershell
node C:\Users\BY\.codex\skills\impeccable\scripts\detect.mjs --json src/components/AdminSidebarMenu.vue src/layouts/AppLayout.vue src/styles/index.css
```
Expected: no unverified detector findings remain. Fix verified findings in one batch, then perform at most one confirmation browser pass.
### Task 7: Update project documentation and complete verification
**Files:**
- Modify: `apps/admin-web/README.md`
- Modify: `apps/admin-web/AGENTS.md`
- [ ] **Step 1: Document tests and grouped navigation**
Add:
````markdown
测试:
```bash
pnpm test
```
后台菜单按“业务管理”和“系统管理”分组;当前页面使用 Pinia 模拟数据,不请求尚未确定的 Java 业务接口。
````
Update the quality-gate section of `apps/admin-web/AGENTS.md` to include `pnpm test`, and replace the obsolete statement that the project has no automated test script with a requirement to run unit tests for navigation or other tested behavior changes.
- [ ] **Step 2: Run final verification from admin-web**
Run:
```powershell
pnpm test
pnpm typecheck
pnpm build
```
Expected: all commands exit with code 0 and `apps/admin-web/dist` is refreshed.
- [ ] **Step 3: Confirm repository isolation**
Run from the repository root:
```powershell
Get-ChildItem -Force -File | Where-Object { $_.Name -in @('package.json','pnpm-workspace.yaml','pnpm-lock.yaml') }
```
Expected: no Node package or workspace files exist at the repository root.
@@ -0,0 +1,316 @@
# Admin Permission Tree Sync Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make permission management and role authorization use the same grouped, recursive static permission hierarchy as the admin sidebar while preserving all existing local interactions.
**Architecture:** Add one pure permission-tree utility that builds arbitrary-depth trees, preserves ancestors during filtering, sorts siblings, and safely traverses descendants. Keep the sidebar and permission data independent, then verify their menu permission codes agree in tests; both views consume the utility, while Pinia remains the static data source and no Java API contract is invented.
**Tech Stack:** Vue 3, TypeScript, Pinia, Element Plus, Vitest, Vue Test Utils, Vite
---
### Task 1: Lock the recursive tree contract with tests
**Files:**
- Create: `apps/admin-web/src/utils/permissionTree.test.ts`
- Create: `apps/admin-web/src/utils/permissionTree.ts`
- [ ] **Step 1: Write failing tests for recursive structure and sibling sorting**
Create fixtures with a directory, menu, and button in deliberately unsorted input order. Import the wished-for `buildPermissionTree` function and assert that it returns all three levels and sorts each sibling group by `sort`.
```ts
import { describe, expect, it } from 'vitest'
import type { Permission } from '@/types/admin'
import { buildPermissionTree } from '@/utils/permissionTree'
const permissions: Permission[] = [
{ id: 'action-b', parentId: 'menu', name: '操作 B', code: 'demo:b', type: '按钮', path: '', method: '-', status: '启用', sort: 20 },
{ id: 'menu', parentId: 'directory', name: '页面', code: 'demo:list', type: '菜单', path: '/demo', method: '-', status: '启用', sort: 10 },
{ id: 'directory', parentId: null, name: '目录', code: 'demo', type: '目录', path: '', method: '-', status: '启用', sort: 10 },
{ id: 'action-a', parentId: 'menu', name: '操作 A', code: 'demo:a', type: '按钮', path: '', method: '-', status: '启用', sort: 10 },
]
it('builds arbitrary-depth trees and sorts siblings', () => {
const tree = buildPermissionTree(permissions)
expect(tree[0]?.children?.[0]?.children?.map((node) => node.id)).toEqual(['action-a', 'action-b'])
})
```
- [ ] **Step 2: Run the focused test and verify RED**
Run: `pnpm test -- src/utils/permissionTree.test.ts`
Expected: FAIL because `@/utils/permissionTree` does not exist.
- [ ] **Step 3: Add failing tests for ancestor-preserving search and cycle-safe descendants**
Add assertions that a matcher for `demo:a` retains `directory -> menu -> action-a`, and that `getDescendantIds` returns each reachable child once even when malformed fixture data includes a cycle.
```ts
import { buildPermissionTree, getDescendantIds } from '@/utils/permissionTree'
it('keeps the ancestor chain when a deep descendant matches', () => {
const tree = buildPermissionTree(permissions, (item) => item.code === 'demo:a')
expect(tree.map((node) => node.id)).toEqual(['directory'])
expect(tree[0]?.children?.[0]?.children?.map((node) => node.id)).toEqual(['action-a'])
})
it('collects descendants once when malformed data contains a cycle', () => {
const malformed = [
...permissions,
{ id: 'cycle-a', parentId: 'cycle-b', name: 'A', code: 'cycle:a', type: '目录', path: '', method: '-', status: '启用', sort: 1 },
{ id: 'cycle-b', parentId: 'cycle-a', name: 'B', code: 'cycle:b', type: '目录', path: '', method: '-', status: '启用', sort: 2 },
] satisfies Permission[]
expect(getDescendantIds(malformed, 'cycle-a')).toEqual(['cycle-b'])
})
```
- [ ] **Step 4: Implement the minimal recursive utility**
Create `permissionTree.ts` with a `PermissionTreeNode` type, a sorted parent index, path-local cycle protection for tree construction, ancestor-preserving matching, and breadth-first descendant traversal whose visited set starts with the selected node.
```ts
import type { Permission } from '@/types/admin'
export type PermissionTreeNode = Permission & { children?: PermissionTreeNode[] }
export type PermissionMatcher = (permission: Permission) => boolean
function groupByParent(permissions: Permission[]) {
const childrenByParent = new Map<string | null, Permission[]>()
permissions.forEach((permission) => {
const siblings = childrenByParent.get(permission.parentId) ?? []
siblings.push(permission)
childrenByParent.set(permission.parentId, siblings)
})
childrenByParent.forEach((siblings) => siblings.sort((left, right) => left.sort - right.sort))
return childrenByParent
}
export function buildPermissionTree(permissions: Permission[], matches: PermissionMatcher = () => true): PermissionTreeNode[] {
const childrenByParent = groupByParent(permissions)
const buildNode = (permission: Permission, ancestors: Set<string>): PermissionTreeNode | null => {
if (ancestors.has(permission.id)) return null
const nextAncestors = new Set(ancestors).add(permission.id)
const children = (childrenByParent.get(permission.id) ?? [])
.map((child) => buildNode(child, nextAncestors))
.filter((child): child is PermissionTreeNode => child !== null)
if (!matches(permission) && children.length === 0) return null
return { ...permission, ...(children.length ? { children } : {}) }
}
return (childrenByParent.get(null) ?? [])
.map((permission) => buildNode(permission, new Set()))
.filter((node): node is PermissionTreeNode => node !== null)
}
export function getDescendantIds(permissions: Permission[], permissionId: string): string[] {
const childrenByParent = groupByParent(permissions)
const visited = new Set([permissionId])
const descendants: string[] = []
const queue = [...(childrenByParent.get(permissionId) ?? [])]
while (queue.length) {
const current = queue.shift()
if (!current || visited.has(current.id)) continue
visited.add(current.id)
descendants.push(current.id)
queue.push(...(childrenByParent.get(current.id) ?? []))
}
return descendants
}
```
- [ ] **Step 5: Run the focused test and verify GREEN**
Run: `pnpm test -- src/utils/permissionTree.test.ts`
Expected: all permission-tree utility tests pass with no warnings.
### Task 2: Restructure the static permission and role fixtures
**Files:**
- Modify: `apps/admin-web/src/stores/adminData.ts`
- Create: `apps/admin-web/src/stores/adminData.test.ts`
- [ ] **Step 1: Write failing tests for the grouped hierarchy**
Activate a fresh Pinia instance, read `useAdminDataStore()`, build its tree, and assert the roots are `工作台`, `业务管理`, and `系统管理`; assert `业务管理` owns the customer/order pages and `系统管理` owns the three system pages.
```ts
import { beforeEach, describe, expect, it } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import { useAdminDataStore } from '@/stores/adminData'
import { buildPermissionTree } from '@/utils/permissionTree'
beforeEach(() => setActivePinia(createPinia()))
it('groups page permissions under business and system directories', () => {
const tree = buildPermissionTree(useAdminDataStore().permissions)
expect(tree.map((node) => node.name)).toEqual(['工作台', '业务管理', '系统管理'])
expect(tree[1]?.children?.map((node) => node.name)).toEqual(['客户管理', '头程订单'])
expect(tree[2]?.children?.map((node) => node.name)).toEqual(['内部用户', '角色管理', '菜单权限'])
})
```
- [ ] **Step 2: Write a failing navigation consistency test**
Compare all `菜单` permission codes with `getNavigationLeaves(adminNavigation)` and assert both sets are equal, proving visible routes and static permission resources cannot silently drift.
```ts
it('keeps menu permission codes aligned with navigation leaves', () => {
const store = useAdminDataStore()
const menuCodes = store.permissions.filter((item) => item.type === '菜单').map((item) => item.code).sort()
const navigationCodes = getNavigationLeaves(adminNavigation).map((item) => item.permission).sort()
expect(menuCodes).toEqual(navigationCodes)
})
```
- [ ] **Step 3: Run store tests and verify RED**
Run: `pnpm test -- src/stores/adminData.test.ts`
Expected: FAIL because current permissions expose five flat roots and contain redundant view nodes.
- [ ] **Step 4: Replace the permission fixtures**
Use stable IDs and the confirmed hierarchy:
```text
P001 工作台 (菜单)
P010 业务管理 (目录)
P100 客户管理 (菜单)
P102 新增客户 (按钮)
P103 审核客户 (按钮)
P200 头程订单 (菜单)
P202 创建订单 (按钮)
P203 更新状态 (按钮)
P020 系统管理 (目录)
P300 内部用户 (菜单)
P302 维护用户 (按钮)
P400 角色管理 (菜单)
P402 角色授权 (按钮)
P500 菜单权限 (菜单)
P502 维护权限 (按钮)
```
Menu nodes keep their real route paths and route permission codes. Directory and button nodes use an empty path and `method: '-'`; do not add `接口` fixtures or `/api/...` paths before the Java contract exists.
- [ ] **Step 5: Update all five role permission ID lists**
Keep `R001` mapped to all resources. Include grouping directory IDs for the other roles and grant only the confirmed menu/action resources described by the design document.
```ts
R002: P001, P010, P100, P103, P200, P203
R003: P001, P010, P200, P202, P203
R004: P001, P010, P200
R005: P001, P010, P100, P102, P200
```
- [ ] **Step 6: Run store and full tests and verify GREEN**
Run: `pnpm test -- src/stores/adminData.test.ts`
Expected: grouped hierarchy and navigation consistency tests pass.
Run: `pnpm test`
Expected: all existing navigation/sidebar tests and new tests pass.
### Task 3: Integrate the recursive utility into both views
**Files:**
- Modify: `apps/admin-web/src/views/PermissionsView.vue`
- Modify: `apps/admin-web/src/views/RolesView.vue`
- Read before UI edits: `C:/Users/BY/.codex/skills/impeccable/reference/audit.md`
- Read before UI edits: `C:/Users/BY/.codex/skills/impeccable/reference/craft-floor.md`
- [ ] **Step 1: Replace the permission page's local two-level builder**
Import `buildPermissionTree` and `getDescendantIds`. Compute `treeRows` with `buildPermissionTree(store.permissions, matches)` so filters preserve full ancestor chains. Compute a blocked ID set from `form.id` plus all descendants and exclude those resources from `parentOptions`.
```ts
const treeRows = computed(() => buildPermissionTree(store.permissions, matches))
const blockedParentIds = computed(() => new Set(form.id ? [form.id, ...getDescendantIds(store.permissions, form.id)] : []))
const parentOptions = computed(() => store.permissions.filter((item) =>
['目录', '菜单'].includes(item.type) && !blockedParentIds.value.has(item.id),
))
```
- [ ] **Step 2: Update permission-page copy for static API scope**
Keep all existing interactions, but change copy that implies interface resources are already active. The table description should say API permissions will be added after the service contract is fixed, the fourth metric should show `0` interface permissions as reserved, and path labels/placeholders should distinguish routes from future interfaces without fabricating endpoints.
- [ ] **Step 3: Replace the role page's local two-level builder**
Import `buildPermissionTree`, map its recursive nodes to the Element Plus node shape through one recursive mapper, and keep `default-expand-all`, checked keys, half-checked keys, and save behavior unchanged.
```ts
function toRolePermissionNode(node: PermissionTreeNode): RolePermissionTreeNode {
return {
id: node.id,
label: node.name,
code: node.code,
...(node.children?.length ? { children: node.children.map(toRolePermissionNode) } : {}),
}
}
const permissionTree = computed(() => buildPermissionTree(store.permissions).map(toRolePermissionNode))
```
- [ ] **Step 4: Correct role permission summaries**
Count `菜单` items only as menus and count `按钮` plus future `接口` items as operations. Do not count `目录` nodes as menu entries.
```ts
const menuCount = store.permissions.filter((item) => row.permissionIds.includes(item.id) && item.type === '菜单').length
const actionCount = store.permissions.filter((item) => row.permissionIds.includes(item.id) && ['按钮', '接口'].includes(item.type)).length
```
- [ ] **Step 5: Run tests and type checking**
Run: `pnpm test`
Expected: all tests pass.
Run: `pnpm typecheck`
Expected: Vue and TypeScript checks exit successfully with no errors.
### Task 4: Verify production and real interactions
**Files:**
- Verify: `apps/admin-web/src/views/PermissionsView.vue`
- Verify: `apps/admin-web/src/views/RolesView.vue`
- [ ] **Step 1: Run the production build**
Run: `pnpm build`
Expected: `vue-tsc --noEmit` and `vite build` both exit with code 0; output stays within `apps/admin-web/dist`.
- [ ] **Step 2: Start or reuse the admin development server**
Run: `pnpm dev`
Expected: Vite serves the admin application on an available local port, normally `http://localhost:5173`.
- [ ] **Step 3: Perform one bounded browser inspection pass**
At desktop width, inspect `/system/permissions` and verify three roots, deep search ancestor preservation, editing, adding a child, and exclusion of self/descendants in the parent selector. Open `/system/roles`, configure a role, and verify the grouped authorization tree, checked/half-checked state, and corrected summary.
At 390px width, verify both pages have no incoherent overlap, drawers remain operable, the tree hierarchy is readable, and table horizontal scrolling remains available.
- [ ] **Step 4: Run Impeccable detector once**
Run the detector command documented by the loaded Impeccable audit reference against the modified admin UI. Fix only actionable findings caused by this change in one batch.
- [ ] **Step 5: Perform at most one confirmation pass**
Recheck only the affected routes and any fixed findings. Stop after this pass per the bounded Impeccable verification rule.
- [ ] **Step 6: Record final verification evidence**
Report exact test counts, type-check/build exit status, browser paths and viewport sizes, and any remaining risk. State explicitly that `src/api/http.ts` remains only the transport/auth wrapper and domain APIs are deferred until the Java contract is known.
> Git commits are intentionally omitted: `C:/Users/BY/Desktop/code/chongchong-global` is not a Git repository, so worktrees, commits, merges, and pull requests are unavailable.
@@ -0,0 +1,396 @@
# Client Web Navigation Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将 client-web 的扁平侧栏改为参考原型的两级业务分组导航,并修复移动端抽屉导航。
**Architecture:** 在 client-web 内新增带图标、权限和 children 的导航树与独立侧栏菜单组件。`AppLayout.vue` 只管理租户壳层、当前路由和移动抽屉状态;桌面与移动端复用同一份菜单组件和配置,不扩展共享包的单层 `NavigationItem`。
**Tech Stack:** Vue 3、TypeScript、Vue Router、Element Plus、Lucide Vue、Vite。
---
### Task 1: 建立客户端导航树
**Files:**
- Create: `apps/client-web/src/navigation/clientNavigation.ts`
- Modify: `apps/client-web/src/router/index.ts:1-33`
- [ ] **Step 1: 定义导航节点类型和叶子展开工具**
创建完整文件:
```ts
import type { Component } from 'vue'
import {
Building2,
ClipboardList,
KeyRound,
LayoutGrid,
Settings,
ShieldCheck,
Truck,
Users,
} from '@lucide/vue'
export interface ClientNavigationNode {
key: string
title: string
icon: Component
path?: string
permission?: string
children?: ClientNavigationNode[]
}
export interface ClientNavigationLeaf extends ClientNavigationNode {
path: string
permission: string
children?: never
}
```
- [ ] **Step 2: 写入已确认的导航树**
在同一文件写入:
```ts
export const clientNavigation: ClientNavigationNode[] = [
{ key: 'dashboard', title: '业务总览', icon: LayoutGrid, path: '/', permission: 'dashboard:view' },
{
key: 'first-leg',
title: '头程业务',
icon: Truck,
children: [
{ key: 'first-leg-orders', title: '头程订单', icon: ClipboardList, path: '/orders/first-leg', permission: 'order:first-leg:list' },
],
},
{
key: 'company',
title: '企业管理',
icon: Settings,
children: [
{ key: 'company-profile', title: '企业资料', icon: Building2, path: '/company/profile', permission: 'company:profile:view' },
{ key: 'company-members', title: '企业成员', icon: Users, path: '/organization/members', permission: 'company:member:list' },
{ key: 'company-roles', title: '角色管理', icon: ShieldCheck, path: '/organization/roles', permission: 'company:role:list' },
{ key: 'company-permissions', title: '菜单权限', icon: KeyRound, path: '/organization/permissions', permission: 'company:permission:list' },
],
},
]
export function getNavigationLeaves(nodes: ClientNavigationNode[]): ClientNavigationLeaf[] {
return nodes.flatMap((node) => {
if (node.children?.length) return getNavigationLeaves(node.children)
if (!node.path || !node.permission) return []
return [node as ClientNavigationLeaf]
})
}
```
不要加入原型中尚未实现的一键导入、快速下单、海外仓或财务结算。
- [ ] **Step 3: 删除 router 中的扁平 `clientNavigation` 配置并改为从本地导航模块导出**
删除 `NavigationItem` 类型导入和扁平数组,临时保留兼容导出,让尚未修改的布局仍能通过类型检查:
```ts
export { clientNavigation } from '@/navigation/clientNavigation'
```
Task 3 改造布局后删除这行兼容导出,使 router 最终只保留路由和权限元数据。
- [ ] **Step 4: 运行类型检查**
Run: `pnpm typecheck`
Expected: 成功退出,0 个 TypeScript 错误;不要引入 `any`。
### Task 2: 实现可复用客户端侧栏菜单组件
**Files:**
- Create: `apps/client-web/src/components/ClientSidebarMenu.vue`
- [ ] **Step 1: 使用 Element Plus 菜单组件实现递归一级/二级渲染**
组件脚本为:
```vue
<script setup lang="ts">
import { computed } from 'vue'
import { clientNavigation, type ClientNavigationNode } from '@/navigation/clientNavigation'
const props = defineProps<{ activePath: string }>()
const emit = defineEmits<{ navigate: [] }>()
function findParentKey(nodes: ClientNavigationNode[], path: string): string | undefined {
for (const node of nodes) {
if (node.children?.some((child) => child.path === path)) return node.key
if (node.children?.length) {
const parentKey = findParentKey(node.children, path)
if (parentKey) return parentKey
}
}
}
const defaultOpeneds = computed(() => {
const parentKey = findParentKey(clientNavigation, props.activePath)
return parentKey ? [parentKey] : []
})
function handleSelect() {
emit('navigate')
}
</script>
```
- [ ] **Step 2: 计算当前路由所在分组**
模板使用同一导航树,并把当前路由所在父分组传给 `default-openeds`:
```vue
<template>
<el-menu
class="client-nav-menu"
:default-active="activePath"
:default-openeds="defaultOpeneds"
router
@select="handleSelect"
>
<template v-for="item in clientNavigation" :key="item.key">
<el-sub-menu v-if="item.children?.length" :index="item.key">
<template #title>
<component :is="item.icon" :size="18" :stroke-width="1.9" />
<span>{{ item.title }}</span>
</template>
<el-menu-item v-for="child in item.children" :key="child.key" :index="child.path || child.key">
<component :is="child.icon" :size="17" :stroke-width="1.9" />
<template #title>{{ child.title }}</template>
</el-menu-item>
</el-sub-menu>
<el-menu-item v-else :index="item.path || item.key">
<component :is="item.icon" :size="18" :stroke-width="1.9" />
<template #title>{{ item.title }}</template>
</el-menu-item>
</template>
</el-menu>
</template>
```
- [ ] **Step 3: 暴露菜单选择事件**
确认 `handleSelect()` 只 emit `navigate`;路由跳转由 `el-menu router` 完成,父布局只负责关闭移动抽屉。
- [ ] **Step 4: 运行类型检查**
Run: `pnpm typecheck`
Expected: 组件 props、事件和导航节点类型全部通过严格检查。
### Task 3: 接入 AppLayout 的移动抽屉状态
**Files:**
- Modify: `apps/client-web/src/layouts/AppLayout.vue:1-75`
- [ ] **Step 1: 增加移动菜单状态和路由联动**
脚本增加以下状态与清理逻辑:
```ts
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { X } from '@lucide/vue'
import ClientSidebarMenu from '@/components/ClientSidebarMenu.vue'
const mobileMenuOpen = ref(false)
let previousBodyOverflow = ''
function closeMobileMenu() {
mobileMenuOpen.value = false
}
function handleEscape(event: KeyboardEvent) {
if (event.key === 'Escape') closeMobileMenu()
}
watch(() => route.fullPath, closeMobileMenu)
watch(mobileMenuOpen, (open) => {
if (open) {
previousBodyOverflow = document.body.style.overflow
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = previousBodyOverflow
}
})
onMounted(() => document.addEventListener('keydown', handleEscape))
onBeforeUnmount(() => {
document.removeEventListener('keydown', handleEscape)
document.body.style.overflow = previousBodyOverflow
})
```
- [ ] **Step 2: 替换内联 `v-for` 侧栏链接**
删除 `clientNavigation`、六个菜单图标和 `icons` 数组的布局导入;删除 router 中的临时兼容导出。侧栏替换为:
```vue
<aside id="client-sidebar" class="client-sidebar" aria-label="客户工作台导航">
<div class="client-sidebar-mobile-header">
<strong>功能导航</strong>
<button type="button" aria-label="关闭导航" @click="closeMobileMenu"><X :size="20" /></button>
</div>
<nav>
<ClientSidebarMenu :active-path="route.path" @navigate="closeMobileMenu" />
</nav>
<div class="sidebar-help"><strong>需要帮助?</strong><span>联系您的专属客户经理</span></div>
</aside>
```
- [ ] **Step 3: 给移动菜单按钮补齐语义和行为**
根节点、遮罩和菜单按钮使用:
```vue
<div class="client-shell" :class="{ 'mobile-menu-open': mobileMenuOpen }">
<button v-if="mobileMenuOpen" class="client-sidebar-scrim" type="button" aria-label="关闭导航" @click="closeMobileMenu" />
<!-- existing header and sidebar -->
<button type="button" aria-label="打开导航" aria-controls="client-sidebar" :aria-expanded="mobileMenuOpen" @click="mobileMenuOpen = true">
<Menu :size="20" />
</button>
</div>
```
- [ ] **Step 4: 添加遮罩和移动关闭按钮**
确认遮罩位于侧栏之前、侧栏 z-index 高于遮罩;关闭按钮只在移动断点可见。
- [ ] **Step 5: 运行类型检查**
Run: `pnpm typecheck`
Expected: 通过;退出登录确认取消时保持现有页面。
### Task 4: 调整 client-web 导航样式和响应式行为
**Files:**
- Modify: `apps/client-web/src/styles/index.css:20-75, 220-270`
- [ ] **Step 1: 将侧栏改为可滚动的分组菜单容器**
用以下菜单变量与结构样式替换原 `.client-sidebar nav a` 规则:
```css
.client-sidebar { overflow: hidden; }
.client-sidebar nav { flex: 1; min-height: 0; overflow-y: auto; }
.client-nav-menu {
border-right: 0;
--el-menu-bg-color: transparent;
--el-menu-text-color: #52616b;
--el-menu-hover-bg-color: #f2f7f6;
--el-menu-active-color: var(--primary);
}
.client-nav-menu .el-menu-item,
.client-nav-menu .el-sub-menu__title { height: 44px; margin: 3px 0; border-radius: 7px; line-height: 44px; }
.client-nav-menu .el-menu-item.is-active { background: #e9f4f2; color: var(--primary); font-weight: 650; }
.client-nav-menu .el-sub-menu .el-menu-item { min-width: 0; padding-left: 46px !important; }
.client-nav-menu .el-menu-item svg,
.client-nav-menu .el-sub-menu__title svg { flex: 0 0 auto; margin-right: 11px; }
.client-nav-menu .el-menu-item span,
.client-nav-menu .el-sub-menu__title span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
```
- [ ] **Step 2: 增加焦点与触控反馈**
增加:
```css
.client-nav-menu .el-menu-item:focus-visible,
.client-nav-menu .el-sub-menu__title:focus-visible,
.client-page-heading button:focus-visible,
.client-sidebar-mobile-header button:focus-visible {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
.client-sidebar-mobile-header { display: none; }
.client-sidebar-scrim { display: none; }
```
- [ ] **Step 3: 实现移动抽屉与遮罩**
在现有 `@media (max-width: 720px)` 中删除 `.client-sidebar { display: none; }`,加入:
```css
.client-sidebar {
display: flex;
width: min(286px, 86vw);
transform: translateX(-100%);
transition: transform .2s ease;
}
.mobile-menu-open .client-sidebar { transform: translateX(0); }
.client-sidebar-scrim {
position: fixed;
inset: 68px 0 0 0;
z-index: 18;
display: block;
border: 0;
background: rgba(23, 50, 77, .46);
}
.client-sidebar-mobile-header {
display: flex;
min-height: 52px;
align-items: center;
justify-content: space-between;
padding: 0 14px;
border-bottom: 1px solid var(--border);
}
.client-sidebar-mobile-header button { display: grid; width: 44px; height: 44px; place-items: center; border: 0; background: transparent; color: var(--navy); }
```
- [ ] **Step 4: 防止移动端横向溢出**
保留 `html, body, #app { min-width: 320px; }`,并加入:
```css
.client-shell { overflow-x: hidden; }
.top-header, .client-main { max-width: 100%; }
```
在 320px 浏览器检查中确认 `document.documentElement.scrollWidth === document.documentElement.clientWidth`。
- [ ] **Step 5: 运行构建**
Run: `pnpm build`
Expected: `vue-tsc --noEmit` 与 `vite build` 均成功。
### Task 5: 完成浏览器与 Impeccable 验收
**Files:**
- Verify: `apps/client-web/src/navigation/clientNavigation.ts`
- Verify: `apps/client-web/src/components/ClientSidebarMenu.vue`
- Verify: `apps/client-web/src/layouts/AppLayout.vue`
- Verify: `apps/client-web/src/styles/index.css`
- [ ] **Step 1: 启动客户端开发服务器**
Run: `pnpm dev`
Expected: 输出 `http://localhost:5174/`;若端口被占用,记录实际端口并使用该端口验收。
- [ ] **Step 2: 验证桌面菜单**
使用预填账号 `customer / 123456` 登录,确认只有“业务总览”直接一级可达;“头程业务”和“企业管理”可展开;四个企业入口和头程订单都能到达原有路由;当前路由高亮正确。
- [ ] **Step 3: 验证移动菜单**
在 720px、390px、320px 视口确认:点击汉堡按钮打开抽屉,遮罩出现;点击二级菜单后抽屉关闭并完成路由切换;点击遮罩和按 Escape 均可关闭;页面无横向溢出。
- [ ] **Step 4: 运行 Impeccable 机械检测**
Run: `node C:\Users\BY\.codex\skills\impeccable\scripts\detect.mjs --json src\layouts\AppLayout.vue src\components\ClientSidebarMenu.vue src\navigation\clientNavigation.ts`
Expected: 0 条未解释告警;若有告警,逐条处理或在交付说明中记录明确原因。
- [ ] **Step 5: 复跑质量门禁**
Run: `pnpm typecheck; pnpm build`
Expected: 两个命令均成功后再声称完成。浏览器不可用时必须说明采用源码与 HTTP 回退证据。
@@ -0,0 +1,259 @@
# PDA 拣货静态页面 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将 PDA 拣货 HTML 原型完整迁移为 Android PDA 可用的 uni-app 本地交互页面。
**Architecture:** 首页承载登录态和拣货态,业务数据与 UI 状态保留在页面中;可独立验证的状态计算放在 `utils/picking-state.js`。不引入后端、状态管理库、厂商 SDK 或新 npm 依赖。
**Tech Stack:** uni-app、Vue Options API、SCSS、Node.js 内置测试运行器
---
当前目录没有 Git 元数据,因此计划不包含提交步骤;本机也没有 HBuilderX 且项目没有 `package.json`,不得声称 App 构建已通过。
### Task 1: 用失败测试锁定拣货状态规则
**Files:**
- Create: `apps/pda-uniapp/tests/picking-state.test.mjs`
- Create: `apps/pda-uniapp/tests/page-contract.test.mjs`
- Create later: `apps/pda-uniapp/utils/picking-state.js`
- [ ] **Step 1: 编写状态测试**
测试通过 `data:` URL 加载 ES module,使源文件继续使用 uni-app 兼容的 `export` 语法且无需新增 `package.json`。
```js
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
const source = await readFile(new URL('../utils/picking-state.js', import.meta.url), 'utf8')
const state = await import(`data:text/javascript;base64,${Buffer.from(source).toString('base64')}`)
test('createItemStates initializes every item as pending', () => {
assert.deepEqual(state.createItemStates([{ id: 1 }, { id: 2 }]), { 1: 'pending', 2: 'pending' })
})
test('getPickSummary reproduces prototype submit rules', () => {
assert.equal(state.getPickSummary([{ id: 1 }, { id: 2 }], { 1: 'pending', 2: 'pending' }).canSubmit, false)
assert.equal(state.getPickSummary([{ id: 1 }, { id: 2 }], { 1: 'picked', 2: 'pending' }).buttonText,
'提交拣货完成(1件待拣将视为未拣)')
})
test('submitStates turns pending items into skipped', () => {
assert.deepEqual(state.submitStates([{ id: 1 }, { id: 2 }], { 1: 'picked', 2: 'pending' }), {
states: { 1: 'picked', 2: 'skipped' },
autoSkippedIds: [2],
})
})
```
同时创建页面契约测试,锁定自定义导航、最小权限和原型工作流入口:
```js
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
test('page configuration uses the custom prototype header', async () => {
const pages = JSON.parse(await readFile(new URL('../pages.json', import.meta.url), 'utf8'))
assert.equal(pages.pages[0].style.navigationStyle, 'custom')
})
test('static prototype does not request unused Android permissions', async () => {
const manifest = await readFile(new URL('../manifest.json', import.meta.url), 'utf8')
assert.doesNotMatch(manifest, /android\.permission\.(VIBRATE|ACCESS_NETWORK_STATE)/)
})
test('page contains every required workflow entry', async () => {
const page = await readFile(new URL('../pages/index/index.vue', import.meta.url), 'utf8')
for (const text of ['登录', '确认拣出', '部分拣货', '跳拣', '提交拣货完成', '下一箱']) {
assert.match(page, new RegExp(text))
}
})
```
- [ ] **Step 2: 运行测试并确认正确失败**
Run: `node --test tests/*.test.mjs`(工作目录 `apps/pda-uniapp`)
Expected: FAIL;状态测试因 `utils/picking-state.js` 尚不存在,页面契约测试因当前仍是官方示例页、原生导航和未使用权限而失败。
### Task 2: 实现纯状态模块
**Files:**
- Create: `apps/pda-uniapp/utils/picking-state.js`
- Test: `apps/pda-uniapp/tests/picking-state.test.mjs`
- [ ] **Step 1: 实现原型状态计算**
```js
export const ITEM_STATUS = Object.freeze({
PENDING: 'pending',
PICKED: 'picked',
PARTIAL: 'partial',
SKIPPED: 'skipped',
})
export function createItemStates(items) {
return items.reduce((states, item) => {
states[item.id] = ITEM_STATUS.PENDING
return states
}, {})
}
export function getPickSummary(items, states) {
const summary = { total: items.length, picked: 0, partial: 0, skipped: 0, pending: 0 }
items.forEach((item) => { summary[states[item.id] || ITEM_STATUS.PENDING] += 1 })
summary.processed = summary.picked + summary.partial + summary.skipped
summary.progress = summary.total ? Math.round(summary.processed / summary.total * 100) : 0
summary.canSubmit = summary.total > 0 && (summary.pending === 0 || summary.picked > 0 || summary.partial > 0)
summary.buttonText = summary.pending > 0 && (summary.picked > 0 || summary.partial > 0)
? `提交拣货完成(${summary.pending}件待拣将视为未拣)`
: '提交拣货完成'
return summary
}
export function submitStates(items, states) {
const nextStates = { ...states }
const autoSkippedIds = []
items.forEach((item) => {
if ((nextStates[item.id] || ITEM_STATUS.PENDING) === ITEM_STATUS.PENDING) {
nextStates[item.id] = ITEM_STATUS.SKIPPED
autoSkippedIds.push(item.id)
}
})
return { states: nextStates, autoSkippedIds }
}
```
- [ ] **Step 2: 运行状态测试**
Run: `node --test tests/picking-state.test.mjs`
Expected: 3 tests passed, 0 failed。
### Task 3: 迁移原型页面与本地交互
**Files:**
- Modify: `apps/pda-uniapp/pages/index/index.vue`
- Import: `apps/pda-uniapp/utils/picking-state.js`
- [ ] **Step 1: 建立页面状态和演示数据**
页面使用 Options API,并导入状态模块:
```js
import { ITEM_STATUS, createItemStates, getPickSummary, submitStates } from '@/utils/picking-state.js'
export default {
data() {
return {
currentUser: null,
currentBoxIndex: 0,
itemStates: {},
partialQuantities: {},
completedBoxes: {},
scanValue: '',
activeOverlay: '',
boxDropdownOpen: false,
toast: { visible: false, message: '', type: '' },
}
},
}
```
演示账号、三组箱号、货品和五个跳拣原因逐项复制自 `docs/悦/PDA拣货/pda-picking.html`。
- [ ] **Step 2: 实现登录态和拣货态模板**
模板必须包含以下层级和原型文案:
```vue
<template>
<view class="page-shell">
<view v-if="!currentUser" class="login-page"><!-- 登录表单与快捷账号 --></view>
<view v-else class="work-page"><!-- 自定义头部、箱号选择、扫码、任务列表、统计与提交栏 --></view>
<view v-if="activeOverlay" class="modal-overlay"><!-- 当前业务弹层 --></view>
<view v-if="toast.visible" class="app-toast">{{ toast.message }}</view>
</view>
</template>
```
- [ ] **Step 3: 实现原型交互方法**
实现并绑定 `login`、`quickLogin`、`logout`、`selectBox`、`handleScan`、`pickItem`、`undoItem`、`openPartial`、`confirmPartial`、`openSkip`、`confirmSkip`、`openBoxList`、`submitPicking`、`nextBox`、`closeOverlay`、`restoreScanFocus` 和 `onBackPress`。
扫码输入使用 `:focus="scanFocused"` 配合短暂置 false/true 恢复焦点;部分数量输入打开时接管焦点。系统返回依次关闭弹层、下拉并执行统一退出逻辑。
- [ ] **Step 4: 完整迁移原型样式**
样式使用原型色值和密度:
```scss
$primary: #1a73e8;
$success: #0d9488;
$danger: #dc2626;
$warning: #f59e0b;
$page-bg: #f5f7fa;
$text: #1f2937;
$text-secondary: #6b7280;
$border: #e5e7eb;
```
覆盖 `320–480px` 常见 PDA 宽度、状态栏安全区、底部导航栏安全区、固定提交栏和软键盘场景。不得引入新视觉方向。
### Task 4: 修正 Android 页面配置与权限
**Files:**
- Modify: `apps/pda-uniapp/pages.json`
- Modify: `apps/pda-uniapp/manifest.json`
- Modify: `apps/pda-uniapp/App.vue`
- Modify: `apps/pda-uniapp/README.md`
- [ ] **Step 1: 使用自定义导航并统一页面背景**
```json
{
"path": "pages/index/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "集装箱拣货"
}
}
```
`App.vue` 移除官方示例日志与 `.example-info`,全局页面背景改为 `#f5f7fa`。
- [ ] **Step 2: 移除当前未使用的手工权限**
`manifest.json` 的 Android `permissions` 改为空数组;本阶段不使用网络、震动、相机或存储。
- [ ] **Step 3: 记录可执行验证命令和剩余风险**
README 记录 `node --test tests/picking-state.test.mjs`,并说明本机无 HBuilderX,Android App 构建与真机扫描头尚未验证。
### Task 5: 自动检查与原型验收
**Files:**
- Verify: `apps/pda-uniapp/tests/page-contract.test.mjs`
- Verify: `apps/pda-uniapp/pages/index/index.vue`
- Verify: `apps/pda-uniapp/pages.json`
- Verify: `apps/pda-uniapp/manifest.json`
- [ ] **Step 1: 运行全部自动检查**
Run: `node --test tests/*.test.mjs`
Expected: 所有测试通过,0 failed。
- [ ] **Step 2: 对照原型逐项验收**
检查登录、快捷账号、仓库选择、三个箱号、有效/无效/重复扫码、确认拣出、部分数量校验、跳拣原因、撤销、提交启用规则、完成统计、返回查看、下一箱、弹层关闭和系统返回。
- [ ] **Step 3: 运行机械 UI 检测并记录限制**
Run: `node C:\Users\BY\.codex\skills\impeccable\scripts\detect.mjs --json apps/pda-uniapp/pages/index/index.vue apps/pda-uniapp/App.vue`
Expected: 不存在阻塞级设计规则问题。Android 构建、真机截图、扫描头、弱网和前后台切换因当前环境缺少 HBuilderX 与目标设备,必须在交付说明中列为未验证。
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,95 @@
# Admin 导航分组与静态交互设计
## 目标
将 admin 当前全部平铺的一级菜单改为可扩展的二级分组导航,同时保留现有静态页面和 Pinia 交互。Java 接口及响应结构尚未确定,本阶段不提前设计领域 API 契约。
## 范围
本次只处理 `apps/admin-web`:
- 调整侧边栏信息架构与折叠交互。
- 保留现有页面、路由地址和模拟数据行为。
- 增加 admin 项目内部测试,不在仓库根目录增加 Node 配置或依赖。
不处理 client、小程序、APP、PDA 或 Java 服务;不猜测最终后端 URL、响应信封、业务错误码或分页格式。
## 导航结构
采用两级分组,工作台保留为直接入口:
```text
工作台
业务管理
├── 客户管理
└── 头程订单
系统管理
├── 内部用户
├── 角色管理
└── 菜单权限
```
侧边栏使用 Element Plus 的菜单和子菜单能力,以获得稳定的展开、折叠、当前路由高亮和折叠态浮层。页面路由地址保持不变。
菜单配置放在 `src/navigation/adminNavigation.ts`,使用 admin 本地的 `AdminNavigationNode` 类型,避免修改共享包中的单层 `NavigationItem` 并影响 client。节点包含唯一标识、标题、图标、路径、权限编码和可选子节点。图标跟随节点配置,不再依赖 `iconMap[index]` 的数组位置。
当前静态阶段显示全部菜单,不根据模拟角色执行权限过滤,因为现有登录用户没有后端返回的权限编码集合。节点保留权限编码字段;后端权限契约确定后,再以叶子菜单为单位过滤,并只显示至少包含一个可访问子菜单的分组。
移动端继续使用现有抽屉式侧栏:选择页面后自动关闭,点击右侧遮罩关闭。桌面折叠时由 Element Plus 展示子菜单浮层,避免二级入口丢失。
## 静态数据与交互
客户、订单、内部用户、角色和权限页面继续使用 `useAdminDataStore`:
- 查询、筛选和分页在前端执行。
- 新增、编辑、审核、启停和状态流转直接修改 Pinia 内存数据。
- 刷新页面后恢复初始模拟数据,不增加持久化。
- 页面不发起真实业务网络请求。
该策略用于先验收页面结构和操作流程,不伪装成已完成后端联调。
## API 当前边界
保留现有 `src/api/http.ts` 作为 Axios 基础实例,仅包含基础地址、超时和 admin Token 注入。当前不新增客户、订单、用户、角色或权限领域 API,不定义响应信封解包,不实现业务错误转换,也不在 401 拦截器中直接引用 Router 或 Pinia Store。
后端提供接口文档后,再单独确定以下内容:
- URL 前缀和版本规则。
- 统一响应信封及成功业务码。
- 分页参数和分页结果结构。
- 业务错误码与用户提示映射。
- 401 处理方式;若需要全局退出,使用应用启动层注册的 `onUnauthorized` 回调,避免请求层与 Router、Store 循环依赖。
- 按认证、客户、头程订单、内部用户、角色和权限拆分领域模块。
真实接口接入后,由 Store 调用领域 API,并负责加载状态、错误状态和数据缓存。页面继续读取 Store,不直接调用 Axios。
## 测试与验收
在 `apps/admin-web` 内增加 Vitest,不影响其他项目。测试覆盖:
- 导航只有工作台是直接一级入口,业务管理和系统管理拥有预期子菜单。
- 每个叶子菜单的路径和权限编码唯一。
- 菜单图标由节点自身定义,不依赖数组下标。
- 静态阶段不因权限字段存在而隐藏任何菜单。
完成后执行:
```bash
cd apps/admin-web
pnpm test
pnpm typecheck
pnpm build
```
浏览器验收桌面展开态、桌面折叠态和 390px 移动端,确认二级菜单可访问、当前页面正确高亮、移动侧栏可关闭,并且原有业务交互不回归。
## 完成标准
- admin 菜单不再全部平铺在一级。
- 新增业务模块时可以放入既有分组或新增分组,不需要改写侧栏组件。
- 当前六个页面入口全部可达且路由不变。
- 静态页面的筛选、表单、详情和状态操作继续可用。
- 页面不会请求不存在的 Java 业务接口,也不会固化未经确认的响应结构。
- admin 的测试、类型检查和生产构建全部通过。
@@ -0,0 +1,100 @@
# Admin 权限树分组同步设计
## 目标
让权限管理和角色授权中的资源层级与已完成的 admin 侧边栏一致,并支持后续继续增加更深层级,而不需要修改页面的树构建逻辑。
## 方案选择
有三种可选方式:
1. 在权限页面和角色页面分别编写递归逻辑。改动直接,但会产生两份容易漂移的实现。
2. 使用共享的权限树构建器。两个页面消费同一棵树,筛选、排序和层级行为一致,作为本次采用方案。
3. 直接从侧边栏导航生成权限树。可以保证菜单一致,但导航不包含按钮和接口权限,且未来后端动态权限尚未确定,不适合当前阶段。
## 权限层级
静态权限数据调整为:
```text
工作台
业务管理
├── 客户管理
│ ├── 新增客户
│ └── 审核客户
└── 头程订单
├── 创建订单
└── 更新状态
系统管理
├── 内部用户
│ └── 维护用户
├── 角色管理
│ └── 角色授权
└── 菜单权限
└── 维护权限
```
“业务管理”和“系统管理”是目录;“工作台、客户管理、头程订单、内部用户、角色管理、菜单权限”是菜单;具体操作是按钮权限。菜单本身使用现有路由权限编码,例如 `customer:list` 和 `system:role:list`,不再额外保留同义的“查看客户”“查看角色”节点。
接口权限待 Java 接口契约确定后再补充。本次不根据静态 URL 猜测接口资源;现有按钮权限上的临时 `/api/...` 路径和 HTTP 方法一并清空,按钮只保留稳定的权限编码。
## 共享树构建器
在 `src/utils/permissionTree.ts` 提供纯函数:
- 根据 `parentId` 递归构建任意深度的权限树。
- 同级按 `sort` 排序。
- 接收可选匹配函数;子节点匹配时保留完整祖先链。
- 防止异常循环引用造成无限递归。
- 提供后代 ID 查询,编辑上级资源时排除自身及全部后代,避免创建循环。
权限管理页面使用完整 `Permission` 节点,以便编辑、启停和添加下级。角色页面将同一结果映射为 Element Plus 树需要的 `id`、`label`、`code` 和 `children`。
## 静态角色授权
同步更新五个模拟角色的 `permissionIds`:
- 超级管理员拥有全部资源。
- 运营主管拥有工作台、业务管理、客户审核、订单查看和状态处理。
- 操作员拥有工作台及头程订单创建、处理权限。
- 财务拥有工作台及头程订单查看权限。
- 销售拥有工作台、客户维护及头程订单查看权限。
分组目录 ID 一并保留在角色授权中,以确保回显和父子半选状态明确。
## 页面行为
权限管理:
- 默认展开完整三级树。
- 搜索命中按钮时仍显示所属菜单及目录。
- 新增下级、编辑、启停继续使用现有静态交互。
- 编辑上级资源时不能选择自身或自己的后代。
角色管理:
- 授权抽屉展示相同的目录、菜单和按钮层级。
- 保存时继续合并已选和半选节点。
- 权限摘要按菜单与操作分别计数,不把目录计入菜单数量。
## 测试与验收
使用 admin 现有 Vitest:
- 先写失败测试固定三级结构和同级排序。
- 验证搜索命中深层节点时保留祖先链。
- 验证循环引用不会导致递归失控。
- 验证后代 ID 查询能阻止把节点移动到自己的后代下。
- 验证静态权限编码与导航叶子权限编码一致。
最后执行 `pnpm test`、`pnpm typecheck`、`pnpm build`,并在浏览器检查权限管理页面和角色授权抽屉的桌面及移动端层级展示。UI 改动完成后运行一次 Impeccable 检测。
## 完成标准
- 权限管理和角色授权不再显示五个独立一级业务目录。
- 两个页面使用同一个递归树构建器。
- 搜索、排序、授权回显和静态编辑交互保持可用。
- 不新增真实 API 调用或猜测后端响应格式。
- 测试、类型检查、构建及浏览器验收通过。
@@ -0,0 +1,168 @@
# Client Web 导航分级与移动侧栏设计
## 背景
`apps/client-web` 当前把业务总览、企业资料、企业成员、角色管理、菜单权限和头程订单全部渲染为同级侧栏链接。权威原型 `docs/悦/头程下单/柏悦客户端.html` 采用“一级业务域 + 二级具体任务”的导航结构,现有实现没有延续这一信息架构,也无法承接后续业务扩展。
Impeccable 验收还发现一个阻断性问题:720px 以下侧栏被隐藏,但页面上的菜单按钮没有打开移动导航的行为,导致移动端无法切换模块。
## 目标
- 将客户工作台的平铺菜单改为可扩展的两级分组导航。
- 保留现有路由、页面、文案、品牌颜色与 Element Plus 技术体系。
- 修复移动端导航不可用的问题。
- 让菜单节点显式携带图标与权限信息,避免依赖数组下标。
- 为后续真实权限集合接入保留清晰边界,不在本次伪造授权数据。
## 非目标
- 不新增原型中尚未实现的一键导入、快速下单、海外仓或财务结算入口。
- 不改变现有路由地址,不拆分当前 `ModuleView.vue` 页面。
- 不照搬原型的紫色渐变视觉,不重做登录、注册或业务内容页。
- 不实现服务端权限校验,也不把前端菜单隐藏当作安全控制。
- 不新增测试框架;当前项目尚未声明测试脚本。
## 导航信息架构
采用两级分组,业务总览保留直接入口:
```text
业务总览 /
头程业务 可展开分组
└── 头程订单 /orders/first-leg
企业管理 可展开分组
├── 企业资料 /company/profile
├── 企业成员 /organization/members
├── 角色管理 /organization/roles
└── 菜单权限 /organization/permissions
```
分组遵循原型的结构原则,但只呈现当前真实存在的客户端路由。后续功能只有在页面与路由实现后,才加入对应业务分组。
## 组件与数据结构
### 导航配置
新增客户端本地导航配置 `src/navigation/clientNavigation.ts`。定义客户端专用的 `ClientNavigationNode`,包含:
- 稳定 `key`;
- 用户可见 `title`;
- Lucide 图标组件;
- 可选 `path`;
- 可选 `permission`;
- 可选 `children`。
共享包中的 `NavigationItem` 仍保持单层公共模型,避免为了单个应用的壳层结构扩大跨项目契约。
### 侧栏菜单组件
新增独立的客户端侧栏菜单组件,使用 Element Plus 的 `el-menu`、`el-sub-menu` 与 `el-menu-item`:
- `active-path` 由当前路由提供;
- 父分组可展开和收起;
- 当前路由所在分组默认展开;
- 叶子菜单负责实际路由跳转;
- 图标来自节点配置,不依赖渲染顺序;
- 桌面与移动端复用同一组件和同一份导航数据。
本轮不额外实现展开状态持久化;Element Plus 在当前会话中维护用户开合状态,且当前路由所在分组始终可识别。持久化不是修复平铺结构与移动阻断所必需的能力。
### 应用壳层
`AppLayout.vue` 继续负责品牌、租户、用户和页面标题,同时增加:
- `mobileMenuOpen` 状态;
- 移动菜单按钮的 `aria-expanded`;
- 遮罩关闭;
- 路由切换后自动关闭;
- Escape 键关闭;
- 打开时阻止背景页面滚动;
- 退出确认取消时不产生未处理异常。
桌面侧栏保持固定宽度和现有绿色服务型视觉,不引入后台的深色侧栏,也不增加桌面折叠模式。
## 交互与状态流
### 桌面端
1. 路由加载后,菜单以 `route.path` 高亮对应叶子。
2. “业务总览”为直接入口。
3. “头程业务”和“企业管理”为分组触发器。
4. 当前页面属于某分组时,该分组保持展开并显示父级激活语义。
5. 用户可以独立展开或收起其他分组。
### 移动端
1. 720px 以下侧栏移出视口,而不是使用 `display: none` 永久移除。
2. 点击页面标题前的菜单按钮打开抽屉并显示遮罩。
3. 点击遮罩、按 Escape 或选择任一叶子路由后关闭抽屉。
4. 抽屉打开时禁止背景滚动,关闭或组件卸载时恢复。
5. 抽屉沿用完整租户导航,不维护第二份移动菜单。
### 权限
导航节点继续携带权限编码。当前模拟用户没有可靠的权限集合,因此静态阶段显示全部已实现菜单。真实认证接口返回权限集合后,再增加叶子过滤;父分组没有可访问子项时隐藏。服务端必须继续执行租户与权限校验。
## 视觉与可访问性
- 保留现有 `--primary`、`--navy`、`--border` 和 `--muted` 设计令牌。
- 一级分组与二级叶子通过缩进、字号、颜色和激活背景形成清晰层级。
- 分组触发器与移动菜单按钮使用真实可聚焦控件。
- 保持可见 `:focus-visible` 状态,不只依赖 hover 或颜色表达当前状态。
- 菜单触控目标不低于约 44px。
- 二级菜单文本允许单行省略,避免长租户业务名称破坏侧栏宽度。
- 侧栏导航区域允许纵向滚动;底部帮助区保持可达。
- 320px、720px 与桌面宽度均不能依赖水平滚动完成导航。
## 错误与边界处理
- 用户取消退出确认时保持当前页面,不产生控制台未处理异常。
- 移动抽屉在快速路由切换、浏览器后退和组件卸载后不得残留锁定滚动状态。
- 不存在或被移除的路由继续走现有兜底重定向。
- 当前不虚构客户经理电话、微信或帮助中心链接;帮助区保留为信息提示,后续有真实渠道后再做可执行入口。
- 权限为空或数据尚未加载时,本轮不提前隐藏菜单,避免模拟阶段出现无法恢复的空侧栏。
## 实施范围
预计修改或新增:
- `apps/client-web/src/navigation/clientNavigation.ts`
- `apps/client-web/src/components/ClientSidebarMenu.vue`
- `apps/client-web/src/layouts/AppLayout.vue`
- `apps/client-web/src/router/index.ts`
- `apps/client-web/src/styles/index.css`
仅当实现需要声明自动导入组件时,依赖现有插件自动生成声明,不手工编辑 `auto-imports.d.ts` 或 `components.d.ts`。
## 验收与验证
### 自动验证
在 `apps/client-web` 执行:
```bash
pnpm typecheck
pnpm build
```
项目当前没有 test 或 lint 脚本,因此不声称相应检查通过,也不为本次导航改动引入整套测试设施。
### Impeccable 验收
- 运行一次机械检测,目标覆盖本次变更的 Vue 文件。
- 桌面检查:一级分组、二级缩进、当前路由高亮、可见焦点和侧栏滚动。
- 720px 检查:断点切换时导航仍可达。
- 320px 检查:抽屉、遮罩、触控目标与页面内容不发生水平溢出。
- 浏览器可用时检查菜单打开、选择路由、遮罩关闭和 Escape 关闭;若浏览器不可用,明确记录回退证据,不伪装成已完成视觉实测。
## 完成标准
- 侧栏不再把全部功能放在一级菜单。
- 当前六个页面入口全部可达,路由地址不变。
- 桌面端当前页面与父分组状态清晰。
- 720px 以下菜单按钮能够打开和关闭真实导航抽屉。
- 导航图标不会因菜单调序或新增分组而错位。
- 未实现的原型功能不会作为无效入口出现。
- 类型检查、生产构建与 Impeccable 检测无未解释失败。
@@ -0,0 +1,105 @@
# PDA 拣货静态页面设计
## 目标
在 `apps/pda-uniapp` 中将 `docs/悦/PDA拣货/pda-picking.html` 转换为可运行在 Android PDA 上的 uni-app 页面。原型是唯一视觉和交互依据;实现保留原型文案、布局、颜色、页面密度、演示数据和本地状态流。
本次不接后端、不实现真实鉴权、不接厂商扫描 SDK、不实现持久化或离线队列。
## 实现方案
采用单页面作业流:登录态与拣货态均由 `pages/index/index.vue` 承载,使用 Vue 本地响应式状态切换,而不拆成多个路由。原因如下:
- 原型本身是连续的单页状态机,登录后立即进入同一作业上下文。
- 本阶段没有后端、深链、权限路由或跨页面数据恢复需求。
- 单页面可避免箱号、货品状态和弹层状态在路由之间同步,保持改动小而聚焦。
为避免页面文件承担过多重复结构,可将货品卡片和通用弹层拆成局部业务组件;演示数据与状态转换仍由页面统一管理。当前不引入状态管理库或新依赖。
## 页面结构
### 登录态
- 蓝色渐变背景、系统标识、`柏悦物流 · PDA拣货` 标题和副标题。
- 工号、密码、作业仓库三个字段。
- 普通登录按钮和三个演示账号快捷登录按钮。
- 空工号、空密码和账号密码错误使用原型文案提示。
- 登录成功后显示欢迎提示并进入拣货态;退出后清空登录表单与作业状态。
### 拣货态
- 顶部蓝色作业栏:页面标题、仓库、退出、当前箱号、订单号和人员身份。
- `pages.json` 将首页设置为 `navigationStyle: "custom"`,不显示当前标题为 `uni-app` 的原生导航栏,避免与原型作业栏重复;页面自行处理状态栏安全区。
- 箱号选择器:显示全部演示箱号与订单号,切换时重置当前箱的未提交本地操作。
- 扫码区:输入或扫描 SKU 后回车确认;匹配当前箱中待拣 SKU 时标记已拣,错误码显示失败提示。登录进入拣货态、成功或失败扫码、确认拣出、撤销以及关闭任一弹层后,都应把焦点恢复到扫码输入框;打开需要输入数量的部分拣货弹层时,焦点临时交给数量输入框。成功扫码后清空输入值,无效码和重复扫描已处理 SKU 时保留明确错误反馈,并在反馈后继续接收下一次扫描。
- 箱号与订单信息卡。
- 拣货进度、货品列表、集装箱完成/待拣统计和底部提交栏。
- 底部提交栏固定在可视区域底部,并避让 Android 导航栏安全区。
- 提交按钮初始禁用;当所有货品均已处理时启用并显示“提交拣货完成”;当至少存在一件已拣或部分拣货、同时仍有待拣货品时启用并显示“提交拣货完成(N件待拣将视为未拣)”;仅发生部分跳拣且仍有待拣货品时保持禁用。
### 货品状态
每个货品使用以下本地状态之一:
- `pending`:待拣,提供确认拣出、部分拣货、跳拣。
- `picked`:已拣,显示完成标签与撤销入口。
- `partial`:部分拣货,显示实际数量、目标数量与撤销入口。
- `skipped`:跳拣或提交时自动未拣,显示原因与撤销入口。
待拣货品排在已处理货品之前。进度按已拣、部分拣和跳拣的已处理数量计算。
## 弹层与反馈
- 跳拣弹层:单选原型中的五个原因,未选择时禁止确认并提示。
- 部分拣货弹层:显示 SKU、描述、总量、单位、快捷比例和数量输入;数量必须大于 0 且小于需求总量。
- 集装箱列表弹层:分别展示已完成与待拣箱,允许从待拣列表切换箱号。
- 完成弹层:展示已拣、部分拣、跳拣/未拣统计,并提供返回查看和下一箱。
- 短反馈沿用原型顶部提示条及成功、警告、错误配色。
Android 系统返回键按以下优先级处理:关闭当前弹层,关闭箱号下拉;若处于拣货态且无覆盖层,则调用与“退出”按钮相同的重置流程返回登录态;登录态交由系统退出页面。
## 本地数据与状态
保留原型中的三个箱号、订单号、货品、库位、单位、演示账号和跳拣原因。页面维护:
- 当前用户与仓库;
- 当前箱号索引;
- 当前箱内各 SKU 状态;
- 部分拣货数量;
- 已完成箱号集合;
- 当前下拉、弹层、目标货品和提示条状态;
- 扫码输入值。
提交时,未处理货品按原型标记为“未拣出(提交时自动标记)”,保存当前箱的最终演示状态并更新集装箱统计。刷新、退出或重启应用后允许数据重置,因为本阶段没有持久化要求。
## 视觉与响应式
- 颜色遵循原型:主色 `#1a73e8`、成功 `#0d9488`、危险 `#dc2626`、警告 `#f59e0b`、页面背景 `#f5f7fa`。
- 保留 12px 圆角、浅阴影、蓝色当前货品边线、状态色和卡片密度。
- 使用 uni-app 支持的 `rpx`、百分比和安全区变量适配 Android PDA,以 `360 × 640 dp` 为主要检查尺寸,兼容约 `320–480 dp` 宽度。
- 不引入新字体、图片生成素材、第三方图标库或新的视觉语言。
- 图标优先使用简单 CSS/文本符号或现有静态资源,避免依赖网页专属 SVG 注入方式。
- 本阶段不使用网络、震动、相机或存储能力,因此实现时移除 `manifest.json` 中手工声明的 `VIBRATE` 和 `ACCESS_NETWORK_STATE` 权限;未来接入对应能力时再按明确用途恢复最小权限。
## 边界状态
- 登录加载态:当前纯本地操作无需异步加载,不添加虚假等待动画。
- 空态:保留“该箱号暂无拣货任务”“暂无已拣完成的集装箱”“所有集装箱已拣完成”等原型语义。
- 错误态:覆盖登录校验、空扫码、无效 SKU、未选跳拣原因、无效部分数量和最后一箱提示。
- 重复操作:已处理货品不再匹配扫码;撤销后恢复为待拣。
- 权限态:本阶段没有服务端权限;人员角色仅作演示展示,不据此隐藏功能。
- 软键盘:输入区与部分拣货输入不得被键盘永久遮挡,关闭弹层后恢复扫码输入上下文。
## 验证
仓库当前没有项目根级依赖清单、可核实的构建脚本或自动化测试设施,因此不新增或编造测试命令。实现后进行以下可核实检查:
1. 静态检查 `pages.json`、Vue 模板、脚本和样式是否符合现有 uni-app 结构。
2. 若当前环境可用 HBuilderX/uni-app 运行能力,则以 Android App 方式运行;否则明确记录未执行构建。
3. 在常见 PDA 尺寸核对登录、主作业、弹层、底部固定栏和软键盘遮挡。
4. 手工走通快捷登录、箱号切换、有效/无效/重复扫码、确认拣出、部分拣货、跳拣、撤销、提前提交、完整提交和下一箱,并核对每次关键操作后的扫码焦点。
5. 检查提交按钮在初始、仅部分跳拣、存在已拣且仍有待拣、全部处理四种状态下的启用状态和文案。
6. 检查系统返回对弹层、下拉、拣货态和登录态的处理,并确认返回登录态时执行完整状态重置。
7. 检查首页未出现重复原生导航栏,并确认 Android 配置不再手工声明当前未使用的网络与震动权限。
真实扫描速度、连续扫描、低电量、断网、前后台切换、厂商扫描头和正式打包发布不属于本次静态实现的可验证范围,需在设备与工具链确定后补测。
@@ -0,0 +1,217 @@
# Admin 静态 RBAC 权限预览设计
## 背景
管理后台已经具备内部用户、角色、权限资源和分组菜单的静态交互,但角色授权结果尚未作用到登录后的菜单、路由和操作按钮。路由中的 `meta.permission` 目前只是元数据,无法演示角色权限是否真正生效。
本阶段建立一个可操作的静态 RBAC 演示闭环,用于前端原型验收。它不代表真实服务端鉴权,也不定义 Java API、Token 内容、权限响应结构或错误码。
## 目标
- 在管理后台顶部切换初始模拟数据中的五个启用角色,并继续支持后续新增或停用角色后的动态列表。
- 根据角色授权实时过滤侧边栏菜单。
- 阻止静态原型访问无权限路由,并提供清晰的无权限页面。
- 对已经存在稳定权限编码的操作按钮进行显示控制。
- 角色授权数据发生变化时,权限预览同步更新。
- 保留现有登录、页面交互、响应式布局和独立前端工程结构。
## 不在本次范围
- 不调用真实登录、用户、角色、菜单或权限接口。
- 不修改 `src/api/http.ts` 的请求或响应约定。
- 不新增或猜测 Java API 路径、响应包络、分页结构和错误码。
- 不把前端菜单隐藏或路由跳转描述为安全边界;服务端仍需在后续接口阶段执行最终鉴权。
- 不为当前静态权限模型中没有定义的操作擅自增加权限编码。
- 不模拟角色的 `dataScope` 数据范围,不因切换“全部数据”“本部门数据”或“本人数据”角色而过滤客户、订单等模拟记录。
- 不处理企业客户端、官网、小程序、APP 或 PDA 的权限体系。
## 方案选择
### 采用方案:顶部切换演示角色
在后台顶栏提供“演示角色”切换入口,角色变化后菜单、路由和明确映射的操作按钮立即更新。该方式不要求反复退出登录,适合静态原型展示和权限配置验收。
### 未采用方案
- 使用不同账号重新登录:更接近真实登录流程,但重复退出登录会降低原型验收效率,且当前没有服务端身份契约。
- 只控制菜单和路由:改动较少,但无法验证现有按钮权限是否产生可见效果。
## 架构边界
### 权限预览状态
新增 admin 专属 Pinia 状态层,职责仅限静态权限预览:
- 保存当前演示角色 ID。
- 从 `useAdminDataStore` 中读取当前启用角色和权限资源。
- 根据角色的 `permissionIds` 计算权限编码集合,但只纳入 `status === '启用'` 的权限资源;角色仍保留已停用权限 ID 时,不授予对应访问权。
- 提供 `can(permissionCode)` 判断。
- 提供当前角色、可选角色及第一个可访问菜单。
- 提供登录成功后的初始化、退出时重置和两阶段角色切换动作;页面和布局不直接写入持久化角色 ID。
认证状态仍由 `src/stores/auth.ts` 管理。权限预览状态不写入 Token,不修改 `CurrentUser` 的稳定共享模型,也不在页面中复制认证逻辑。
演示角色 ID 使用 admin 项目专属的 `localStorage` 键保存,以便刷新后保留预览上下文。保存值失效、角色被删除或角色被停用时,回退到启用的超级管理员;如果超级管理员不可用,则回退到第一个启用角色。此回退只服务静态原型,不代表后端策略。
预览状态与认证状态采用明确的生命周期边界:
- 页面刷新保留当前演示角色。
- 退出操作先调用权限预览 Store 的重置动作,清除内存状态与持久化演示角色 ID,再调用 `auth.logout()`;认证 Store 不承担预览状态职责。
- 登录成功后由登录流程调用权限预览 Store 的初始化动作,默认选择启用的超级管理员;超级管理员不可用时选择第一个启用角色。未登录阶段不自动初始化或重新持久化默认角色。
- 顶栏固定区分两个来源:`登录身份:{auth.user.name}({auth.user.account})` 与 `权限预览:{previewRole.name}`。认证角色 `auth.user.roles` 不作为无标签副标题展示,也不被预览状态读取或改写。
- `localStorage` 在同源标签页之间共享,但本期不监听 `storage` 事件做实时跨标签同步;其他标签页刷新后读取最新持久化角色。
### 权限数据流
```text
adminData.roles + adminData.permissions
↓
静态权限预览 Store
↓ ↓ ↓
侧边栏 路由决策 页面操作
↓
未保存内容保护
```
角色授权编辑保存到 `adminData.roles` 后,权限编码集合通过计算属性立即更新,不复制第二份角色或权限数据。
## 菜单与路由行为
### 菜单过滤
在现有 `adminNavigation` 基础上增加纯函数过滤:
- 叶子菜单只在其 `permission` 存在于当前权限编码集合时保留。
- 目录仅作为导航组织节点,其自身启停状态不级联授予或撤销后代权限;叶子菜单只判断自身对应的启用权限资源。
- 任意层级目录在递归过滤后没有任何子菜单时隐藏。
- 菜单原始配置保持不可变,过滤函数返回新节点。
- 工作台继续使用 `dashboard:view`,与其他菜单采用相同规则。
- “第一个可访问菜单”固定为过滤后 `adminNavigation` 配置顺序中的第一个叶子,采用深度优先遍历,不使用权限资源的 `sort` 决定回退目标。
工作台路由必须显式声明 `meta.permission: 'dashboard:view'`。所有受导航权限控制的叶子页面都应同时具有对应路由权限元数据,不能只过滤菜单而遗漏直接地址访问。
`AdminSidebarMenu` 接收过滤后的导航数据,不直接读取 Pinia,并通过递归菜单节点支持后续新增更深层级;权限来源与具体渲染层级不耦合。
### 路由守卫
登录校验继续先于权限校验执行:
1. 未登录访问受保护页面时跳转登录页。
2. 已登录且目标路由声明 `meta.permission` 时,使用静态权限预览判断访问权。
3. 无权限的直接访问跳转到静态 `/403` 页面,使用 `query.from` 保留经过校验的站内来源路径用于纯文本提示;只接受以单个 `/` 开头的内部路径,不把该值作为外部跳转地址。
4. `/403` 本身需要登录,但不要求业务权限,避免重定向循环。
5. 404 重定向保持现状。
为便于单元测试,路由权限决策下沉为不依赖浏览器导航的纯函数;Vue Router 守卫只负责读取状态并执行决策结果。
除导航守卫外,登录态布局还需要响应式监听当前有效权限集合、当前演示角色有效性和当前路由。当角色授权被修改、当前角色被停用或删除、权限资源被停用等变化导致当前页面失去访问权时,立即重新校验当前路由:优先跳转到回退后角色的第一个可访问叶子菜单;没有可访问菜单时进入 `/403`。
导航守卫负责首次进入和显式导航,登录态布局只负责已渲染页面上的响应式权限变化;两者调用同一个纯路由决策函数。布局执行修正前比较目标与当前 `fullPath`,使用单次导航锁避免并发修正,并统一调用 `router.replace`,使浏览器后退不会再次回到刚被拒绝的地址。目标已经相同或当前已处于无菜单可恢复的 `/403` 时不重复导航。
### 主动切换角色
用户从顶栏切换角色采用“请求—确认—提交”两阶段流程,不在下拉选择事件中直接覆盖当前角色:
- 先根据候选角色计算候选权限、当前页面可访问性和回退目标。
- 当前页面的新增/编辑抽屉或对话框存在已修改但未保存的内容,且切换会离开当前路由或撤销该表单保存所需的操作权限时,显示确认框,明确候选角色和将离开的页面;取消后角色、路由和表单内容均保持不变。
- 各业务页面通过 admin 专属的轻量离开保护协调器登记 `{ dirty, requiredPermission, label }`;只在字段实际变化后标记 dirty,关闭或成功保存时注销,不把业务表单数据复制到全局 Store。
- 用户确认或没有受影响的未保存内容后,才提交演示角色 ID;需要修正路由时统一使用 `router.replace`。
- 顶栏随后显示新的演示角色名称,并显示 `已切换至“{角色名}”,当前仅为静态权限预览`。
- 当前页面仍可访问时保持原路由。
- 当前页面不再可访问时,跳转到该角色第一个可访问菜单。
- 如果切换角色时当前页面是 `/403`,只要新角色存在可访问菜单,就始终跳转到第一个可访问叶子菜单,不把 `/403` 视为应继续停留的业务页面。
- 如果角色没有任何菜单权限,进入 `/403`,显示“当前演示角色暂无可访问菜单”。
- 自动跳转完成后将焦点移到新页面主标题,并通过 `aria-live="polite"` 播报当前预览角色与目标页面;仅停留原页面时也播报角色变化。
## 操作权限映射
本次只控制现有静态模型中已有明确编码的操作:
| 页面 | 操作 | 权限编码 |
|---|---|---|
| 客户管理 | 新增客户 | `customer:create` |
| 客户管理 | 审核客户、详情中的通过审核 | `customer:audit` |
| 头程订单 | 创建订单、创建并提交、保存新草稿 | `first-leg:create` |
| 头程订单 | 单笔处理、批量更新、更新状态 | `first-leg:status` |
| 内部用户 | 新增、编辑、重置密码、启停、保存 | `system:user:write` |
| 角色管理 | 新增、配置、启停、删除、保存 | `system:role:grant` |
| 菜单权限 | 新增、编辑、添加下级、启停、保存 | `system:permission:write` |
详情、查询、筛选和已有页面级导出继续由对应菜单权限覆盖。客户编辑/启停、订单编辑等没有独立稳定操作编码的功能暂不额外限制;待 Java 权限契约确定后再细分,避免静态原型创造错误的长期编码。
页面通过同一个权限预览 Store 的 `can` 方法控制明确映射的操作,不创建页面私有权限判断逻辑。无权限操作不渲染,避免用户误以为按钮可用后才收到拒绝。
## 顶栏交互
- 在现有用户信息和退出按钮之间增加“演示角色”下拉入口。
- 使用 Lucide 图标和 Element Plus 现有组件,不引入新依赖。
- 下拉只展示启用角色,并标记当前角色。
- 桌面端持续显示 `权限预览:{角色名}`,并提供“仅预览菜单、页面和操作;数据范围未模拟”的可聚焦说明;登录用户区域显示 `登录身份:{姓名}({账号})`,两者不共用“角色”副标题。
- 窄屏至少保留可识别图标、`权限预览:{角色名},数据范围未模拟` 的可访问名称和 44px 触控区域;长角色名称使用省略与完整 tooltip,不挤压退出按钮。
- 角色下拉支持 Tab 聚焦、Enter/Space 打开、方向键移动、Enter 选择和 Escape 关闭,并保持可见焦点。
- 入口和反馈文案始终包含“演示”或“预览”,避免被理解为真实用户换号或数据范围切换。
## 无权限页面
新增轻量静态 403 页面:
- 说明当前演示角色无权访问目标页面。
- 显示当前演示角色名称。
- 来源路径只作为经过校验、可截断换行的纯文本显示,不生成“返回来源地址”链接。
- 当前角色存在可访问菜单时,提供“返回可访问首页”操作。
- 当前角色没有任何可访问菜单时,提示用户使用顶栏入口切换演示角色。
- 不使用夸张插画或与现有后台不一致的视觉语言。
- 页面作为登录态布局的子路由呈现,使顶栏角色切换始终可用。
## 状态与边界处理
- 角色切换只修改本地预览状态,不修改内部用户的真实 `roleIds`。
- 停用或删除当前演示角色时自动执行回退,不保留悬空角色 ID。
- 停用权限资源会立即从当前角色的有效权限编码集合中移除,即使该权限 ID 仍存在于角色授权数据中。
- 停用目录资源只改变该目录资源自身的有效编码,不级联改变启用后代的菜单、路由或操作权限;权限管理页的停用确认明确说明这一点。
- 超级管理员通过角色现有的全部 `permissionIds` 获得权限,不在代码中使用绕过所有检查的特殊布尔值。
- 角色拥有目录权限但没有任何叶子菜单权限时,不显示空目录。
- 如果异常模拟数据中不存在任何启用角色,当前角色为空、有效权限集合为空并进入 `/403`;正常页面继续保护内置超级管理员,避免产生该状态。
- 多角色合并不在本次预览范围;预览入口一次选择一个角色,真实用户多角色合并规则等待后端契约。
- 静态权限预览不静默回退到远程数据,也不发起网络请求。
## 测试策略
使用 admin 现有 Vitest,按测试驱动方式实现:
- 权限预览 Store:两阶段角色选择、权限编码派生、停用权限资源不授予访问权、失效角色回退、零启用角色、角色授权变更实时生效、退出登录重置且刷新保留、未登录时不重新初始化默认角色。
- 导航过滤:隐藏无权限叶子、目录停用不级联后代、递归移除空目录、保持原配置不可变、按 `adminNavigation` 深度优先顺序确定第一个可访问菜单。
- 路由配置与决策:工作台显式声明 `dashboard:view`;覆盖未登录、已登录有权限、已登录无权限、403 免业务权限四种分支。
- 当前路由再校验:角色授权减少、角色停用或删除、权限资源停用后,当前无权限页面通过 `router.replace` 跳转到第一个可访问叶子或 `/403`;覆盖目标相同、导航锁和浏览器后退不返回被拒页面。
- 侧边栏组件:只渲染传入的过滤结果,并覆盖三层导航数据以证明递归渲染不受当前两层结构限制。
- 顶栏角色切换:当前角色回显、准确的登录身份/权限预览标签、窄屏可访问名称、键盘操作、长角色名,以及从 `/403` 切换到有权限角色后恢复到第一个可访问菜单。
- 未保存内容保护:覆盖无修改直接切换、有修改但权限不受影响、有修改且将离开或失去保存权限、取消保持全部状态、确认后提交角色并 replace 跳转。
- 无障碍动态状态:覆盖成功切换和自动跳转的 polite 播报、跳转后主标题焦点与 Escape 关闭角色菜单。
- 状态来源与生命周期:用户姓名/账号来自认证 Store,演示角色来自权限预览 Store;退出清除预览角色,重新登录使用默认启用角色。
- 页面操作:至少覆盖每个操作权限域的一项代表按钮,确认无权限时不渲染。
- 数据范围边界:角色切换不改变模拟记录集合,界面持续说明 `dataScope` 未模拟。
交付前运行:
```bash
pnpm test
pnpm typecheck
pnpm build
```
浏览器验收覆盖桌面、390px 和项目约定的 320px 最小宽度,并检查 200% 浏览器缩放:角色切换、菜单变化、未保存内容确认/取消、自动跳转、直接访问 403、代表性按钮显示与隐藏、键盘完整操作、可见焦点、动态播报、顶栏不重叠或裁切以及控制台错误。UI 修改完成后运行一次 Impeccable 检测。
## 完成标准
- 初始模拟数据中的五个启用角色可在顶栏切换,后续角色新增、停用或删除后列表与回退实时更新。
- 菜单、路由和已映射操作与当前角色的启用权限资源一致,停用权限不产生访问权。
- 权限预览明确不包含 `dataScope`,角色切换不改变模拟记录,桌面和窄屏均持续提供这一说明。
- 无权限直接访问有明确 403 反馈,不出现循环跳转。
- 角色授权、角色有效性或权限资源状态变化能即时影响当前预览;当前路由失去权限时立即修正到第一个可访问叶子或 `/403`。
- 目录停用语义固定为不级联后代;递归菜单过滤与渲染支持后续增加更深菜单层级。
- 角色切换不会静默丢失已修改表单;取消保持原角色、路由和内容,确认后使用 replace 完成必要跳转。
- 从 `/403` 切换到具有菜单权限的角色后能恢复到可访问页面。
- 刷新保留演示角色,退出登录清除预览状态,重新登录采用默认启用角色;`登录身份` 与 `权限预览` 的标签、数据来源和视觉层级清晰分离。
- 键盘、读屏播报、焦点管理、320px/390px、200% 缩放与长角色名称均通过验收。
- 不产生任何真实 API 请求或新的后端契约假设。
- admin 项目测试、类型检查、构建、桌面/移动端浏览器验收及 Impeccable 检测通过。
+23
View File
@@ -0,0 +1,23 @@
# 一期文档
一期目标是完成平台后台与企业客户端的真实业务基础,以及头程订单最小闭环。
## 文档
- [一期开发进度表](一期开发进度表.md)
- [一期头程订单闭环设计](一期头程订单闭环设计.md)
- `虫虫出海一期开发进度表.xlsx`:面向团队日常维护的 Excel 进度表。
## 原型来源
- 产品原型统一位于 `docs/悦/`。
- 企业登录与注册分别参考 `docs/悦/合并/login.html`、`docs/悦/合并/register.html`;一期只默认包含中文账号密码登录和基础企业注册闭环。
- 快递公司和往来单位参考 `docs/悦/货代SAAS平台后台/货代SaaS平台原型.html` 中“通用”菜单下的对应页面。
- 原型用于确认信息架构、字段和视觉方向;是否已进入当前 Vue 应用、是否具备完整交互与数据持久化,以一期开发进度表为准。
## 当前边界
- 平台后台:内部账号登录、客户及企业资料审核、内部用户、角色权限、平台收货仓、快递公司、往来单位和头程订单;工作台不作为一期交付项。
- 企业客户端:中文邮箱/手机号加密码登录、免审核企业注册、企业资料、成员角色权限、头程下单、订单列表和详情;企业资料经平台人工审核通过后才允许提交订单,一期不接短信/邮箱验证码及微信、企业微信登录。
- 服务端:认证、租户隔离、权限、文件、基础资料、订单状态机、幂等和审计。
- 不包含工作台、部门/组织架构、完整 WMS、FBA/海外目的仓服务、在线报价、财务结算、完整报关清关、外部物流接口、多语言及微信/企业微信登录;快递公司和往来单位一期支持批量导入,不提供导出。
+376
View File
@@ -0,0 +1,376 @@
# 一期头程订单闭环设计
> 状态:产品决策已确认,前端静态闭环实施中
> 创建日期:2026-07-28
> 适用范围:`apps/client-web`、`apps/admin-web`、`packages/shared`、`services/api`
## 一、目标
一期形成可验收的最小订单闭环:
1. 企业注册成功后可直接登录,不经过注册审核。
2. 企业提交资料,由平台人员人工点击审核通过;只有审核通过的企业才可提交头程订单。
3. 企业客户创建头程订单并保存草稿,提交后平台后台收到待审核任务。
4. 平台人员审核订单;信息有误时由后台人工修改,或联系客户修改,不设置驳回操作。
5. 平台人员受理、订舱并更新运输状态,企业客户查看订单详情和处理进度。
6. 企业客户在允许的状态下取消订单。
本设计先统一业务契约,不预先绑定尚未确认的 Java 框架、数据库、中间件或微服务拆分方式。
## 原型依据与迁移原则
一期原型统一以 `docs/悦/` 为依据。快递公司、往来单位的直接参考页面位于 `docs/悦/货代SAAS平台后台/货代SaaS平台原型.html` 的“通用”菜单:
| 原型页面 | 已有内容 | 一期迁移说明 |
|---|---|---|
| 快递公司 | 公司名称、编码、官网、客服电话、合作状态;名称/编码和状态筛选;新增、导入、导出、编辑入口 | 保留核心字段与列表方向;状态统一映射为 `ACTIVE` / `INACTIVE`;一期实现标准模板导入,不实现导出 |
| 往来单位 | 单位名称、类型、联系人、电话、启停状态;名称、类型和状态筛选;新增、导入、导出、编辑、详情入口 | 补充稳定编码、邮箱和完整地址;类型与一期订单的发货方/收货方角色统一;一期实现标准模板导入,不实现导出 |
现有 HTML 原型已完成菜单、静态列表、演示数据和表格渲染,但多数按钮没有完整的数据提交、校验、失败反馈和持久化流程。因此迁移到 `apps/admin-web`、`apps/client-web` 时,必须按当前项目的 Vue、Pinia、RBAC、响应式和测试约定重新实现,不能把原型存在等同于当前工程已经完成。
原型负责提供信息架构、字段和视觉参考;一期范围、数据隔离、权限和历史快照规则以本设计及开发进度表为准。原型中的 FBA、海外仓、库位、货架、托盘、船期价格和完整 WMS 菜单不因复用同一页面而进入一期。
## 二、一期边界
### 纳入一期
- 客户端:订单列表、组合筛选、创建、编辑草稿、提交、详情、取消。
- 管理后台:企业资料审核通过、订单列表、详情、待审核订单人工修改、受理、负责人分配和状态更新。
- 服务端:真实租户隔离、权限校验、状态机、幂等提交、并发控制和操作时间线。
- 公共契约:订单状态、运输方式、优先级、列表摘要和详情模型。
- 平台收货仓:后台维护国内收货仓档案和启停状态;客户端新建订单只选择已启用仓库。
- 快递公司:后台人员统一维护快递公司档案和启停状态;企业客户端不维护,订单详情只读展示履约快递信息。
- 往来单位:后台人员维护发货单位、收货单位、供应商和服务商;企业客户端只读选择当前可用的发货方和收货方。
- 货物信息:一期至少支持一条货物明细,并允许同一订单包含多条明细。
### 暂不纳入一期
- 在线询价、自动报价和费率计算。
- 应收应付、对账、结算和利润核算。
- 报关、清关、保险和完整单证中心。
- 船期自动同步、承运商接口和实时物流轨迹。
- Excel 批量下单、快速下单和复制订单。
- 客户端批量取消或批量修改。
- FBA/海外目的仓、库存、库位、货架及入出库等 WMS 作业。
- 将旧架构文档中的 11 个微服务直接作为一期落地方案。
原型中的 FBA 批量下单、渠道、商检、退税和复杂货物属性作为后续扩展依据,不进入一期最小闭环的必填字段。
## 三、参与者与数据边界
| 参与者 | 数据范围 | 主要操作 |
|---|---|---|
| 企业成员 | 当前租户订单 | 查看列表与详情 |
| 企业订单操作员 | 当前租户订单 | 创建、编辑草稿、提交、取消 |
| 企业管理员 | 当前租户订单 | 拥有企业端全部订单权限 |
| 平台运营 | 获授权数据范围内订单 | 审核、人工修改、受理、分配负责人 |
| 平台操作员 | 获授权数据范围内订单 | 订舱、更新运输状态与处理说明 |
| 平台超级管理员 | 全部订单 | 平台端全部订单操作 |
服务端必须从已验证身份中取得租户上下文。客户端传入的 `tenantId`、`customerId` 或查询参数不能作为授权依据。
企业客户端菜单和操作权限由企业管理员为本企业角色分配。一个成员可拥有多个角色,权限取角色权限并集;一期不引入部门或组织架构,成员直接归属企业。企业管理员不能配置平台后台菜单,也不能跨企业授权。
客户读取、更新、提交和取消订单时,服务端必须同时匹配订单 ID、当前租户 ID 和删除标记;不允许先按全局 ID 查出订单后再仅依赖前端隐藏。
平台国内收货仓由后台统一维护,企业客户只读选择。停用仓库不得用于新建或重新提交订单,但不能破坏历史订单已经保存的仓库名称;一期首批覆盖义乌、深圳、青岛、上海、宁波和厦门,不将其与 FBA/海外目的仓混为同一模型。
快递公司和往来单位均由当前平台后台人员维护。企业客户端不提供对应的新增、编辑、启停菜单和写接口:快递公司只通过订单详情展示,往来单位只用于下单时只读查询和选择。服务端必须按后台人员权限和数据范围校验写操作,并按当前企业可见范围过滤客户端往来单位查询,不能依赖前端隐藏按钮实现边界。
## 四、一期订单状态
### 已确认的统一状态
| 稳定编码 | 中文名称 | 含义 |
|---|---|---|
| `DRAFT` | 草稿 | 客户尚未提交,可继续编辑 |
| `PENDING_REVIEW` | 待审核 | 客户已提交,等待平台审核 |
| `ACCEPTED` | 已受理 | 平台审核通过并进入履约准备 |
| `BOOKED` | 已订舱 | 已完成舱位或对应运输资源确认 |
| `IN_TRANSIT` | 运输中 | 货物已进入运输过程 |
| `COMPLETED` | 已完成 | 一期订单履约结束 |
| `CANCELLED` | 已取消 | 客户或平台按规则取消 |
该方案与当前管理后台实现最接近。`packages/shared` 现有“处理中”以及旧架构文档中的“已确认、已报关、到港待提”不作为一期并存状态;产品确认后统一迁移。
### 允许的状态转换
```text
草稿 ──客户提交──> 待审核 ──平台受理──> 已受理 ──平台订舱──> 已订舱
│ │ │
└──客户取消──> 已取消 ├──后台人工修改/联系客户修改──> 待审核 │
└──客户取消申请/平台取消──> 已取消 └──发运──> 运输中 ──完成──> 已完成
```
- 草稿可由客户编辑、提交或取消。
- 待审核订单允许获授权的后台人员人工修改;平台无法直接确认的信息由平台联系客户修改。客户和后台修改后订单仍保持待审核,修改前后值、操作者和时间必须写入审计记录。
- 已受理后取消可能已经产生成本,一期默认仅平台可执行;客户只能联系平台处理。
- 已订舱、运输中和已完成不允许客户取消。
- 已取消和已完成为终态,不允许直接恢复。
- 所有状态变更必须校验当前状态,不能由前端提交任意目标状态。
## 五、一期数据模型
### 企业资料审核状态
企业账号状态与企业资料审核状态分离:注册成功即可创建账号和登录,不能用禁用账号或注册待审核状态代替下单资格。企业资料至少保存 `verificationStatus`(`UNSUBMITTED`、`PENDING`、`APPROVED`)、`submittedAt`、`approvedAt`、`approvedBy` 和资料版本;只有 `APPROVED` 可提交订单。资料提交、修改和审核通过均写入审计记录。
### 订单主信息
| 字段 | 必填阶段 | 说明 |
|---|---|---|
| `id` | 服务端生成 | 内部稳定 ID,不承载租户信息 |
| `orderNo` | 提交时生成 | 客户可见业务单号;租户内和平台范围均应唯一 |
| `tenantId` | 服务端注入 | 不接受客户指定 |
| `referenceNo` | 可选 | 客户内部参考号;建议租户内非强制唯一 |
| `receivingWarehouseId` | 提交必填 | 平台国内收货仓 ID;提交时必须存在且处于启用状态 |
| `receivingWarehouseName` | 服务端保存 | 下单时仓库名称冗余,兼容列表和旧消费者 |
| `receivingWarehouseSnapshot` | 服务端保存 | 下单时仓库展示快照,至少包含 ID、编码、名称、省、市和详细地址 |
| `shipperCounterpartyId` | 提交必填 | 后台维护的启用发货单位 ID;服务端按当前数据范围校验 |
| `shipperSnapshot` | 服务端保存 | 下单时发货单位名称、联系人、电话和地址快照 |
| `consigneeCounterpartyId` | 提交必填 | 后台维护的启用收货单位 ID;服务端按当前数据范围校验 |
| `consigneeSnapshot` | 服务端保存 | 下单时收货单位名称、联系人、电话和地址快照 |
| `deliveryInfo` | 平台维护 | 可选履约信息,包含快递公司 ID/快照和快递单号;企业客户端只读 |
| `transportMode` | 提交必填 | FCL、LCL、AIR、ROAD |
| `businessType` | 提交必填 | 一期支持 EXPORT、IMPORT |
| `origin` | 提交必填 | 起运地或起运港 |
| `destination` | 提交必填 | 目的地或目的港 |
| `expectedDepartureDate` | 可选 | 客户期望出发日期,不等同于平台确认 ETD |
| `etd`、`eta` | 平台维护 | 平台受理后的预计节点 |
| `priority` | 可选 | NORMAL、URGENT、EXPEDITED,默认 NORMAL |
| `customerRemark` | 可选 | 客户和平台均可见 |
| `internalRemark` | 可选 | 仅平台可见,不得返回客户接口 |
| `operatorId` | 平台维护 | 负责人内部用户 ID |
| `status` | 服务端维护 | 使用稳定英文编码 |
| `version` | 服务端维护 | 乐观锁版本,防止覆盖并发修改 |
| `createdBy`、`createdAt`、`updatedAt` | 服务端维护 | 审计信息 |
订单金额不由客户创建接口提交。一期若后台继续展示金额,应标记为平台估算字段,并在报价/结算范围确认后单独设计。
### 平台收货仓
| 字段 | 说明 |
|---|---|
| `id` | 服务端稳定 ID |
| `code` | 平台内唯一仓库编码,统一大写 |
| `name` | 客户可见仓库名称 |
| `province`、`city`、`address` | 国内收货地址 |
| `contactName`、`contactPhone` | 收货联系信息 |
| `businessHours` | 收货时间说明 |
| `status` | `ACTIVE` 或 `INACTIVE` |
| `remark`、`updatedAt` | 运营说明和更新时间 |
订单只持有仓库 ID 与不可编辑的必要展示快照,不复制联系人、电话、营业时间、运营备注等会持续维护的完整仓库档案。快照字段为 `id`、`code`、`name`、`province`、`city`、`address`;订单详情优先读取快照,不能回查当前仓库主数据覆盖历史值。真实服务端接入后,创建和提交均需重新校验仓库可用性,并在同一事务中持久化快照,不能只依赖客户端下拉选项。
### 快递公司
| 字段 | 说明 |
|---|---|
| `id` | 服务端稳定 ID |
| `code` | 平台内唯一稳定编码 |
| `name` | 快递公司名称 |
| `customerServicePhone` | 客服电话,可选 |
| `trackingUrl` | 官方查询网址,可选 |
| `status` | `ACTIVE` 或 `INACTIVE` |
| `remark`、`updatedAt` | 后台说明和更新时间 |
快递公司只能由获授权的后台人员维护。停用后不得新增到订单履约信息,但历史订单继续展示保存的快递公司名称和编码。企业客户端不直接查询快递公司主数据,只通过订单详情响应读取后台已保存的履约信息。一期不接快递轨迹订阅和自动回传;一个订单只支持一组快递公司与快递单号,多包裹和多段运输后续扩展。
### 往来单位
| 字段 | 说明 |
|---|---|
| `id` | 服务端稳定 ID |
| `code` | 所属数据范围内唯一编码 |
| `name` | 单位名称 |
| `types` | 可多选的稳定单位类型集合;一期覆盖供应商、客户、承运商、报关行、发货单位和收货单位 |
| `contactName`、`contactPhone`、`email` | 主要联系人信息 |
| `country`、`province`、`city`、`postalCode`、`address` | 主要地址信息 |
| `status` | `ACTIVE` 或 `INACTIVE` |
| `dataScopeId` | 所属业务数据范围,由服务端维护 |
| `remark`、`updatedAt` | 后台说明和更新时间 |
往来单位只能由获授权的后台人员维护,企业客户端只读查询当前可见且已启用的发货单位和收货单位。单位类型使用稳定编码,同一单位允许拥有供应商、客户、承运商、报关行、发货单位、收货单位中的多个类型。数据按所属业务主体隔离,后台人员只能在授权范围维护和查看。订单提交时服务端重新校验单位类型、启用状态和数据范围,并保存发货方、收货方快照;主数据后续改名、改地址或停用不得覆盖历史订单。订单内允许调整本单快照,但不得反向修改往来单位档案。
### 基础资料批量导入
一期确认支持后台批量导入快递公司和往来单位,企业客户端不提供导入入口。平台提供版本化的 `.xlsx` 标准模板;后台人员上传后,服务端先完成整批校验,存在任一错误时不写入业务表,并返回包含行号、字段和原因的错误报告。
导入至少校验必填字段、稳定编码、名称、单位类型、联系方式、地址、数据范围和启停值;同一文件内重复、与现有档案重复或越过操作人数据范围的记录必须拒绝,不得静默覆盖已有档案。一期批量导入只新增,不更新或覆盖已有档案;任一记录失败时整批不写入。每次导入记录操作人、原始文件名、总行数、成功/失败数量、失败原因摘要和服务端时间。文件大小和最大行数使用服务端可配置上限;快递公司和往来单位导出不纳入一期。
### 货物明细
| 字段 | 必填 | 说明 |
|---|---|---|
| `id` | 服务端生成 | 明细 ID |
| `lineNo` | 是 | 订单内序号 |
| `sku` | 否 | 客户 SKU |
| `cargoNameCn` | 是 | 中文品名 |
| `cargoNameEn` | 否 | 英文品名 |
| `pieces` | 是 | 件数,正整数 |
| `packageCount` | 否 | 箱数/包装数,非负整数 |
| `grossWeightKg` | 是 | 毛重,必须大于 0 |
| `volumeCbm` | 是 | 体积,必须大于 0 |
| `containerType` | FCL 时必填 | 20GP、40GP、40HQ 等 |
| `containerCount` | FCL 时必填 | 柜量,正整数 |
| `remark` | 否 | 客户可见备注 |
主表中的总件数、总重量、总体积应由服务端根据明细汇总,不接受客户端独立提交两套可能冲突的合计。
### 时间线
每次提交、待审核订单修改、受理、状态变化、取消和负责人变化均写入不可由客户端编辑的时间线:
- 事件类型和事件标题;
- 变更前后状态;
- 操作说明、修改摘要或取消原因;
- 操作者 ID、名称和操作者类型;
- 服务端发生时间;
- 请求关联 ID,便于审计和排错。
## 六、接口草案
统一前缀暂用 `/api/v1`。具体服务拆分不影响浏览器端契约。
### 客户端接口
| 方法 | 路径 | 权限编码 | 说明 |
|---|---|---|---|
| `GET` | `/api/v1/client/first-leg-orders` | `order:first-leg:list` | 当前租户订单分页列表 |
| `POST` | `/api/v1/client/first-leg-orders` | `order:first-leg:create` | 创建草稿 |
| `GET` | `/api/v1/client/first-leg-orders/{id}` | `order:first-leg:view` | 当前租户订单详情 |
| `PUT` | `/api/v1/client/first-leg-orders/{id}` | `order:first-leg:update` | 编辑草稿;平台联系客户后可修改本企业待审核订单,状态保持待审核并记录审计 |
| `POST` | `/api/v1/client/first-leg-orders/{id}/submit` | `order:first-leg:submit` | 幂等提交草稿;提交前强制校验企业资料审核已通过 |
| `POST` | `/api/v1/client/first-leg-orders/{id}/cancel` | `order:first-leg:cancel` | 按状态规则取消 |
| `GET` | `/api/v1/client/platform-receiving-warehouses` | 登录用户 | 查询客户端可选择的启用收货仓 |
| `GET` | `/api/v1/client/counterparties` | 登录用户 | 按当前数据范围只读查询已启用的发货单位和收货单位 |
| `GET` | `/api/v1/client/me` | 登录用户 | 查询当前成员的用户名、姓名、手机、邮箱、微信和所属公司 |
| `PUT` | `/api/v1/client/me` | 登录用户 | 更新真实姓名、手机、邮箱和微信;用户名与所属公司不可修改 |
| `POST` | `/api/v1/client/me/password` | 登录用户 | 校验当前密码后修改密码并记录安全审计 |
| `GET` | `/api/v1/client/company-profile` | `company:profile:view` | 查询当前企业基本信息、法人信息、联系信息和地址 |
| `PUT` | `/api/v1/client/company-profile` | `company:profile:write` | 按企业权限更新允许维护的公司信息 |
| `GET` | `/api/v1/client/company-qualifications` | `company:qualification:view` | 查询资质文件类型、状态、有效期和安全访问地址 |
| `POST` | `/api/v1/client/company-qualifications` | `company:qualification:write` | 上传或更新企业资质文件 |
### 管理后台接口
| 方法 | 路径 | 权限编码 | 说明 |
|---|---|---|---|
| `GET` | `/api/v1/admin/first-leg-orders` | `first-leg:list` | 按平台数据范围查询 |
| `POST` | `/api/v1/admin/first-leg-orders` | `first-leg:create` | 平台代客创建订单 |
| `GET` | `/api/v1/admin/first-leg-orders/{id}` | `first-leg:list` | 订单完整详情,含内部信息 |
| `PUT` | `/api/v1/admin/first-leg-orders/{id}` | `first-leg:update` | 按状态规则维护订单 |
| `POST` | `/api/v1/admin/first-leg-orders/{id}/accept` | `first-leg:status` | 受理并可分配负责人 |
| `POST` | `/api/v1/admin/first-leg-orders/{id}/assign` | `first-leg:assign` | 分配负责人 |
| `POST` | `/api/v1/admin/first-leg-orders/{id}/transition` | `first-leg:status` | 执行服务端允许的后续状态转换 |
| `POST` | `/api/v1/admin/first-leg-orders/{id}/cancel` | `first-leg:status` | 平台取消,原因必填 |
| `GET` | `/api/v1/admin/company-verifications` | `company:verification:list` | 查询企业资料审核状态和待审核企业 |
| `GET` | `/api/v1/admin/company-verifications/{companyId}` | `company:verification:list` | 查看企业资料与资质详情 |
| `POST` | `/api/v1/admin/company-verifications/{companyId}/approve` | `company:verification:approve` | 人工点击审核通过并开通下单资格;一期无驳回动作 |
| `GET` | `/api/v1/admin/platform-receiving-warehouses` | `warehouse:list` | 分页查询全部平台收货仓 |
| `POST` | `/api/v1/admin/platform-receiving-warehouses` | `warehouse:write` | 新增平台收货仓 |
| `PUT` | `/api/v1/admin/platform-receiving-warehouses/{id}` | `warehouse:write` | 编辑平台收货仓 |
| `POST` | `/api/v1/admin/platform-receiving-warehouses/{id}/status` | `warehouse:write` | 启用或停用收货仓 |
| `GET` | `/api/v1/admin/express-companies` | `express-company:list` | 分页查询快递公司 |
| `POST` | `/api/v1/admin/express-companies` | `express-company:write` | 新增快递公司 |
| `PUT` | `/api/v1/admin/express-companies/{id}` | `express-company:write` | 编辑快递公司 |
| `POST` | `/api/v1/admin/express-companies/{id}/status` | `express-company:write` | 启用或停用快递公司 |
| `POST` | `/api/v1/admin/express-companies/import` | `express-company:import` | 使用标准模板批量导入快递公司,整批校验后提交 |
| `GET` | `/api/v1/admin/counterparties` | `counterparty:list` | 按授权数据范围分页查询往来单位 |
| `POST` | `/api/v1/admin/counterparties` | `counterparty:write` | 在授权数据范围内新增往来单位 |
| `PUT` | `/api/v1/admin/counterparties/{id}` | `counterparty:write` | 编辑往来单位 |
| `POST` | `/api/v1/admin/counterparties/{id}/status` | `counterparty:write` | 启用或停用往来单位 |
| `POST` | `/api/v1/admin/counterparties/import` | `counterparty:import` | 在授权数据范围内使用标准模板批量导入往来单位 |
| `PUT` | `/api/v1/admin/first-leg-orders/{id}/delivery-info` | `first-leg:update` | 后台维护快递公司和快递单号 |
一期不使用通用的“传任意目标状态”接口替代状态动作。服务端根据当前状态、动作、操作者和权限决定目标状态。
### 请求一致性
- 创建和提交接口接受 `Idempotency-Key`,服务端按租户、用户、接口和键去重。
- 更新请求携带 `version`;版本不一致返回冲突,不静默覆盖。
- 提交和状态动作在同一事务内更新订单、版本和时间线。
- 分页参数统一为 `page`、`pageSize`,并设置服务端最大页容量。
- API 响应继续采用仓库已有 `ApiResponse<T>` 外层结构;错误码需在后端设计时统一补充。
## 七、页面范围
### 我的信息
按客户端原型提供“基本信息”和“修改密码”两个页签。基本信息展示用户名、真实姓名、手机号、邮箱、微信和所属公司,支持进入编辑、保存与取消;用户名和所属公司始终只读,真实姓名、手机号、邮箱和微信允许按权限编辑。修改密码要求当前密码、新密码和确认密码,覆盖强度校验、两次输入不一致、错误反馈、重复提交防护及成功后的会话处理。
### 公司信息
按客户端原型提供“企业基本信息、联系信息、资质文件”三个页签。基本信息覆盖公司名称、统一信用代码、企业类型、注册地、成立日期、注册资本、法人姓名和法人电话;联系信息覆盖联系人、手机、邮箱、微信和地址;资质文件展示文件类型、验证状态、有效期、上传时间,并支持查看、下载及获授权用户更新。企业数据只能读取当前企业,服务端根据企业角色控制可编辑字段和资质操作。注册不审核;企业资料未审核通过时可继续登录、查看和补充资料,但创建页必须显示下单资格提示,提交订单接口必须拒绝并返回可识别错误码。
### 客户工作台
订单列表包含订单号、参考号、运输方式、起运地、目的地、状态、负责人、预计时间和更新时间;支持关键词、状态、运输方式和创建日期筛选。
创建页按以下顺序组织:
1. 运输信息和平台收货仓;
2. 从后台维护的往来单位中选择发货方和收货方;
3. 货物明细;
4. 客户备注;
5. 保存草稿或提交审核;提交前校验企业资料已经平台人工审核通过。
详情页展示订单摘要、发货方/收货方快照、货物明细、服务端时间线、履约快递公司、快递单号和当前可用操作。内部备注、成本和平台内部操作信息不得进入客户响应;企业客户端不能修改快递信息或往来单位档案。
### 管理后台
新增企业资料审核、平台收货仓、快递公司和往来单位管理。企业资料审核只有“审核通过”,资料不正确时由平台联系客户修改;基础资料页面支持列表筛选、新增、编辑和启停,并由 RBAC 与数据范围控制。快递公司、往来单位页面沿用 `docs/悦` 原型的信息层级和主要字段,但使用当前后台视觉体系和交互组件重新实现;一期实现标准模板下载、文件上传、校验结果和错误报告,不提供导出入口。保留当前订单列表和详情方向,将自由状态下拉改为服务端返回的可用动作;补充待审核订单人工修改、受理、分配、订舱、发运、完成、取消和履约快递信息维护动作,不提供驳回入口。
## 八、测试与验收
- 租户 A 无法读取、更新、提交或取消租户 B 的订单。
- 草稿可以反复保存,提交请求重复发送只产生一次状态变化。
- 待审核订单仅允许当前企业获授权成员或获授权平台人员修改核心业务字段,修改后状态保持待审核且必须完整留痕;其他非草稿状态禁止修改。
- 服务端拒绝非法状态跳转和缺少权限的状态动作。
- 并发修改返回明确冲突,客户端保留用户输入并支持重新加载。
- 客户接口永不返回内部备注、成本或不必要的人员信息。
- 列表覆盖加载、空数据、失败、无权限和分页边界。
- 创建页覆盖校验错误、提交中、重复点击、失败恢复和离开未保存提示。
- 管理后台覆盖待审核订单人工修改、联系客户修改后的并发冲突、受理、负责人失效及状态变更失败,并确认无驳回入口和接口。
- 仓库编码必须唯一;停用仓库不出现在客户新建订单选项中,仓库改名、改地址或停用后,历史订单仍展示下单时的编码、名称和地址快照。
- 企业客户端无法调用快递公司或往来单位写接口,也不显示维护入口;伪造请求必须由服务端拒绝。
- 往来单位查询只返回当前企业可见且已启用的发货单位和收货单位;跨数据范围 ID 在保存草稿和提交时均被拒绝。
- 往来单位改名、改地址或停用后,历史订单继续展示下单时快照;停用单位不能用于新订单提交。
- 快递公司编码和名称满足唯一规则;停用公司不能新增到履约信息,历史订单继续展示已保存快照。
- 客户端订单详情只读展示快递公司和快递单号,接口不得接受客户修改履约快递信息。
- 我的信息只能读取当前登录成员;修改所属公司或其他成员资料的伪造请求必须拒绝,修改密码必须校验当前密码并写入安全审计。
- 注册成功后无需审核即可登录;公司信息只返回当前企业数据,未通过企业资料审核时服务端拒绝提交订单,审核通过后允许提交。普通成员无写权限时仍可按授权查看,资质文件下载使用受控地址并覆盖过期、缺失、上传失败和无权限状态。
- 快递公司和往来单位批量导入仅允许具备对应导入权限的后台人员调用;企业客户端和普通写权限不能绕过独立导入权限。
- 导入文件覆盖模板版本、空文件、超限文件、缺列、类型错误、重复编码、跨数据范围和恶意文件名;任一行失败时业务数据不发生部分写入,并可下载逐行错误报告。
## 九、实施顺序
1. 将已确认的注册、企业资料审核、订单修改和基础资料规则冻结为接口契约。
2. 将稳定订单状态、企业资料审核状态、运输方式、基础资料和订单快照模型统一到 `packages/shared`。
3. 在 `services/api` 初始化后定义企业资料审核、订单、收货仓、快递公司和往来单位 OpenAPI,并以服务端状态机和权限校验作为唯一业务规则来源。
4. 实现后台基础资料维护、快递公司/往来单位批量导入和企业客户端往来单位只读查询。
5. 实现客户工作台订单列表、创建和详情页面,静态阶段通过 API 适配层提供可替换数据源。
6. 将管理后台订单 Pinia 模拟操作迁移到同一 API 契约,并接入履约快递信息。
7. 增加跨租户、状态机、幂等、基础资料越权和前端业务流程测试。
## 十、已确认实施决策
以下决策于 2026-07-28 至 2026-07-29 确认:
1. 采用“草稿、待审核、已受理、已订舱、运输中、已完成、已取消”七个一期状态,不设置“已驳回”。
2. 待审核订单无驳回操作;信息有误时由后台人工修改,或由平台联系客户修改,订单保持待审核并完整记录修改审计。
3. 已受理后客户不能直接取消,一期由客户联系平台并由平台执行取消。
4. 保留平台代客户创建订单的能力。
5. 一期使用本设计的精简必填字段;FBA 批量下单、渠道、商检和退税后续扩展。
6. 快递公司和往来单位均由当前平台后台人员维护;企业客户端不提供维护入口和写接口,仅按权限只读使用。
7. 一期包含快递公司、往来单位标准模板批量导入,不包含对应导出功能。
8. 一期客户端包含我的信息和公司信息,范围以 `docs/悦/头程下单/柏悦客户端.html` 原型为基准并接入真实权限与接口。
9. 企业注册不需要审核,注册成功即可登录;提交订单前必须提交企业资料并由平台人工点击审核通过。
10. 一期支持一个用户拥有多个角色,权限取角色权限并集。
11. 往来单位按所属业务主体隔离并支持一个单位拥有多个稳定编码类型;一期类型覆盖供应商、客户、承运商、报关行、发货单位和收货单位。
12. 一个订单一期只支持一组快递公司和快递单号。
13. 快递公司、往来单位批量导入一期只新增,不更新或覆盖已有档案;重复或任一行错误时整批失败。
14. “我的信息”中的用户名只读,不允许修改。
15. 企业客户端菜单和操作权限由企业管理员为本企业角色分配,一期不做部门/组织架构。
16. 企业注册一期不接短信或邮箱验证码,只做基础字段、密码和账号重复校验。
17. 一期仅支持中文账号密码登录,不做多语言、微信和企业微信登录。
+312
View File
@@ -0,0 +1,312 @@
# 虫虫出海一期开发进度表
> 创建日期:2026-07-27
> 更新日期:2026-07-30
> 当前阶段:一期双端持续浏览器联调;Client 一期核心静态页面与路由已完成(静态完成度 100%)。后端 2026-07-30 业务成熟度回归已确认认证、个人资料、成员、角色、仓库、往来单位、订单草稿和履约快递能力通过真实 API 检查;前端已继续接入个人资料、修改密码、成员完整维护、角色权限回显/保存和商户往来单位候选,仍需完成关键写操作、资质图片及 Admin 目标租户订单的人工浏览器验收
> 总体状态:开发中
头程订单一期闭环范围与接口草案见 `一期头程订单闭环设计.md`。
## 原型依据与进度口径
- 一期产品原型统一以 `docs/悦/` 为依据;企业登录、注册页面分别来源于 `docs/悦/合并/login.html`、`docs/悦/合并/register.html`,快递公司、往来单位页面来源为 `docs/悦/货代SAAS平台后台/货代SaaS平台原型.html`。
- 登录原型包含邮箱/手机号、密码、记住我、忘记密码提示和其他登录方式占位;注册原型包含企业名称、联系人、邮箱、手机号、8–20 位密码及服务协议/隐私政策确认。
- 一期只做中文账号密码登录和基础企业注册闭环,不接短信/邮箱验证码、微信/企业微信登录及多语言;独立找回密码和工作台也不纳入一期。
- 原型已经包含“通用 → 快递公司 / 往来单位”菜单、列表、筛选、状态、操作按钮和演示数据,可作为字段、布局和交互方向参考。
- 原型中的新增、编辑、详情、查询、导入和导出多数仅有静态入口或表格渲染,不等同于 `apps/admin-web`、`apps/client-web` 已完成开发。
- 进度表将“原型核对”和“Vue 工程实现”分开统计:原型核对可标记完成,未进入当前 Vue 应用的页面、状态管理、权限和测试仍按待开始或 0% 记录。
- Client 静态页面完成度与接口联调完成度分开统计:当前一期客户端路由均已落到正式页面,无在用占位页,静态页面完成度为 100%;各任务百分比继续反映真实接口、数据和验收进度。
- 一期权限与业务边界以本目录文档为准;旧原型中的租户角色、FBA、海外仓和 WMS 菜单不自动纳入一期。
## 一、一期目标
一期完成两个端的基础业务闭环:
- 平台后台:内部账号登录、客户与企业资料审核、内部用户、角色、权限和头程订单管理;工作台不作为一期交付项。
- 企业客户端:用户名加密码登录、免审核企业注册、企业资料、企业成员、角色、菜单及权限管理;企业资料审核通过后才可提交订单。
- 头程订单:提供客户端下单和订单查询能力,形成前后台业务闭环。
- 平台收货仓:平台统一维护国内收货仓,客户端下单时选择已启用仓库并保存仓库 ID、编码、名称和地址快照。
- 快递公司:当前平台后台人员统一维护快递公司档案,企业客户端不维护;订单履约时记录快递公司与快递单号。
- 往来单位:当前平台后台人员维护发货单位、收货单位、供应商和服务商,企业客户端只读选择并在头程订单中保存快照。
一期基础资料范围包括平台收货仓、快递公司和往来单位。仓库能力限定为义乌、深圳、青岛、上海、宁波、厦门等国内收货仓的档案、启停、查询和订单关联,不包含 FBA/海外目的仓、库存、库位、货架、入库、出库、盘点等 WMS 作业;快递能力不包含轨迹订阅和自动回传。
权限边界暂按以下模型设计:
- 平台后台 RBAC:平台内部员工使用,管理所有企业和订单。
- 企业客户端 RBAC:企业管理员使用,只管理本企业的成员、角色和权限。
- 企业之间按 `tenant_id` 隔离,客户端用户不能访问其他企业数据。
- 快递公司和往来单位的新增、编辑、启停仅开放给获授权的后台人员;企业客户端不提供维护入口和写接口。
## 二、总体进度
| 模块 | 范围 | 状态 | 进度 | 计划开始 | 计划完成 | 负责人 | 备注 |
|---|---|---|---:|---|---|---|---|
| 基础工程 | 前后端工程、数据库、环境、接口规范 | 进行中 | 55% | 2026-07-27 | 待排期 | 待分配 | 双端统一响应、Token 会话和 Gateway 代理已接入;订单与基础资料契约仍待补齐 |
| 后台客户管理 | 客户档案、企业资料审核、启停和资料维护 | 联调中 | 82% | 2026-07-27 | 待排期 | 待分配 | 客户档案仍使用 Pinia 模拟数据;企业审核已接真实待审、租户资质、鉴权文件预览及审核接口 |
| 后台管理 | 内部用户、角色、权限 | 进行中 | 70% | 2026-07-27 | 待排期 | 待分配 | RBAC 前端交互已完成,等待接口与真实鉴权 |
| 后台头程订单 | 订单列表、详情、处理和状态流转 | 进行中 | 70% | 2026-07-27 | 待排期 | 待分配 | 前端创建、查询、详情、指派及状态处理已完成 |
| 平台收货仓 | 平台维护国内收货仓;客户端选择启用仓库 | 联调中 | 80% | 2026-07-28 | 待排期 | 待分配 | 客户端仓库分页、启用过滤、空态禁建和订单快照已接真实 API;当前返回 0 条,后台维护接口与持久化仍待联调 |
| 快递公司维护 | 平台后台维护快递公司档案、启停、批量导入及订单履约信息关联 | 进行中 | 75% | 2026-07-28 | 待排期 | 待分配 | 原型字段核对与 Vue 静态实现均已完成;维护、权限、标准模板、整批校验和订单单组履约信息可验收,等待业务 API |
| 往来单位维护 | 平台后台维护发货单位、收货单位、供应商、服务商档案、批量导入及订单引用 | 进行中 | 75% | 2026-07-28 | 待排期 | 待分配 | 原型核对与 Vue 静态实现分别完成;六类多选、联系人、完整地址、启停、模板下载和文件基础检查可验收,工作表解析与整批导入等待后端 API |
| 登录注册与客户入驻 | Admin 登录、企业端免审核注册、我的信息、公司信息及企业资料审核 | 联调中 | 85% | 2026-07-27 | 待排期 | 待分配 | Client 已接真实注册登录、退出、刷新令牌、企业资料编辑与营业执照鉴权预览;接口曾完成基础实测,当前局域网 Gateway 请求超时,待恢复后完成浏览器验收 |
| 客户端权限体系 | 企业成员、菜单、角色、权限 | 联调中 | 82% | 2026-07-28 | 待排期 | 待分配 | 已接角色列表/新增和成员列表;授权写接口为全量替换,但缺少已有权限 ID 与已有角色 ID 回显,暂保持只读 |
| 客户端头程订单 | 下单、列表、详情、平台收货仓及往来单位只读选择 | 联调中 | 85% | 2026-07-28 | 待排期 | 待分配 | 列表、详情、创建/编辑草稿和提交已接真实 API;待审核企业可存草稿但不能提交,往来单位接口、有效仓库种子数据、真实写入和服务端重验仍待完成 |
| 联调与上线 | 测试、验收、部署、初始化数据 | 待开始 | 0% | 待排期 | 待排期 | 待分配 | 一期发布 |
## 三、详细任务表
| ID | 端 | 模块 | 功能/交付内容 | 优先级 | 状态 | 进度 | 依赖 | 负责人 | 备注 |
|---|---|---|---|---|---|---:|---|---|---|
| BASE-01 | 公共 | 工程初始化 | 创建后台前端、客户端前端和后端工程,区分开发/测试/生产环境 | P0 | 进行中 | 70% | - | 待分配 | admin/client 已独立安装与构建,API 工程待初始化 |
| BASE-02 | 公共 | 数据库 | 用户、企业、企业资料审核、成员、角色、菜单、权限、订单、仓库等核心表设计 | P0 | 待开始 | 0% | BASE-01 | 待分配 | 企业资料审核状态与注册账号状态分离;仓库主数据与订单仓库快照分离 |
| BASE-03 | 公共 | 认证 | Admin 与企业端登录、退出、Token、密码加密、登录状态和接口鉴权 | P0 | 联调中 | 55% | BASE-02 | 待分配 | Client 已接真实登录/退出、Access/Refresh Token、MERCHANT 身份校验、并发 401 单次刷新;2026-07-30 Gateway/Auth 登录均出现 503,待后端恢复服务发现后验收 |
| BASE-04 | 公共 | 通用能力 | 统一响应、错误码、分页、操作日志、文件上传 | P0 | 进行中 | 25% | BASE-01 | 待分配 | Client 已实现 code 字符串响应解包、字段错误模型、常见网络错误和 multipart 营业执照上传;分页与日志待接入 |
| BASE-05 | 公共 | 接口文档 | 输出并维护一期 API 文档 | P0 | 待开始 | 0% | BASE-01 | 待分配 | 建议使用 OpenAPI;同步企业资料审核、下单资格、仓库、快递公司、往来单位与订单快照契约 |
| ADM-AUTH-01 | 后台 | 登录认证 | 平台内部员工账号密码登录、退出、登录回跳、路由守卫和无权限跳转 | P0 | 进行中 | 70% | BASE-03 | 待分配 | 已接 `/api/auth/**` 与 `/api/platform/v1/iam/permissions`,校验 PLATFORM 身份并按 JWT 与启用权限资源交集授权;等待 Gateway/Nacos 路由恢复后完成真实环境验收 |
| ADM-CUSTOMER-01 | 后台 | 客户管理 | 客户列表、搜索、分页、详情、新增、编辑和启停 | P0 | 进行中 | 70% | BASE-03 | 待分配 | 前端已完成并通过浏览器验收,等待接口联调 |
| ADM-CUSTOMER-02 | 后台 | 客户管理 | 查看企业资料和资质,人工点击审核通过并维护下单资格 | P0 | 联调中 | 82% | ADM-CUSTOMER-01、CLI-PROFILE-02 | 待分配 | 独立审核页已接 IAM 待审列表、按租户资质查询、平台 Token 文件预览、通过与驳回接口;待使用真实待审记录验证完整写操作 |
| ADM-USER-01 | 后台 | 内部用户 | 用户列表、搜索、分页和详情 | P0 | 进行中 | 70% | BASE-03 | 待分配 | 前端列表、筛选和分页已完成 |
| ADM-USER-02 | 后台 | 内部用户 | 新增、编辑、启停、重置密码 | P0 | 进行中 | 65% | ADM-USER-01 | 待分配 | 前端交互已完成,操作日志与接口待接入 |
| ADM-USER-03 | 后台 | 内部用户 | 给用户分配一个或多个角色 | P0 | 进行中 | 75% | ADM-ROLE-01 | 待分配 | 多角色选择与回显已完成 |
| ADM-ROLE-01 | 后台 | 角色 | 角色列表、新增、编辑、删除、启停 | P0 | 联调中 | 82% | BASE-03 | 待分配 | 平台角色列表与新增已接 `/api/platform/v1/iam/roles`;编辑、删除、启停接口未提供,真实会话中明确禁用且不回退模拟写入 |
| ADM-ROLE-02 | 后台 | 角色 | 给角色分配菜单和操作权限 | P0 | 已阻塞 | 75% | ADM-PERM-01 | 待分配 | 写接口是全量替换,但缺少角色当前 `permissionIds` 回显;真实会话暂不开放保存,避免清空已有授权 |
| ADM-PERM-01 | 后台 | 权限 | 菜单树、页面权限、按钮权限和接口权限模型 | P0 | 联调中 | 88% | BASE-02 | 待分配 | `GET/POST /api/platform/v1/iam/permissions` 已接入;真实模式只创建 `MENU/BUTTON`,编辑和启停接口待补 |
| ADM-PERM-02 | 后台 | 权限 | 登录后返回当前用户菜单和权限集合 | P0 | 联调中 | 80% | ADM-PERM-01 | 待分配 | 已实现 JWT 权限与 IAM 启用资源取交集、动态菜单和会话恢复;待 Gateway 恢复后复验权限变更后的 Token 刷新 |
| ADM-PERM-03 | 后台 | 权限 | 未授权页面、按钮隐藏和接口拒绝访问 | P0 | 联调中 | 65% | ADM-PERM-02 | 待分配 | 前端路由、菜单和主要按钮已按稳定权限码控制;后端文档确认方法级按钮权限尚未覆盖所有管理接口 |
| ADM-ORDER-01 | 后台 | 头程订单 | 订单列表、组合查询、分页和导出 | P0 | 进行中 | 70% | BASE-03 | 待分配 | 前端组合筛选、分页、选择与导出反馈已完成 |
| ADM-ORDER-02 | 后台 | 头程订单 | 订单详情、货物、收发货和平台收货仓信息 | P0 | 进行中 | 75% | ADM-ORDER-01 | 待分配 | 前端详情按下单时仓库快照展示;API 与数据库持久化待接入 |
| ADM-ORDER-03 | 后台 | 头程订单 | 订单审核、人工修改、受理、取消和状态流转 | P0 | 进行中 | 80% | ADM-ORDER-02 | 待分配 | Vue 已删除订单驳回状态/动作;后台修改待审核订单后仍保持待审核并自动追加审计记录,联系客户修改不改变状态;等待业务 API |
| ADM-ORDER-04 | 后台 | 头程订单 | 订单指派、内部备注和操作记录 | P1 | 进行中 | 65% | ADM-ORDER-03 | 待分配 | 负责人、处理说明和时间线前端已完成 |
| ADM-WH-01 | 后台 | 平台收货仓 | 仓库列表、关键词和状态筛选、分页及空态 | P0 | 进行中 | 75% | BASE-03、WH-BASE-01 | 待分配 | 静态页面和权限已完成,等待接口联调 |
| ADM-WH-02 | 后台 | 平台收货仓 | 新增、编辑、启用和停用仓库;校验编码唯一性 | P0 | 进行中 | 75% | ADM-WH-01 | 待分配 | 停用仓库不允许新订单选择,不物理删除历史引用 |
| WH-BASE-01 | 公共 | 仓库模型 | 定义国内平台收货仓、启停状态及订单仓库关联 | P0 | 进行中 | 65% | BASE-02 | 待分配 | 共享仓库及快照模型已完成,数据库与 API 契约待实现 |
| MD-PROTO-01 | 公共 | 基础资料原型 | 核对 `docs/悦` 中快递公司、往来单位菜单、列表、字段、状态和现有交互,形成一期迁移口径 | P0 | 已完成 | 100% | - | 待分配 | 已确认原型存在;原型资产与当前 Vue 工程实现分开统计 |
| MD-BASE-01 | 公共 | 基础资料模型 | 统一快递公司、往来单位的稳定编码、启停、审计、数据范围和订单快照规则 | P0 | 进行中 | 10% | BASE-02、BASE-04、MD-PROTO-01 | 待分配 | 设计文档已有字段草案;共享类型、数据库与 API 契约尚未实现 |
| ADM-EXP-01 | 后台 | 快递公司 | 快递公司列表、关键词搜索、按状态筛选及详情 | P0 | 进行中 | 80% | BASE-03、MD-BASE-01 | 待分配 | Vue 列表、组合筛选、详情和响应式展示完成;Pinia 静态数据等待新架构业务 API |
| ADM-EXP-02 | 后台 | 快递公司 | 后台人员新增、编辑、启用和停用;校验稳定编码与名称唯一性 | P0 | 进行中 | 80% | ADM-EXP-01 | 待分配 | Vue 维护、权限控制、编码/名称不区分大小写唯一校验和历史引用提示完成;服务端校验待接入 |
| ADM-EXP-03 | 后台 | 快递公司 | 使用标准模板批量导入快递公司档案 | P1 | 进行中 | 60% | ADM-EXP-02、BASE-04 | 待分配 | 标准 XLSX 下载、文件选择、扩展名和大小检查已完成;工作表解析、模板版本、重复校验、整批事务及错误报告均由后端负责,等待导入 API |
| ADM-CP-01 | 后台 | 往来单位 | 往来单位列表、关键词搜索、按单位类型/状态筛选及详情 | P0 | 进行中 | 80% | BASE-03、MD-BASE-01 | 待分配 | Vue 列表、关键词/六类角色/状态筛选、详情和空态完成;原型核对与 Vue 实现分别统计 |
| ADM-CP-02 | 后台 | 往来单位 | 后台人员新增、编辑、启用和停用;维护单位编码、名称和类型 | P0 | 进行中 | 80% | ADM-CP-01 | 待分配 | Vue 新增/编辑/启停、多类型和编码唯一校验完成;企业客户端无维护入口,服务端数据范围待接入 |
| ADM-CP-03 | 后台 | 往来单位 | 维护联系人、电话、邮箱、国家/地区、州省、城市、邮编和详细地址 | P0 | 进行中 | 80% | ADM-CP-02 | 待分配 | Vue 联系人与完整地址字段、邮箱格式和必填校验完成;订单快照 API 待接入 |
| ADM-CP-04 | 后台 | 往来单位 | 使用标准模板批量导入往来单位档案 | P1 | 进行中 | 60% | ADM-CP-03、BASE-04 | 待分配 | 标准 XLSX、六类多值说明、文件类型和大小检查已完成;解析、数据范围、整批失败和错误报告由后端返回,等待导入 API |
| CLI-AUTH-01 | 客户端 | 登录认证 | 按后端契约实现用户名密码登录、记住我、登录反馈及登录/注册互跳 | P0 | 联调中 | 85% | BASE-03 | 待分配 | 已接 `/api/auth/login|refresh|logout`、Token 轮换、MERCHANT JWT 和会话恢复;Gateway 在线但登录转发 503,待路由恢复后验证错误码与过期刷新 |
| CLI-REG-01 | 客户端 | 注册 | 按后端契约维护用户名、8–72 位密码、企业、联系人、法人、手机、邮箱、国家和地址 | P0 | 联调中 | 85% | BASE-03 | 待分配 | 已按 DTO 补齐全部字段并接 `/api/auth/register`;Gateway/Auth 环境不稳定,待恢复后验证重复用户名与 422 字段错误 |
| CLI-REG-02 | 客户端 | 注册 | 完成基础格式校验、协议确认、免审核注册成功反馈和登录互跳 | P0 | 联调中 | 90% | CLI-REG-01 | 待分配 | 注册成功跳登录并带入用户名,账号与企业资料审核状态分离;待 Gateway 真实验收 |
| ADM-CUSTOMER-03 | 后台 | 企业资料审核 | 企业资料待审核列表、详情和人工审核通过操作 | P0 | 联调中 | 85% | ADM-CUSTOMER-02、CLI-PROFILE-02 | 待分配 | 已接待审列表、`/tenants/{tenantId}/qualifications`、平台资质文件二进制接口及真实审核动作;文件不再使用无 Token 直链,待审样本写操作仍待人工验收 |
| CLI-MY-01 | 客户端 | 我的信息 | 查看用户名、姓名、联系电话、邮箱和所属公司,支持进入编辑、保存和取消 | P0 | 联调中 | 90% | CLI-AUTH-01 | 待分配 | 已接个人资料查询/编辑接口;用户名和所属公司只读,电话与邮箱按 OpenAPI 可选约束处理,保存失败保留输入 |
| CLI-MY-02 | 客户端 | 我的信息 | 使用当前密码、新密码和确认密码修改登录密码 | P0 | 联调中 | 90% | CLI-MY-01、BASE-03 | 待分配 | 已接 `PUT /api/auth/password`,按 8–64 位且同时包含字母数字校验;成功后清理旧会话并返回登录页 |
| CLI-PROFILE-01 | 客户端 | 公司信息 | 企业基本信息、法人信息、联系人、手机、邮箱、微信和地址查看与维护 | P0 | 进行中 | 65% | CLI-REG-01 | 待分配 | 原型按“企业基本信息/联系信息”分页签展示;当前静态编辑交互已完成,等待接口和真实权限控制 |
| CLI-PROFILE-02 | 客户端 | 公司信息 | 一期营业执照状态、查看和上传 | P1 | 联调中 | 90% | BASE-04 | 待分配 | 已接资料编辑、资质查询、营业执照上传及携带 Token 的图片预览;按后端限制为单张 JPG/PNG、最大 5MB,待稳定环境完成浏览器验收 |
| CLI-MEMBER-01 | 客户端 | 企业成员 | 成员列表、新增、编辑、启停和重置密码 | P0 | 联调中 | 92% | CLI-REG-01 | 待分配 | 姓名、联系方式、memberNo 和 roleIds 已真实回显;新增、编辑、启停、重置密码均接精确 membershipId 路径,所有者保护由服务端最终校验 |
| CLI-ROLE-01 | 客户端 | 角色 | 企业角色新增、编辑、删除、启停 | P0 | 联调中 | 80% | CLI-MEMBER-01 | 待分配 | 已接角色列表和新增角色;编辑、删除、启停接口未在联调文档中提供,页面进入 API 模式后不再回退模拟写入 |
| CLI-ROLE-02 | 客户端 | 角色 | 为成员分配一个或多个角色 | P0 | 联调中 | 92% | CLI-ROLE-01 | 待分配 | 成员 roleIds 已回显,新增/编辑成员通过精确 membershipId 全量保存角色;待浏览器执行一次真实写入验收 |
| CLI-MENU-01 | 客户端 | 菜单分配 | 企业管理员为本企业角色分配客户端菜单和操作权限 | P0 | 进行中 | 60% | CLI-ROLE-01 | 待分配 | 已确认操作人为企业管理员;分组选择交互已完成,动态菜单生成与平台菜单隔离需由服务端契约保证 |
| CLI-PERM-01 | 客户端 | 权限 | 页面、按钮和接口权限定义及角色授权 | P0 | 联调中 | 90% | CLI-MENU-01 | 待分配 | 角色 permissionIds 与启用权限资源已映射回显;非内置启用角色可全量保存权限,待准备自定义角色完成浏览器写入验收 |
| CLI-PERM-02 | 客户端 | 权限 | 按当前成员权限生成菜单并执行接口鉴权 | P0 | 进行中 | 55% | CLI-PERM-01 | 待分配 | 已完成演示账号权限集合、动态菜单、路由 403、首个可访问页面、会话恢复和按钮权限预览;服务端仍必须校验 `tenant_id` 与权限 |
| CLI-CP-01 | 客户端 | 往来单位查询 | 只读查询当前可用的发货单位和收货单位,支持关键词和类型筛选 | P0 | 联调中 | 88% | ADM-CP-03、BASE-03 | 待分配 | 已接 `/api/merchant/v1/partners/candidates`,按 SENDER/RECEIVER 查询当前租户启用候选;无维护入口且不回退演示数据 |
| CLI-WH-01 | 客户端 | 平台收货仓选择 | 查询已启用仓库,展示名称、地址和收货时间 | P0 | 测试中 | 95% | WH-BASE-01、ADM-WH-02 | 待分配 | 浏览器已读取 1 条真实启用仓库,选择后成功加载深圳地址和每日 16:00 截单时间;历史订单引用失效仓库时要求重新选择,不回退模拟仓库 |
| CLI-ORDER-01 | 客户端 | 头程订单 | 新建头程订单、保存草稿和提交;提交前校验企业资料审核状态 | P0 | 联调中 | 92% | BASE-03、ADM-CUSTOMER-03 | 待分配 | 企业审核状态改为进入订单页时真实加载;创建/编辑/提交契约、幂等键和权限映射已接。为避免污染联调数据,本轮只读验收未执行保存和提交 |
| CLI-ORDER-02 | 客户端 | 头程订单 | 我的订单列表和详情 | P0 | 测试中 | 95% | CLI-ORDER-01 | 待分配 | 浏览器真实读取 1 条草稿及详情,货物、双方快照和状态历史正常回显;编辑前强制加载完整详情,修复列表简要数据导致空货物和响应式复制异常 |
| CLI-ORDER-03 | 客户端 | 头程订单 | 下单选择平台收货仓;提交时保存仓库 ID 和展示快照 | P0 | 测试中 | 90% | CLI-WH-01、CLI-ORDER-01 | 待分配 | 新订单真实仓库选择和详情回显已验证;当前旧草稿引用已不存在的仓库 `2001`,客户端清空失效选择并提示重新选择,服务端样本关联待修复 |
| CLI-ORDER-04 | 客户端 | 头程订单 | 从已启用往来单位选择发货方/收货方并带入联系人和地址,提交时保存快照 | P0 | 测试中 | 92% | CLI-CP-01、CLI-ORDER-01 | 待分配 | 真实候选接口和订单双方快照已接;当前新建表单候选为空,旧订单快照可完整回显并以“历史快照”标识已不在候选中的单位 |
| ADM-ORDER-05 | 后台 | 头程订单 | 维护履约快递公司、快递单号并在订单详情展示 | P1 | 进行中 | 80% | ADM-EXP-02、ADM-ORDER-02 | 待分配 | Vue 列表/详情/维护弹窗、单组限制、停用历史回显和审计记录完成;一期不接轨迹订阅,业务 API 待新架构确认 |
| CLI-ORDER-05 | 客户端 | 头程订单 | 订单详情只读展示后台维护的快递公司和快递单号 | P1 | 测试中 | 75% | ADM-ORDER-05、CLI-ORDER-02 | 待分配 | 真实订单详情已验证“平台尚未维护快递信息”空态,客户侧无编辑入口;仍需后台准备含快递公司和单号的样本复测有值回显 |
| TEST-01 | 公共 | 测试 | 用户、角色、权限和越权访问测试 | P0 | 进行中 | 40% | 权限模块完成 | 待分配 | 客户端已覆盖权限过滤、403、首个可访问页面、按钮权限和会话恢复;跨企业越权及接口拒绝必须待服务端测试 |
| TEST-02 | 公共 | 测试 | Admin 登录、企业端免审核注册、企业资料审核、下单资格和头程订单业务流程测试 | P0 | 进行中 | 45% | 业务模块完成 | 待分配 | 客户端已覆盖免审核注册、资料待审核可存草稿、提交阻断和资料引导;仍需接口失败、审核通过提交及服务端越权测试 |
| TEST-03 | 公共 | 测试 | 编码唯一、启停控制、客户端只读选择及订单仓库关联测试 | P0 | 进行中 | 65% | ADM-WH-02、CLI-ORDER-03 | 待分配 | 前端自动化已覆盖停用限制和主数据变更后的历史快照;服务端测试待补 |
| TEST-04 | 公共 | 测试 | 快递公司和往来单位编码唯一、数据范围、启停控制、批量导入、只读查询、订单带入及快照测试 | P0 | 进行中 | 25% | ADM-EXP-03、ADM-CP-04、CLI-ORDER-05 | 待分配 | 补充模板版本、必填/格式/重复校验、整批失败、错误报告、导入权限和审计测试 |
| REL-01 | 公共 | 发布 | 测试环境部署、初始化账号、基础菜单权限和首批基础资料 | P0 | 待开始 | 0% | BASE-01、ADM-WH-02、ADM-EXP-02、ADM-CP-03 | 待分配 | 首批收货仓、快递公司、往来单位数据需业务验收;禁止使用通用弱密码 |
| REL-02 | 公共 | 验收 | 一期验收、问题修复和生产发布 | P0 | 待开始 | 0% | TEST-01、TEST-02、TEST-03、TEST-04 | 待分配 | 形成验收清单 |
## 四、建议里程碑
| 里程碑 | 交付目标 | 状态 | 计划完成 |
|---|---|---|---|
| M1 基础工程 | 工程、数据库、登录认证、通用能力可用 | 待排期 | 待排期 |
| M2 后台权限 | 内部用户、角色、权限完整可用 | 进行中 | 待排期 |
| M3 企业入驻 | 免审核注册、企业资料人工审核、下单资格和企业成员可用 | 进行中 | 待排期 |
| M4 企业权限 | 客户端角色、菜单和权限完整可用 | 进行中 | 待排期 |
| M5 基础资料主数据 | 后台可维护平台收货仓、快递公司、往来单位;客户端只读查询并受数据范围控制 | 进行中 | 待排期 |
| M6 订单闭环 | 客户选择收货仓和往来单位提交订单,后台维护履约信息,历史展示稳定 | 进行中 | 待排期 |
| M7 一期上线 | 联调、测试、验收和生产发布完成 | 待排期 | 待排期 |
## 五、产品决策与待确认事项
### 已确认决策
| 编号 | 决策 | 结论 |
|---|---|---|
| Q1 | 客户端一期是否包含头程下单、订单列表和详情? | 包含,作为一期前后台订单闭环的必要范围 |
| Q2 | 企业注册与企业资料审核关系 | 注册无需审核,成功后即可登录;提交订单前必须提交企业资料并由平台人工点击审核通过 |
| Q3 | 客户端“菜单分配”的操作人 | 企业管理员为本企业角色分配菜单和操作权限,不能管理其他企业或平台后台菜单 |
| Q4 | 一个用户能否拥有多个角色? | 一期支持多角色,服务端按角色权限并集授权 |
| Q5 | 企业注册是否使用短信或邮箱验证码? | 一期不接验证码,仅做必填、格式、密码强度和账号重复校验 |
| Q6 | 头程订单一期状态 | 草稿、待审核、已受理、已订舱、运输中、已完成、已取消;不设置已驳回状态 |
| Q7 | 是否需要部门/组织架构? | 一期不纳入,成员直接关联企业并通过角色授权 |
| Q8 | 一期仓库范围与维护方 | 平台后台统一维护国内收货仓,客户端只读选择启用仓库;FBA/海外目的仓不纳入本次 |
| Q9 | 仓库停用后的订单处理 | 禁止新单选择;历史订单继续展示下单时仓库编码、名称和地址快照 |
| Q10 | 快递公司和往来单位维护方 | 均由当前平台后台人员维护;企业客户端无维护入口和写接口,仅可按权限查询、选择和查看 |
| Q11 | 往来单位的数据范围 | 按所属业务主体隔离,平台人员仅在授权数据范围内维护和查看 |
| Q12 | 往来单位一期类型 | 使用稳定编码并支持一个单位多选类型;一期覆盖供应商、客户、承运商、报关行、发货单位和收货单位 |
| Q13 | 一个订单支持几组快递公司和快递单号? | 一期只支持一组履约快递信息;多包裹、多段运输后续扩展 |
| Q15 | Admin 工作台是否纳入一期? | 不纳入一期交付与验收;一期先完成简单登录、企业资料审核、权限、基础资料和订单闭环 |
| Q14 | 快递公司、往来单位批量导入是否纳入一期? | 已确认纳入一期,均由获授权的后台人员使用标准模板导入 |
| Q16 | 多语言、微信和企业微信登录是否纳入一期? | 不纳入;一期仅交付中文账号密码登录,第三方登录后续单独排期 |
| Q17 | 快递公司、往来单位导出是否纳入一期? | 不纳入;一期仅实现标准模板批量导入 |
| Q18 | 一期客户端是否包含我的信息和公司信息? | 已确认包含;按现有客户端原型覆盖个人资料编辑、修改密码、企业基本/联系信息和资质文件查看维护 |
| Q19 | 待审核订单信息不正确时如何处理? | 一期无驳回操作;后台可人工修改,不能直接确认时联系客户修改,订单保持待审核并记录修改审计 |
| Q20 | 批量导入是否更新已有档案? | 一期仅新增;重复编码或唯一键冲突时整批失败,不覆盖已有档案 |
| Q21 | “我的信息”中的用户名能否修改? | 用户名只读,不允许修改 |
### 待确认事项
| 编号 | 待确认问题 | 当前建议 |
|---|---|---|
| Q22 | 企业资料审核通过需要哪些必填资料? | 待产品给出字段和资质文件清单;在确认前按现有公司信息与营业执照能力保持可配置校验 |
| Q23 | 审核通过后修改哪些关键资料需要重新审核? | 建议公司名称、统一信用代码、营业执照等关键资料变更后回到待审核;联系人等普通信息不触发 |
| Q24 | 企业资料未审核通过时允许进行到哪一步? | 当前实现允许保存订单草稿但禁止提交;是否连新建订单也禁止仍需产品确认 |
## 六、状态维护规则
- 状态统一使用:待确认、待开始、进行中、联调中、测试中、已完成、已阻塞。
- 开始开发时填写负责人、计划日期并将状态改为“进行中”。
- 提交测试时更新为“测试中”,测试通过并验收后才更新为“已完成”。
- 出现阻塞时在备注中写明阻塞原因、责任人和预计解除时间。
## 七、后台前端阶段验收(2026-07-27)
| 验收项 | 结果 | 证据/说明 |
|---|---|---|
| 客户管理 | 通过(静态,待调整) | 列表、筛选、分页、新增/编辑、详情和启停已完成;原注册审核交互需改为企业资料审核通过 |
| Admin 登录 | 通过(静态) | 登录必填/失败反馈、退出确认、安全回跳、403 路由及按角色首个业务页落地已完成;一期工作台入口已移除,真实 Token 与会话失效处理待接入 |
| 内部用户 | 通过 | 列表、筛选、新增/编辑、多角色、重置密码、启停及管理员保护 |
| 角色管理 | 通过 | 新增/编辑/删除/启停、权限树、数据范围及内置角色保护 |
| 权限管理 | 通过 | 目录/菜单/按钮/接口资源树、上下级维护、状态与唯一编码校验 |
| 头程订单 | 通过 | 创建/编辑、草稿/提交、详情、时间线、单笔/批量处理、指派与导出反馈 |
| 响应式与可访问性 | 通过 | 桌面与 390px 手机视口实测;菜单可收起,表格可滚动,关键触控区 44px |
| 自动化测试 | 通过 | `apps/admin-web` 执行 `pnpm test`,21 个测试文件、148 项通过,覆盖企业审核、租户资质鉴权预览、平台角色/权限 IAM 路径、错误追踪信息、导入边界、真实列表/失败态、登录安全回跳及开发账号环境隔离 |
| TypeScript 类型检查 | 通过 | `pnpm typecheck` |
| 生产构建 | 通过 | `pnpm build`,输出至 `apps/admin-web/dist`;产物扫描确认不包含平台联调账号、密码和开发提示 |
| impeccable 样式检测 | 通过(已修复告警) | 修复字体栈以及 width/margin 布局动画;保留既有后台视觉体系 |
> 客户档案、内部用户和基础资料仍使用 Pinia 模拟数据;企业审核、平台角色、菜单及按钮权限资源已切换到真实 IAM 接口。后台前端阶段完成不等于一期业务完成,仍需后端数据、真实 RBAC 鉴权、联调和测试后才能标记“已完成”。
### Admin 认证联调记录(2026-07-29)
- 前端已统一通过 Gateway 前缀调用 `/api/auth/**` 和 `/api/platform/v1/iam/**`,本地 Vite 使用 `/api` 代理,不拼接 Nacos 服务名或服务直连端口。
- 已实现 Access/Refresh Token 持久化、PLATFORM 身份域校验、刷新令牌轮换、并发 401 单次刷新、服务端退出调用,以及 JWT 权限与 IAM 启用资源交集。
- `192.168.110.149:9000` Gateway 的 Auth、IAM、头程订单和仓库路由已恢复并完成基础连通验证;企业审核列表当前返回空数组。
- 企业审核已新增独立路由 `/customers/reviews`,权限为 `platform:customer:review`,通过/驳回分别校验 `platform:customer:approve` 与 `platform:customer:reject`,提交参数使用审核接口返回的 `reviewId`。
- `file-service:9206` 当前拒绝连接;IAM Swagger/OpenAPI 当前返回 500,审核 DTO 的可选展示字段仍以真实返回为准。前端不增加服务直连或模拟数据静默回退。
### Admin 平台角色与权限联调记录(2026-07-30)
- 已接入 `GET/POST /api/platform/v1/iam/roles`,真实会话使用 IAM 角色列表,新增角色使用后端返回的真实角色记录。
- 新增角色按钮严格使用 `platform:role:create`;角色编辑、删除、启停接口未在最新联调文档中提供,真实会话中不展示这些模拟操作。
- 角色权限写接口是全量替换,但联调文档没有角色当前 `permissionIds` 回显。为避免覆盖已有授权,真实会话暂不开放角色授权保存。
- 已接入 `GET/POST /api/platform/v1/iam/permissions`;真实会话使用 IAM 权限资源树,并严格把新增类型限制为 `MENU` 和 `BUTTON`。
- 新增权限使用 `platform:permission:create`,正确提交真实父资源 ID、路由和排序;权限编辑、启停接口未提供,真实会话不展示对应模拟操作。
- 2026-07-30 验收时 `192.168.110.149:9000` 连接被拒绝,无法执行真实登录和角色读写现场验证;专项自动化测试已覆盖接口路径、真实列表模式和失败恢复。
## 八、客户端前端阶段验收(更新至 2026-07-30)
| 验收项 | 结果 | 证据/说明 |
|---|---|---|
| 企业资料 | 通过(静态) | 基本信息、联系信息、资质文件选择与校验、移除、提交防重、元数据详情和移动端卡片已完成 |
| 企业成员 | 通过(静态) | 列表与移动卡片、筛选、新增/编辑、多角色、重置密码、启停和管理员保护已完成 |
| 企业角色 | 通过(静态) | 新增/编辑/删除/启停、成员和权限数量、内置角色保护及权限入口已完成 |
| 菜单权限 | 通过(静态) | 角色选择、分组菜单与操作权限、全选/半选、保存/重置和只读保护已完成 |
| 我的信息与修改密码 | 通过(静态) | 原型字段、查看/编辑/保存/取消、只读账号与企业、当前密码错误、密码强度和静态会话/审计结果已完成 |
| 头程订单与仓库 | 通过(静态) | 下单、草稿、提交、列表、详情、启用仓库选择、历史仓库快照及后端必填字段已完成;取消不纳入本轮一期客户端验收 |
| 静态页面完整性与响应式 | 通过(100%) | 一期客户端导航和路由均落到正式页面,无在用占位页;桌面、720px 和 390px 布局、移动导航、44px 主要操作及无横向溢出已验证 |
| 自动化测试 | 通过 | `apps/client-web` 执行 `pnpm test`,94 项通过,覆盖认证、个人资料、成员、角色权限、超大整数响应、业务总览真实加载、往来单位历史快照、仓库详情、头程订单完整详情编辑、幂等、快递字段、失败恢复、未保存保护、响应式和关键源码契约 |
| TypeScript 类型检查 | 通过 | `apps/client-web` 执行 `pnpm typecheck` |
| 生产构建 | 通过 | `apps/client-web` 执行 `pnpm build`;产物扫描确认不包含商户联调账号和开发提示 |
| 登录注册 | 代码接入(待环境验收) | 已移除模拟登录成功,接入真实注册、登录、退出、Token 轮换、统一错误与 MERCHANT 身份校验 |
| 下单资格 | 通过(静态) | 企业资料待审核时可创建、编辑和保存草稿,提交时阻断并引导至企业资料;审核通过状态允许提交 |
| 客户端权限效果 | 通过(静态) | 按演示账号过滤导航,拒绝无权限路由并显示 403,恢复刷新后的权限身份,控制主要业务按钮和首个可访问页面 |
| 往来单位与订单快照 | 通过(静态) | 仅选择启用的发货/收货单位,可调整本单联系人和地址;保存后形成独立快照,不反向修改主数据 |
| 快递信息 | 通过(静态) | 订单详情只读展示平台维护的快递公司和单号,并提供未维护空态;客户侧无写入口 |
| 服务端与数据安全 | 联调中 | 认证、权限资源、企业资料、营业执照、个人资料、成员完整维护、角色权限保存、仓库详情、往来单位、头程订单及快递只读字段已接真实 API;角色编辑/启停/删除路径仍待 Gateway OpenAPI 明确,客户端不猜测接口 |
> 客户端静态页面阶段通过不等于一期业务完成。菜单隐藏和页面只读只用于前端体验,服务端仍必须按当前用户、`tenant_id` 和权限编码执行授权校验。
### Client 认证与企业资料联调记录(2026-07-29)
- 已按后端联调文档统一使用 Gateway `/api` 前缀,开发代理目标更新为 `http://127.0.0.1:9000`。
- 已实现字符串业务码响应解包、422 字段错误载体、401 并发单次刷新、Refresh Token 轮换、退出清理,以及 403 不误跳登录。
- 客户端只接受 `app_code=MERCHANT`,权限由 JWT 权限与 IAM 启用资源取交集后显式映射到现有页面权限。
- 企业资料状态已统一为 `PROFILE_REQUIRED / PENDING / ACTIVE / REJECTED / DISABLED`;营业执照按后端限制只接受单张 JPG/PNG、最大 5MB。
- 本机 `http://127.0.0.1:9000` 当前拒绝连接,真实登录与上传尚未执行;订单、仓库、往来单位接口文档缺少精确请求响应结构,本轮未猜测接入。
### Client IAM 角色与成员联调记录(2026-07-29)
- 已接入 `GET/POST /api/merchant/v1/iam/roles` 和 `GET /api/merchant/v1/iam/members`;新增角色使用后端返回的真实角色记录。
- 已封装角色权限与成员角色的全量替换接口,请求分别使用 `roleId + permissionIds`、`membershipId + roleIds`,未误用 `userId`。
- 成员列表接口目前只返回 membershipId、userId、memberType、status、joinedAt,页面不会伪造姓名、联系方式或角色关系。
- 联调文档没有返回角色当前 permissionIds 和成员当前 roleIds 的查询契约。由于写接口是全量替换,页面暂禁用授权保存,避免覆盖已有权限。
- 角色编辑、删除、启停、成员新增、资料编辑、启停和密码重置接口未提供;API 模式下相关静态写操作不再开放,也不会在请求失败后回退模拟成功。
### Client 联调环境复测记录(2026-07-30)
- 客户端开发服务 `http://localhost:5176` 返回 200,静态资源和前端运行环境正常。
- 本机 `127.0.0.1:9000/9100/9201/9206` 均未监听;局域网环境 `192.168.110.149` 的 Gateway、Auth、IAM、File 健康端点曾同时返回 `UP`。
- 通过 Gateway 请求未登录的商户权限接口返回预期 401,证明 Gateway 可达且鉴权入口生效;使用文档初始化商户账号调用 `/api/auth/login` 返回 503,无法取得 Token,后续企业资料、权限、角色和成员真实读取未能执行。
- Auth 直连登录同样出现 503,随后端口短暂拒绝连接,表现为服务发现或服务重启不稳定;前端仍坚持只调用 Gateway,未把直连服务地址写入配置。
- 后端恢复后优先复测:登录、Token 刷新与退出、企业状态、资质列表、权限资源、角色列表和成员列表;授权全量替换继续保持禁用,直到补齐当前 `permissionIds` 与 `roleIds` 回显。
### Client 头程订单新接口接入记录(2026-07-30)
- 新版联调文档和 Gateway Swagger 已确认可用;实测商户登录、企业资料、资质、权限、角色、成员、仓库分页、订单分页与订单详情均返回 `code=0`,订单详情不返回平台内部备注。
- 已接企业资料 `PUT`、资质图片鉴权下载、仓库分页、订单分页/详情、创建/编辑草稿和提交;`SUBMITTED/PROCESSING`、`transportMode/containerMode` 均通过适配层映射,未把 FCL/LCL 错当运输方式。
- 页面新增渠道、提货方式、报关模式、目的仓类型、包装类型等后端必填字段;订单和仓库请求失败不回退模拟数据,仓库为空时禁用创建。
- Swagger 虽存在订单取消接口,但新版任务文档明确取消不纳入本轮本地一期验收,因此客户端不暴露取消写操作。
- 远端环境在浏览器验收阶段再次无响应;`http://127.0.0.1:5190` 登录页正常加载并发起真实登录,但 Gateway 健康检查与登录请求均超时,未进入工作台。
- 代码已再次通过 81 项自动化测试和 TypeScript 检查;本轮生产构建与浏览器验收结果见交付记录,真实草稿写入、资质预览与空仓禁建仍需在稳定 Gateway 和有效仓库数据下继续验收。
### 最新本地联调契约收口(2026-07-30)
- 最新文档再次确认浏览器只通过 Gateway `9000` 和同源 `/api` 访问,禁止直连 `9100/9201/9204/9205/9206` 等服务端口;后端文档记录 8 个应用健康检查均为 `UP`,平台账号也已由人工登录进入 Admin。
- 本轮真实接入范围为商户企业资料/资质、商户仓库和头程订单 CRUD/提交,以及平台待审列表、指定租户资质、鉴权文件查看和审核动作。平台订单审核、受理、履约、完成、取消及 Admin 快递公司/往来单位业务接口未纳入最新本地联调契约,前端继续保留静态实现,不猜测接口。
- 后端任务文档写有“`DRAFT`、`REJECTED` 可编辑”,与已确认的一期订单七状态规则冲突。产品确认优先:订单不恢复“已驳回”状态;兼容旧接口返回的订单 `REJECTED` 时映射为待审核且不开放客户编辑。企业资料和资质审核的 `REJECTED` 仍正常保留,这是独立业务状态。
- 平台企业审核详情改为按租户查询资质,文件通过平台 Token 请求二进制后临时预览,不再直接打开商户文件 URL;覆盖加载、空数据、失败重试和对象地址释放。
- 商户订单创建与提交请求增加 `Idempotency-Key`,并将 `merchant:order:create/edit/submit` 映射为三个独立前端操作权限;商户详情仍不读取或渲染 `internalNotes`。
- 交付门禁通过:Admin `21` 个测试文件、`147` 项测试,Client `81` 项测试;两端 `pnpm typecheck` 与 `pnpm build` 均通过。已使用真实平台账号进入 `http://localhost:5173/customers/reviews`,待审列表真实返回空数据且无控制台错误;桌面和 390px 窄屏无页面级横向溢出。由于当前无待审记录,资质抽屉的真实文件预览仍待准备样本后人工验收。
### Admin 前端质量收口(2026-07-30)
- 主数据导入架构明确为后端解析 `.xlsx`:前端仅检查扩展名和 10MB 大小限制,不读取工作表内容;已移除按文件名制造校验结果、模拟整批成功和虚假的错误报告。后端接口接入后,前端展示服务端返回的上传进度、逐行错误和报告地址。
- API 错误适配层保留业务错误码、`requestId`、`traceId`;登录、企业审核、角色与权限主要失败态展示可复制追踪标识,接口失败仍不回退模拟成功。
- 浏览器逐页检查 Admin 9 个主要业务路由;桌面与 390px 窄屏均无页面级横向溢出,修复了头程订单日期范围控件将窄屏筛选网格撑宽的问题,控制台无业务错误。
- 两端登录页完成生产环境隔离:本地开发继续提供联调账号预填/提示,生产表单默认留空。Admin 21 个测试文件、148 项测试和 Client 82 项测试均通过,两端 `pnpm typecheck`、`pnpm build` 均通过;扫描生产 `dist` 对平台/商户联调账号、联调密码及开发提示零匹配。
- 已读取后端 `phase1-business-maturity-regression-report.md` 并从 Gateway OpenAPI/真实只读请求复核契约:商户个人资料、成员资料及 roleIds、角色 permissionIds、往来单位候选均真实返回。Client 已移除个人资料和成员静态成功路径,接入修改密码、成员新增/编辑/启停/重置密码、角色权限全量保存与 SENDER/RECEIVER 候选;82 项测试、类型检查和生产构建通过,生产联调账号扫描零匹配。角色编辑/启停/删除路径未进入 OpenAPI,Admin 订单查询又要求显式 target tenantId,前端暂不猜测这两部分契约。
### Client 成熟度报告接入与联调加固(2026-07-30)
- 后端成熟度报告确认个人资料、修改密码、成员完整维护、角色权限回显与保存、仓库详情、往来单位候选、订单草稿/提交及平台快递只读信息已通过回归;客户端相应真实接口均已接入,不回退模拟成功。
- 订单列表与详情已区分真实空数据和加载失败,均提供原位重试;编辑抽屉、路由离开和浏览器刷新/关闭均增加未保存草稿保护。
- 选择收货仓时加载真实详情并回显地址、截单时间;历史订单优先展示仓库地址快照。订单详情映射快递公司、快递单号、收货仓地址和收货时间。
- 往来单位按 `SENDER -> SHIPPER`、`RECEIVER -> CONSIGNEE` 查询启用候选;当前测试数据清理后候选为空,客户端保持真实空态,不恢复演示单位。
- 角色 `permissionIds` 已映射为客户端权限码,权限保存执行真实全量替换;角色编辑、启停、删除虽已通过后端回归,但路径未进入当前 Gateway OpenAPI,页面继续隐藏对应写入口。
- 代码门禁通过:Client 89 项自动化测试、`pnpm typecheck` 和 `pnpm build` 均成功。最新复测时 `192.168.110.149:9000` 再次拒绝连接,完整浏览器联调待 Gateway 恢复后继续。
### Client Gateway 重启后浏览器联调(2026-07-30)
- Gateway 重启后健康检查返回 `UP`,商户登录业务码为 `0`;浏览器真实登录成功,工作台、我的信息、企业成员和角色管理页均正常读取,控制台无错误。
- 个人资料真实回显用户名、姓名、电话、邮箱和企业;成员接口真实返回 5 条记录,其中 1 条启用、4 条历史回归数据已停用,角色和联系方式均正常展示。
- 浏览器发现角色页直接打开时未加载成员关系,导致成员数显示为 0;现已改为在拥有成员列表权限时并行加载角色与成员,统计按真实 `roleIds` 计算。
- 服务随后再次超时,菜单权限页初始化期间原实现会因会话上下文失败清空 Token 并跳回登录。现已调整为临时网络故障或刷新超时保留有效会话,只有明确 401/`A0401` 才清理会话。
- 修复后 89 项自动化测试、TypeScript 检查和生产构建通过;Gateway 当前健康检查仍超时,菜单权限、企业资料、仓库和头程订单浏览器验收待服务恢复后继续。
### Client 后端修改后重新联调(2026-07-30)
- Gateway 健康检查恢复为 `UP`,商户账号可重新登录;浏览器完成业务总览、头程订单列表、订单详情、创建表单、仓库详情和草稿编辑的真实只读验收。
- 修复业务总览仍展示演示初始值的问题:首页现在按权限加载真实企业资料、成员和订单,显示企业已审核、1 名启用成员和 1 条真实草稿;接口失败时提供页面内重试。
- 修复头程订单页未加载企业资料导致错误显示“待审核”的问题;页面现在真实显示企业已审核,可提交资格与服务端状态一致。
- 修复真实草稿点击编辑时对 Vue 响应式对象调用 `structuredClone` 导致的 `DataCloneError`;保存与提交前也统一复制为纯草稿数据。
- 修复从列表直接编辑只使用简要记录、造成货物和收发货快照为空的问题;编辑前先读取完整详情,并为已不在候选中的往来单位展示“历史快照”名称。
- 真实仓库详情已回显“深圳头程收货仓”、广东省深圳市地址和每日 16:00 截单时间。现有旧草稿仍引用已失效仓库 `2001`,客户端会清空选择并提示重新选择;该样本关联需后端清理。
- 当前订单详情的快递公司和快递单号均为空,客户端正确显示只读空态;需后端准备含快递信息的样本后复测有值场景。
- 当前订单的部分中文字段由接口直接返回 `????`,涉及仓库名称、发货方、品名、渠道名称和备注;英文、数字、货物数量重量及收货方地址正常。该问题应由后端检查初始化脚本、数据库字符集或写入编码。
- 过程中 Gateway 曾短暂返回 502/401,页面失败态、重试和会话清理行为符合预期;重新登录后恢复。最终代码门禁通过:Client 94 项自动化测试、`pnpm typecheck` 和 `pnpm build` 均成功。
Binary file not shown.
+16
View File
@@ -0,0 +1,16 @@
# 三期文档
三期以头程物流“03 出仓”为范围,完成从排柜、订舱、拖车和仓内拣货,到封柜、报关、国内出仓并等待装船的业务闭环。
## 文档
- [三期开发规划:国内出仓](三期开发规划.md)
- [三期开发进度表](三期开发进度表.md):参考一期采用“大任务 → 详细任务 → 里程碑 → 未确认事项 → 开工准入 → 验收”结构维护。
- [虫虫出海三期开发进度表(Excel)](虫虫出海三期开发进度表.xlsx):可筛选、可下拉维护并使用公式汇总的项目进度工作簿。
- [三期需求确认表](三期需求确认表.md):集中跟踪 P3-Q1~P3-Q13、负责人和最终结论。
- [三期页面与原型清单](三期页面与原型清单.md):按管理后台、企业客户端和 PDA 维护页面范围及原型状态。
- `参考资料/出仓流程.png`:本期 9 个核心动作及业务终点的原始参考图。
## 当前状态
三期处于范围规划阶段,当前开工结论为 `NO-GO`。当前以原型图片中的“完成国内出仓,等待装船”为阶段终点。动作依赖与并行关系、排柜规则、PDA 设备、拖车协作、柜封照片清单、报关回执及放行门禁、港务/船期信息范围和来源仍需在启动前确认。产品和业务完成确认后先进入契约与任务准备,六项开工准入全部通过后再进入全面功能开发。
+310
View File
@@ -0,0 +1,310 @@
# 虫虫出海三期开发规划:国内出仓
> 创建日期:2026-07-28
> 状态:范围规划中
> 流程依据:[出仓流程](参考资料/出仓流程.png)中的头程物流“03 出仓”;当前以图片中的“完成国内出仓,等待装船”为阶段终点
> 前置阶段:[一期文档](../一期/README.md) · [二期文档](../二期/README.md)
## 一、三期目标
三期承接二期“货物完成到仓、实测、差异处理和指定库位”的结果,将已受理货物转化为可执行、可追踪、可审计的国内出仓任务。
流程覆盖九个动作:
```text
智能排柜 → 订舱 → 预约拖车
↓
PDA 拣货 → 装柜并上传货柜/铅封照片
↓
客户上传报关资料 → 平台整理提交报关代理 → 上传回执供客户下载
↓
查询或维护预计开船、到港等港务/船期信息
↓
完成国内出仓,等待装船
```
三期完成标准不是“订单改成已出仓”,而是每个柜次能够回答:装了哪些订单和货物、依据什么方案排柜、订了什么舱、谁承运拖车、仓库实际拣装了什么、柜号与铅封是什么、报关资料和回执在哪里、实际何时出仓、当前等待哪一班船。
### 原型动作覆盖
| 原型动作 | 规划模块 | 开发任务 | 覆盖结果 |
|---|---|---|---|
| 1. 智能排柜 | 智能排柜 | P3-PLAN-01、P3-PLAN-02 | 已覆盖,规则待 P3-Q1 确认 |
| 2. 订舱 | 订舱与港务/船期信息 | P3-BOOK-01 | 已覆盖 |
| 3. 预约托车 | 预约拖车 | P3-TRUCK-01 | 已覆盖,用词待 P3-Q3 确认 |
| 4. PDA 拣货 | PDA 拣货与复核 | P3-PICK-01、P3-PDA-01、P3-PDA-02 | 已覆盖 |
| 5. 拍货柜及铅封照上传 | 装柜及柜封证据 | P3-LOAD-01、P3-LOAD-02 | 已补充现场拍照上传任务,清单待 P3-Q6 确认 |
| 6. 客户上传报关资料 | 报关资料协同 | P3-CUS-01 | 已覆盖 |
| 7. 整理给到报关代理报关 | 报关资料协同 | P3-CLEAR-01 | 已覆盖 |
| 8. 回执上传到系统供客户下载 | 报关回执与客户下载 | P3-CLEAR-01、P3-CUS-02 | 已覆盖,回执类型待 P3-Q12 确认 |
| 9. 查港务信息(预计开船、到港等) | 订舱与港务/船期信息 | P3-PORT-01、P3-CUS-02 | 已补充独立查询任务,范围和数据源待 P3-Q9 确认 |
## 二、阶段边界
### 纳入三期
- 出仓单、柜次、智能排柜建议、人工调整和版本确认。
- 订舱申请、舱位结果,以及原型所述预计开船、到港等港务/船期信息;具体字段和数据源待确认。
- 预约拖车、车辆司机、到仓装柜和离仓记录。
- PDA 任务领取、扫码拣货、部分拣货、异常、复核和幂等提交。
- 装柜、柜号、铅封号及货柜/铅封照片证据。
- 客户上传报关资料、平台审核补件、整理并提交报关代理。
- 报关回执上传、版本、客户可见范围和授权下载。
- 国内出仓门禁、确认、冲正、时间线和站内通知。
### 明确不纳入三期
- 入库、上架、实时库存、移库、盘点、冻结、补货和完整 WMS。
- 港区闸口、车队、船公司、海关单一窗口的正式自动化接口。
- GPS 实时跟车、电子铅封、称重设备、摄像头 OCR 和 IoT 自动采集。
- 自动报关、关税计算、退税、完整清关业务和自动单证识别。
- 海上运输、海外清关、目的港提柜、海外仓入库和末端派送。
- 无人工确认的全自动排柜、自动订舱和不可逆业务操作。
## 三、参与角色与数据边界
| 角色 | 数据范围 | 主要操作 |
|---|---|---|
| 企业订单操作员 | 当前企业订单与出仓单 | 上传/补充报关资料、查看出仓和港务/船期进度、下载回执 |
| 平台运营 | 授权客户、订单和出仓单 | 创建出仓单、确认排柜、订舱、维护港务/船期信息和协调异常 |
| 仓库主管 | 被授权的平台收货仓 | 分配拣货任务、复核差异、确认装柜和出仓条件 |
| 仓库作业员 | 分配给本人或本仓的任务 | PDA 扫码、数量确认、异常上报和任务提交 |
| 拖车调度员 | 授权出仓单与车辆范围 | 预约车辆、维护司机/车牌、到仓和离仓节点 |
| 报关专员 | 授权客户、订单和报关批次 | 审核、退回、整理、提交资料并上传回执 |
| 平台管理员 | 平台授权范围 | 维护权限、规则、服务商和高风险冲正 |
服务端必须依据登录身份确定企业、仓库、客户和数据范围。前端传入的订单 ID、出仓单 ID、仓库 ID、客户 ID、作业员 ID 或报关批次 ID 不能单独作为授权依据。
## 四、流程与状态设计
原型以 1 至 9 编号表达动作顺序,但尚未说明哪些是强制前置、哪些允许并行。当前按原型顺序规划业务节点,同时使用主状态和子流程状态保留后续调整空间;在 P3-Q11 确认前,不把并行关系写成固定业务规则。
### 1. 出仓单主状态
| 稳定编码 | 中文名称 | 含义 |
|---|---|---|
| `DRAFT` | 草稿 | 出仓单已创建,排柜方案尚未确认 |
| `PLANNING` | 排柜中 | 系统生成建议或人工调整方案 |
| `PREPARING` | 出仓准备中 | 按已确认的依赖关系处理订舱、拖车、拣货和报关 |
| `READY_TO_DISPATCH` | 待出仓 | 必要子流程和证据已满足 |
| `DISPATCHED` | 已出仓 | 货柜已离开国内仓库 |
| `WAITING_FOR_LOADING` | 等待装船 | 国内出仓已完成,等待对应船班装船 |
| `CANCELLED` | 已取消 | 按规则取消并保留原因与历史 |
| `BLOCKED` | 已阻塞 | 存在必须人工解除的关键异常 |
### 2. 子流程状态
- 订舱:待订舱、订舱中、已订舱、订舱失败、已取消。
- 拖车:待预约、已预约、车辆已到仓、装柜中、已离仓、改约、已取消。
- 拣货:待分配、待拣货、拣货中、待复核、已完成、异常挂起、已取消。
- 报关:资料待上传、资料待审核、待补件、待提交、已提交代理、回执待上传、回执已上传、已退回、已取消。海关放行如需管理,作为待确认的回执类型或后续状态,不默认作为三期出仓门禁。
- 港务/船期:待维护、已确认、信息变更、已开船;“已开船”属于三期之后的运输阶段,不是当前三期完成条件。
主状态由服务端根据已确认的必要子流程推进。客户端不得通过传任意目标状态跳过拣货复核、柜封证据或已配置的出仓门禁。
## 五、业务模块
### 1. 出仓单
- 从已上架且允许出仓的头程订单创建一张或多张出仓单。
- 支持一张订单分多个柜次,也支持多个订单拼柜,具体开放范围由产品确认。
- 保存订单、客户、仓库、货物、往来单位和计量数据快照。
- 维护计划出仓时间、优先级、负责人、异常和整体时间线。
- 重复创建和确认使用幂等键,并通过版本字段防止并发覆盖。
### 2. 智能排柜
- 原型仅明确“智能排柜”,具体硬约束、软偏好、输入数据和输出结果以 P3-Q1 的业务规则表为准。
- 柜型容量、重量、体积、箱数、同票/混票、不可混装、装载顺序和利用率暂列为候选能力,不作为未经确认的固定验收项。
- 当前建议首版采用“系统建议、人工确认”;是否允许调整、是否强制填写原因由排柜规则确认。
- 建议排柜方案保留版本、规则版本、输入快照、操作者和前后差异,以支持追溯。
- 未确认方案不得生成正式拣货任务或执行不可逆状态变化。
### 3. 订舱与港务/船期信息
- 原型明确需要查询预计开船、到港等港务信息;“港务信息”的准确业务含义、字段范围和归属以 P3-Q9 为准。
- 船公司/订舱代理、航线、订舱号、船名航次、起运港、目的港、截单、截关、截港、ETD 和 ETA 暂列为候选字段。
- 订舱失败、改船、甩柜、船期延误和取消暂列为候选异常场景,按业务确认结果实现。
- 港务信息查询与人工维护分开设计:三期至少支持查询系统内已维护的信息,并记录查询条件、数据来源和更新时间;如后续确认可靠外部数据源,再通过预留适配边界接入,不在三期默认承诺正式接口。
- 客户查看预计开船、到港等信息时展示数据来源和最近更新时间,避免把过期结果显示为实时信息。
- 当前建议首版由平台人工维护,外部港务或船期接口只预留适配边界。
- 建议关键信息变更保留旧值、新值、原因,并向授权用户发送站内提醒。
### 4. 预约拖车
- 从往来单位选择已启用拖车服务商,维护车牌、司机、电话和预约时间窗。
- 记录提柜、到仓、装柜、离仓、还柜节点和交接说明。
- 支持改约、取消、迟到、未到、车辆更换和柜号变更。
- 司机手机号等个人信息按最小权限展示,不进入不必要的客户响应。
- 图片原文“预约托车”建议统一为行业常用的“预约拖车”,需产品最终确认。
### 5. PDA 拣货与复核
- 按已确认排柜方案生成仓库、柜次、订单和货物明细级任务。
- PDA 支持登录、领取、箱号选择、扫码、数量确认、部分拣货、跳拣、撤销和异常上报。
- 服务端校验任务归属、状态、重复扫码、超量、错货和重复提交。
- 拣货完成后必须复核;差异未处理前不得进入待出仓。
- 扫描器、声音、震动、网络和缓存通过设备适配层实现,不直接绑定单一厂商 SDK。
### 6. 装柜及柜封证据
- 维护柜号、铅封号、装柜开始/完成时间、操作人和异常说明。
- 支持按可配置清单上传货柜及铅封照片;柜体、开门空柜、装柜过程、满柜、关门和铅封照仅为候选类型,必传类型、角度和数量以 P3-Q6 为准。
- 现场执行端支持直接拍照或从相册选择后上传,上传前显示柜次、柜号、铅封号和照片类型,防止证据关联错误。
- 支持上传进度、失败重试、重复照片提示和提交前预览;弱网下不得因重复点击产生多份有效证据。
- 照片记录文件哈希、上传人、上传时间、关联柜次和客户可见范围。
- 铅封号重复、已确认的必传照片缺失或复核未通过时禁止确认出仓。
- 修改柜号或铅封号需要二次确认、原因和完整审计。
### 7. 报关资料协同
- 客户按资料清单上传发票、箱单、合同、委托书和申报要素等文件。
- 资料清单可按国家/地区、渠道和货物类型配置,并保存订单使用的清单版本。
- 平台报关专员审核完整性,可退回具体文件并记录补件原因和时限。
- 提交报关代理时生成提交批次,冻结该批次关联的文件版本。
- 首版采用系统内整理加线下提交,不直接对接海关单一窗口。
### 8. 报关回执与客户下载
- 上传代理回传的报关回执并关联提交批次;回执类型可区分接收、申报、退单、查验或放行等,实际类型及出仓门禁以 P3-Q12 为准。
- 替换文件产生新版本,不覆盖原记录;退单或补件形成明确任务。
- 客户只能下载本企业且标记为客户可见的回执。
- 文件下载使用授权地址并记录审计,不暴露内部存储路径。
### 9. 国内出仓确认
- 服务端校验排柜已确认、拣货复核完成、已确认的柜封证据齐全、车辆已离仓,以及已配置的报关回执和港务/船期信息门禁。
- 当前不默认要求“海关已放行”后才能确认国内出仓;是否需要放行或其他特定回执作为门禁,以 P3-Q12 的业务结论为准。
- 授权人员执行确认出仓,记录实际出仓时间、车辆、柜号、铅封号和说明。
- 确认操作具备幂等、权限、版本和并发校验,重复请求只产生一次有效事件。
- 完成后进入“等待装船”,企业客户可查看允许公开的节点、港务/船期信息和回执。
- 出仓后发现错误不得直接回退数据库状态,必须使用授权冲正动作和反向审计事件。
## 六、核心数据对象
| 数据对象 | 关键内容 |
|---|---|
| 出仓单 | 订单、企业、仓库、计划/实际时间、主状态、负责人和版本 |
| 排柜方案 | 经确认的规则版本、输入快照、柜次、货物分配、规则计算结果和确认记录 |
| 柜次 | 柜型、柜号、铅封号、装载顺序、重量、体积和关联订单 |
| 订舱记录 | 船公司/代理、订舱号、船名航次、港口、舱位和异常 |
| 港务/船期节点 | 经确认的信息字段、变更历史和来源;候选字段包括截单、截关、截港、ETD 和 ETA |
| 拖车预约 | 服务商、车辆、司机、预约时间窗、到离仓节点和异常 |
| 拣货任务 | 柜次、货物明细、分配人、作业员、扫描、数量、差异和复核 |
| 装柜证据 | 柜号、铅封号、照片类型、文件、上传人、时间和可见范围 |
| 报关批次 | 资料清单版本、文件版本、补件、提交代理和回执 |
| 出仓时间线 | 事件、前后状态、操作者、发生时间、说明和请求关联 ID |
基础资料引用必须同时保存稳定 ID 和必要快照。订单、客户、仓库、往来单位、港务/船期信息或文件后续修改时,不得改变历史出仓记录。
## 七、应用范围
### 管理后台 `apps/admin-web`
- 出仓单列表、筛选、详情、异常和状态操作。
- 按已确认的排柜规则提供建议、冲突展示、人工调整、版本对比和确认。
- 订舱、船期、拖车预约、装柜、柜号、铅封和照片。
- 报关资料审核、补件、提交批次、回执上传和客户可见控制。
- 权限至少拆分为查看、排柜、订舱、拖车、仓内复核、报关、确认出仓和冲正。
### 企业客户端 `apps/client-web`
- 查看本企业出仓单、柜次和允许公开的进度时间线。
- 上传、补充和查看报关资料及处理结果。
- 下载客户可见的报关回执。
- 查看预计开船、到港和船期变更,不暴露内部成本、供应商敏感信息和内部备注。
### PDA `apps/pda-uniapp`
- 仓库人员登录、任务领取、箱号选择、扫码拣货和数量确认。
- 部分拣货、跳拣、撤销、异常上报、待复核和完成提交。
- 按授权柜次拍摄或选择货柜、铅封照片,确认关联信息后上传,并处理进度、失败重试和重复提交。
- 弱网、断网、前后台切换、重复提交和换班清理。
- 真实设备、扫描头和发布方式确认前,不绑定厂商 SDK。
### 服务端 `services/api`
- 最终保证身份、租户、仓库数据范围、权限、状态、门禁、幂等、并发和审计。
- 管理出仓、排柜、订舱、拖车、拣货、装柜、报关和时间线的事务边界。
- 文件上传校验类型、大小、内容和权限;使用不可预测标识和授权下载。
- 为船期、车队、海关等外部接口预留适配层,但三期不承诺正式对接。
## 八、建议里程碑
| 里程碑 | 交付目标 | 进入条件 |
|---|---|---|
| P3-M0 规则与设备锁定 | 流程、角色、状态、排柜规则、PDA、文件和验收确认 | 二期到仓契约稳定 |
| P3-M1 出仓单与排柜 | 出仓单可创建,并按确认范围提供排柜建议、人工确认与追溯 | 排柜规则确认 |
| P3-M2 订舱与拖车 | 订舱、船期、拖车预约和异常处理可用 | 服务商与权限可用 |
| P3-M3 PDA 拣货 | 任务生成、扫码、差异、复核和幂等提交可用 | 目标设备与仓库流程确认 |
| P3-M4 装柜封柜 | 柜号、铅封和照片证据完整,门禁生效 | 文件能力与必传清单确认 |
| P3-M5 报关协同 | 客户上传、平台审核、补件、提交和回执下载闭环 | 报关资料清单确认 |
| P3-M6 国内出仓 | 必要子流程满足后可确认出仓并进入等待装船 | 出仓门禁和冲正规则确认 |
| P3-M7 联调验收 | 跨端、设备、安全、弱网、性能、迁移和发布完成 | P3-M1 至 P3-M6 验收通过 |
## 九、建议开发任务
| ID | 端 | 模块 | 交付内容 | 优先级 | 依赖 |
|---|---|---|---|---|---|
| P3-BASE-01 | 公共 | 出仓契约 | 主/子状态、权限、事件、错误码、门禁和快照模型 | P0 | 二期到仓契约 |
| P3-BASE-02 | 公共 | 数据模型 | 出仓单、排柜、柜次、订舱、拖车、拣货、装柜、报关和时间线 | P0 | P3-BASE-01 |
| P3-DSP-01 | 后台 | 出仓单 | 列表、筛选、创建、详情、异常和取消 | P0 | P3-BASE-02 |
| P3-PLAN-01 | 公共 | 排柜规则 | 按已确认规则实现输入、约束、偏好和冲突判断 | P0 | P3-BASE-02、P3-Q1 |
| P3-PLAN-02 | 后台 | 智能排柜 | 按已确认范围实现建议、人工确认和追溯 | P0 | P3-PLAN-01 |
| P3-BOOK-01 | 后台 | 订舱 | 订舱和经确认的船名航次、港口、变更及异常 | P0 | P3-DSP-01、P3-Q9 |
| P3-PORT-01 | 后台/服务端 | 港务信息查询 | 查询系统内预计开船、到港等信息,记录来源和更新时间,并提供授权人工维护兜底 | P0 | P3-BOOK-01、P3-Q9 |
| P3-TRUCK-01 | 后台 | 拖车预约 | 服务商、车辆司机、时间窗、到离仓和异常 | P0 | P3-DSP-01 |
| P3-PICK-01 | 公共 | 拣货任务 | 按排柜方案生成任务、明细、差异和复核规则 | P0 | P3-PLAN-02 |
| P3-PDA-01 | PDA | 扫码拣货 | 领取、扫描、部分拣货、跳拣、撤销和异常 | P0 | P3-PICK-01 |
| P3-PDA-02 | PDA/服务端 | 弱网幂等 | 重试、去重、冲突、换班和设备缓存清理 | P0 | P3-PDA-01 |
| P3-LOAD-01 | 后台 | 装柜封柜 | 柜号、铅封号、装柜节点、可配置照片清单和复核 | P0 | P3-PICK-01、P3-Q6 |
| P3-LOAD-02 | PDA/服务端 | 柜封拍照上传 | 现场拍照/相册选择、关联确认、进度、预览、失败重试、去重和提交 | P0 | P3-LOAD-01、文件能力可用 |
| P3-CUS-01 | 客户端 | 报关资料 | 清单、上传、补件、版本和进度查看 | P0 | 文件能力可用 |
| P3-CLEAR-01 | 后台 | 报关协同 | 审核、退回、提交代理、批次、回执和可见范围 | P0 | P3-CUS-01 |
| P3-CUS-02 | 客户端 | 回执及港务信息 | 回执下载、预计开船/到港信息、数据来源、更新时间和变更时间线 | P0 | P3-PORT-01、P3-CLEAR-01 |
| P3-GATE-01 | 服务端 | 出仓门禁 | 按确认规则校验必要子流程、确认出仓、等待装船和冲正 | P0 | P3-PORT-01、P3-TRUCK-01、P3-LOAD-01、P3-LOAD-02、P3-CLEAR-01、P3-Q11、P3-Q12 |
| P3-NOTIFY-01 | 公共 | 通知异常 | 站内通知、阻塞、责任人、时限和失败补偿 | P1 | 核心流程完成 |
| P3-TEST-01 | 公共 | 自动化测试 | 权限、排柜、幂等、并发、文件、门禁和状态测试 | P0 | 所有 P0 模块 |
| P3-DEVICE-01 | PDA | 真机验收 | 扫描头、弱网、性能、电量、前后台和发布验证 | P0 | 目标设备确认 |
| P3-REL-01 | 公共 | 发布验收 | 初始化规则、迁移、联调、演练、灰度和生产发布 | P0 | P3-TEST-01、P3-DEVICE-01 |
## 十、测试与验收重点
- 系统排柜符合已确认的业务规则;如启用建议解释、人工调整、版本或原因记录,对应结果可追溯。
- 多订单拼柜和一单多柜的件数、重量、体积能与二期实测版本对账。
- PDA 重复扫码、超量、错货、断网重试和重复提交不产生重复扣减或完成事件。
- 柜封照片能够在现场完成拍摄、关联确认和上传;失败重试或重复提交不产生多份有效证据。
- 未完成复核、缺少已确认的必传柜封证据或存在关键阻塞时不能确认出仓。
- 企业 A 无法查看、上传或下载企业 B 的报关资料和回执。
- 报关资料替换、退回、补件、提交和回执均能追溯到明确版本。
- 港务/船期查询结果展示来源和更新时间;变更保留历史并通知授权用户,旧信息不被无痕覆盖。
- 确认出仓的并发请求只产生一次状态变化和一次有效审计事件。
- 管理后台覆盖加载、空态、失败、无权限和数据冲突;PDA 覆盖弱网、前后台切换和硬件扫描。
- 当前三期验收基线为“完成国内出仓,等待装船”,不以海上运输或海外签收作为完成条件;最终以 P3-Q13 确认结果为准。
## 十一、启动前待确认事项
| 编号 | 待确认问题 | 当前建议 |
|---|---|---|
| P3-Q1 | 智能排柜需考虑哪些硬约束和软偏好? | 业务先提供可执行规则表;建议必须可解释且人工确认 |
| P3-Q2 | 是否存在拼柜、一单多柜和拆批出仓? | 数据模型全部支持,页面按实际高频场景逐步开放 |
| P3-Q3 | “预约托车”是否统一为“预约拖车”? | 建议统一使用行业常用的“拖车”,并确认业务实际含义 |
| P3-Q4 | 目标 PDA 型号、系统和扫描头接入方式是什么? | 设备确认前保持适配层,不绑定厂商 SDK |
| P3-Q5 | 拣货是否依赖实时库存和库位可用量? | 若需要,完整 WMS 前置范围会显著扩大,必须单独评估 |
| P3-Q6 | 柜封照片有哪些必传角度和留存期限? | 启动前由仓库、业务和合规共同锁定 |
| P3-Q7 | 报关资料清单是否按国家、渠道和货物类型变化? | 建议配置化,并保留下单使用的清单版本 |
| P3-Q8 | 报关代理如何接收资料和返回回执? | 首版系统内整理加线下提交,正式接口后续评估 |
| P3-Q9 | 原型中的“港务信息”具体指船期、港口作业状态还是两者,包含哪些字段,由谁维护? | 先确认业务词义和字段;首版建议平台人工维护并审计,外部接口仅预留适配层 |
| P3-Q10 | 确认出仓后发现错误如何处理? | 不直接回退状态,使用授权冲正动作和反向审计事件 |
| P3-Q11 | 九个动作中哪些是强制前置,哪些允许并行? | 确认前按原型编号顺序规划,不承诺固定并行关系 |
| P3-Q12 | “报关回执”具体包含哪些类型,海关放行是否为国内出仓硬门禁? | 当前不默认以放行为门禁;由业务明确接收、申报、退单、查验和放行的含义及门禁 |
| P3-Q13 | 三期终点以“等待装船”还是“装船发运”为准? | 当前以本期出仓图片的“完成国内出仓,等待装船”为基线,完整流程 SVG 的不同表述待产品确认 |
## 十二、建议实施顺序
1. 锁定待确认事项、业务规则、目标设备和协作责任人。
2. 统一出仓单、柜次、订舱、拖车、拣货、报关和港务/船期信息的稳定编码。
3. 先实现服务端权限、状态、门禁、幂等、并发和审计,再接各端页面。
4. 完成管理后台的出仓编排,再接 PDA 现场执行和客户资料协同。
5. 使用脱敏数据演练拼柜、一单多柜、错拣、补件、改船和冲正。
6. 在真实 PDA 和代表性仓库网络环境中完成联调、验收和灰度发布。
+152
View File
@@ -0,0 +1,152 @@
# 虫虫出海三期开发进度表
> 创建日期:2026-07-28
> 当前阶段:范围规划与大任务拆分
> 总体状态:待确认
> 规划依据:`三期开发规划.md`
> 维护方式:参考一期进度表,采用“总体大任务 → 详细任务 → 里程碑 → 未确认事项 → 开工准入 → 验收”的结构。
## 一、三期目标
三期承接二期到仓、实测、差异处理、处理单元编码和当前位置记录,完成智能排柜、订舱、拖车预约、PDA 拣货、柜封拍照、报关协同、港务信息查询和国内出仓闭环。
当前验收基线为“完成国内出仓,等待装船”。在 P3-Q13 最终确认前,不把装船发运纳入三期完成条件。
## 二、总体进度(大任务)
| 大任务 ID | 大任务 | 范围 | 状态 | 进度 | 计划开始 | 计划完成 | 负责人 | 关键依赖/备注 |
|---|---|---|---|---:|---|---|---|---|
| T01 | 需求确认与原型 | 锁定阶段边界、流程依赖、排柜规则、设备、文件、报关和港务信息;完成页面原型 | 进行中 | 40% | 2026-07-28 | 待排期 | 待分配 | 已盘点 `docs/悦` 三期相关原型;后台有较完整参考,客户端三期页面和 PDA 柜封拍照仍待补 |
| T02 | 公共契约与数据模型 | 主/子状态、权限、事件、错误码、出仓单、柜次、任务、证据、报关和审计模型 | 待确认 | 0% | 待排期 | 待排期 | 待分配 | 依赖 T01、二期输出契约 |
| T03 | 出仓单与柜次 | 出仓单创建、列表、详情、订单拆分/拼柜、柜次和时间线 | 待确认 | 0% | 待排期 | 待排期 | 待分配 | 依赖 T02、P3-Q2、P3-Q11 |
| T04 | 智能排柜 | 排柜规则、系统建议、冲突、人工确认和追溯 | 待确认 | 0% | 待排期 | 待排期 | 待分配 | 依赖 T03、P3-Q1、P3-Q2 |
| T05 | 订舱与港务信息 | 订舱、变更异常、预计开船/到港信息、来源和更新时间 | 待确认 | 0% | 待排期 | 待排期 | 待分配 | 依赖 T03、P3-Q9、P3-Q11 |
| T06 | 拖车预约 | 服务商、车辆司机、预约时间窗、到离仓节点和异常 | 待确认 | 0% | 待排期 | 待排期 | 待分配 | 依赖 T03、一期往来单位、P3-Q3 |
| T07 | PDA 拣货与复核 | 任务生成、领取、扫码、部分拣货、异常、弱网幂等和后台复核 | 待确认 | 0% | 待排期 | 待排期 | 待分配 | 依赖 T04、二期处理单元/位置、P3-Q4、P3-Q5 |
| T08 | 装柜及柜封证据 | 装柜节点、柜号、铅封号、PDA 拍照上传、照片清单和复核 | 待确认 | 0% | 待排期 | 待排期 | 待分配 | 依赖 T07、文件能力、P3-Q6 |
| T09 | 报关资料与回执 | 客户上传、后台审核补件、提交代理、回执上传和客户下载 | 待确认 | 0% | 待排期 | 待排期 | 待分配 | 依赖文件能力、P3-Q7、P3-Q8、P3-Q12 |
| T10 | 国内出仓门禁 | 必要条件校验、确认出仓、阻塞、通知、冲正和审计 | 待确认 | 0% | 待排期 | 待排期 | 待分配 | 依赖 T05~T09、P3-Q10~P3-Q13 |
| T11 | 客户端出仓协同 | 出仓进度、柜次、回执、预计开船/到港和变更时间线 | 待确认 | 0% | 待排期 | 待排期 | 待分配 | 依赖 T05、T09、T10;客户可见范围 |
| T12 | 联调测试与发布 | 跨端联调、自动化测试、PDA 真机、安全、弱网、迁移、验收和发布 | 待开始 | 0% | 待排期 | 待排期 | 待分配 | 依赖 T02~T11 完成 |
## 三、详细任务表
详细任务按大任务逐项拆分。当前先建立任务骨架;每次拆分一个大任务,确认交付内容、优先级和依赖后再进入开发。
| ID | 大任务 | 端 | 模块 | 功能/交付内容 | 优先级 | 状态 | 进度 | 依赖 | 负责人 | 备注 |
|---|---|---|---|---|---|---|---:|---|---|---|
| P3-T01-00 | T01 | 公共 | 需求确认与原型 | 完成需求确认表、页面清单和原型评审,锁定开工阻断项 | P0 | 进行中 | 40% | - | 待分配 | 已建立文档骨架并完成 `docs/悦` 原型初审,后续逐项确认和裁剪 |
| P3-T02-00 | T02 | 公共 | 公共契约与数据模型 | 待 T01 确认后拆分 | P0 | 待确认 | 0% | P3-T01-00 | 待分配 | 下一层将拆状态、权限、数据模型、接口和审计 |
| P3-T03-00 | T03 | 后台/服务端 | 出仓单与柜次 | 待 T02 契约明确后拆分 | P0 | 待确认 | 0% | P3-T02-00 | 待分配 | - |
| P3-T04-00 | T04 | 后台/服务端 | 智能排柜 | 待排柜规则确认后拆分 | P0 | 待确认 | 0% | P3-T03-00、P3-Q1 | 待分配 | - |
| P3-T05-00 | T05 | 后台/服务端 | 订舱与港务信息 | 待港务范围和字段确认后拆分 | P0 | 待确认 | 0% | P3-T03-00、P3-Q9 | 待分配 | - |
| P3-T06-00 | T06 | 后台/服务端 | 拖车预约 | 待流程依赖确认后拆分 | P0 | 待确认 | 0% | P3-T03-00、P3-Q11 | 待分配 | - |
| P3-T07-00 | T07 | PDA/后台/服务端 | PDA 拣货与复核 | 待拣货输入和设备确认后拆分 | P0 | 待确认 | 0% | P3-T04-00、P3-Q4、P3-Q5 | 待分配 | 已有静态 PDA 原型可复用 |
| P3-T08-00 | T08 | PDA/后台/服务端 | 装柜及柜封证据 | 待照片清单确认后拆分 | P0 | 待确认 | 0% | P3-T07-00、P3-Q6 | 待分配 | 需新增柜封拍照原型 |
| P3-T09-00 | T09 | 客户端/后台/服务端 | 报关资料与回执 | 待资料和回执规则确认后拆分 | P0 | 待确认 | 0% | P3-Q7、P3-Q8、P3-Q12 | 待分配 | - |
| P3-T10-00 | T10 | 后台/服务端 | 国内出仓门禁 | 待流程、回执和终点确认后拆分 | P0 | 待确认 | 0% | P3-T05-00~P3-T09-00 | 待分配 | 高风险状态操作 |
| P3-T11-00 | T11 | 客户端/服务端 | 客户端出仓协同 | 待客户可见字段确认后拆分 | P0 | 待确认 | 0% | P3-T05-00、P3-T09-00、P3-T10-00 | 待分配 | - |
| P3-T12-00 | T12 | 公共 | 联调测试与发布 | 待各模块拆分完成后细化测试与发布任务 | P0 | 待开始 | 0% | P3-T02-00~P3-T11-00 | 待分配 | - |
| P3-T01-PROTO-01 | T01 | 公共 | 原型盘点 | 盘点流程图、后台、企业客户端和 PDA 中与三期相关的页面及交互 | P0 | 已完成 | 100% | - | 待分配 | 2026-07-28 已完成首次盘点,来源为 `docs/悦` |
| P3-T03-PROTO-01 | T03 | 后台 | 出仓单原型核验 | 对照通用 WMS 出库单、货柜和装柜清单,确定三期出仓单可复用字段与需重做部分 | P0 | 进行中 | 50% | P3-T01-PROTO-01、P3-Q11 | 待分配 | 通用“出库”不能直接等同三期“国内出仓” |
| P3-T04-PROTO-01 | T04 | 后台 | 智能排柜原型核验 | 映射智能装柜的线路页签、筛选、智能锁定、订单分柜、拖放调整和集装箱详情 | P0 | 已完成 | 100% | P3-T01-PROTO-01 | 待分配 | 原型叫“智能装柜”,与“智能排柜”的业务边界待 P3-Q1 确认 |
| P3-T05-PROTO-01 | T05 | 后台 | 订舱港务原型核验 | 映射订舱列表、新增/详情、放舱、船名航次、港口和 ETD/ATD/ETA/ATA 字段 | P0 | 已完成 | 100% | P3-T01-PROTO-01 | 待分配 | 港务信息是否独立模块仍待 P3-Q9 确认 |
| P3-T06-PROTO-01 | T06 | 后台 | 拖车原型核验 | 映射拖车列表、新增表单、运输公司、司机、车头号、装箱日期、费用和附件 | P0 | 已完成 | 100% | P3-T01-PROTO-01 | 待分配 | 预约时间窗和到离仓节点仍需按三期流程补充 |
| P3-T07-PROTO-01 | T07 | 后台/PDA | 拣货原型核验 | 对照通用 WMS 出库拣货和现有 PDA 集装箱拣货,锁定可复用交互与真实接口缺口 | P0 | 已完成 | 100% | P3-T01-PROTO-01 | 待分配 | PDA 静态交互已有,任务、鉴权、持久化和弱网仍待开发 |
| P3-T08-PROTO-01 | T08 | 后台 | 装柜证据原型核验 | 映射货柜列表、预装清单、装柜清单、柜号、单证、船期和装柜明细 | P0 | 已完成 | 100% | P3-T01-PROTO-01 | 待分配 | 后台参考较完整,但现场拍照上传原型缺失 |
| P3-T08-PROTO-02 | T08 | PDA | 柜封拍照原型 | 新增柜次确认、货柜/铅封拍照、照片类型、预览、重试和提交页面原型 | P0 | 待开始 | 0% | P3-Q4、P3-Q6 | 待分配 | `docs/悦` 暂无对应原型 |
| P3-T09-PROTO-01 | T09 | 后台 | 报关单证原型核验 | 映射报关单列表、单证管理、报关/清关/放行资料及装柜清单附件 | P0 | 已完成 | 100% | P3-T01-PROTO-01 | 待分配 | 客户上传和回执下载页面仍需新建 |
| P3-T10-PROTO-01 | T10 | 后台 | 出仓门禁原型 | 设计统一门禁检查、阻塞原因、确认出仓和冲正页面 | P0 | 进行中 | 30% | P3-Q10~P3-Q13 | 待分配 | 现有原型只有分散状态和放行字段,无统一出仓门禁页 |
| P3-T11-PROTO-01 | T11 | 客户端 | 客户端三期原型 | 新增出仓进度、报关资料上传/补件、回执下载和港务/船期页面原型 | P0 | 待开始 | 0% | P3-Q7~P3-Q9、P3-Q12、P3-Q13 | 待分配 | 现有客户端原型未覆盖三期专用页面 |
## 四、原型对照与开发影响
| 大任务 | `docs/悦` 现有原型 | 覆盖判断 | 开发处理 |
|---|---|---|---|
| T03 出仓单与柜次 | 通用 WMS 出库单、货柜列表、装柜清单 | 部分参考 | 保留列表、状态和明细交互,按国内出仓业务重新定义对象和门禁 |
| T04 智能排柜 | 入库单列表中的“智能装柜”弹窗 | 已有参考 | 复用线路、筛选、锁定、分柜和拖放交互,按 P3-Q1 裁剪规则 |
| T05 订舱与港务信息 | 订舱列表、新增/详情、放舱、货柜船期字段 | 已有参考 | 复用订舱字段和页面结构,确认港务信息范围及来源后裁剪 |
| T06 拖车预约 | 拖车列表、新增拖车 | 已有参考 | 复用车辆司机和费用字段,补预约时间窗、到仓及离仓节点 |
| T07 PDA 拣货与复核 | 通用 WMS 出库、PDA 集装箱拣货 | 已有参考 | 保留 PDA 交互,重做真实任务、权限、幂等、弱网和后台复核 |
| T08 装柜及柜封证据 | 货柜列表、预装清单、装柜清单 | 部分参考 | 后台页面裁剪复用;PDA 货柜/铅封拍照页面新建 |
| T09 报关资料与回执 | 报关单列表、单证管理、装柜清单附件 | 部分参考 | 后台裁剪复用;客户上传、补件和回执下载页面新建 |
| T10 国内出仓门禁 | 分散的出库状态、国内报关成功和放行资料 | 部分参考 | 新建统一门禁检查和确认出仓页面,避免直接照搬“已放行”规则 |
| T11 客户端出仓协同 | 头程下单客户端只有船期字段和出仓时间展示 | 覆盖不足 | 新建三期客户端出仓进度、报关和港务信息页面 |
> `货代SaaS平台原型.html` 是全量平台参考原型,其中海外仓、完整 WMS、费用和清关能力不自动纳入三期;所有复用项仍需经过三期范围裁剪。
## 五、建议拆分模板
每个大任务按照同一顺序拆成可开发、可验收的小任务:
1. `RULE`:业务规则、状态、权限和异常。
2. `DATA`:数据表、字段、快照、版本和审计。
3. `API`:接口、鉴权、幂等、并发和错误码。
4. `ADM` / `CLI` / `PDA`:各端页面与交互。
5. `TEST`:单元、接口、端到端、权限和边界测试。
6. `ACCEPT`:业务验收、证据和遗留问题。
例如 T04 智能排柜后续可使用 `P3-T04-RULE-01`、`P3-T04-DATA-01`、`P3-T04-API-01`、`P3-T04-ADM-01`、`P3-T04-TEST-01` 编号。
## 六、建议里程碑
| 里程碑 | 交付目标 | 状态 | 计划完成 |
|---|---|---|---|
| P3-M0 需求与原型锁定 | 阶段边界、流程、规则、设备、文件和页面原型确认 | 待确认 | 待排期 |
| P3-M1 公共契约 | 二三期输入、状态、权限、数据模型和接口契约稳定 | 待排期 | 待排期 |
| P3-M2 出仓编排 | 出仓单、排柜、订舱、港务和拖车可用 | 待排期 | 待排期 |
| P3-M3 仓内出仓 | PDA 拣货、复核、装柜和柜封证据闭环 | 待排期 | 待排期 |
| P3-M4 报关协同 | 客户资料、后台审核、提交代理、回执上传下载闭环 | 待排期 | 待排期 |
| P3-M5 国内出仓 | 门禁满足后可确认出仓并进入当前约定终点 | 待排期 | 待排期 |
| P3-M6 三期上线 | 联调、真机、安全、测试、验收和生产发布完成 | 待排期 | 待排期 |
## 七、未确认事项(需求确认进度)
当前共 13 项待确认事项,其中 7 项为开工阻断、5 项为模块阻断、1 项为非阻断。完整建议、负责人、计划确认日期和最终结论统一维护在 [三期需求确认表](三期需求确认表.md);开工阻断项未确认前,受影响的大任务保持“待确认”。
| 编号 | 阻断级别 | 待确认事项 | 状态 | 主要影响 |
|---|---|---|---|---|
| P3-Q1 | 开工阻断 | 智能排柜的硬约束和软偏好 | 待确认 | T04 智能排柜、T07 拣货任务 |
| P3-Q2 | 开工阻断 | 是否支持拼柜、一单多柜和拆批出仓 | 待确认 | T03 出仓单与柜次、T04 智能排柜 |
| P3-Q3 | 非阻断 | “预约托车”是否统一为“预约拖车” | 待确认 | T06 拖车预约、页面与接口文案 |
| P3-Q4 | 模块阻断 | PDA 型号、系统和扫描头接入方式 | 待确认 | T07 PDA 拣货、T08 柜封拍照、真机验收 |
| P3-Q5 | 开工阻断 | 拣货是否依赖实时库存和库位可用量 | 待确认 | T02 二三期契约、T07 PDA 拣货 |
| P3-Q6 | 模块阻断 | 柜封照片必传类型、角度、数量和留存期限 | 待确认 | T08 装柜证据、T10 出仓门禁 |
| P3-Q7 | 模块阻断 | 报关资料清单是否按国家、渠道和货物类型变化 | 待确认 | T09 报关资料、文件版本 |
| P3-Q8 | 模块阻断 | 报关代理接收资料和返回回执的方式 | 待确认 | T09 报关协同、回执上传 |
| P3-Q9 | 开工阻断 | 港务信息的业务含义、字段和维护方 | 待确认 | T05 订舱港务、T11 客户端展示 |
| P3-Q10 | 模块阻断 | 确认出仓后的错误处理和冲正方式 | 待确认 | T10 出仓门禁、权限和审计 |
| P3-Q11 | 开工阻断 | 九个动作的强制前置和并行关系 | 待确认 | T03~T10 全流程和状态机 |
| P3-Q12 | 开工阻断 | 报关回执类型及海关放行是否为出仓硬门禁 | 待确认 | T09 报关状态、T10 出仓门禁 |
| P3-Q13 | 开工阻断 | 三期终点是等待装船还是装船发运 | 待确认 | 阶段边界、状态、里程碑和验收 |
## 八、开工准入清单
产品和业务完成需求确认后,先进入“开工准备”;以下准入项全部通过后,三期才能从范围规划正式切换到全面开发。
| 准入编号 | 准入条件 | 当前状态 | 责任方 | 完成证据 |
|---|---|---|---|---|
| P3-GO-01 | P3-Q1~P3-Q13 均形成明确结论,7 项开工阻断全部解除 | 进行中 | 产品、业务 | `三期需求确认表.md` 状态均为已确认并填写最终结论 |
| P3-GO-02 | 确认结论已同步开发规划、进度表、页面清单和验收范围 | 待开始 | 产品、项目负责人 | 四份文档口径一致,无未标注的候选规则 |
| P3-GO-03 | 关键缺失原型已补齐并评审,包括出仓门禁、客户端三期页面和 PDA 柜封拍照 | 进行中 | 产品、设计、业务 | 页面清单中的关键页面达到已确认状态 |
| P3-GO-04 | T02 公共契约完成,二期输入、状态机、权限、数据模型、接口、错误码和审计口径稳定 | 待开始 | 前后端、架构/技术负责人 | 契约评审记录及稳定的数据/API 定义 |
| P3-GO-05 | T03~T12 已拆成 RULE、DATA、API、ADM/CLI/PDA、TEST、ACCEPT 任务,并填写负责人和计划 | 待开始 | 项目负责人、研发、测试 | 详细任务可独立开发、联调和验收 |
| P3-GO-06 | 产品、业务、研发和测试完成开工评审,确认范围、依赖、风险和首批任务 | 待开始 | 项目负责人 | 开工评审纪要及明确的 GO 结论 |
准入口径:`P3-GO-01` 完成后可以进入 T02 契约设计和详细任务拆分;`P3-GO-01~P3-GO-06` 全部通过后,才进入全面功能开发。
## 九、状态维护规则
- 状态统一使用:待确认、待开始、进行中、联调中、测试中、已完成、已阻塞。
- 进度填写 `0%~100%`;只有开发、测试和业务验收均完成后才能填写 `100%`。
- 开始开发时填写负责人、计划开始和计划完成日期。
- 出现阻塞时在备注中填写阻塞原因、责任人和预计解除时间。
- 大任务进度应由其详细任务加权汇总;当前骨架阶段先人工维护,拆分完成后再确定权重。
## 十、下一步拆分顺序
1. 先处理 T01,逐项确认 P3-Q1~P3-Q13,并补齐关键页面原型。
2. 拆 T02 公共契约与数据模型,固定二期输入和三期核心对象。
3. 按业务顺序拆 T03~T10,每个模块补齐规则、数据、接口、页面和测试。
4. 拆 T11 客户端可视范围和交互。
5. 最后拆 T12 联调、真机、验收和发布任务。
+69
View File
@@ -0,0 +1,69 @@
# 虫虫出海三期需求确认表
> 创建日期:2026-07-28
> 当前状态:确认中
> 用途:记录会影响三期范围、流程、数据模型、页面和验收的产品决策。
## 一、维护规则
- 状态统一使用:待确认、确认中、已确认、已取消。
- 阻断级别统一使用:开工阻断、模块阻断、非阻断。
- 结论确认后同步更新 `三期开发规划.md`、`三期开发进度表.md` 和受影响的页面原型。
- 未确认事项只能作为候选方案,不能直接写成固定开发或验收承诺。
## 二、确认事项
| 编号 | 决策/问题 | 当前建议 | 阻断级别 | 状态 | 负责人 | 计划确认 | 最终结论 | 影响模块 |
|---|---|---|---|---|---|---|---|---|
| P3-Q1 | 智能排柜需考虑哪些硬约束和软偏好? | 业务提供可执行规则表,建议可解释并由人工确认 | 开工阻断 | 待确认 | 待分配 | 待排期 | - | 智能排柜、拣货任务 |
| P3-Q2 | 是否存在拼柜、一单多柜和拆批出仓? | 数据模型预留能力,页面按高频场景逐步开放 | 开工阻断 | 待确认 | 待分配 | 待排期 | - | 出仓单、柜次、排柜、拣货 |
| P3-Q3 | 原型“预约托车”是否统一为“预约拖车”? | 建议使用行业常用的“拖车”,同时确认业务实际含义 | 非阻断 | 待确认 | 待分配 | 待排期 | - | 页面文案、接口字段说明 |
| P3-Q4 | 目标 PDA 型号、系统和扫描头接入方式是什么? | 确认前保持设备适配层,不绑定厂商 SDK | 模块阻断 | 待确认 | 待分配 | 待排期 | - | PDA 拣货、真机验收 |
| P3-Q5 | 拣货是否依赖实时库存和库位可用量? | 默认使用二期处理单元和当前位置;如需完整库存,单独评估 WMS 范围 | 开工阻断 | 待确认 | 待分配 | 待排期 | - | 二三期契约、拣货、库存边界 |
| P3-Q6 | 柜封照片有哪些必传类型、角度、数量和留存期限? | 使用可配置清单,由仓库、业务和合规共同确认 | 模块阻断 | 待确认 | 待分配 | 待排期 | - | 装柜、PDA 拍照、文件存储、出仓门禁 |
| P3-Q7 | 报关资料清单是否按国家、渠道和货物类型变化? | 清单配置化,并保存业务使用的清单版本 | 模块阻断 | 待确认 | 待分配 | 待排期 | - | 客户资料、报关审核、文件版本 |
| P3-Q8 | 报关代理如何接收资料并返回回执? | 首版系统内整理、线下提交,正式接口后续评估 | 模块阻断 | 待确认 | 待分配 | 待排期 | - | 报关协同、回执上传 |
| P3-Q9 | “港务信息”指船期、港口作业状态还是两者,包含哪些字段,由谁维护? | 先确认词义和字段;首版平台人工维护并审计 | 开工阻断 | 待确认 | 待分配 | 待排期 | - | 订舱、港务查询、客户展示 |
| P3-Q10 | 确认出仓后发现错误如何处理? | 不直接回退状态,使用授权冲正和反向审计事件 | 模块阻断 | 待确认 | 待分配 | 待排期 | - | 出仓门禁、权限、审计 |
| P3-Q11 | 九个动作中哪些是强制前置,哪些允许并行? | 确认前按原型编号顺序规划 | 开工阻断 | 待确认 | 待分配 | 待排期 | - | 全流程、状态机、页面进度 |
| P3-Q12 | 报关回执包含哪些类型,海关放行是否为国内出仓硬门禁? | 当前不默认以放行为门禁,业务明确各类回执含义 | 开工阻断 | 待确认 | 待分配 | 待排期 | - | 报关状态、出仓门禁、客户回执 |
| P3-Q13 | 三期终点以“等待装船”还是“装船发运”为准? | 当前以“完成国内出仓,等待装船”为基线 | 开工阻断 | 待确认 | 待分配 | 待排期 | - | 阶段边界、状态、里程碑、验收 |
## 三、建议确认顺序
1. 先确认 P3-Q13、P3-Q11,锁定阶段终点和流程依赖。
2. 再确认 P3-Q1、P3-Q2、P3-Q5,锁定排柜、柜次和拣货输入。
3. 然后确认 P3-Q12、P3-Q7、P3-Q8,锁定报关和出仓门禁。
4. 最后确认设备、照片、港务信息、冲正和文案事项。
## 四、产品与业务确认会议
### 4.1 参会角色
- 产品负责人:逐项说明问题、候选方案和影响范围,维护最终结论。
- 业务负责人:确认流程、规则、例外、责任边界和客户可见范围。
- 仓库/运营代表:确认现场作业、PDA、照片、拖车、装柜和冲正可执行性。
- 研发与测试代表:识别数据、接口、设备、权限和验收风险,不代替业务作决定。
### 4.2 会议输入
- 本确认表中的 P3-Q1~P3-Q13。
- `三期开发规划.md` 的流程、状态、里程碑和任务骨架。
- `三期页面与原型清单.md` 中已有参考原型和待补页面。
- 二期输出契约、目标 PDA 信息、文件存储能力及报关/港务协作现状。
### 4.3 每项确认要求
1. 必须填写负责人、计划确认日期和可执行的最终结论,不能只记录“按建议处理”。
2. 最终结论需明确正常流程、异常/冲正、权限责任和验收口径。
3. 暂缓事项必须写明本期是否排除、临时方案、影响模块和再次确认日期。
4. 开工阻断项未解除时,不得将受影响模块标记为可开发。
5. 会议结束后,将结论同步到开发规划、开发进度表、页面原型和后续验收任务。
### 4.4 会议输出
- 13 项确认事项的状态和最终结论。
- 需要新增或调整的原型清单。
- 三期范围内/范围外清单及跨期依赖。
- 仍有风险的事项、责任人和完成日期。
- 是否允许进入 T02 契约设计与任务拆分的明确结论。
+55
View File
@@ -0,0 +1,55 @@
# 虫虫出海三期页面与原型清单
> 创建日期:2026-07-28
> 当前状态:页面盘点中
> 原型依据:`参考资料/出仓流程.png`、现有 PDA 拣货原型
## 一、维护规则
- 页面状态统一使用:待确认、待原型、原型中、待评审、已确认、开发中、已完成。
- 页面只有在业务流程、角色权限和关键字段确认后才能标记“已确认”。
- 页面原型至少覆盖正常态、空态、加载态、失败态、无权限态和关键冲突态。
- 当前先列页面级大项,后续按开发进度表的大任务逐项拆交互、字段和验收。
## 二、管理后台
| 页面 ID | 页面/入口 | 主要能力 | 对应大任务 | 原型状态 | 依赖确认项 | 备注 |
|---|---|---|---|---|---|---|
| P3-ADM-01 | 出仓单列表 | 查询、筛选、创建、异常和状态展示 | T03 出仓单与柜次 | 部分参考原型 | P3-Q2、P3-Q11 | 可参考通用 WMS 出库单,但需重定义国内出仓对象 |
| P3-ADM-02 | 出仓单详情 | 柜次、任务、证据、报关和时间线 | T03 出仓单与柜次 | 部分参考原型 | P3-Q11、P3-Q13 | 可组合参考货柜列表和装柜清单,缺统一详情页 |
| P3-ADM-03 | 智能排柜 | 生成建议、冲突、调整和确认 | T04 智能排柜 | 已有参考原型 | P3-Q1、P3-Q2 | 参考“智能装柜”弹窗,需确认名称和规则边界 |
| P3-ADM-04 | 订舱管理 | 订舱申请、结果、变更和异常 | T05 订舱与港务信息 | 已有参考原型 | P3-Q9、P3-Q11 | 已有订舱列表、新增和详情 |
| P3-ADM-05 | 港务/船期信息 | 查询系统信息、人工维护、来源和更新时间 | T05 订舱与港务信息 | 部分参考原型 | P3-Q9 | 订舱详情和货柜列表已有船期字段,缺独立查询口径 |
| P3-ADM-06 | 拖车预约 | 服务商、车辆司机、时间窗和节点 | T06 拖车预约 | 已有参考原型 | P3-Q3、P3-Q11 | 已有拖车列表和新增表单,需补预约及到离仓节点 |
| P3-ADM-07 | 拣货任务与复核 | 分配、进度、差异、复核和异常 | T07 PDA 拣货与复核 | 部分参考原型 | P3-Q4、P3-Q5 | 可参考通用 WMS 出库和 PDA 原型,缺三期真实任务页 |
| P3-ADM-08 | 装柜封柜 | 柜号、铅封号、照片清单和复核 | T08 装柜及柜封证据 | 部分参考原型 | P3-Q6 | 已有货柜/预装/装柜清单,缺现场照片复核闭环 |
| P3-ADM-09 | 报关协同 | 资料审核、补件、提交批次和回执上传 | T09 报关资料与回执 | 部分参考原型 | P3-Q7、P3-Q8、P3-Q12 | 已有报关单和单证管理,缺客户补件协同闭环 |
| P3-ADM-10 | 国内出仓确认 | 门禁检查、确认、阻塞和冲正 | T10 国内出仓门禁 | 待原型 | P3-Q10、P3-Q11、P3-Q12、P3-Q13 | 现有状态分散,无统一门禁确认页 |
## 三、企业客户端
| 页面 ID | 页面/入口 | 主要能力 | 对应大任务 | 原型状态 | 依赖确认项 | 备注 |
|---|---|---|---|---|---|---|
| P3-CLI-01 | 出仓进度列表 | 查看本企业出仓单、柜次和当前节点 | T11 客户端出仓协同 | 待原型 | P3-Q11、P3-Q13 | 现有客户端未覆盖三期专用页面 |
| P3-CLI-02 | 出仓进度详情 | 时间线、柜号、船期和允许公开的证据 | T11 客户端出仓协同 | 待原型 | P3-Q6、P3-Q9 | 现有客户端只有零散船期/出仓时间字段,需新建详情页 |
| P3-CLI-03 | 报关资料上传 | 清单、上传、补件、版本和处理进度 | T09 报关资料与回执 | 待原型 | P3-Q7 | 文件规则确认后细化 |
| P3-CLI-04 | 报关回执下载 | 回执列表、类型、版本和授权下载 | T09 报关资料与回执 | 待原型 | P3-Q8、P3-Q12 | 区分客户可见回执 |
| P3-CLI-05 | 港务/船期信息 | 预计开船、到港、来源、更新时间和变更 | T11 客户端出仓协同 | 待原型 | P3-Q9 | 不将过期信息展示为实时信息 |
## 四、PDA
| 页面 ID | 页面/入口 | 主要能力 | 对应大任务 | 原型状态 | 依赖确认项 | 备注 |
|---|---|---|---|---|---|---|
| P3-PDA-01 | 登录 | 人员登录和快捷登录 | T07 PDA 拣货与复核 | 已有静态原型 | P3-Q4 | 真实鉴权待服务端接入 |
| P3-PDA-02 | 箱号与任务选择 | 领取任务、选择箱号和查看进度 | T07 PDA 拣货与复核 | 已有静态原型 | P3-Q4、P3-Q5 | 需对接真实任务和处理单元 |
| P3-PDA-03 | 扫码拣货 | 扫码、整拣、部分拣货、跳拣和撤销 | T07 PDA 拣货与复核 | 已有静态原型 | P3-Q4、P3-Q5 | 已有本地交互,缺服务端 |
| P3-PDA-04 | 拣货提交 | 汇总、异常、提交和完成统计 | T07 PDA 拣货与复核 | 已有静态原型 | P3-Q5 | 需补幂等和冲突处理 |
| P3-PDA-05 | 柜封拍照上传 | 柜次确认、拍照/相册、类型、预览和重试 | T08 装柜及柜封证据 | 待原型 | P3-Q4、P3-Q6 | `docs/悦` 中暂无对应页面,属于明确原型缺口 |
## 五、下一步拆分顺序
1. 先补 P3-ADM-01、P3-ADM-02,确定三期后台信息架构。
2. 根据 P3-Q1 结论补 P3-ADM-03 智能排柜原型。
3. 根据 P3-Q5 复核现有 PDA 拣货原型并补真实状态。
4. 根据 P3-Q6 优先补 P3-PDA-05 柜封拍照上传原型。
5. 报关和港务规则确认后,再补客户端及对应后台页面。
Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.
+20
View File
@@ -0,0 +1,20 @@
# 二期文档
二期以头程物流“02 到仓”为作业主线,完成预约取货、送货上门、入库测量、差异核对和指定库位闭环,并增加费用规则、应收应付、对账、线下收付款核销和单票利润的轻量财务闭环。
## 文档
- [二期开发规划:头程到仓](二期开发规划.md)
- [二期计费与财务模块规划](二期计费财务模块规划.md)
- [二期计费财务需求确认表](二期计费财务需求确认表.md)
- [二期计费财务需求确认表(Excel)](二期计费财务需求确认表.xlsx)
- [二期产品待确认事项(发产品填写)](二期产品待确认事项.md)
- [二期开发进度表](二期开发进度表.md)
- [二期开发进度表(Excel)](虫虫出海二期开发进度表.xlsx)
- [二期需求确认清单:产品与业务确认](二期需求确认清单.md)
- [二期需求确认清单(会议填写 Excel)](二期需求确认清单.xlsx)
- [规划依据:头程物流流程图](参考资料/头程物流流程图.svg)
## 当前状态
二期已经启动,采用“需求确认与开发并行”的推进方式。当前产品岗位空缺,由项目负责人会同业务、仓库、财务和技术形成临时需求基线;后续产品到岗后复核,不静默改写已开发规则。当前有 11 项仍需补齐,其中到仓作业 7 项、计费财务 4 项;其余事项按已有业务方向、原型和安全实现规则形成二期基线。具体任务状态和实际进度由开发负责人回填,当前文档不代表已承诺完成日期。
+64
View File
@@ -0,0 +1,64 @@
# 虫虫出海二期产品待确认事项
> 更新日期:2026-07-30
> 适用范围:头程物流“02 到仓”及二期计费财务
> 正式确认与状态维护以 [二期需求确认清单](二期需求确认清单.md) 的“会议确认主表”为准;本文件仅作为便于发送给产品的精简视图。
> 填写方式:产品空缺期间由项目负责人组织会议填写“临时结论”和“状态”,不需要填写确认人和确认时间。
> 生效规则:状态为“临时基线”且结论可直接转为业务规则时用于当前开发;产品到岗复核后改为“已确认”。状态为“暂缓”时必须写明二期临时处理方式。
## 一、结论摘要
- 需要产品补充确认:11 项。
- 到仓作业:7 项,分别为 P2-Q1、Q4、Q5、Q6、Q8、Q10、Q11。
- 计费财务:4 项,分别为 P2-Q13 至 P2-Q16。
- 其余 13 项已有产品方向、原型依据或可采用安全实现基线,不再重复发给产品确认。
## 二、需要产品确认的 11 项
| 编号 | 已有结论/原型情况 | 会议需要补充确认 | 当前建议 | 临时结论(会议填写) | 状态 |
|---|---|---|---|---|---|
| P2-Q1 | 已提出“以实际货物入库为准”;原型没有少货关闭流程 | 谁能关闭少货到仓单;关闭条件;原因、照片或交接凭证是否必填;是否通知客户 | 收货员登记少货,仓库主管关闭;必填原因并保留凭证;关闭后通知客户 | 待填写 | 待确认 |
| P2-Q4 | 已给出“毛重÷363”和“(长×宽×高÷6000)×箱数”;原型有 cm、kg、m³ 和材积重 | `/363` 的业务含义;各输入单位;计费重是否取毛重与材积重较大值;精度和舍入 | 长 cm、重量 kg、体积 m³;保留原始值和公式版本;最终结果统一四舍五入 | 待填写 | 待确认 |
| P2-Q5 | 已确认差异需要客户确认;原型没有拒绝和超时规则 | 是否所有差异都确认;客户拒绝或超时怎么办;确认前是否暂停上架和计费 | 数量、重量、体积、破损及归属差异均需确认;确认前暂停后续作业,拒绝后转人工处理 | 待填写 | 待确认 |
| P2-Q6 | 已确认没有独立隔离区 | 差异期间货物放置方式;谁能解除业务暂停;解除条件 | 保持当前物理位置并标记业务暂停;客户确认且仓库主管复核后解除 | 待填写 | 待确认 |
| P2-Q8 | 已确认上传称重照片和测量照片;原型没有附件规则 | 是否必传;按箱件还是按收货批次上传;数量、大小、格式和补传规则 | 每个收货批次必传称重、测量照片;支持补传但保留首次提交和补传记录 | 待填写 | 待确认 |
| P2-Q10 | 已暂缓无预报货物认领;原型没有临时处理方式 | 暂缓期间是拒收,还是允许线下登记后待认领;若允许,谁能绑定订单 | 二期系统拒绝将无预报货物直接绑定企业订单;线下处置,不进入正式库存和计费 | 待填写 | 待确认 |
| P2-Q11 | 已确认保存 12 个月;原型没有保留策略 | 从哪个时间点起算;到期删除还是归档;联系方式、照片、凭证和审计是否同期限 | 从业务单关闭日起算;联系方式匿名化,照片和凭证到期删除,审计记录按合规要求归档 | 待填写 | 待确认 |
| P2-Q13 | 原型覆盖费用、应收应付、账单、收付款和利润,但不能证明二期全部纳入 | 二期财务闭环终点 | 做到费用、应收应付、对账、线下收付款登记与核销、单票利润;不做在线支付、会计凭证和总账 | 待填写 | 待确认 |
| P2-Q14 | 原型费用项目较多,混合头程、仓储和其他阶段 | “02 到仓”首批费用项目白名单,以及每项应收/应付方向、单位、币种和税率 | 只纳入到仓实际使用项目;白名单外费用允许人工新增但必须审核 | 待填写 | 待确认 |
| P2-Q15 | 原型有费用分类和应收/应付,没有明确阶梯价、最低收费和规则优先级 | 二期需要的计费维度;是否支持阶梯价、最低收费、封顶、组合价和冲突优先级 | 至少支持客户、往来单位、仓库、服务类型和生效区间;复杂价格结构按实际需要裁剪 | 待填写 | 待确认 |
| P2-Q16 | 原型有费用日期和审核状态,没有明确到仓事件的自动计费节点 | 各类费用在哪个节点试算、生成、生效和锁定;人工费用何时生效 | 实测完成生成试算,差异确认后生效并锁定;非测量费用按已确认业务事件生成 | 待填写 | 待确认 |
## 三、P2-Q14 首批费用项目填写模板
产品确认 P2-Q14 时,至少补齐以下字段;没有实际业务的费用不要为了匹配原型而纳入二期。
| 费用编码 | 费用名称 | 业务阶段 | 应收/应付 | 往来对象 | 计费单位 | 默认币种 | 默认税率 | 自动/人工 | 触发节点 |
|---|---|---|---|---|---|---|---|---|---|
| 待填写 | 待填写 | 02 到仓 | 待填写 | 待填写 | 待填写 | 待填写 | 待填写 | 待填写 | 待填写 |
## 四、无需再次发产品确认的 13 项
| 编号 | 二期采用的结论 | 依据 |
|---|---|---|
| P2-Q2 | 二期不做系统服务商调度;取货安排在线下完成,系统只记录结果和人工费用 | 产品回传结论已明确系统边界 |
| P2-Q3 | 预约在线下完成;系统记录预约日期时间、改期/取消结果及操作日志 | 产品回传方向和原型时间字段足以形成最小基线 |
| P2-Q7 | 二期不做库位容量校验;库位停用和状态合法性仍做基础校验 | 产品已明确当前没有库位容量 |
| P2-Q9 | 二期暂不接 PDA,使用后台或响应式作业页 | 产品已暂缓,且 PDA 原型属于“03 出仓” |
| P2-Q12 | 标签打印机为佳博 1324D 热敏标签打印机 | 产品已确认;纸张、连接、驱动和指令协议属于开发前实机核验 |
| P2-Q17 | 本位币 CNY;保存原币、人工业务汇率和折算金额,费用生效时锁定汇率快照 | 原型已有币种、汇率及人民币:美元汇率字段 |
| P2-Q18 | 保存未税金额、税率、税额和含税总额;金额/税额 2 位、汇率 6 位,服务端统一舍入 | 原型已有 0%/6%/9%/13% 税率及税额、含税合计 |
| P2-Q19 | 经办提交、财务审核;生效费用只能调整、红冲或作废并留痕 | 原型已有新增、客服提交和财务通过等状态;审计要求禁止直接覆盖 |
| P2-Q20 | 支持按选择和按截止时间生成账单,保存结算期间、账期和到期日 | 原型已有两种账单生成方式和结算字段 |
| P2-Q21 | 账单审核、确认、拒绝、锁定;争议调整后生成新版本 | 原型已有待审核、审核通过、审核拒绝、待确认、明细和支付历史 |
| P2-Q22 | 支持线下收付款、部分收付、余额、核销和反核销;不做预收预付、跨币种核销及银行自动认领 | 原型已有部分收款、已结清、余额、支付历史和核销入口 |
| P2-Q23 | 二期只登记发票信息和状态,不接电子税局或第三方开票平台 | 原型只有发票登记字段,没有外部开票接口 |
| P2-Q24 | 利润=已生效应收-已生效应付,利润率=利润÷收入;仅财务授权角色可见 | 原型已有收入、成本、利润、利润率和多维报表 |
## 五、确认后的处理
1. 产品空缺期间由项目负责人组织填写 11 项的“临时结论”和“状态”。
2. 产品确认后保留原始回传副本,不覆盖本文件或既有回传文件。
3. 技术审核结论是否足以转成状态、权限、计算、异常和验收规则。
4. 审核通过后同步更新二期开发规划、需求确认清单、开发进度表及 Excel 版本。
5. P2-Q13 至 P2-Q16 未形成临时基线前,计费财务只开发可逆的基础结构,不锁定闭环范围、复杂价格和自动计费节点。
+416
View File
@@ -0,0 +1,416 @@
# 虫虫出海二期开发规划:头程到仓与计费财务
> 创建日期:2026-07-28
> 状态:开发已启动,需求确认并行
> 流程依据:[头程物流流程图](参考资料/头程物流流程图.svg)中的头程物流“02 到仓”
> 前置阶段:[一期文档](../一期/README.md)
## 一、二期目标
二期承接一期“客户下单并选择平台收货仓”的结果,完成货物从待到仓到入仓、测量、差异确认和指定库位的作业闭环,并在实测数据之上完成费用生成、应收应付、对账、线下收付款核销和单票利润的最小财务闭环。
流程严格对应五个动作:
```text
预约取货 / 送货上门
↓
货物到达平台收货仓
↓
入库测量(件数、重量、体积和计费数据)
↓
申报与实测差异核对
↓
仓库作业(指定库位)
↓
计费、应收应付、对账与核销
```
二期完成标准不是“增加到仓状态”,而是每票货能够回答:怎么到仓、何时到仓、谁接收、实际收到什么、与订单差多少、差异如何处理、最终放在哪个库位、产生了哪些收入与成本、是否完成对账和收付款核销。
### 当前启动策略
- 二期已经开工,产品岗位暂时空缺;现阶段由项目负责人组织业务、仓库、财务和技术形成临时需求基线。
- 可先行开发稳定且可逆的能力:公共数据结构、权限与审计骨架、列表和详情、原始测量与附件、标签适配层、费用及账单基础数据结构。
- 依赖 P2-Q1、Q4、Q5、Q6、Q8、Q10、Q11、Q13 至 Q16 的自动流转、计算参数和最终锁定规则,在结论明确前必须配置化或保持可替换,不写死高成本业务假设。
- 后续产品到岗后复核临时基线。已进入开发的规则发生变化时,必须记录影响范围、数据迁移和验收调整,不直接覆盖历史版本。
## 二、阶段边界
### 纳入二期
- 客户预约取货和送货上门两种到仓方式。
- 平台收货仓到仓预报、预约安排、到仓签到和收货登记。
- 实收件数、重量、体积、尺寸、外包装和现场照片记录。
- 申报数据与实测数据对比、差异单、确认和调整留痕。
- 最小库位档案、上架任务、指定库位和上架完成记录。
- 箱件/托盘处理单元、标签和当前库位位置记录,为后续出仓提供可追踪输入。
- 客户端查看预约、到仓、实测、差异和上架结果。
- 管理后台进行预约调度、仓库收货、差异处理和库位分配。
- 费用项目、计费规则版本、费用试算与生效、应收应付和调整审计。
- 客户/供应商对账单、线下收付款登记、部分核销、反核销和余额查询。
- 发票状态登记、客户账单中心和单票利润查询。
- 服务端完成权限、状态、幂等、并发、审计和历史快照。
### 明确不纳入二期
- 流程图头程“03 出仓”:智能排柜、订舱、预约拖车、PDA 拣货、装柜、报关和港务信息。
- 完整 WMS:可用库存、库存成本、常规移库、盘点、冻结、拣货、复核和出库。二期只保留支撑指定库位所需的处理单元和位置记录。
- 正式 PDA 硬件扫描头或厂商 SDK 接入;二期仓库作业先由管理后台或响应式作业页完成。
- 对客询价、报价审批、报价转订单和合约报价体系。
- 在线支付、银企直连、银行流水自动认领、电子税局开票、会计凭证和总账报表;二期财务边界见 [二期计费与财务模块规划](二期计费财务模块规划.md)。
- 快递轨迹、地图调度、车队 GPS 和外部承运商接口自动同步。
- FBA/海外目的仓及尾程到港、到仓、出仓、签收流程。
## 三、参与角色与数据边界
| 角色 | 数据范围 | 主要操作 |
|---|---|---|
| 企业订单操作员 | 当前企业订单、到仓单和对客账单 | 提交预约、查看到仓结果、确认差异和查看账单 |
| 企业管理员 | 当前企业全部订单、到仓单和对客账单 | 查看、确认差异、确认/争议账单、管理企业联系人 |
| 平台客服/运营 | 授权客户和订单范围 | 审核预约、协调取货、处理异常、差异和对账争议 |
| 仓库收货员 | 被授权的平台收货仓 | 到仓签到、收货、测量、拍照和异常登记 |
| 仓库上架员 | 被授权的平台收货仓和库位 | 领取上架任务、指定库位、确认完成 |
| 计费专员 | 授权客户、仓库和业务范围 | 费用试算、人工加费、调整申请和对账准备 |
| 财务审核员 | 平台财务授权范围 | 费用审核、对账审核、收付款与核销审核 |
| 财务管理员 | 平台财务授权范围 | 费用项目、计费规则、汇率、结算参数和利润查询 |
| 平台管理员 | 平台授权范围 | 维护仓库、库位、快递公司、往来单位和权限 |
服务端必须依据登录身份确定企业、平台数据范围和仓库权限。订单 ID、企业 ID、仓库 ID 或操作员 ID 均不能由前端参数单独决定授权。
## 四、业务模块
### 1. 到仓单与到仓预报
- 规划基线采用“订单 1:N 到仓单、到仓单 1:N 收货批次、收货批次 1:N 处理单元”。一张订单可以分次取货或送货,每次独立预约形成到仓单;同一次预约允许分批实际到仓。
- 到仓单记录所属订单、平台收货仓、到仓方式、预计时间、联系人、货物摘要和备注。
- 生成客户可见的到仓单号和仓库收货识别码。
- 客户、平台代客创建和系统补录必须记录来源及操作者。
- 重复提交使用幂等键,避免网络重试产生重复预约。
- 到仓单保存计划件数/箱数、累计实收、剩余数量和是否允许继续到仓。未全部到仓时显示“部分收货”,由后续批次补齐或由授权人员填写原因后人工关闭。
- 到仓单分配到具体订单货物明细及计划数量;同一货物明细在所有有效到仓单中的累计计划量不得超过订单可到仓量,取消或关闭后按规则释放未使用分配量。
- 已人工关闭的少货到仓单不得继续登记新批次;需要补货时创建新的到仓单并关联原单。
### 2. 预约取货
- 客户选择取货地址、联系人、联系电话和期望取货时间窗。
- 带入往来单位联系人与地址,允许本次预约调整但不反向覆盖档案。
- 平台审核预约,分配快递公司或服务商、司机/联系人和预约时间。
- 支持待审核、已安排、已取货、运输中、已到仓、已取消和异常结果。
- 取货调度结果使用独立子状态维护;到仓单主状态只表达货物整体到仓进度,二者通过业务动作联动但不能相互覆盖。
- 一期快递公司和往来单位作为调度基础资料;外部轨迹和自动派单不进入二期。
- 仓库维护营业时间、停收日期、时间窗容量和人工超额确认权限;预约变更、爽约和重复时间窗冲突必须留痕。
### 3. 送货上门
- 客户选择送达仓库、预计送达时间和送货联系人。
- 可记录快递公司、快递单号或自送车辆信息。
- 仓库按到仓单号、订单号、快递单号或收货识别码检索预报。
- 无预报货物允许登记为“待认领”,但不得直接归入其他企业订单。
- 取消、过期、改仓和重复到仓必须保留处理记录。
- 到仓前改仓需取消原到仓单并新建目标仓到仓单,不直接改写已确认的仓库快照。
### 4. 到仓签到与收货
- 记录实际到仓时间、到仓方式、交接人、收货员和交接凭证。
- 登记实际包裹/箱数、包装状态、唛头、破损、受潮、缺失和多货等异常。
- 上传现场照片,照片关联到仓单和收货批次并受权限控制。
- 支持一票分批到仓;每个收货批次有独立时间、实收和异常记录。
- 到仓签到与实收提交均需幂等,防止重复扫码或重复点击。
- 每个实际箱件或托盘生成稳定处理单元编码,可打印和补打标签;补打、换标、作废和重复码处理必须保留操作者及原因。
- 二期箱件/托盘标签打印设备确定为佳博 1324D 热敏标签打印机。标签纸尺寸、连接方式、驱动和打印指令协议须在开发前通过实机核验,不依据型号名称直接假定。
- 托盘处理单元可以组合多个箱件,保存父子关系和各箱件对应的订单货物明细;拆托或重新组托必须生成新的组合版本。
- 错绑企业、订单或到仓单时不得删除收货记录。由有权限的主管发起冲正,保留原绑定、冲正原因、审批人和重新认领结果。
### 5. 入库测量
- 按收货批次或箱件记录实测数量、毛重、体积和尺寸。
- 服务端汇总订单实收件数、总重量和总体积,不接受客户端独立提交冲突合计。
- 保留测量设备/方式、操作员、时间和修订版本;修改实测数据必须填写原因。
- 测量规则使用独立版本,明确长度、重量和体积单位、体积重系数、精度及舍入方式。每条实测记录保存原始值、换算值、规则版本和复核结果,确保历史计费数据可复算。
- 二期输出体积重、计费重等结果,并作为费用试算输入;测量或计费规则版本发布后不反向改写已确认测量记录和已生效费用。
- 覆盖测量中、待复核、已完成和测量异常状态。
### 6. 调整差异核对
- 自动对比订单申报与仓库实测,形成数量、重量、体积、包装和货物归属差异。
- 差异支持待处理、待客户确认、客户已确认、平台调整完成和已关闭。
- 小于阈值的差异是否自动接受必须由产品配置,不在代码中写死。
- 客户确认后冻结本次实测版本;再次调整需创建新版本并记录原因。
- 差异未关闭时默认进入业务暂停。货物可以临时上架到隔离区,但不能标记正式上架完成,也不能进入后续出仓。
- 隔离区库位具有明确类型和暂停原因;差异关闭后执行释放并生成从隔离区到正式库位的位置变更记录。
- 客户对差异有异议时记录争议说明、证据、责任人和处理期限;超时策略必须配置,不能静默自动确认。
### 7. 指定库位与上架
- 维护最小库位档案:仓库、库区、库位编码、名称、类型、状态、容量约束和备注。库位类型至少区分正式库位、隔离区和待认领区。
- 创建上架任务,关联到仓单、收货批次、目标库位、执行人和优先级。
- 支持待分配、待上架、上架中、已完成、已取消和异常状态。
- 同一收货批次允许拆分到多个库位;每条分配记录保留数量或箱件范围。
- 上架完成时写入处理单元的当前库位位置,并保留进入、移出和历史位置。位置记录是三期拣货的输入,不等同于建设完整可用库存或库存成本台账。
- 目标库位停用、容量不足或不允许该货物类型时,服务端拒绝上架并要求重新分配。
### 8. 客户可视与通知
- 客户端订单详情增加到仓时间线:预约、取货/送货、到仓、实测、差异、上架。
- 客户只能查看本企业数据、公开照片和对客异常说明。
- 差异待确认、预约变更和到仓异常提供站内提醒。
- 短信、邮件或微信通知仅在渠道和合规方案确认后接入。
- 司机电话、客户联系人、照片和交接凭证按角色脱敏和授权下载,并明确保留期限、导出审计和到期清理策略。
### 9. 计费规则与费用台账
- 建立费用项目、应收/应付方向、计费单位、币种、税务属性和启停状态。
- 计费规则按客户、供应商/往来单位、仓库、服务类型和有效期版本化;历史费用保存输入、规则版本和计算快照。
- 实测确认、差异关闭或其他已确认业务节点触发费用草稿,重复事件不得重复计费。
- 生效费用不直接覆盖,改费、红冲、作废通过独立调整记录和审核链处理。
### 10. 应收应付与对账
- 应收关联客户,应付关联快递公司、仓库服务商或其他往来单位。
- 支持按票或按周期生成客户/供应商对账单,并维护审核、确认、争议、关闭和作废状态。
- 客户端提供账单列表、明细、确认和异议入口,不返回供应商成本、利润和内部备注。
### 11. 收付款、发票与利润
- 登记线下收款、付款、退款和调整,支持部分核销、多对多核销、反核销和凭证审计。
- 二期只登记发票申请、号码、金额、税率、日期、状态和附件,不连接外部开票平台。
- 单票利润按已生效应收减已生效应付计算;多币种使用锁定汇率折算,仅财务授权角色可见。
- 详细范围、任务和验收规则见 [二期计费与财务模块规划](二期计费财务模块规划.md)。
## 五、建议状态模型
不要把所有过程混成一个订单状态,建议使用到仓进度、差异、上架任务和异常处理四个相互关联的状态域。异常只暂停业务,不覆盖货物当前进度阶段。
### 到仓单状态
| 稳定编码 | 中文名称 | 含义 |
|---|---|---|
| `PENDING_ARRANGEMENT` | 待安排 | 到仓预报已创建,尚未完成取货或送货安排 |
| `SCHEDULED` | 已预约 | 取货或送货时间已确认 |
| `IN_TRANSIT_TO_WAREHOUSE` | 运往仓库 | 已取货或客户已发出 |
| `ARRIVED` | 已到仓 | 已完成到仓签到,等待收货测量 |
| `PARTIALLY_RECEIVED` | 部分收货 | 已确认至少一个收货批次,仍有计划货物待到仓或待人工关闭 |
| `MEASURING` | 测量中 | 正在登记和复核实收数据 |
| `RECEIVED` | 已收货 | 实收版本已确认 |
| `PUTAWAY_PENDING` | 待上架 | 实收已确认,等待正式库位分配或差异解除 |
| `PUTAWAY_COMPLETED` | 已上架 | 指定库位作业完成,二期流程结束 |
| `CANCELLED` | 已取消 | 到仓前按规则取消 |
主状态根据到仓单及其收货批次汇总。部分收货不能仅凭前端设置;服务端根据计划数量、累计实收和人工关闭标记计算可用动作。少货人工关闭只结束剩余到仓等待,不等于差异已确认;已有实收仍须完成测量、差异处理和正式上架后才能进入 `PUTAWAY_COMPLETED`。
### 取货调度子状态
`PENDING_REVIEW`、`SCHEDULED`、`PICKED_UP`、`IN_TRANSIT`、`ARRIVED`、`CANCELLED`。调度异常由异常单表达,不增加会覆盖正常进度的 `EXCEPTION` 状态。
送货上门不创建取货调度记录;到仓单从预约确认或发货动作推进主状态。取货车辆到达仓库后只能发起签到,必须由有收货权限的人员确认签到成功,才能把到仓单推进为 `ARRIVED`;签到、收货和后续处理仍以到仓单及收货批次为准。
### 差异状态
`NONE`、`PENDING_REVIEW`、`PENDING_CUSTOMER_CONFIRMATION`、`DISPUTED`、`CONFIRMED`、`ADJUSTED`、`CLOSED`。
### 上架任务状态
`PENDING_ASSIGNMENT`、`PENDING_PUTAWAY`、`IN_PROGRESS`、`COMPLETED`、`CANCELLED`。异常和暂停由独立异常单及 `businessHold` 表达。
### 异常与业务暂停
异常单状态使用 `OPEN`、`PROCESSING`、`WAITING_CUSTOMER`、`RESOLVED`、`CLOSED`。异常单记录发生阶段、类型、责任人、处理期限、证据、解决结果和恢复动作。
到仓单、收货批次或处理单元可设置 `businessHold`、暂停原因和解除条件。异常解决后恢复原进度阶段,不通过把主状态从 `EXCEPTION` 猜回原状态。
订单主状态只保留对全链路有意义的粗粒度节点,到仓、差异、上架和异常细节由对应业务对象维护。
## 六、核心数据对象
| 数据对象 | 关键内容 |
|---|---|
| 到仓单 | 订单、企业、仓库、方式、预计时间、联系人、状态、来源和版本 |
| 到仓计划分配 | 到仓单、订单货物明细、计划数量、累计实收、剩余数量、释放数量和人工关闭信息 |
| 取货预约 | 取货地址快照、时间窗、服务商、司机/联系人和调度状态 |
| 预约容量 | 仓库、营业时间、停收日期、时间窗、容量、已占用量和超额授权 |
| 收货批次 | 实际到仓时间、交接人、收货员、实收摘要和现场凭证 |
| 处理单元 | 稳定箱件/托盘编码、标签状态、所属批次、订单货物明细、数量、父子组合和当前业务暂停状态 |
| 组托版本 | 托盘、成员箱件、组合版本、生效/失效时间、操作人和拆托/重组原因 |
| 标签记录 | 处理单元、标签编码、设备型号、打印结果、生成、打印、补打、换标、作废及原因 |
| 测量规则版本 | 单位、体积重系数、精度、舍入方式、生效时间和版本状态 |
| 测量明细 | 处理单元、原始尺寸/重量、换算值、体积、体积重、计费重、规则版本、设备、操作员和复核版本 |
| 差异单 | 申报值、实测值、差异类型、阈值、责任人、确认和调整记录 |
| 异常单 | 发生阶段、类型、暂停范围、责任人、期限、证据、解决和恢复记录 |
| 库位 | 仓库、库区、稳定编码、名称、类型、容量约束、状态和数据范围 |
| 上架任务 | 收货批次、目标库位、数量/箱件范围、执行人、状态和完成时间 |
| 位置记录 | 处理单元、库位、数量/箱件范围、进入时间、移出时间和当前标记 |
| 冲正记录 | 原企业/订单/到仓单绑定、冲正原因、审批人、新绑定和完成时间 |
| 到仓时间线 | 事件、前后状态、操作者、发生时间、说明和请求关联 ID |
| 费用项目/计费规则 | 应收应付方向、单位、币种、税务属性、适用维度、计费方式、生效区间和规则版本 |
| 费用记录/调整 | 业务来源、往来方、计费输入、规则快照、原币金额、汇率、税额、状态和调整链 |
| 对账单/明细 | 对账对象、周期、费用、调整、已核销、余额、到期日、状态、版本和争议 |
| 收付款/核销 | 往来方、收付类型、金额、币种、流水、凭证、核销金额、反核销和审核记录 |
| 发票/汇率版本 | 发票方向、号码、金额、税率、日期、状态、附件,以及汇率日期、来源和锁定状态 |
所有基础资料引用都同时保存稳定 ID 和必要快照。仓库、往来单位、快递公司或库位后续改名、停用时,不改变历史到仓记录。
## 七、应用范围
### 管理后台 `apps/admin-web`
- 到仓单列表、筛选、详情和状态操作。
- 预约取货审核、仓库时间窗容量和服务商调度。
- 到仓签到、收货批次、处理单元标签、测量和照片。
- 差异处理、异常暂停、客户确认结果、调整和主管冲正。
- 库区/库位维护、隔离区、上架任务及当前货物位置查询。
- 费用项目、计费规则、费用审核与调整、应收应付、客户/供应商对账、收付款核销、发票登记和利润查询。
- 权限至少拆分为查看、预约调度、收货、标签、测量、差异处理、异常解除、冲正、库位维护、上架、规则发布、费用调整、费用审核、对账审核、收付款、核销、反核销、发票登记和利润查看。
### 企业客户端 `apps/client-web`
- 创建取货预约或送货预报。
- 查看预约安排、到仓时间线、实测结果和公开照片。
- 查看并确认差异;确认失败时保留页面输入和上下文。
- 查看部分到仓、剩余待到仓数量、业务暂停和异常处理结果。
- 查看上架完成结果,不暴露其他企业、内部库位运营信息或平台内部备注。
- 查看本企业账单、费用明细和付款状态,确认或提出异议;不暴露应付成本和内部利润。
### 服务端 `services/api`
- 最终保证身份、租户、仓库数据范围、状态转换、幂等、并发和审计。
- 管理到仓单、预约容量、收货批次、处理单元、标签、测量规则、差异、异常、库位、位置记录、上架、冲正及时间线事务边界。
- 管理计费规则版本、费用生成幂等、费用调整、应收应付、对账单版本、收付款核销、发票登记、汇率锁定和利润权限边界。
- 文件访问使用不可预测标识和授权下载,不信任客户端文件名与 MIME 类型。
- 联系方式、司机信息、照片和交接凭证执行字段脱敏、最小返回、导出审计、保留期限和到期清理。
- 为未来 PDA 或外部调度接口预留适配边界,但二期不绑定厂商 SDK。
## 八、建议里程碑
| 里程碑 | 交付目标 | 进入条件 |
|---|---|---|
| P2-M0 规则锁定 | 到仓规则、计费财务边界、费用项目、规则版本、结算、核销、汇率和权限基线确认 | 一期订单及基础资料契约稳定 |
| P2-M1 到仓预报 | 预约取货、送货上门预报及平台调度形成闭环 | 快递公司、往来单位和收货仓 API 可用 |
| P2-M2 到仓收货 | 到仓签到、分批收货、处理单元标签、异常、冲正和照片可追溯 | 文件存储和仓库权限可用 |
| P2-M3 测量差异 | 测量规则版本、可复算结果、自动对比、客户确认、隔离和调整闭环可用 | 测量口径和阈值确认 |
| P2-M4 库位上架 | 库位档案、隔离区、上架任务、处理单元当前位置和历史位置可查 | 库位编码、容量和拆分规则确认 |
| P2-M5 计费财务 | 费用生成、应收应付、客户/供应商对账、收付款核销和单票利润闭环可用 | Q13-Q16 确认,P2-M3 实测数据可用 |
| P2-M6 联调验收 | 跨端联调、迁移、安全、异常、财务勾稽、性能和发布完成 | P2-M1 至 P2-M5 验收通过 |
## 九、建议开发任务
| ID | 端 | 模块 | 交付内容 | 优先级 | 依赖 |
|---|---|---|---|---|---|
| P2-BASE-01 | 公共 | 到仓契约 | 状态、枚举、权限、事件、错误码和快照模型 | P0 | 一期订单契约 |
| P2-BASE-02 | 公共 | 数据模型 | 到仓单、计划分配、预约容量、批次、处理单元、标签、组托版本、测量规则、差异、异常、库位、位置、冲正和时间线 | P0 | P2-BASE-01 |
| P2-ADM-01 | 后台 | 到仓列表 | 查询、筛选、分页、详情、空态和异常态 | P0 | P2-BASE-01 |
| P2-ADM-02 | 后台 | 预约调度 | 审核预约、分配服务商、调整时间和取消 | P0 | P2-ADM-01 |
| P2-SCH-01 | 后台 | 预约容量 | 营业时间、停收日期、时间窗容量、冲突和人工超额授权 | P0 | P2-ADM-02 |
| P2-CLI-01 | 客户端 | 到仓预报 | 预约取货、送货上门、联系人地址带入、可用时间窗和提交 | P0 | P2-BASE-01、P2-SCH-01 |
| P2-WH-01 | 后台 | 到仓签到 | 识别到仓单、分批收货、交接和异常登记 | P0 | P2-BASE-02 |
| P2-LABEL-01 | 后台 | 处理单元标签 | 箱件/托盘编码、佳博 1324D 实机适配、打印结果、失败重试、补打、换标、作废和重复码处理 | P0 | P2-WH-01 |
| P2-REV-01 | 后台 | 收货冲正 | 错绑企业/订单/到仓单的主管审批、解绑和重新认领 | P0 | P2-WH-01 |
| P2-WH-02 | 后台 | 入库测量 | 原始测量、规则版本、换算、复核、照片和历史复算 | P0 | P2-LABEL-01 |
| P2-DIFF-01 | 公共 | 差异引擎 | 申报实测对比、阈值、差异单和阻塞规则 | P0 | P2-WH-02、P2-HOLD-01 |
| P2-ADM-03 | 后台 | 差异处理 | 复核、说明、调整、关闭和审计 | P0 | P2-DIFF-01 |
| P2-CLI-02 | 客户端 | 差异确认 | 查看实测、公开照片、确认或反馈差异 | P0 | P2-DIFF-01 |
| P2-HOLD-01 | 公共 | 异常与暂停 | 异常单、业务暂停、隔离、责任期限、解除和恢复阶段 | P0 | P2-BASE-02 |
| P2-LOC-01 | 后台 | 库位维护 | 库区、库位编码、类型、容量、状态和权限范围 | P0 | P2-BASE-02 |
| P2-PUT-01 | 后台 | 上架任务 | 创建、分配、拆分、隔离上架、异常和完成 | P0 | P2-LOC-01、P2-WH-02、P2-HOLD-01 |
| P2-POS-01 | 公共 | 位置记录 | 处理单元当前位置、进入/移出历史和库位占用校验 | P0 | P2-PUT-01 |
| P2-SEC-01 | 公共 | 隐私与保留 | 联系方式脱敏、照片授权、导出审计、保留期限和清理 | P0 | P2-WH-01 |
| P2-TIME-01 | 公共 | 时间线通知 | 对客事件、站内提醒和失败补偿记录 | P1 | 核心流程完成 |
| P2-FEE-01 | 后台 | 费用项目 | 费用项目、方向、单位、币种、税务属性和启停 | P0 | P2-BASE-01 |
| P2-RATE-01 | 后台/服务端 | 计费规则 | 规则维度、版本、生效区间、阶梯/最低价和发布审核 | P0 | P2-FEE-01、P2-WH-02 |
| P2-CALC-01 | 服务端 | 计费引擎 | 规则匹配、试算、事件触发、幂等、计算快照和复算 | P0 | P2-RATE-01 |
| P2-FEE-02 | 后台 | 费用台账 | 自动/人工费用、审核、调整、红冲、锁定和审计 | P0 | P2-CALC-01 |
| P2-ARAP-01 | 后台 | 应收应付 | 客户应收、供应商应付、查询汇总、权限和导出 | P0 | P2-FEE-02、一期往来单位 |
| P2-RECON-01 | 后台 | 对账单 | 按票/月结生成、审核、确认、争议、版本和关闭 | P0 | P2-ARAP-01 |
| P2-CLI-03 | 客户端 | 账单中心 | 对客账单列表、明细、确认、异议和付款状态 | P0 | P2-RECON-01 |
| P2-CASH-01 | 后台 | 收付款核销 | 收付款登记、凭证、审核、部分核销、反核销和余额 | P0 | P2-RECON-01 |
| P2-INV-01 | 后台 | 发票登记 | 开票/收票申请、号码、金额、税率、状态和附件 | P1 | P2-RECON-01 |
| P2-PROFIT-01 | 后台 | 单票利润 | 收入、成本、毛利、毛利率、折算和权限控制 | P1 | P2-ARAP-01、汇率版本 |
| P2-TEST-01 | 公共 | 自动化测试 | 越权、幂等、计费复算、费用调整、对账勾稽、核销、反核销、标签、差异、位置、文件和隐私测试 | P0 | 所有 P0 模块 |
| P2-REL-01 | 公共 | 发布验收 | 初始化预约日历、库区库位、测量规则、标签打印参数、迁移、联调、验收和生产发布 | P0 | P2-TEST-01 |
## 十、测试与验收重点
- 企业 A 不能读取、预约、确认企业 B 的到仓单和差异。
- 无仓库权限的员工不能收货、测量或分配库位。
- 重复预约、签到、标签生成、测量提交和上架完成不会产生重复记录。
- 取货调度、到仓主进度、差异、上架和异常状态独立维护;任一异常解除后能恢复到原业务阶段,不覆盖或倒退其他状态域。
- 一张订单多到仓单、一个到仓单多批收货的累计、剩余、部分收货和人工关闭结果正确。
- 少货人工关闭后不能继续登记原到仓单批次,也不会跳过测量、差异确认或正式上架;后续补货通过新到仓单关联原单。
- 多张有效到仓单不能超额分配同一订单货物明细;取消和人工关闭后未使用计划量按规则释放。
- 未认领货物不会归入错误订单或企业;错误绑定只能通过授权冲正恢复,原记录不可删除。
- 标签补打、换标和作废可追溯,重复标签不能同时绑定两个有效处理单元。
- 使用佳博 1324D 实机验证标签内容、尺寸、清晰度、连续打印、失败提示和重试;断电、缺纸或连接中断不会误记为打印成功。
- 组托、拆托和重新组托保留箱件父子关系及版本,任一箱件不会同时属于两个有效托盘。
- 实测修改保留旧版本、原因和操作者,客户确认绑定明确版本。
- 历史实测使用保存的单位、系数、精度、舍入规则和公式版本可复算出相同结果。
- 差异未关闭时严格执行已确认的上架和后续流程阻塞规则。
- 隔离区上架不会标记正式上架完成;解除暂停后生成到正式库位的位置变更。
- 上架完成后每个处理单元有且只有一个有效当前位置,停用或容量不足库位被服务端拒绝。
- 停用仓库、快递公司、往来单位和库位不能用于新业务,但历史记录可读。
- 照片和附件覆盖类型、大小、恶意内容、越权访问和删除引用。
- 联系方式按角色脱敏,敏感文件下载和批量导出有审计,过期数据按保留策略清理。
- 相同计费事件重试不重复生成费用,历史费用能按规则、输入和汇率快照复算一致。
- 费用调整、红冲、对账版本、核销和反核销形成不可删除的审计链。
- 对账单费用、调整、已核销和余额勾稽一致;多币种不直接相加且折算可追溯。
- 客户及非财务角色不能读取供应商成本、内部利润和其他企业财务数据。
- 页面覆盖加载、空态、失败、无权限、重复操作、数据过期和窄屏场景。
- 二期验收终点为指定库位完成,不以库存或出仓功能作为完成条件。
## 十一、启动前待确认事项
### 已采用的规划基线
| 编号 | 规划基线 | 说明 |
|---|---|---|
| P2-B1 | 订单 1:N 到仓单,到仓单 1:N 收货批次,批次 1:N 处理单元 | 支持分次预约和分批实际到仓 |
| P2-B2 | 异常单和业务暂停独立于到仓主状态 | 异常解除后恢复原进度,不覆盖或猜测主阶段 |
| P2-B3 | 二期建立处理单元和位置记录 | 只支撑标签、指定库位和三期拣货输入,不扩张为完整库存系统 |
| P2-B4 | 差异未关闭默认业务暂停,不建设单独隔离区 | 暂停期间不得进入正式上架完成和计费锁定节点 |
| P2-B5 | 错误收货通过主管冲正处理 | 不删除原记录,保留原绑定、原因、审批和重新认领结果 |
### 已确认的业务决策
| 编号 | 已确认事项 | 实施说明 |
|---|---|---|
| P2-Q1 | 少货处理以实际入库货物为准 | 实收和计划差异进入客户确认,原到仓单按实收结果处理 |
| P2-Q2 | 不走系统服务商调度 | 取货安排在线下完成,系统不建设自动服务商分配;相关费用人工添加 |
| P2-Q3 | 人工线下预约时间 | 系统记录预约结果、改期和取消留痕,不建设时间窗容量调度 |
| P2-Q4 | 毛重及材积重公式已确认 | 毛重相关值按毛重÷363;材积重按(长×宽×高÷6000)×箱数,单位和舍入写入规则版本 |
| P2-Q5 | 差异需要客户确认 | 差异确认前保持业务暂停 |
| P2-Q6 | 不建设单独隔离区域 | 以业务暂停控制后续操作,不生成隔离库位流程 |
| P2-Q7 | 当前不做库位容量校验 | 保留库位启停和权限校验,不按重量、箱数或体积拦截 |
| P2-Q8 | 上传称重照片和测量照片 | 收货测量流程提供对应附件上传和权限控制 |
| P2-Q11 | 敏感数据和凭证保存 12 个月 | 起算点及到期删除/归档方式在技术规则中统一定义 |
| P2-Q12 | 箱件/托盘标签使用佳博 1324D 热敏标签打印机 | 设备型号已锁定;标签纸尺寸、连接方式、驱动、指令协议和仓库部署参数在开发前通过实机核验 |
### 暂缓事项
| 编号 | 暂缓事项 | 二期临时基线 |
|---|---|---|
| P2-Q9 | 是否使用 PDA | 二期不接 PDA,使用后台或响应式作业页 |
| P2-Q10 | 无预报货物认领和超期处理 | 无预报货物不得直接绑定企业订单,完整认领和超期规则后续确认 |
### 仍需产品确认
以下事项由项目负责人依据 [二期需求确认清单](二期需求确认清单.md) 组织业务、仓库、财务和技术形成临时结论。本表保留规划摘要;状态为“临时基线”且结论可转为开发规则时可用于当前开发,产品到岗复核后改为“已确认”。
| 编号 | 待确认问题 | 当前建议 |
|---|---|---|
| P2-Q13 | 二期计费财务做到哪一个闭环终点? | 建议做到线下收付款登记和核销,不做总账会计 |
| P2-Q14 | 首批费用项目及应收/应付方向是什么? | 按到仓相关费用建立白名单,其他费用人工新增需审核 |
| P2-Q15 | 计费规则需要哪些维度、阶梯和最低收费? | 至少支持客户、供应商、仓库、服务和有效期版本 |
| P2-Q16 | 费用在什么业务节点生成并锁定? | 实测试算,差异关闭后确认;特殊费用按事件生成 |
P2-Q17 至 P2-Q24 已根据财务原型和安全实现规则形成二期基线,不再要求产品重复确认;详细口径见 [二期计费财务需求确认表](二期计费财务需求确认表.md)。
## 十二、建议实施顺序
1. 锁定仍需业务确认的口径,并完成到仓领域契约和数据关系评审。
2. 先实现服务端到仓单、预约容量、权限和部分收货汇总,再接客户预报与后台调度。
3. 实现到仓签到、分批收货、处理单元标签、照片、主管冲正和待认领流程。
4. 实现测量规则版本、原始测量、历史复算、差异对比和客户确认闭环。
5. 实现异常单、业务暂停、隔离区、解除和恢复阶段。
6. 实现库位容量、上架任务、处理单元当前位置和历史位置,终点锁定为“指定库位完成”。
7. 建立费用项目、计费规则和汇率版本,接入实测数据完成试算、审核、调整和应收应付台账。
8. 实现客户/供应商对账、账单中心、线下收付款、核销、反核销、发票登记和单票利润。
9. 完成跨租户、财务权限、幂等、并发、计费复算、对账勾稽、文件、隐私、异常和冲正流程测试。
10. 使用脱敏初始化数据联调、验收和灰度发布。
+130
View File
@@ -0,0 +1,130 @@
# 虫虫出海二期开发进度表:到仓与计费财务
> 创建日期:2026-07-28
> 更新日期:2026-07-30
> 当前阶段:开发已启动,需求确认并行
> 总体状态:已开工,具体任务进度待回填
二期严格以头程物流“02 到仓”为范围,规划和业务问题分别见 [二期开发规划](二期开发规划.md)与[二期需求确认清单](二期需求确认清单.md)。`docs/悦` 中的原型用于确认已有业务线索,不代表功能已经开发完成。
## 一、进度口径
- 开发任务共 31 项。项目已开工,但尚未收到具体已开工任务、完成量和负责人信息,因此任务表暂不虚填进度;开发负责人应在本表逐项回填状态和百分比。
- 二期共跟踪 24 项:4 项已确认、6 项确认中、2 项暂缓、4 项待确认、8 项原型基线。
- 仍需产品处理 11 项:P2-Q1、Q4、Q5、Q6、Q8、Q10、Q11、Q13 至 Q16。
- 无需重复确认:P2-Q2、Q3、Q7、Q9、Q12,以及已采用原型基线的 P2-Q17 至 Q24。
- 原型覆盖只表示存在页面、字段或流程参考,不能计入开发进度。
- 产品岗位空缺期间,业务、仓库、财务和技术在 [二期需求确认清单](二期需求确认清单.md) 中形成临时基线;后续产品复核产生的变化按需求变更处理。
## 二、总体进度
| 工作流 | 范围 | 状态 | 开发进度 | 需求状态 | 负责人 | 备注 |
|---|---|---|---:|---|---|---|
| 规则与数据 | 到仓契约、数据模型、安全和隐私 | 需求确认中 | 0% | 到仓规则已确认;财务规则待确认 | 待分配 | 一期订单及基础资料契约是前置依赖 |
| 到仓预报 | 预约取货、送货上门、预约容量和调度 | 待开始 | 0% | P2-Q2、Q3 已确认 | 待分配 | 改为人工线下预约,系统记录预约结果 |
| 到仓收货 | 签到、分批收货、标签、异常和冲正 | 需求确认中 | 0% | P2-Q1、Q8 已确认;Q10 暂缓 | 待分配 | 无预报货物临时禁止直接绑定企业订单 |
| 测量差异 | 实测、审核、差异阈值、客户确认和暂停 | 待开始 | 0% | P2-Q4、Q5、Q6 已确认 | 待分配 | 不设隔离区,差异期间采用业务暂停 |
| 库位上架 | 库位、状态、上架任务和位置历史 | 待开始 | 0% | P2-Q7 已确认;Q9 暂缓 | 待分配 | 不做容量校验,二期暂不接 PDA |
| 计费财务 | 费用规则、应收应付、对账、核销、发票和利润 | 需求确认中 | 0% | P2-Q13 至 Q16 待形成临时基线;Q17 至 Q24 原型基线 | 待分配 | 当前会议需确定范围、首批费用、规则复杂度和计费节点 |
| 联调发布 | 时间线、测试、初始化和发布 | 待开始 | 0% | 依赖全部 P0 决策 | 待分配 | 需求基线确认后再排期 |
## 三、详细任务表
| ID | 里程碑 | 端 | 模块 | 功能/交付内容 | 优先级 | 状态 | 进度 | 原型覆盖 | 需求阻塞 | 依赖 | 负责人 | 计划开始 | 计划完成 | 备注 |
|---|---|---|---|---|---|---|---:|---|---|---|---|---|---|---|
| P2-BASE-01 | P2-M0 | 公共 | 到仓契约 | 状态、枚举、权限、事件、错误码和快照模型 | P0 | 需求确认中 | 0% | 部分 | Q1-Q11 | 一期订单契约 | 待分配 | 待排期 | 待排期 | 原型存在状态线索,最终契约以业务确认结果为准 |
| P2-BASE-02 | P2-M0 | 公共 | 数据模型 | 到仓单、计划分配、预约容量、批次、处理单元、标签、组托、测量、差异、异常、库位、位置和冲正 | P0 | 需求确认中 | 0% | 部分 | Q1-Q11 | P2-BASE-01 | 待分配 | 待排期 | 待排期 | 原型字段可参考,数据关系按二期规划建设 |
| P2-ADM-01 | P2-M1 | 后台 | 到仓列表 | 查询、筛选、分页、详情、空态和异常态 | P0 | 待开始 | 0% | 较完整 | - | P2-BASE-01 | 待分配 | 待排期 | 待排期 | 后台原型有入库单列表、筛选和详情入口 |
| P2-ADM-02 | P2-M1 | 后台 | 预约调度 | 审核预约、分配服务商、调整时间和取消 | P0 | 待开始 | 0% | 部分 | Q2、Q3 | P2-ADM-01 | 待分配 | 待排期 | 待排期 | 原型未明确服务商分配和改期规则 |
| P2-SCH-01 | P2-M1 | 后台 | 预约容量 | 营业时间、停收日期、时间窗容量、冲突和人工超额授权 | P0 | 待开始 | 0% | 无 | Q3 | P2-ADM-02 | 待分配 | 待排期 | 待排期 | 需要业务确认时间窗及容量口径 |
| P2-CLI-01 | P2-M1 | 客户端 | 到仓预报 | 预约取货、送货上门、联系人地址带入、可用时间窗和提交 | P0 | 待开始 | 0% | 部分 | Q2、Q3 | P2-BASE-01、P2-SCH-01 | 待分配 | 待排期 | 待排期 | 原型有提货方式和提货信息,没有完整预约闭环 |
| P2-WH-01 | P2-M2 | 后台 | 到仓签到 | 识别到仓单、分批收货、交接和异常登记 | P0 | 待开始 | 0% | 部分 | Q1、Q8、Q10 | P2-BASE-02 | 待分配 | 待排期 | 待排期 | 原型有待到仓、收货中状态,未覆盖无预报认领 |
| P2-LABEL-01 | P2-M2 | 后台 | 处理单元标签 | 箱件/托盘编码、佳博 1324D 实机适配、打印结果、失败重试、补打、换标和作废 | P0 | 待开始 | 0% | 部分 | 设备参数核验 | P2-WH-01 | 待分配 | 待排期 | 待排期 | 型号已确认;纸张、连接、驱动和指令协议待实机核验 |
| P2-REV-01 | P2-M2 | 后台 | 收货冲正 | 错绑企业/订单/到仓单的主管审批、解绑和重新认领 | P0 | 待开始 | 0% | 无 | Q10 | P2-WH-01 | 待分配 | 待排期 | 待排期 | 原记录不可删除 |
| P2-WH-02 | P2-M3 | 后台 | 入库测量 | 原始测量、规则版本、换算、复核、照片和历史复算 | P0 | 待开始 | 0% | 较完整 | Q4、Q8 | P2-LABEL-01 | 待分配 | 待排期 | 待排期 | 原型有 cm、kg、m³、材积重及测量审核 |
| P2-DIFF-01 | P2-M3 | 公共 | 差异引擎 | 申报实测对比、阈值、差异单和阻塞规则 | P0 | 待开始 | 0% | 部分 | Q4、Q5 | P2-WH-02、P2-HOLD-01 | 待分配 | 待排期 | 待排期 | 原型有对比和审核,没有阈值 |
| P2-ADM-03 | P2-M3 | 后台 | 差异处理 | 复核、说明、调整、关闭和审计 | P0 | 待开始 | 0% | 部分 | Q5、Q6 | P2-DIFF-01 | 待分配 | 待排期 | 待排期 | 原型有审核不通过和失败原因 |
| P2-CLI-02 | P2-M3 | 客户端 | 差异确认 | 查看实测、公开照片、确认或反馈差异 | P0 | 待开始 | 0% | 无 | Q5、Q8 | P2-DIFF-01 | 待分配 | 待排期 | 待排期 | 客户确认流程需要新增 |
| P2-HOLD-01 | P2-M3 | 公共 | 异常与暂停 | 异常单、业务暂停、隔离、责任期限、解除和恢复阶段 | P0 | 待开始 | 0% | 部分 | Q1、Q5、Q6、Q10 | P2-BASE-02 | 待分配 | 待排期 | 待排期 | 原型仅有是否扣货和扣货理由 |
| P2-LOC-01 | P2-M4 | 后台 | 库位维护 | 库区、库位编码、类型、容量、状态和权限范围 | P0 | 待开始 | 0% | 较完整 | Q7 | P2-BASE-02 | 待分配 | 待排期 | 待排期 | 原型有库区、类型、最大承重和占用状态 |
| P2-PUT-01 | P2-M4 | 后台 | 上架任务 | 创建、分配、拆分、隔离上架、异常和完成 | P0 | 待开始 | 0% | 较完整 | Q6、Q7、Q9 | P2-LOC-01、P2-WH-02、P2-HOLD-01 | 待分配 | 待排期 | 待排期 | 原型有待上架、上架中、已完成及目标库位 |
| P2-POS-01 | P2-M4 | 公共 | 位置记录 | 处理单元当前位置、进入/移出历史和库位占用校验 | P0 | 待开始 | 0% | 部分 | Q7 | P2-PUT-01 | 待分配 | 待排期 | 待排期 | 原型有库存流水线索,二期不建设完整库存系统 |
| P2-SEC-01 | P2-M0 | 公共 | 隐私与保留 | 联系方式脱敏、照片授权、导出审计、保留期限和清理 | P0 | 需求确认中 | 0% | 无 | Q11 | P2-WH-01 | 待分配 | 待排期 | 待排期 | 保存期限需业务、法务和运维确认 |
| P2-FEE-01 | P2-M5 | 后台 | 费用项目 | 费用项目、方向、单位、币种、税务属性和启停 | P0 | 需求确认中 | 0% | 较完整 | Q14、Q18 | P2-BASE-01 | 待分配 | 待排期 | 待排期 | 后台原型有费用项目维护 |
| P2-RATE-01 | P2-M5 | 后台/服务端 | 计费规则 | 规则维度、版本、生效区间、阶梯/最低价和发布审核 | P0 | 需求确认中 | 0% | 部分 | Q15-Q18 | P2-FEE-01、P2-WH-02 | 待分配 | 待排期 | 待排期 | 原型有费用字段,缺可审计的规则版本 |
| P2-CALC-01 | P2-M5 | 服务端 | 计费引擎 | 规则匹配、试算、事件触发、幂等、计算快照和复算 | P0 | 需求确认中 | 0% | 无 | Q15-Q18 | P2-RATE-01 | 待分配 | 待排期 | 待排期 | 实测数据是主要计费输入 |
| P2-FEE-02 | P2-M5 | 后台 | 费用台账 | 自动/人工费用、审核、调整、红冲、锁定和审计 | P0 | 需求确认中 | 0% | 较完整 | Q16、Q18、Q19 | P2-CALC-01 | 待分配 | 待排期 | 待排期 | 原型有费用新增、审核和删除入口,删除需改为红冲 |
| P2-ARAP-01 | P2-M5 | 后台 | 应收应付 | 客户应收、供应商应付、查询汇总、权限和导出 | P0 | 需求确认中 | 0% | 较完整 | Q13、Q17-Q19 | P2-FEE-02、一期往来单位 | 待分配 | 待排期 | 待排期 | 原型有客户应收应付与供应商费用 |
| P2-RECON-01 | P2-M5 | 后台 | 对账单 | 按票/月结生成、审核、确认、争议、版本和关闭 | P0 | 需求确认中 | 0% | 较完整 | Q20、Q21 | P2-ARAP-01 | 待分配 | 待排期 | 待排期 | 原型有客户/供应商对账单与明细 |
| P2-CLI-03 | P2-M5 | 客户端 | 账单中心 | 对客账单列表、明细、确认、异议和付款状态 | P0 | 需求确认中 | 0% | 部分 | Q20、Q21 | P2-RECON-01 | 待分配 | 待排期 | 待排期 | 客户端已有应付账单列表和明细参考 |
| P2-CASH-01 | P2-M5 | 后台 | 收付款核销 | 收付款登记、凭证、审核、部分核销、反核销和余额 | P0 | 需求确认中 | 0% | 较完整 | Q17、Q22 | P2-RECON-01 | 待分配 | 待排期 | 待排期 | 原型有收付款状态,核销勾稽规则需补充 |
| P2-INV-01 | P2-M5 | 后台 | 发票登记 | 开票/收票申请、号码、金额、税率、状态和附件 | P1 | 需求确认中 | 0% | 部分 | Q18、Q23 | P2-RECON-01 | 待分配 | 待排期 | 待排期 | 二期建议不接外部开票平台 |
| P2-PROFIT-01 | P2-M5 | 后台 | 单票利润 | 收入、成本、毛利、毛利率、折算和权限控制 | P1 | 需求确认中 | 0% | 较完整 | Q17、Q24 | P2-ARAP-01、汇率版本 | 待分配 | 待排期 | 待排期 | 原型有利润表和收入成本明细 |
| P2-TIME-01 | P2-M6 | 公共 | 时间线通知 | 对客事件、站内提醒和失败补偿记录 | P1 | 待开始 | 0% | 部分 | Q1、Q5、Q8、Q21 | 核心流程完成 | 待分配 | 待排期 | 待排期 | 外部通知渠道不纳入未确认范围 |
| P2-TEST-01 | P2-M6 | 公共 | 自动化测试 | 越权、幂等、计费复算、费用调整、对账勾稽、核销、反核销、标签、差异、位置、文件和隐私测试 | P0 | 待开始 | 0% | 不适用 | 全部 P0 决策 | 所有 P0 模块 | 待分配 | 待排期 | 待排期 | 原型覆盖不能代替自动化测试 |
| P2-REL-01 | P2-M6 | 公共 | 发布验收 | 初始化业务/财务基础数据、迁移、联调、勾稽检查、验收和发布 | P0 | 待开始 | 0% | 不适用 | 全部 P0 决策 | P2-TEST-01 | 待分配 | 待排期 | 待排期 | 需求基线确认后才能承诺排期 |
## 四、里程碑
| 里程碑 | 交付目标 | 状态 | 进度 | 进入条件 | 计划完成 |
|---|---|---|---:|---|---|
| P2-M0 规则锁定 | 业务决策、到仓/财务契约、数据关系和权限基线确认 | 需求确认中 | 42% | 一期订单及基础资料契约稳定 | 待排期 |
| P2-M1 到仓预报 | 预约取货、送货预报和平台调度闭环 | 待开始 | 0% | Q2、Q3 确认 | 待排期 |
| P2-M2 到仓收货 | 签到、分批收货、标签、异常、冲正和照片可追溯 | 待开始 | 0% | Q1、Q8、Q10 及标签参数确认 | 待排期 |
| P2-M3 测量差异 | 实测、复算、差异、客户确认和隔离闭环 | 待开始 | 0% | Q4、Q5、Q6 确认 | 待排期 |
| P2-M4 库位上架 | 库位、上架任务和位置历史可查 | 待开始 | 0% | Q7、Q9 确认 | 待排期 |
| P2-M5 计费财务 | 计费、应收应付、对账、核销和利润闭环可用 | 需求确认中 | 0% | Q13-Q16 确认,P2-M3 实测数据可用 | 待排期 |
| P2-M6 联调验收 | 跨端联调、安全、勾稽、测试、初始化和发布完成 | 待开始 | 0% | P2-M1 至 P2-M5 验收通过 | 待排期 |
## 五、原型映射
| 流程/能力 | 原型依据 | 覆盖判断 | 二期任务 | 缺口或边界 |
|---|---|---|---|---|
| 预约取货/送货上门 | 入库单提货方式、提货时间和联系人字段 | 部分 | P2-ADM-02、P2-SCH-01、P2-CLI-01 | 缺服务商分配、时间窗容量、改期和爽约规则 |
| 到仓收货 | 海外仓入库单的待到仓、收货中、已上架状态 | 部分 | P2-WH-01、P2-REV-01 | 缺分批收货、待认领和冲正闭环 |
| 入库测量 | 入库单测量、测量审核、原单/实测对比 | 较完整 | P2-WH-02 | 缺计算规则版本、精度、舍入和历史复算 |
| 差异核对 | 审核通过/不通过、失败原因、是否扣货 | 部分 | P2-DIFF-01、P2-ADM-03、P2-CLI-02、P2-HOLD-01 | 缺阈值、客户确认、隔离和释放规则 |
| 指定库位 | 库位维护、最大承重、上架任务和上架状态 | 较完整 | P2-LOC-01、P2-PUT-01、P2-POS-01 | 缺容量强校验、隔离区和位置历史契约 |
| 标签打印 | 唛头导出、打印列表、Zebra/HP 打印机示例 | 部分 | P2-LABEL-01 | 二期实际设备为佳博 1324D,需实机适配 |
| 费用与应收应付 | 费用项目、客户应收应付、供应商费用 | 较完整 | P2-FEE-01、P2-FEE-02、P2-ARAP-01 | 缺规则版本、幂等、红冲和权限契约 |
| 对账与核销 | 客户/供应商对账、结算单应收应付、收付款状态 | 较完整 | P2-RECON-01、P2-CLI-03、P2-CASH-01 | 缺多对多核销、反核销和勾稽规则 |
| 发票与利润 | 发票状态、利润表、收入成本明细 | 部分 | P2-INV-01、P2-PROFIT-01 | 外部开票不纳入;利润和汇率采用原型基线 |
| PDA | 头程“03 出仓”PDA 拣货原型 | 不纳入二期 | - | 二期到仓不接 PDA,待业务确认边界 |
| 完整库存/WMS | 库存查询、理货、盘点、出库等原型 | 不纳入二期 | - | 二期仅维护处理单元和位置记录 |
## 六、需求确认进度
| 编号 | 状态 | 主题 | 产品结论 | 涉及业务角色 |
|---|---|---|---|---|
| P2-Q1 | 确认中 | 少货到仓单人工关闭 | 方向已确认,关闭权限、条件、凭证和通知待补 | 仓库、平台运营 |
| P2-Q2 | 已确认 | 预约取货服务商类型 | 不走系统服务商调度,人工添加费用项目 | 业务、调度 |
| P2-Q3 | 已确认 | 精确时间或预约时间窗 | 人工线下预约时间,系统记录结果 | 仓库、调度 |
| P2-Q4 | 确认中 | 实测单位和计费公式 | 公式方向已确认,含义、单位、计费重和舍入待补 | 仓库、财务/计费 |
| P2-Q5 | 确认中 | 差异阈值和客户确认 | 确认方向已明确,拒绝、超时和阻塞规则待补 | 业务、仓库、客服 |
| P2-Q6 | 确认中 | 隔离区释放规则 | 不设隔离区,暂停位置、解除权限和条件待补 | 仓库、业务 |
| P2-Q7 | 已确认 | 库位容量校验口径 | 当前不做库位容量校验 | 仓库 |
| P2-Q8 | 确认中 | 到仓照片必传节点 | 照片类型已明确,必传粒度和附件规则待补 | 仓库、客服 |
| P2-Q9 | 暂缓 | 二期是否使用 PDA | 临时按二期不接 PDA 处理 | 仓库、产品 |
| P2-Q10 | 暂缓 | 无预报货物认领和超期处理 | 临时禁止直接绑定企业订单 | 仓库、平台运营 |
| P2-Q11 | 确认中 | 敏感数据和凭证保存期限 | 12 个月已明确,起算点和到期处理待补 | 业务、法务、运维 |
| P2-Q12 | 已确认 | 标签打印设备 | 佳博 1324D 热敏标签打印机 | 业务 |
| P2-Q13 | 待确认 | 二期计费财务闭环终点 | 待确认 | 产品、业务、财务、技术 |
| P2-Q14 | 待确认 | 首批费用项目及收付方向 | 待确认 | 业务、财务/计费 |
| P2-Q15 | 待确认 | 计费维度、阶梯和最低收费 | 待确认 | 业务、财务/计费 |
| P2-Q16 | 待确认 | 费用生成和锁定节点 | 待确认 | 业务、仓库、财务/计费 |
| P2-Q17 | 原型基线 | 币种、本位币和汇率 | CNY 本位币,保存原币、人工汇率和锁定快照 | 财务 |
| P2-Q18 | 原型基线 | 税务、金额精度和舍入 | 保存未税、税率、税额、含税;金额 2 位、汇率 6 位 | 财务/税务 |
| P2-Q19 | 原型基线 | 人工费用调整和审批 | 经办提交、财务审核,生效后只允许调整或红冲 | 财务、内控 |
| P2-Q20 | 原型基线 | 对账方式和结算周期 | 按选择或截止时间生成,保存结算期间和账期 | 业务、财务、客服 |
| P2-Q21 | 原型基线 | 对账审核、确认和争议 | 审核、确认、拒绝、锁定,争议产生新版本 | 业务、财务、客服 |
| P2-Q22 | 原型基线 | 收付款与核销规则 | 线下收付、部分核销和反核销,不含预收及跨币种 | 财务、内控 |
| P2-Q23 | 原型基线 | 发票管理边界 | 只登记,不接外部开票平台 | 财务/税务、产品 |
| P2-Q24 | 原型基线 | 利润口径与查看权限 | 生效应收减应付,利润率为利润÷收入,财务授权可见 | 财务、管理层 |
## 七、状态维护规则
- 状态统一使用:需求确认中、待开始、进行中、联调中、测试中、已完成、已阻塞。
- 原型覆盖统一使用:较完整、部分、无、不适用;不得把“较完整”换算为开发进度。
- 负责人、计划开始和计划完成由排期会议填写;在需求基线确认前保持“待分配/待排期”。
- 开始编码后才可以填写大于 0% 的开发进度;开发、测试和验收均完成后才更新为“已完成”。
- 需求确认结论先回写 [二期需求确认清单](二期需求确认清单.md),再同步更新本表、开发规划、任务依赖和验收标准。
+167
View File
@@ -0,0 +1,167 @@
# 虫虫出海二期计费与财务模块规划
> 创建日期:2026-07-28
> 状态:开发已启动,范围确认并行
> 关联规划:[二期开发规划](二期开发规划.md)
> 原型依据:`docs/悦/货代SAAS平台后台/货代SaaS平台原型.html`、`docs/悦/头程下单/柏悦客户端.html`
## 一、建设目标
二期在“02 到仓”作业闭环上增加最小可用的计费与财务闭环,使每票到仓业务能够回答:按什么规则计费、产生了哪些应收和应付、是否完成对账、已收付多少、剩余多少,以及本票业务的收入、成本和毛利是多少。
```text
到仓实测/业务事件
↓
费用规则匹配与试算
↓
费用审核、调整与锁定
↓
应收 / 应付费用台账
↓
客户 / 供应商对账单
↓
收付款登记与核销
↓
单票利润与财务状态
```
## 二、二期纳入范围
### 1. 费用项目与计费规则
- 维护费用项目、应收/应付方向、默认币种、计费单位、税务属性和启停状态。
- 计费规则支持客户、供应商/往来单位、仓库、服务类型、货物属性、生效日期和失效日期等适用维度。
- 支持按票、箱、托、件、重量、体积、计费重、次数和固定金额计费;阶梯价、最低收费和封顶规则是否进入二期由业务确认。
- 规则必须版本化。历史费用保存规则版本、计费输入、计算过程和结果快照,不因新规则发布而变化。
- 二期的计费规则用于实际业务计费,不等同于对客询价或正式报价体系。
### 2. 费用生成与调整
- 到仓签到、实测确认、异常处理或指定库位完成后,根据已确认的触发节点生成费用草稿。
- 自动费用与人工费用均关联订单、到仓单、收货批次或处理单元,并记录客户/供应商、费用项目、数量、单价、币种、汇率、税额和来源。
- 同一业务事件重复投递不得重复生成费用;费用生成使用稳定来源键保证幂等。
- 费用支持试算、待审核、生效、已锁定、已冲销和作废状态。
- 生效费用不允许直接覆盖。调整通过新增调整行或红冲行处理,并保留原因、原记录、申请人和审核人。
### 3. 应收与应付
- 应收面向客户企业,应付面向快递公司、仓库服务商或其他往来单位。
- 支持按订单、到仓单、客户、供应商、费用项目、币种、审核状态和结算状态查询。
- 应收和应付分别授权;客户侧永不返回供应商成本、内部利润和内部调整备注。
- 多币种费用按原币保存,不允许直接跨币种相加;折算金额使用锁定汇率和汇率日期。
### 4. 对账单与账单
- 支持按票结算或按结算周期汇总生成客户/供应商对账单。
- 对账单包含期初、当期费用、调整、已核销、未核销、结算起止日期和到期日。
- 支持草稿、待审核、待对方确认、已确认、有争议、已关闭和已作废状态。
- 对账单确认后锁定明细;争议通过退回、调整费用和重新生成版本处理,不直接修改已确认版本。
- 企业客户端可以查看账单列表和明细、确认或提出异议;二期供应商对账采用后台登记确认结果,不建设供应商端。
### 5. 收付款与核销
- 二期支持线下收款、付款、退款和调整的登记,不接在线支付渠道。
- 支持部分核销、一笔收付款核销多张账单、一张账单被多笔收付款核销。
- 保存交易日期、往来单位、金额、币种、流水号、凭证、经办人和审核记录。
- 已审核核销不得删除;错误处理采用反核销或冲正,并保留完整审计链。
- 二期不纳入预收、预付及跨币种核销;确需支持时按范围变更处理。
### 6. 发票与利润
- 二期只登记发票申请、发票号码、金额、税率、开票/收票日期、状态和附件,不直接连接电子税局。
- 单票利润按已生效应收减已生效应付计算,同时展示未对账、未核销和异常费用提示。
- 利润报表至少支持按订单、客户、供应商、仓库和月份筛选;不同币种先按锁定汇率折算到本位币。
- 利润和成本只向财务及授权管理角色开放。
## 三、明确不纳入二期
- 对客询价、报价审批、报价转订单和合约报价体系。
- 在线支付、支付网关、自动扣款、银企直连和银行流水自动认领。
- 电子税局或第三方开票平台直连、税务申报和税控设备集成。
- 会计科目、会计凭证、总账、资产负债表、现金流量表及法定财务报表。
- 自动获取外部汇率;二期由财务维护并锁定业务使用汇率。
- 预算、资金计划、信用保险和复杂授信管理。
## 四、角色与权限
| 角色 | 主要权限 | 数据边界 |
|---|---|---|
| 计费专员 | 维护费用草稿、试算、发起调整 | 授权客户、仓库和业务范围 |
| 财务审核员 | 审核费用、对账单、收付款和核销 | 平台授权范围 |
| 财务管理员 | 维护费用项目、规则、汇率、结算参数和财务权限 | 平台授权范围 |
| 客服/运营 | 查看对客费用、发起对账、处理争议 | 授权客户范围,不可查看供应商成本和利润 |
| 企业客户 | 查看本企业账单、明细、付款状态并确认或提出异议 | 当前企业 |
| 平台管理员 | 配置角色权限和审计查询 | 不因管理员身份默认获得财务明细权限 |
关键动作至少拆分为:规则发布、费用新增、费用调整、费用审核、对账单审核、收款登记、付款登记、核销、反核销、发票登记、利润查看和财务导出。
## 五、核心数据对象
| 数据对象 | 关键内容 |
|---|---|
| 费用项目 | 编码、名称、应收/应付方向、计费单位、默认币种、税务属性和状态 |
| 计费规则版本 | 适用客户/供应商/仓库/服务、计费方式、阶梯、最低价、币种、生效区间和版本状态 |
| 费用记录 | 业务来源、应收/应付、往来方、计费输入、单价、金额、税额、币种、汇率、规则快照和状态 |
| 费用调整 | 原费用、调整类型、差额、原因、申请人、审核人和冲销链 |
| 对账单 | 对账对象、结算周期、币种、期初、费用、调整、核销、余额、到期日、状态和版本 |
| 对账单明细 | 对账单、费用记录、业务单据、原币金额、折算金额和争议状态 |
| 收付款记录 | 收/付/退/调、往来方、金额、币种、日期、流水号、凭证和审核状态 |
| 核销记录 | 收付款记录、账单/费用、核销金额、汇兑差额、操作者、时间和冲正关系 |
| 发票记录 | 发票方向、抬头、税号、号码、金额、税率、日期、状态、附件和关联账单 |
| 汇率版本 | 原币、目标币、汇率、汇率日期、来源、审核人和锁定状态 |
所有金额使用定点小数,不使用浮点数。金额和税额保留 2 位、业务汇率保留 6 位,由服务端统一四舍五入;原币金额、折算金额和使用汇率必须同时保存。
## 六、开发任务建议
| ID | 端 | 模块 | 交付内容 | 优先级 | 依赖 |
|---|---|---|---|---|---|
| P2-FEE-01 | 后台 | 费用项目 | 费用项目、方向、单位、币种、税务属性和启停 | P0 | P2-BASE-01 |
| P2-RATE-01 | 后台/服务端 | 计费规则 | 规则维度、版本、生效区间、阶梯/最低价和发布审核 | P0 | P2-FEE-01、P2-WH-02 |
| P2-CALC-01 | 服务端 | 计费引擎 | 规则匹配、试算、事件触发、幂等、计算快照和复算 | P0 | P2-RATE-01 |
| P2-FEE-02 | 后台 | 费用台账 | 自动/人工费用、审核、调整、红冲、锁定和审计 | P0 | P2-CALC-01 |
| P2-ARAP-01 | 后台 | 应收应付 | 客户应收、供应商应付、查询汇总、权限和导出 | P0 | P2-FEE-02、一期往来单位 |
| P2-RECON-01 | 后台 | 对账单 | 按票/月结生成、审核、确认、争议、版本和关闭 | P0 | P2-ARAP-01 |
| P2-CLI-03 | 客户端 | 账单中心 | 对客账单列表、明细、确认、异议和付款状态 | P0 | P2-RECON-01 |
| P2-CASH-01 | 后台 | 收付款核销 | 收付款登记、凭证、审核、部分核销、反核销和余额 | P0 | P2-RECON-01 |
| P2-INV-01 | 后台 | 发票登记 | 开票/收票申请、号码、金额、税率、状态和附件 | P1 | P2-RECON-01 |
| P2-PROFIT-01 | 后台 | 单票利润 | 收入、成本、毛利、毛利率、折算和权限控制 | P1 | P2-ARAP-01、汇率版本 |
## 七、启动前业务确认
### 仍需产品确认
| 编号 | 待确认问题 | 当前建议 |
|---|---|---|
| P2-Q13 | 二期计费财务做到哪一个闭环终点? | 做到线下收付款登记和核销,不做在线支付、会计凭证和总账 |
| P2-Q14 | “02 到仓”首批费用项目及应收/应付方向是什么? | 建立费用白名单,白名单外费用只允许人工新增并审核 |
| P2-Q15 | 计费规则需要哪些维度、阶梯和最低收费? | 至少支持客户、往来单位、仓库、服务和有效期;复杂价格结构按实际需要裁剪 |
| P2-Q16 | 各类费用在什么业务节点生成、审核和锁定? | 实测完成试算,差异确认后生效;其他费用按已确认事件生成 |
### 已由原型和实现基线确定
| 编号 | 已确定结论 | 原型依据摘要 |
|---|---|---|
| P2-Q17 | 本位币 CNY;保存原币、人工业务汇率和折算金额,费用生效时锁定汇率快照 | 币种、汇率、CNY/USD/EUR 及人民币:美元汇率字段 |
| P2-Q18 | 保存未税金额、税率、税额、含税总额;金额/税额 2 位、汇率 6 位,服务端统一舍入 | 0%/6%/9%/13% 税率及税额、含税合计 |
| P2-Q19 | 经办提交、财务审核;生效费用只能调整、红冲或作废并留痕 | 新增、客服提交、财务通过等状态 |
| P2-Q20 | 支持按选择和按截止时间生成账单,保存结算期间、账期和到期日 | 两种生成账单入口及结算字段 |
| P2-Q21 | 账单审核、确认、拒绝、锁定和争议后新版本流程 | 待审核、审核通过、审核拒绝、待确认及明细/历史 |
| P2-Q22 | 线下收付款、部分收付、余额、核销和反核销;不含预收预付、跨币种和银行自动认领 | 待收款、部分收款、已结清、余额、支付历史和核销入口 |
| P2-Q23 | 只做发票信息和状态登记,不接外部开票平台 | 发票抬头、日期、状态、税率和税额字段 |
| P2-Q24 | 利润=已生效应收-已生效应付,利润率=利润÷收入;仅财务授权角色可见 | 收入、成本、利润、利润率及多维报表 |
P2-Q13 至 P2-Q16 纳入 [二期需求确认清单](二期需求确认清单.md)。产品岗位空缺期间可先开发费用、账单和审计基础结构,但闭环范围、费用白名单、复杂价格规则和自动计费节点必须配置化;四项临时结论回写后再锁定最终业务流。P2-Q17 至 P2-Q24 不再要求重复确认。
## 八、验收重点
- 相同业务事件重试不会重复生成费用。
- 历史费用使用保存的规则版本、计费输入和汇率能够复算一致。
- 费用调整、红冲、核销和反核销形成不可删除的完整审计链。
- 多币种不直接汇总;所有折算都能追溯到汇率版本和日期。
- 对账单明细之和、调整、核销和余额能够勾稽一致。
- 部分核销、多对多核销和反核销后的未结余额正确且不为负数。
- 客户不能查看供应商成本、内部利润、内部备注或其他企业账单。
- 未授权运营人员不能查看利润、付款和供应商成本。
- 已确认对账单不能静默修改,重新计算必须生成调整或新版本。
- 财务导出、凭证下载和敏感金额查询具有权限与审计记录。
@@ -0,0 +1,34 @@
# 二期计费财务需求确认表
> 用途:产品空缺期间由项目负责人组织确认原型无法决定、且会改变二期范围或计费模型的事项。
> 不需要填写确认人和确认日期;状态为“临时基线”且有可开发结论时用于当前开发,产品到岗复核后改为“已确认”。
> 关联文档:[二期计费与财务模块规划](二期计费财务模块规划.md)
## 一、需要产品确认的 4 项
| 编号 | 需要产品确认的事项 | 当前建议 | 最终结论 | 状态 |
|---|---|---|---|---|
| P2-Q13 | 二期计费财务做到哪一个闭环终点? | 做到费用生成、应收应付、对账、线下收付款登记与核销、单票利润;不做在线支付、会计凭证和总账 | 待填写 | 待确认 |
| P2-Q14 | “02 到仓”首批费用项目白名单及应收/应付方向是什么? | 逐项给出费用编码、名称、方向、单位、默认币种和税率;未进白名单的费用只允许人工新增并审核 | 待填写 | 待确认 |
| P2-Q15 | 二期计费规则需要支持到什么复杂度? | 至少按客户、往来单位、仓库、服务类型和生效区间匹配;阶梯价、最低收费、封顶和组合价只实现业务明确需要的部分 | 待填写 | 待确认 |
| P2-Q16 | 各类费用在哪个业务节点生成、审核和锁定? | 实测完成生成试算,差异确认后生效并锁定;非测量费用按已确认业务事件生成 | 待填写 | 待确认 |
## 二、原型已经明确,不再发产品确认
| 编号 | 采用的二期基线 | 原型依据 |
|---|---|---|
| P2-Q17 | 本位币采用 CNY;费用保存原币、业务汇率和折算金额;二期人工维护汇率,不接外部汇率源,费用生效时锁定汇率快照 | 原型已有 CNY、USD、EUR、币种、汇率及“人民币:美元汇率”字段 |
| P2-Q18 | 保存未税金额、税率、税额和含税总额;税额按未税金额×税率计算;金额/税额保留 2 位、汇率保留 6 位并由服务端统一四舍五入 | 原型已有 0%、6%、9%、13% 税率及金额、税额、含税合计 |
| P2-Q19 | 经办提交、财务审核;生效费用不直接删除或覆盖,只能调整、红冲或作废并保留审计链 | 原型已有“新增、客服提交、财务通过”等审核状态;直接删除入口按财务审计要求收紧 |
| P2-Q20 | 支持勾选费用生成账单和按截止时间批量生成;账单保存结算起止日、账期和到期日,二期不额外建设复杂自动月结调度 | 原型已有“按选择生成账单”“按截止时间生成账单”、结算起止日和账期 |
| P2-Q21 | 账单按草稿/待审核/审核通过/审核拒绝/待确认/已关闭流转;确认后锁定,争议调整后生成新版本 | 原型已有待审核、审核通过、审核拒绝、待确认、账单明细和支付历史 |
| P2-Q22 | 支持线下收付款登记、部分收付、余额和核销;错误核销用反核销留痕;二期不做预收预付、跨币种核销和银行自动认领 | 原型已有待收款、部分收款、已结清、逾期、应付/已付/余额、支付历史和核销入口 |
| P2-Q23 | 二期仅登记开票/收票信息、税率、税额、日期、状态和附件,不接电子税局或第三方开票平台 | 原型只有发票抬头、开票日期、已开票/未开票、税率和税额,没有外部开票接口 |
| P2-Q24 | 单票利润=已生效应收-已生效应付;利润率=利润÷收入;支持按客户、货柜、单票和月份查看,仅财务授权角色可见 | 原型直接展示收入、成本、利润、利润率及相应查询维度 |
## 三、会议填写规则
- 认可当前建议:复制为临时结论,将“状态”改为“临时基线”。
- 修改当前建议:填写会议确定的临时口径,再将“状态”改为“临时基线”。
- 暂不确定:将“状态”改为“暂缓”,并填写二期临时处理方式。
- P2-Q17 至 P2-Q24 已作为原型和安全实现基线,不再重复确认;产品到岗后如提出变更,再按需求变更流程更新。
Binary file not shown.
+319
View File
@@ -0,0 +1,319 @@
# 虫虫出海二期需求确认清单:头程到仓与计费财务
> 创建日期:2026-07-28
> 更新日期:2026-07-30
> 当前状态:开发已启动;11 项待形成临时结论,13 项已有结论或原型基线
> 确认范围:头程物流“02 到仓”及二期计费财务闭环
> 关联规划:[二期开发规划](二期开发规划.md)
> 原型依据:[头程物流流程图](参考资料/头程物流流程图.svg)
## 一、文档用途
本清单用于组织业务、仓库、运营、财务和技术共同确认二期需求口径。产品岗位空缺期间由项目负责人主持;产品到岗后再对临时基线进行复核。确认重点不是页面长什么样,而是谁可以操作、什么条件可以推进、异常如何处理以及最终如何验收。
未形成结论的问题不得直接写成不可变的开发默认值。每项临时确认至少记录结论、适用范围和例外情况;状态为“临时基线”且结论可直接转为开发规则时,可用于当前开发。产品到岗复核后改为“已确认”;复核产生变化时按需求变更处理。
## 二、确认参与人
| 参与方 | 主要职责 |
|---|---|
| 项目负责人 | 产品空缺期间主持确认、记录临时结论、维护范围和验收口径 |
| 产品负责人 | 到岗后复核临时基线,处理范围变化和产品验收口径 |
| 业务/运营负责人 | 确认客户服务、调度、差异和异常处理规则 |
| 仓库负责人 | 确认收货、测量、照片、库位、标签和现场作业规则 |
| 财务/计费负责人 | 确认计费、币种汇率、税务、应收应付、对账、收付款、核销、发票和利润口径 |
| 法务/运维负责人 | 确认隐私数据、照片和凭证的保留与清理策略 |
| 技术负责人 | 评估可实现性、系统影响和数据迁移风险,不替代业务作决定 |
## 三、确认顺序
建议按以下顺序召开确认会,前一组结论可能影响后一组的数据和流程设计:
1. 到仓与调度:补充 P2-Q1、P2-Q10。
2. 测量与差异:补充 P2-Q4、P2-Q5、P2-Q6、P2-Q8。
3. 合规与保留:补充 P2-Q11。
4. 计费与财务:确认 P2-Q13 至 P2-Q16。
5. 原型基线复核:P2-Q17 至 P2-Q24 仅在产品要求变更时讨论。
6. 汇总评审:核对跨问题冲突,更新规划基线、任务依赖和验收标准。
## 四、会议确认主表
会议时只维护本表的“临时结论”和“状态”。已有方向不是可开发结论,项目负责人需要组织相关业务角色将例外情况、权限、节点或临时处理方式补充完整。
| 编号 | 已有结论/原型情况 | 本次会议必须确定 | 当前建议 | 临时结论(会议填写) | 状态 |
|---|---|---|---|---|---|
| P2-Q1 | 已提出“以实际货物入库为准”;原型没有少货关闭流程 | 谁能关闭;关闭条件;原因、照片或交接凭证是否必填;是否通知客户 | 收货员登记少货,仓库主管关闭;必填原因并保留凭证;关闭后通知客户 | 待填写 | 确认中 |
| P2-Q4 | 已给出“毛重÷363”和“(长×宽×高÷6000)×箱数”;原型有 cm、kg、m³ 和材积重 | `/363` 的业务含义;输入单位;计费重取值;精度和舍入 | 长 cm、重量 kg、体积 m³;保留原始值和公式版本;最终结果统一四舍五入 | 待填写 | 确认中 |
| P2-Q5 | 已确认差异需要客户确认;原型没有拒绝和超时规则 | 是否所有差异都确认;拒绝或超时如何处理;确认前是否暂停上架和计费 | 数量、重量、体积、破损及归属差异均需确认;确认前暂停后续作业,拒绝后转人工处理 | 待填写 | 确认中 |
| P2-Q6 | 已确认没有独立隔离区 | 差异期间货物放置方式;谁能解除业务暂停;解除条件 | 保持当前物理位置并标记业务暂停;客户确认且仓库主管复核后解除 | 待填写 | 确认中 |
| P2-Q8 | 已确认上传称重照片和测量照片;原型没有附件规则 | 是否必传;按箱件还是收货批次上传;数量、大小、格式和补传规则 | 每个收货批次必传称重、测量照片;允许补传但保留首次提交和补传记录 | 待填写 | 确认中 |
| P2-Q10 | 已暂缓无预报货物认领;原型没有临时处理方式 | 暂缓期间拒收还是允许线下登记待认领;如允许,谁能绑定订单 | 二期系统拒绝将无预报货物直接绑定企业订单;线下处置,不进入正式库存和计费 | 待填写 | 暂缓 |
| P2-Q11 | 已确认保存 12 个月;原型没有保留策略 | 起算时间;到期删除还是归档;联系方式、照片、凭证和审计是否同期限 | 从业务单关闭日起算;联系方式匿名化,照片和凭证到期删除,审计记录归档 | 待填写 | 确认中 |
| P2-Q13 | 原型覆盖费用、应收应付、账单、收付款和利润,但不能证明二期全部纳入 | 二期财务闭环终点 | 做到费用、应收应付、对账、线下收付款登记与核销、单票利润;不做在线支付、会计凭证和总账 | 待填写 | 待确认 |
| P2-Q14 | 原型费用项目较多,混合头程、仓储和其他阶段 | “02 到仓”首批费用白名单,以及每项应收/应付方向、单位、币种和税率 | 只纳入到仓实际使用项目;白名单外费用允许人工新增但必须审核 | 待填写 | 待确认 |
| P2-Q15 | 原型有费用分类和应收/应付,没有明确阶梯价、最低收费和规则优先级 | 计费维度;是否支持阶梯价、最低收费、封顶、组合价和冲突优先级 | 至少支持客户、往来单位、仓库、服务类型和生效区间;复杂价格结构按实际需要裁剪 | 待填写 | 待确认 |
| P2-Q16 | 原型有费用日期和审核状态,没有明确到仓事件的自动计费节点 | 各类费用在哪个节点试算、生成、生效和锁定;人工费用何时生效 | 实测完成生成试算,差异确认后生效并锁定;非测量费用按已确认业务事件生成 | 待填写 | 待确认 |
填写规则:认可当前建议时,可将建议复制到“临时结论”并把状态改为“临时基线”;不能确定时使用“暂缓”,同时写明二期处理方式。产品到岗复核后再改为“已确认”。
## 五、全量确认跟踪表
项目负责人在产品空缺期间维护临时结论和状态。状态使用:待确认、确认中、临时基线、已确认、暂缓、原型基线;产品到岗后负责复核“临时基线”。
| 编号 | 优先级 | 涉及业务角色 | 状态 |
|---|---|---|---|
| P2-Q1 | P0 | 仓库、平台运营 | 确认中 |
| P2-Q2 | P0 | 业务、调度 | 已确认 |
| P2-Q3 | P0 | 仓库、调度 | 已确认 |
| P2-Q4 | P0 | 仓库、财务/计费 | 确认中 |
| P2-Q5 | P0 | 业务、仓库、客服 | 确认中 |
| P2-Q6 | P0 | 仓库、业务 | 确认中 |
| P2-Q7 | P0 | 仓库 | 已确认 |
| P2-Q8 | P0 | 仓库、客服 | 确认中 |
| P2-Q9 | P0 | 仓库、产品 | 暂缓 |
| P2-Q10 | P0 | 仓库、平台运营 | 暂缓 |
| P2-Q11 | P0 | 业务、法务、运维 | 确认中 |
| P2-Q12 | P0 | 仓库、技术 | 已确认 |
| P2-Q13 | P0 | 产品、业务、财务、技术 | 待确认 |
| P2-Q14 | P0 | 业务、财务/计费 | 待确认 |
| P2-Q15 | P0 | 业务、财务/计费 | 待确认 |
| P2-Q16 | P0 | 业务、仓库、财务/计费 | 待确认 |
| P2-Q17 | P0 | 财务 | 原型基线 |
| P2-Q18 | P0 | 财务/税务 | 原型基线 |
| P2-Q19 | P0 | 财务、内控 | 原型基线 |
| P2-Q20 | P0 | 业务、财务、客服 | 原型基线 |
| P2-Q21 | P0 | 业务、财务、客服 | 原型基线 |
| P2-Q22 | P0 | 财务、内控 | 原型基线 |
| P2-Q23 | P1 | 财务/税务、产品 | 原型基线 |
| P2-Q24 | P1 | 财务、管理层 | 原型基线 |
## 六、详细确认事项与依据
### P2-Q1 少货到仓单如何人工关闭
- 优先级:P0,阻塞收货状态和差异流程设计。
- 需确认方:仓库负责人、平台运营负责人。
- 原型情况:未体现少货关闭人、审批动作和证明材料。
- 需要确认:
- 谁可以发起少货关闭?
- 谁拥有最终关闭权限?
- 是否需要客户确认后才能关闭?
- 必须提供哪些证明:实收数量、现场照片、交接凭证或承运商说明?
- 当前建议:收货员登记少货并上传证据,仓库主管确认关闭;平台运营仅在争议场景介入,系统同步通知客户。
- 业务确认结论:待填写。
- 产品确认状态/结论:确认中;“以实际货物入库为准”作为方向保留,关闭权限、条件、凭证和通知规则仍需在本清单“会议确认主表”补齐。
### P2-Q2 预约取货由哪类服务商承接
- 优先级:P0,影响快递公司、往来单位和调度数据模型。
- 需确认方:业务负责人、调度负责人。
- 原型情况:存在快递公司、往来单位和提货方式字段,但没有明确三者的分配关系。
- 需要确认:
- 只允许选择快递公司,还是也允许车队、供应商等往来单位?
- 同一预约能否更换服务商?更换后是否需要重新通知客户?
- 自有司机是否作为一种服务商处理?
- 当前建议:统一抽象为“可分配服务商”,快递公司、车队和其他承运往来单位作为不同类型。
- 业务确认结论:待填写。
- 产品确认状态/结论:已确认,不走系统服务商调度,相关费用由人工添加费用清单项目。
### P2-Q3 预约采用精确时间还是时间窗
- 优先级:P0,影响预约容量、冲突校验和客户表单。
- 需确认方:仓库负责人、调度负责人。
- 原型情况:只有提货时间和预计到仓日期,没有时间窗及容量规则。
- 需要确认:
- 预约精确到日期、小时还是开始/结束时间窗?
- 每个时间窗允许多少票、多少箱或多少车辆?
- 迟到、爽约和临时改期如何处理?
- 当前建议:采用开始时间和结束时间组成的时间窗,由仓库配置营业时间、停收日期和时间窗容量。
- 业务确认结论:待填写。
- 产品确认状态/结论:已确认,采用人工线下预约时间,系统记录预约结果。
### P2-Q4 实测及计费计算口径
- 优先级:P0,阻塞测量数据模型、差异计算和历史复算。
- 需确认方:仓库负责人、财务/计费负责人。
- 原型情况:明确展示 cm、kg、m³、材积重及原单/实测对比,但没有体积重系数、计费公式、精度和舍入规则。
- 必须逐项确认:
| 计算项 | 业务确认值 |
|---|---|
| 长度单位 | 待填写 |
| 重量单位 | 待填写 |
| 体积单位 | 待填写 |
| 体积重公式及除数 | 待填写 |
| 计费重取值规则 | 待填写 |
| 尺寸、重量和体积精度 | 待填写 |
| 每一步还是最终结果舍入 | 待填写 |
| 舍入方式 | 待填写 |
- 当前建议:公式由服务端按版本统一计算,每条测量记录保存原始值和规则版本,规则变更不反向修改历史记录。
- 产品确认状态/结论:确认中;公式方向已保留,`/363` 含义、单位、计费重取值和舍入规则仍需在本清单“会议确认主表”补齐。
### P2-Q5 哪些差异必须由客户确认
- 优先级:P0,影响差异单、业务暂停和客户确认页面。
- 需确认方:业务负责人、仓库负责人、客户服务负责人。
- 原型情况:有原单/实测对比、审核通过、审核不通过和失败原因,没有自动接受阈值及客户确认条件。
- 必须逐项确认:
| 差异类型 | 自动接受范围 | 必须客户确认范围 | 是否暂停后续作业 |
|---|---|---|---|
| 数量/箱数 | 待填写 | 待填写 | 待填写 |
| 重量 | 待填写 | 待填写 | 待填写 |
| 体积 | 待填写 | 待填写 | 待填写 |
| 包装/破损 | 待填写 | 待填写 | 待填写 |
| 货物归属 | 待填写 | 待填写 | 待填写 |
- 当前建议:阈值按差异类型配置,超过阈值必须客户确认;阈值不得写死在客户端。
- 产品确认状态/结论:确认中;“差异需要客户确认”作为方向保留,拒绝、超时和确认前阻塞规则仍需补齐。
### P2-Q6 隔离区货物如何释放
- 优先级:P0,影响差异暂停、隔离位置和正式上架。
- 需确认方:仓库负责人、业务负责人。
- 原型情况:存在“是否扣货/扣货理由”,但没有隔离库位、批准释放人和释放条件。
- 需要确认:
- 哪些异常必须进入隔离区?
- 谁可以批准释放?
- 哪些情况必须先取得客户确认?
- 释放后由谁安排转移到正式库位?
- 当前建议:差异关闭后由仓库主管批准释放;重大数量、破损或货物归属差异需先由客户确认。
- 业务确认结论:待填写。
- 产品确认状态/结论:确认中;不设隔离区并采用业务暂停作为方向保留,货物位置、解除权限和解除条件仍需补齐。
### P2-Q7 库位容量采用什么校验口径
- 优先级:P0,影响库位档案和上架校验。
- 需确认方:仓库负责人。
- 原型情况:库位原型已有“最大承重(kg)”和闲置/占用状态,但没有超容量处理规则。
- 需要确认:
- 二期是否以最大承重作为强校验?
- 箱数和体积是否需要同时强校验?
- 超容量时禁止上架,还是允许主管授权?
- 当前建议:二期以最大承重为强校验,箱数和体积先提示;确需超额时必须由仓库主管授权并留痕。
- 业务确认结论:待填写。
- 产品确认状态/结论:已确认,当前不做库位容量校验。
### P2-Q8 哪些到仓节点必须上传现场照片
- 优先级:P0,影响收货表单、文件存储和验收规则。
- 需确认方:仓库负责人、客户服务负责人。
- 原型情况:照片上传出现在 BOL/派送页面,到仓收货原型没有照片必传规则。
- 需要确认:
- 正常收货是否必须上传照片?
- 破损、受潮、少货、多货和无预报货物是否分别必传?
- 每类异常至少需要几张、拍摄哪些角度?
- 当前建议:破损、受潮、少货、多货和无预报货物必传;正常收货是否必传由仓库确认。
- 业务确认结论:待填写。
- 产品确认状态/结论:确认中;照片类型已明确,必传粒度、附件限制和补传规则仍需补齐。
### P2-Q9 二期是否使用 PDA
- 优先级:P0,影响二期终端范围和技术方案。
- 需确认方:仓库负责人、项目负责人;产品到岗后复核。
- 原型情况:流程图将 PDA 拣货放在头程“03 出仓”,现有 PDA 原型也是集装箱拣货,不是到仓测量或上架。
- 需要确认:二期到仓签到、测量和上架是否统一使用电脑/响应式作业页,不接入 PDA?
- 当前建议:二期不接 PDA,使用管理后台或响应式作业页;PDA 正式接入在“03 出仓/WMS”阶段规划。
- 业务确认结论:待填写。
- 产品确认状态/结论:暂缓;二期开发基线暂按不接 PDA、使用后台或响应式作业页处理。
### P2-Q10 无预报货物如何认领和超期处理
- 优先级:P0,影响租户隔离、待认领区和错绑冲正。
- 需确认方:仓库负责人、平台运营负责人。
- 原型情况:没有无预报货物、待认领区、认领审批和超期处理设计。
- 需要确认:
- 谁可以创建无预报收货记录?
- 认领需要客户提供什么证明?
- 由单人、双人还是主管确认后绑定订单?
- 超过多少天未认领,如何通知、收费、退回或处置?
- 当前建议:先进入待认领区,由主管确认后绑定企业和订单;超期天数及处置方式由业务另行明确。
- 业务确认结论:待填写。
- 产品确认状态/结论:暂缓;无预报货物不得直接绑定企业订单,具体认领和超期规则后续确认。
### P2-Q11 敏感数据和凭证保存多久
- 优先级:P0,影响存储成本、隐私合规和自动清理任务。
- 需确认方:业务负责人、法务负责人、运维负责人。
- 原型情况:没有联系方式、司机信息、现场照片和交接凭证的保留期限。
- 必须逐项确认:
| 数据类型 | 在线保存期限 | 归档期限 | 到期处理方式 |
|---|---|---|---|
| 客户/司机联系方式 | 待填写 | 待填写 | 待填写 |
| 到仓现场照片 | 待填写 | 待填写 | 待填写 |
| 交接凭证 | 待填写 | 待填写 | 待填写 |
| 导出及下载审计 | 待填写 | 待填写 | 待填写 |
- 当前建议:按数据类型分别设置期限;到期后自动删除或匿名化,下载和批量导出必须保留审计。
- 产品确认状态/结论:确认中;12 个月期限作为方向保留,起算点、删除/归档和各类数据适用范围仍需补齐。
### P2-Q13 二期计费财务的闭环终点
- 优先级:P0,决定二期财务系统边界和总体工作量。
- 需确认方:产品、业务、财务和技术负责人。
- 需要确认:二期是否以“线下收付款登记并完成核销”为终点;是否明确排除在线支付、银企直连、外部开票、会计凭证和总账。
- 当前建议:采用 [二期计费与财务模块规划](二期计费财务模块规划.md)中的轻量财务闭环。
- 产品确认状态/结论:待确认。
### P2-Q14 首批费用项目及应收应付方向
- 优先级:P0,阻塞费用项目、权限和数据初始化。
- 需确认方:业务、财务/计费负责人。
- 需要确认:费用编码、名称、应收/应付、计费单位、默认币种、税率/税务属性、可否人工新增和启停规则。
- 当前建议:先建立到仓相关费用白名单,其他费用人工新增必须审核。
- 产品确认状态/结论:待确认。
P2-Q14 费用白名单按下表逐项填写;原型中存在但“02 到仓”实际不用的费用不要纳入二期。
| 费用编码 | 费用名称 | 应收/应付 | 往来对象 | 计费单位 | 默认币种 | 默认税率 | 自动/人工 | 触发节点 |
|---|---|---|---|---|---|---|---|---|
| 待填写 | 待填写 | 待填写 | 待填写 | 待填写 | 待填写 | 待填写 | 待填写 | 待填写 |
### P2-Q15 计费规则维度和价格结构
- 优先级:P0,阻塞规则模型和计费引擎。
- 需确认方:业务、财务/计费负责人。
- 需要确认:是否按客户、供应商、仓库、服务、货物属性和有效期区分;是否支持阶梯价、最低收费、封顶、组合价和优先级冲突处理。
- 当前建议:规则版本化,至少支持客户、供应商、仓库、服务和有效期;复杂组合价未确认前不实现。
- 产品确认状态/结论:待确认。
### P2-Q16 费用生成和锁定节点
- 优先级:P0,阻塞实测、差异和费用状态衔接。
- 需确认方:业务、仓库、财务/计费负责人。
- 需要确认:到仓签到、实测完成、差异关闭、上架完成分别生成哪些费用;何时只是试算,何时生效并禁止直接修改。
- 当前建议:实测完成生成试算,差异关闭后确认;事件来源键保证重复触发不重复计费。
- 产品确认状态/结论:待确认。
### P2-Q17 至 P2-Q24 原型财务基线
以下事项已能从财务原型和必要的审计规则形成二期基线,不再发给产品重复确认。详细依据见 [二期计费财务需求确认表](二期计费财务需求确认表.md)。
| 编号 | 二期采用基线 | 状态 |
|---|---|---|
| P2-Q17 | 本位币 CNY;保存原币、人工业务汇率和折算金额,费用生效时锁定汇率快照 | 原型基线 |
| P2-Q18 | 保存未税金额、税率、税额和含税总额;金额/税额 2 位、汇率 6 位,服务端统一舍入 | 原型基线 |
| P2-Q19 | 经办提交、财务审核;生效费用只能调整、红冲或作废并留痕 | 原型基线 |
| P2-Q20 | 支持按选择和按截止时间生成账单,保存结算期间、账期和到期日 | 原型基线 |
| P2-Q21 | 账单审核、确认、拒绝、锁定;争议处理后生成新版本 | 原型基线 |
| P2-Q22 | 线下收付款、部分收付、余额、核销和反核销;不含预收预付、跨币种及银行自动认领 | 原型基线 |
| P2-Q23 | 只登记发票信息和状态,不接外部开票平台 | 原型基线 |
| P2-Q24 | 利润=已生效应收-已生效应付,利润率=利润÷收入;仅财务授权角色可见 | 原型基线 |
## 七、已确认事项
| 编号 | 确认结论 | 后续核验 | 状态 |
|---|---|---|---|
| P2-Q12 | 箱件/托盘标签使用佳博 1324D 热敏标签打印机 | 标签纸尺寸、连接方式、驱动、打印指令协议和仓库部署参数需在开发前实机核验 | 已确认 |
## 八、确认完成标准
阻塞开发的 P0 问题形成临时基线后,项目负责人应组织一次汇总评审并完成以下动作:
- 将确认结论回写到 [二期开发规划](二期开发规划.md)的规划基线、业务规则和状态模型。
- 调整受影响的开发任务、依赖、优先级和验收标准。
- 对仍存在例外或暂缓决定的事项明确负责人、截止日期和临时处理规则。
- 产品空缺期间由项目负责人、业务负责人、仓库负责人、财务负责人和技术负责人共同形成临时范围基线;产品到岗后复核。
- 范围基线确认后再制定正式排期;确认清单本身不代表已经承诺开发时间。
Binary file not shown.
@@ -0,0 +1,37 @@
# 2026-07-28 二期产品确认回传审核记录
> 回传原件:[2026-07-28-二期需求确认-产品回传.xlsx](2026-07-28-二期需求确认-产品回传.xlsx)
> 审核结论:暂不直接合并为正式需求基线
## 一、确认口径
二期由单一产品负责人统一确认,不要求填写产品跟进人、业务确认人和确认日期。回传文件中状态为“已确认”且已填写最终结论,即视为产品确认结果;状态为“暂缓”的项目需要明确二期临时处理方式。
## 二、需要产品补充澄清
| 编号 | 产品当前结论 | 仍需补充 |
|---|---|---|
| P2-Q1 | 以实际货物入库为准 | 谁可以关闭少货到仓单、关闭条件、必填原因/证明、是否通知客户 |
| P2-Q2 | 不走服务商,以人工添加费用清单项目 | 实际由谁取货、是否只做客户送货上门、快递公司/往来单位是否仍参与调度;费用项目不能替代履约主体 |
| P2-Q3 | 人工线下预约时间 | 系统是否仅记录预约结果;需要记录日期、具体时间还是时间段;改期和取消如何留痕 |
| P2-Q4 | 重量体积(毛重/363);总材积重(长×宽×高/6000)×箱数 | 单位、公式中 `/363` 的业务含义、计费重是否取毛重和材积重较大值、精度和舍入规则 |
| P2-Q5 | 差异需客户确认 | 是否所有差异都确认;客户拒绝、超时未确认以及确认前货物能否上架/计费 |
| P2-Q6 | 没有隔离单独的区域 | 差异未关闭时货物放哪里、系统是否使用“业务暂停”而不使用隔离库位、谁可以解除暂停 |
| P2-Q7 | 当前没有库位容量 | 二期是否完全不做容量校验;库位停用、占用和货物类型不匹配是否仍需拦截 |
| P2-Q8 | 称重照片、测量照片上传 | 是否必传、每个箱件还是每个收货批次上传、允许的文件数量/大小和补传规则 |
| P2-Q9 | 暂缓 | 明确二期不接 PDA,并确定使用后台网页或响应式作业页 |
| P2-Q10 | 暂缓 | 暂缓期间无预报货物是否拒收;如允许收货,需要临时认领和超期规则 |
| P2-Q11 | 保存 12 个月 | 从哪个时间点起算、到期删除还是归档、各类照片/联系方式/凭证是否采用同一期限 |
## 三、当前可保留的结论
- `P2-Q12`:标签打印设备为佳博 1324D 热敏标签打印机;标签纸、连接、驱动和指令协议仍需实机核验。
- `P2-Q8` 已明确需要称重照片和测量照片,可作为规则方向保留,必传粒度和附件限制补齐后再锁定。
- `P2-Q9、P2-Q10` 可以保持“暂缓”,但必须给出二期临时处理规则,否则仍会阻塞对应开发任务。
## 四、下一步
1. 产品在新回传副本中补齐上表需要进一步转成开发规则的内容。
2. 财务负责人同时确认新增的 `P2-Q13` 至 `P2-Q24`。
3. 产品重新回传时使用新文件名,不覆盖本次原件。
4. 完成审核后再统一更新正式需求基线和开发进度表。
+28
View File
@@ -0,0 +1,28 @@
# 二期产品确认回传管理
本目录只保存产品原始回传文件,不直接作为正式开发进度表使用,也不覆盖 `docs/二期/虫虫出海二期开发进度表.xlsx`。
## 文件命名
```text
YYYY-MM-DD-二期需求确认-产品回传-vN.xlsx
```
## 合并条件
每个 `P2-Qxx` 同时具备以下内容后才能合并为“已确认”:
- 最终结论能够直接转成业务规则和验收标准。
- 例外情况、暂缓范围和临时处理规则明确。
- 与其他确认项不存在冲突。
二期由单一产品负责人统一确认,不要求填写确认人、产品跟进人和确认日期。产品将状态设置为“已确认”即代表结论生效。
## 合并流程
1. 原样保存产品回传文件,不在原件上二次修改。
2. 对比正式确认清单,形成回传审核记录。
3. 产品补充不完整或冲突的结论,并将状态设置为“已确认”或“暂缓”。
4. 按 `P2-Qxx` 合并到 `二期需求确认清单.md`。
5. 同步更新开发规划、开发任务、依赖、验收标准和 Excel 进度表。
6. 合并前后的正式版本分别保存到 `docs/二期/版本归档/`。
@@ -0,0 +1,224 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 1100" width="1600" height="1100" role="img" aria-labelledby="title desc">
<title id="title">跨境物流全链路流程图</title>
<desc id="desc">从头程下单、到仓、出仓及装船发运,到尾程到港前、到港、到仓、出仓和签收的完整业务流程。</desc>
<defs>
<linearGradient id="pageBg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#F5F9FF"/>
<stop offset="1" stop-color="#F7FBFA"/>
</linearGradient>
<linearGradient id="firstMile" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#155EEF"/>
<stop offset="1" stop-color="#2878FF"/>
</linearGradient>
<linearGradient id="lastMile" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#087E8B"/>
<stop offset="1" stop-color="#16A085"/>
</linearGradient>
<linearGradient id="transit" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#EAF2FF"/>
<stop offset="0.5" stop-color="#DDF5F1"/>
<stop offset="1" stop-color="#EAF2FF"/>
</linearGradient>
<filter id="shadow" x="-15%" y="-15%" width="130%" height="140%">
<feDropShadow dx="0" dy="8" stdDeviation="10" flood-color="#16324F" flood-opacity="0.09"/>
</filter>
<filter id="softShadow" x="-20%" y="-20%" width="140%" height="150%">
<feDropShadow dx="0" dy="4" stdDeviation="6" flood-color="#0F3057" flood-opacity="0.10"/>
</filter>
<marker id="arrowBlue" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto">
<path d="M1 1L8 5L1 9" fill="none" stroke="#7AA7ED" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
</marker>
<marker id="arrowGreen" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto">
<path d="M1 1L8 5L1 9" fill="none" stroke="#62B8AD" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
</marker>
<style>
text{font-family:"PingFang SC","Microsoft YaHei","Noto Sans SC",Arial,sans-serif}
.title{font-size:34px;font-weight:700;fill:#102A43;letter-spacing:1px}
.subtitle{font-size:15px;fill:#627D98}
.lane-title{font-size:22px;font-weight:700;fill:#fff}
.lane-en{font-size:11px;font-weight:600;fill:#fff;opacity:.72;letter-spacing:1.7px}
.phase-title{font-size:18px;font-weight:700;fill:#102A43}
.phase-no{font-size:11px;font-weight:700;fill:#fff;letter-spacing:.5px}
.step{font-size:13px;fill:#334E68}
.step-small{font-size:12px;fill:#334E68}
.step-no{font-size:10px;font-weight:700;fill:#fff}
.summary-label{font-size:11px;font-weight:700;fill:#627D98;letter-spacing:1px}
.summary-value{font-size:15px;font-weight:700;fill:#243B53}
</style>
</defs>
<rect width="1600" height="1100" fill="url(#pageBg)"/>
<circle cx="1490" cy="50" r="180" fill="#D9F1ED" opacity=".42"/>
<circle cx="90" cy="1080" r="220" fill="#DFEBFF" opacity=".48"/>
<!-- 标题与概览 -->
<text x="52" y="58" class="title">跨境物流全链路流程</text>
<text x="52" y="88" class="subtitle">从客户下单到海外签收 · 头程与尾程全节点协同</text>
<g transform="translate(1195 28)">
<rect width="112" height="60" rx="14" fill="#fff" stroke="#D9E2EC"/>
<text x="18" y="22" class="summary-label">业务阶段</text><text x="18" y="45" class="summary-value">8 个阶段</text>
<rect x="124" width="112" height="60" rx="14" fill="#fff" stroke="#D9E2EC"/>
<text x="142" y="22" class="summary-label">关键动作</text><text x="142" y="45" class="summary-value">35 个节点</text>
<rect x="248" width="112" height="60" rx="14" fill="#fff" stroke="#D9E2EC"/>
<text x="266" y="22" class="summary-label">最终结果</text><text x="266" y="45" class="summary-value">完成签收</text>
</g>
<!-- 头程泳道 -->
<rect x="30" y="120" width="1540" height="430" rx="24" fill="#FFFFFF" stroke="#D7E4F4" filter="url(#shadow)"/>
<path d="M54 120H1546A24 24 0 0 1 1570 144V194H30V144A24 24 0 0 1 54 120Z" fill="url(#firstMile)"/>
<g transform="translate(58 142)">
<circle cx="22" cy="22" r="22" fill="#fff" opacity=".16"/>
<path d="M10 23h24M14 23v11h16V23M17 19v-6h10v6M21 27h3" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<text x="58" y="18" class="lane-title">头程物流</text>
<text x="58" y="38" class="lane-en">FIRST-MILE LOGISTICS · 国内集货与出口</text>
</g>
<g transform="translate(1306 145)">
<rect width="210" height="42" rx="21" fill="#fff" opacity=".15"/>
<text x="105" y="26" text-anchor="middle" font-size="13" font-weight="600" fill="#fff">下单 → 到仓 → 出仓</text>
</g>
<!-- 头程阶段连接 -->
<path d="M435 365H474" stroke="#7AA7ED" stroke-width="2" marker-end="url(#arrowBlue)"/>
<path d="M850 365H889" stroke="#7AA7ED" stroke-width="2" marker-end="url(#arrowBlue)"/>
<!-- 一、下单 -->
<g transform="translate(55 220)">
<rect width="380" height="300" rx="18" fill="#F8FBFF" stroke="#C9DBF7"/>
<circle cx="32" cy="32" r="17" fill="#155EEF"/><text x="32" y="36" text-anchor="middle" class="phase-no">01</text>
<text x="60" y="39" class="phase-title">下单</text>
<text x="326" y="37" font-size="11" fill="#829AB1">4 个动作</text>
<line x1="20" y1="64" x2="360" y2="64" stroke="#D9E8FA"/>
<g transform="translate(20 82)">
<rect width="340" height="42" rx="10" fill="#fff" stroke="#DFEAF8"/><circle cx="22" cy="21" r="11" fill="#3B82F6"/><text x="22" y="25" text-anchor="middle" class="step-no">1</text><text x="44" y="26" class="step">客户下单</text>
<rect y="52" width="340" height="42" rx="10" fill="#fff" stroke="#DFEAF8"/><circle cx="22" cy="73" r="11" fill="#3B82F6"/><text x="22" y="77" text-anchor="middle" class="step-no">2</text><text x="44" y="78" class="step">委托我们下单</text>
<rect y="104" width="340" height="42" rx="10" fill="#fff" stroke="#DFEAF8"/><circle cx="22" cy="125" r="11" fill="#3B82F6"/><text x="22" y="129" text-anchor="middle" class="step-no">3</text><text x="44" y="130" class="step">快速下单(只有唛头)</text>
<rect y="156" width="340" height="42" rx="10" fill="#fff" stroke="#DFEAF8"/><circle cx="22" cy="177" r="11" fill="#3B82F6"/><text x="22" y="181" text-anchor="middle" class="step-no">4</text><text x="44" y="182" class="step">换标下单(中转、退货)</text>
</g>
</g>
<!-- 二、到仓 -->
<g transform="translate(470 220)">
<rect width="380" height="300" rx="18" fill="#F8FBFF" stroke="#C9DBF7"/>
<circle cx="32" cy="32" r="17" fill="#155EEF"/><text x="32" y="36" text-anchor="middle" class="phase-no">02</text>
<text x="60" y="39" class="phase-title">到仓</text>
<text x="326" y="37" font-size="11" fill="#829AB1">5 个动作</text>
<line x1="20" y1="64" x2="360" y2="64" stroke="#D9E8FA"/>
<g transform="translate(20 76)">
<rect width="340" height="36" rx="9" fill="#fff" stroke="#DFEAF8"/><circle cx="20" cy="18" r="10" fill="#3B82F6"/><text x="20" y="22" text-anchor="middle" class="step-no">1</text><text x="40" y="23" class="step">预约取货</text>
<rect y="42" width="340" height="36" rx="9" fill="#fff" stroke="#DFEAF8"/><circle cx="20" cy="60" r="10" fill="#3B82F6"/><text x="20" y="64" text-anchor="middle" class="step-no">2</text><text x="40" y="65" class="step">送货上门</text>
<rect y="84" width="340" height="36" rx="9" fill="#fff" stroke="#DFEAF8"/><circle cx="20" cy="102" r="10" fill="#3B82F6"/><text x="20" y="106" text-anchor="middle" class="step-no">3</text><text x="40" y="107" class="step">入库测量(称重计费)</text>
<rect y="126" width="340" height="36" rx="9" fill="#fff" stroke="#DFEAF8"/><circle cx="20" cy="144" r="10" fill="#3B82F6"/><text x="20" y="148" text-anchor="middle" class="step-no">4</text><text x="40" y="149" class="step">调整差异核对</text>
<rect y="168" width="340" height="36" rx="9" fill="#fff" stroke="#DFEAF8"/><circle cx="20" cy="186" r="10" fill="#3B82F6"/><text x="20" y="190" text-anchor="middle" class="step-no">5</text><text x="40" y="191" class="step">仓库作业(指定库位)</text>
</g>
</g>
<!-- 三、出仓 -->
<g transform="translate(885 220)">
<rect width="660" height="300" rx="18" fill="#F8FBFF" stroke="#C9DBF7"/>
<circle cx="32" cy="32" r="17" fill="#155EEF"/><text x="32" y="36" text-anchor="middle" class="phase-no">03</text>
<text x="60" y="39" class="phase-title">出仓</text>
<text x="604" y="37" font-size="11" fill="#829AB1">9 个动作</text>
<line x1="20" y1="64" x2="640" y2="64" stroke="#D9E8FA"/>
<g transform="translate(20 76)">
<rect width="300" height="36" rx="9" fill="#fff" stroke="#DFEAF8"/><circle cx="20" cy="18" r="10" fill="#3B82F6"/><text x="20" y="22" text-anchor="middle" class="step-no">1</text><text x="40" y="23" class="step-small">智能排柜</text>
<rect y="42" width="300" height="36" rx="9" fill="#fff" stroke="#DFEAF8"/><circle cx="20" cy="60" r="10" fill="#3B82F6"/><text x="20" y="64" text-anchor="middle" class="step-no">2</text><text x="40" y="65" class="step-small">订舱</text>
<rect y="84" width="300" height="36" rx="9" fill="#fff" stroke="#DFEAF8"/><circle cx="20" cy="102" r="10" fill="#3B82F6"/><text x="20" y="106" text-anchor="middle" class="step-no">3</text><text x="40" y="107" class="step-small">预约托车</text>
<rect y="126" width="300" height="36" rx="9" fill="#fff" stroke="#DFEAF8"/><circle cx="20" cy="144" r="10" fill="#3B82F6"/><text x="20" y="148" text-anchor="middle" class="step-no">4</text><text x="40" y="149" class="step-small">PDA 拣货</text>
<rect y="168" width="300" height="36" rx="9" fill="#fff" stroke="#DFEAF8"/><circle cx="20" cy="186" r="10" fill="#3B82F6"/><text x="20" y="190" text-anchor="middle" class="step-no">5</text><text x="40" y="191" class="step-small">拍货柜及铅封照上传</text>
<rect x="320" width="300" height="36" rx="9" fill="#fff" stroke="#DFEAF8"/><circle cx="340" cy="18" r="10" fill="#3B82F6"/><text x="340" y="22" text-anchor="middle" class="step-no">6</text><text x="360" y="23" class="step-small">客户上传报关资料</text>
<rect x="320" y="42" width="300" height="36" rx="9" fill="#fff" stroke="#DFEAF8"/><circle cx="340" cy="60" r="10" fill="#3B82F6"/><text x="340" y="64" text-anchor="middle" class="step-no">7</text><text x="360" y="65" class="step-small">整理给到报关代理报关</text>
<rect x="320" y="84" width="300" height="36" rx="9" fill="#fff" stroke="#DFEAF8"/><circle cx="340" cy="102" r="10" fill="#3B82F6"/><text x="340" y="106" text-anchor="middle" class="step-no">8</text><text x="360" y="107" class="step-small">回执上传系统供客户下载</text>
<rect x="320" y="126" width="300" height="36" rx="9" fill="#fff" stroke="#DFEAF8"/><circle cx="340" cy="144" r="10" fill="#3B82F6"/><text x="340" y="148" text-anchor="middle" class="step-no">9</text><text x="360" y="149" class="step-small">查港务信息(预计开船、到港等)</text>
<g transform="translate(320 171)"><rect width="300" height="32" rx="16" fill="#E7F0FF"/><circle cx="24" cy="16" r="8" fill="#155EEF"/><path d="M20 16h8M24 12v8" stroke="#fff" stroke-width="1.5"/><text x="44" y="21" font-size="12" font-weight="600" fill="#155EEF">完成国内出仓,等待装船</text></g>
</g>
</g>
<!-- 海运衔接带 -->
<g transform="translate(350 570)" filter="url(#softShadow)">
<rect width="900" height="74" rx="37" fill="url(#transit)" stroke="#B9D8DF"/>
<path d="M0 37H286M614 37H900" stroke="#78AFC1" stroke-width="1.5" stroke-dasharray="7 7"/>
<g transform="translate(302 12)">
<circle cx="25" cy="25" r="25" fill="#fff"/>
<path d="M11 29h28l-4 7H16l-5-7Zm5-3h19l-3-11H19l-3 11Zm8-11V9h4v6" fill="none" stroke="#087E8B" stroke-width="2" stroke-linejoin="round"/>
<text x="68" y="23" font-size="16" font-weight="700" fill="#0B6070">装船发运 · 国际海运在途</text>
<text x="68" y="43" font-size="12" fill="#527C86">头程完成,进入海外清关与尾程履约</text>
</g>
</g>
<!-- 尾程泳道 -->
<rect x="30" y="670" width="1540" height="400" rx="24" fill="#FFFFFF" stroke="#CFE7E2" filter="url(#shadow)"/>
<path d="M54 670H1546A24 24 0 0 1 1570 694V744H30V694A24 24 0 0 1 54 670Z" fill="url(#lastMile)"/>
<g transform="translate(58 692)">
<circle cx="22" cy="22" r="22" fill="#fff" opacity=".16"/>
<path d="M10 27h7l3-10h10l3 10h3M15 27a4 4 0 1 0 0 8 4 4 0 0 0 0-8Zm20 0a4 4 0 1 0 0 8 4 4 0 0 0 0-8Z" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<text x="58" y="18" class="lane-title">尾程物流</text>
<text x="58" y="38" class="lane-en">LAST-MILE LOGISTICS · 海外清关、入仓与派送</text>
</g>
<g transform="translate(1185 695)">
<rect width="330" height="42" rx="21" fill="#fff" opacity=".15"/>
<text x="165" y="26" text-anchor="middle" font-size="13" font-weight="600" fill="#fff">到港前 → 到港 → 到仓 → 出仓 → 签收</text>
</g>
<!-- 尾程连接线 -->
<path d="M328 900H352" stroke="#62B8AD" stroke-width="2" marker-end="url(#arrowGreen)"/>
<path d="M633 900H657" stroke="#62B8AD" stroke-width="2" marker-end="url(#arrowGreen)"/>
<path d="M938 900H962" stroke="#62B8AD" stroke-width="2" marker-end="url(#arrowGreen)"/>
<path d="M1243 900H1267" stroke="#62B8AD" stroke-width="2" marker-end="url(#arrowGreen)"/>
<!-- 尾程阶段卡片 -->
<g transform="translate(48 770)">
<rect width="280" height="268" rx="16" fill="#F7FCFB" stroke="#CBE7E1"/>
<circle cx="28" cy="29" r="16" fill="#087E8B"/><text x="28" y="33" text-anchor="middle" class="phase-no">01</text><text x="54" y="35" class="phase-title">到港前</text><text x="54" y="53" font-size="11" fill="#648A87">船在海上</text>
<g transform="translate(16 72)">
<rect width="248" height="36" rx="9" fill="#fff" stroke="#D8EBE7"/><circle cx="18" cy="18" r="10" fill="#13A18D"/><text x="18" y="22" text-anchor="middle" class="step-no">1</text><text x="38" y="23" class="step-small">FBA 仓预约</text>
<rect y="42" width="248" height="44" rx="9" fill="#fff" stroke="#D8EBE7"/><circle cx="18" cy="64" r="10" fill="#13A18D"/><text x="18" y="68" text-anchor="middle" class="step-no">2</text><text x="38" y="61" class="step-small">BOL 预清货</text><text x="38" y="77" font-size="10" fill="#829AB1">时间、费用、目的仓</text>
<rect y="92" width="248" height="42" rx="9" fill="#fff" stroke="#D8EBE7"/><circle cx="18" cy="113" r="10" fill="#13A18D"/><text x="18" y="117" text-anchor="middle" class="step-no">3</text><text x="38" y="110" class="step-small">清关资料上传到</text><text x="38" y="126" class="step-small">对应货柜</text>
<rect y="140" width="248" height="36" rx="9" fill="#fff" stroke="#D8EBE7"/><circle cx="18" cy="158" r="10" fill="#13A18D"/><text x="18" y="162" text-anchor="middle" class="step-no">4</text><text x="38" y="163" class="step-small">明细备注货物中转等</text>
</g>
</g>
<g transform="translate(353 770)">
<rect width="280" height="268" rx="16" fill="#F7FCFB" stroke="#CBE7E1"/>
<circle cx="28" cy="29" r="16" fill="#087E8B"/><text x="28" y="33" text-anchor="middle" class="phase-no">02</text><text x="54" y="35" class="phase-title">到港</text>
<g transform="translate(16 82)">
<rect width="248" height="50" rx="9" fill="#fff" stroke="#D8EBE7"/><circle cx="18" cy="25" r="10" fill="#13A18D"/><text x="18" y="29" text-anchor="middle" class="step-no">1</text><text x="38" y="30" class="step-small">清关放行</text>
<rect y="60" width="248" height="64" rx="9" fill="#fff" stroke="#D8EBE7"/><circle cx="18" cy="92" r="10" fill="#13A18D"/><text x="18" y="96" text-anchor="middle" class="step-no">2</text><text x="38" y="88" class="step-small">DO 或 PU 邮发</text><text x="38" y="106" class="step-small">通知码头车队提柜</text>
<rect y="136" width="248" height="38" rx="19" fill="#E3F6F2"/><text x="124" y="160" text-anchor="middle" font-size="12" font-weight="600" fill="#087E8B">完成提柜 · 运往海外仓</text>
</g>
</g>
<g transform="translate(658 770)">
<rect width="280" height="268" rx="16" fill="#F7FCFB" stroke="#CBE7E1"/>
<circle cx="28" cy="29" r="16" fill="#087E8B"/><text x="28" y="33" text-anchor="middle" class="phase-no">03</text><text x="54" y="35" class="phase-title">到仓</text>
<g transform="translate(16 82)">
<rect width="248" height="42" rx="9" fill="#fff" stroke="#D8EBE7"/><circle cx="18" cy="21" r="10" fill="#13A18D"/><text x="18" y="25" text-anchor="middle" class="step-no">1</text><text x="38" y="26" class="step-small">开柜拍照比对</text>
<rect y="50" width="248" height="42" rx="9" fill="#fff" stroke="#D8EBE7"/><circle cx="18" cy="71" r="10" fill="#13A18D"/><text x="18" y="75" text-anchor="middle" class="step-no">2</text><text x="38" y="76" class="step-small">根据 PDA 需求打托作业</text>
<rect y="100" width="248" height="42" rx="9" fill="#fff" stroke="#D8EBE7"/><circle cx="18" cy="121" r="10" fill="#13A18D"/><text x="18" y="125" text-anchor="middle" class="step-no">3</text><text x="38" y="126" class="step-small">派送车队筛选预约</text>
</g>
</g>
<g transform="translate(963 770)">
<rect width="280" height="268" rx="16" fill="#F7FCFB" stroke="#CBE7E1"/>
<circle cx="28" cy="29" r="16" fill="#087E8B"/><text x="28" y="33" text-anchor="middle" class="phase-no">04</text><text x="54" y="35" class="phase-title">出仓</text>
<g transform="translate(16 72)">
<rect width="248" height="34" rx="9" fill="#fff" stroke="#D8EBE7"/><circle cx="18" cy="17" r="9" fill="#13A18D"/><text x="18" y="21" text-anchor="middle" class="step-no">1</text><text x="36" y="22" class="step-small">预排车转 BOL 送货</text>
<rect y="40" width="248" height="34" rx="9" fill="#fff" stroke="#D8EBE7"/><circle cx="18" cy="57" r="9" fill="#13A18D"/><text x="18" y="61" text-anchor="middle" class="step-no">2</text><text x="36" y="62" class="step-small">PDA 拣货装车</text>
<rect y="80" width="248" height="34" rx="9" fill="#fff" stroke="#D8EBE7"/><circle cx="18" cy="97" r="9" fill="#13A18D"/><text x="18" y="101" text-anchor="middle" class="step-no">3</text><text x="36" y="102" class="step-small">派送车队到仓装车</text>
<rect y="120" width="248" height="34" rx="9" fill="#fff" stroke="#D8EBE7"/><circle cx="18" cy="137" r="9" fill="#13A18D"/><text x="18" y="141" text-anchor="middle" class="step-no">4</text><text x="36" y="142" class="step-small">整柜派送</text>
<rect y="160" width="248" height="34" rx="9" fill="#fff" stroke="#D8EBE7"/><circle cx="18" cy="177" r="9" fill="#13A18D"/><text x="18" y="181" text-anchor="middle" class="step-no">5</text><text x="36" y="182" class="step-small">自提出仓(一件代发)</text>
</g>
</g>
<g transform="translate(1268 770)">
<rect width="284" height="268" rx="16" fill="#F7FCFB" stroke="#CBE7E1"/>
<circle cx="28" cy="29" r="16" fill="#087E8B"/><text x="28" y="33" text-anchor="middle" class="phase-no">05</text><text x="54" y="35" class="phase-title">签收</text>
<g transform="translate(16 82)">
<rect width="252" height="42" rx="9" fill="#fff" stroke="#D8EBE7"/><circle cx="18" cy="21" r="10" fill="#13A18D"/><text x="18" y="25" text-anchor="middle" class="step-no">1</text><text x="38" y="26" class="step-small">私人仓签收回执上传</text>
<rect y="50" width="252" height="42" rx="9" fill="#fff" stroke="#D8EBE7"/><circle cx="18" cy="71" r="10" fill="#13A18D"/><text x="18" y="75" text-anchor="middle" class="step-no">2</text><text x="38" y="76" class="step-small">FBA 仓签收回执上传</text>
<rect y="100" width="252" height="42" rx="9" fill="#fff" stroke="#D8EBE7"/><circle cx="18" cy="121" r="10" fill="#13A18D"/><text x="18" y="125" text-anchor="middle" class="step-no">3</text><text x="38" y="126" class="step-small">送仓异常处理</text>
<g transform="translate(0 156)"><rect width="252" height="38" rx="19" fill="#087E8B"/><circle cx="24" cy="19" r="9" fill="#fff" opacity=".2"/><path d="M20 19l3 3 6-7" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text x="132" y="24" text-anchor="middle" font-size="13" font-weight="700" fill="#fff">全流程完成 · 已签收</text></g>
</g>
</g>
<text x="800" y="1088" text-anchor="middle" font-size="11" fill="#829AB1">HEAD-HAUL &amp; LAST-MILE LOGISTICS PROCESS · 全程节点可追踪</text>
</svg>

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 881 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because it is too large Load Diff
+266
View File
@@ -0,0 +1,266 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>%s</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;600;700;900&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = { theme: { extend: { colors: { brand: { green:'#4CAF50', greenDark:'#2E7D32', blue:'#1565C0', blueDark:'#0D47A1', teal:'#00838F', light:'#F0F7FF', dark:'#0A1628' } }, fontFamily: { sans:['Inter','Noto Sans SC','system-ui','sans-serif'] } } } };
</script>
<style>
:root { --primary:#1565C0; --primary-dark:#0D47A1; --accent:#4CAF50; --accent-dark:#2E7D32; }
* { scroll-behavior:smooth; }
body { font-family:'Inter','Noto Sans SC',system-ui,sans-serif; overflow-x:hidden; }
.gradient-text { background:linear-gradient(135deg,#1565C0 0%,#4CAF50 100%); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }
.gradient-bg { background:linear-gradient(135deg,#0A1628 0%,#1565C0 50%,#00838F 100%); }
.btn-primary { background:linear-gradient(135deg,#1565C0 0%,#0D47A1 100%); transition:all .3s ease; }
.btn-primary:hover { transform:translateY(-2px); box-shadow:0 10px 30px rgba(21,101,192,.35); }
.btn-accent { background:linear-gradient(135deg,#4CAF50 0%,#2E7D32 100%); transition:all .3s ease; }
.btn-accent:hover { transform:translateY(-2px); box-shadow:0 10px 30px rgba(76,175,80,.35); }
.input-field { transition:all .3s ease; }
.input-field:focus { border-color:#1565C0; box-shadow:0 0 0 4px rgba(21,101,192,.1); }
.auth-logo { display:block; filter:brightness(0) saturate(100%) invert(20%) sepia(84%) saturate(1926%) hue-rotate(202deg) brightness(94%) contrast(93%); }
.lang-switcher { display:flex; align-items:center; background:rgba(21,101,192,.08); border-radius:9999px; padding:3px; border:1px solid rgba(21,101,192,.15); }
.lang-switcher button { font-size:12px; font-weight:600; padding:4px 9px; border-radius:9999px; color:#0A1628; transition:all .25s ease; white-space:nowrap; }
.lang-switcher button:hover { color:#1565C0; }
.lang-switcher button.active { background:#1565C0; color:#fff; }
</style>
</head>
<body class="text-gray-800 antialiased bg-gray-50">
<!-- 顶部栏 -->
<header class="fixed top-0 inset-x-0 z-50 bg-white/95 backdrop-blur border-b border-gray-100">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
<a href="index.html" class="flex items-center" aria-label="返回官网">
<img src="assets/logo.png" alt="柏悦物流" class="auth-logo" style="width:161px;height:60px;object-fit:contain;">
</a>
<div class="lang-switcher" id="langSwitcher">
<button data-lang="zh" class="active" title="中文">中</button>
<button data-lang="en" title="English">EN</button>
<button data-lang="es" title="Español (México)">🇲🇽</button>
</div>
</div>
</header>
<section id="loginSection" class="min-h-screen pt-24 pb-12 bg-gray-50 flex items-center">
<div class="w-full max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="bg-white rounded-3xl shadow-2xl overflow-hidden">
<div class="grid lg:grid-cols-2">
<!-- 左侧视觉 -->
<div class="hidden lg:block relative">
<img src="https://images.unsplash.com/photo-1494412574643-ff11b0a5c1c3?ixlib=rb-4.0.3&auto=format&fit=crop&w=2070&q=80" alt="物流" class="absolute inset-0 w-full h-full object-cover">
<div class="absolute inset-0 gradient-bg opacity-90"></div>
<div class="relative z-10 p-12 h-full flex flex-col justify-between text-white">
<div>
<img src="assets/logo.png" alt="虫虫出海" style="width:161px;height:60px;object-fit:contain;">
<h2 class="text-3xl font-bold mb-4" data-i18n="login_welcome">欢迎回来</h2>
<p class="text-white/80" data-i18n="login_desc">登录虫虫出海平台,实时追踪货物轨迹,管理订单与库存。</p>
</div>
<div class="space-y-4">
<div class="flex items-center space-x-3">
<i class="fas fa-check-circle text-brand-green"></i>
<span data-i18n="login_feat1">全程可视化追踪</span>
</div>
<div class="flex items-center space-x-3">
<i class="fas fa-check-circle text-brand-green"></i>
<span data-i18n="login_feat2">智能订单管理</span>
</div>
<div class="flex items-center space-x-3">
<i class="fas fa-check-circle text-brand-green"></i>
<span data-i18n="login_feat3">在线询价下单</span>
</div>
</div>
</div>
</div>
<!-- 右侧表单 -->
<div class="p-8 sm:p-12 lg:p-16">
<div class="max-w-md mx-auto">
<h2 class="text-3xl font-bold text-gray-900 mb-2" data-i18n="login_title">账号登录</h2>
<p class="text-gray-600 mb-8"><span data-i18n="login_no_account">还没有账号?</span><button onclick="window.location.href='register.html'" class="text-brand-blue font-semibold hover:underline" data-i18n="login_register_link">立即注册</button></p>
<form id="loginForm" class="space-y-5">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2" data-i18n="login_account_label">邮箱 / 手机号</label>
<div class="relative">
<i class="fas fa-user absolute left-4 top-1/2 -translate-y-1/2 text-gray-400"></i>
<input type="text" name="account" required placeholder="请输入邮箱或手机号" data-i18n-ph="login_account_ph" class="input-field w-full pl-11 pr-4 py-3.5 border border-gray-200 rounded-xl outline-none">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2" data-i18n="login_password_label">密码</label>
<div class="relative">
<i class="fas fa-lock absolute left-4 top-1/2 -translate-y-1/2 text-gray-400"></i>
<input type="password" name="password" required placeholder="请输入密码" data-i18n-ph="login_password_ph" class="input-field w-full pl-11 pr-12 py-3.5 border border-gray-200 rounded-xl outline-none">
<button type="button" class="toggle-password absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600">
<i class="fas fa-eye"></i>
</button>
</div>
</div>
<div class="flex items-center justify-between text-sm">
<label class="flex items-center text-gray-600 cursor-pointer">
<input type="checkbox" class="w-4 h-4 text-brand-blue rounded border-gray-300 mr-2">
<span data-i18n="login_remember">记住我</span>
</label>
<button type="button" onclick="showToast(t('toast_resetpw'))" class="text-brand-blue hover:underline" data-i18n="login_forgot">忘记密码?</button>
</div>
<button type="submit" class="btn-primary w-full text-white font-bold py-4 rounded-xl text-lg" data-i18n="login_submit">
登 录
</button>
</form>
<div class="mt-8">
<div class="relative">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-200"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="px-2 bg-white text-gray-500" data-i18n="login_other">其他方式登录</span>
</div>
</div>
<div class="flex justify-center space-x-4 mt-6">
<button onclick="showToast('微信登录即将上线')" class="w-12 h-12 rounded-full border border-gray-200 flex items-center justify-center text-green-600 hover:bg-green-50 transition-colors">
<i class="fab fa-weixin text-xl"></i>
</button>
<button onclick="showToast('企业微信登录即将上线')" class="w-12 h-12 rounded-full border border-gray-200 flex items-center justify-center text-blue-600 hover:bg-blue-50 transition-colors">
<i class="fas fa-briefcase text-xl"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section> <!-- Toast 提示 -->
<div id="toast" class="fixed top-24 left-1/2 -translate-x-1/2 z-[60] opacity-0 pointer-events-none transition-all duration-300 transform -translate-y-4">
<div class="bg-gray-900 text-white px-6 py-3 rounded-full shadow-2xl flex items-center space-x-2">
<i class="fas fa-info-circle text-brand-green"></i>
<span id="toastMessage">提示信息</span>
</div>
</div>
<script>
let currentLang = localStorage.getItem('cc_lang') || 'zh';
function t(key) {
return (i18n[currentLang] && i18n[currentLang][key] !== undefined) ? i18n[currentLang][key] : key;
}
const i18n = {
zh: {
login_welcome: "欢迎回来",
login_desc: "登录虫虫出海平台,实时追踪货物轨迹,管理订单与库存。",
login_feat1: "全程可视化追踪",
login_feat2: "智能订单管理",
login_feat3: "在线询价下单",
login_title: "账号登录",
login_no_account: "还没有账号?",
login_register_link: "立即注册",
login_account_label: "邮箱 / 手机号",
login_account_ph: "请输入邮箱或手机号",
login_password_label: "密码",
login_password_ph: "请输入密码",
login_remember: "记住我",
login_forgot: "忘记密码?",
login_submit: "登 录",
login_other: "其他方式登录",
toast_resetpw: "请联系客服重置密码",
toast_login_ok: "登录成功!欢迎回来"
},
en: {
login_welcome: "Welcome Back",
login_desc: "Log in to track shipments in real time and manage orders and inventory.",
login_feat1: "Full Visual Tracking",
login_feat2: "Smart Order Management",
login_feat3: "Online Quoting & Ordering",
login_title: "Account Login",
login_no_account: "No account yet?",
login_register_link: "Sign Up Now",
login_account_label: "Email / Phone",
login_account_ph: "Enter email or phone",
login_password_label: "Password",
login_password_ph: "Enter password",
login_remember: "Remember Me",
login_forgot: "Forgot Password?",
login_submit: "Log In",
login_other: "Other Sign-In Methods",
toast_resetpw: "Please contact support to reset your password",
toast_login_ok: "Login successful! Welcome back"
},
es: {
login_welcome: "Bienvenido de nuevo",
login_desc: "Inicia sesión para rastrear envíos en tiempo real y gestionar pedidos e inventario.",
login_feat1: "Seguimiento visual completo",
login_feat2: "Gestión inteligente de pedidos",
login_feat3: "Cotización y pedidos en línea",
login_title: "Inicio de sesión",
login_no_account: "¿Aún no tienes cuenta?",
login_register_link: "Regístrate ahora",
login_account_label: "Correo / Teléfono",
login_account_ph: "Ingresa correo o teléfono",
login_password_label: "Contraseña",
login_password_ph: "Ingresa tu contraseña",
login_remember: "Recuérdame",
login_forgot: "¿Olvidó su contraseña?",
login_submit: "Iniciar sesión",
login_other: "Otros métodos de acceso",
toast_resetpw: "Contacta a soporte para restablecer tu contraseña",
toast_login_ok: "¡Inicio de sesión exitoso! Bienvenido"
}
};
function applyLanguage(lang) {
if (!i18n[lang]) lang = 'zh';
currentLang = lang;
const langAttr = lang === 'zh' ? 'zh-CN' : (lang === 'es' ? 'es-MX' : 'en');
document.documentElement.setAttribute('lang', langAttr);
document.querySelectorAll('[data-i18n]').forEach(el => {
const k = el.getAttribute('data-i18n');
const v = i18n[lang][k];
if (v !== undefined) { if (v.indexOf('<br>') !== -1) el.innerHTML = v; else el.textContent = v; }
});
document.querySelectorAll('[data-i18n-ph]').forEach(el => {
const k = el.getAttribute('data-i18n-ph');
if (i18n[lang][k] !== undefined) el.setAttribute('placeholder', i18n[lang][k]);
});
document.querySelectorAll('[data-lang]').forEach(b => {
b.classList.toggle('active', b.getAttribute('data-lang') === lang);
});
try { localStorage.setItem('cc_lang', lang); } catch (e) {}
}
document.querySelectorAll('#langSwitcher [data-lang]').forEach(b => {
b.addEventListener('click', () => applyLanguage(b.getAttribute('data-lang')));
});
function showToast(message) {
const toast = document.getElementById('toast');
const toastMessage = document.getElementById('toastMessage');
toastMessage.textContent = message;
toast.classList.remove('opacity-0', 'pointer-events-none', '-translate-y-4');
toast.classList.add('opacity-100', 'translate-y-0');
setTimeout(() => {
toast.classList.add('opacity-0', 'pointer-events-none', '-translate-y-4');
toast.classList.remove('opacity-100', 'translate-y-0');
}, 3000);
}
function updateAuthUI() {}
document.querySelectorAll('.toggle-password').forEach(button => {
button.addEventListener('click', function() {
const input = this.previousElementSibling;
const icon = this.querySelector('i');
if (input.type === 'password') { input.type = 'text'; icon.classList.remove('fa-eye'); icon.classList.add('fa-eye-slash'); }
else { input.type = 'password'; icon.classList.remove('fa-eye-slash'); icon.classList.add('fa-eye'); }
});
});
document.getElementById('loginForm').addEventListener('submit', function(e) {
e.preventDefault();
showToast(t('toast_login_ok'));
setTimeout(() => window.location.href = 'workbench.html', 900);
});
applyLanguage(currentLang);
</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 764 KiB

+286
View File
@@ -0,0 +1,286 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>%s</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;600;700;900&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = { theme: { extend: { colors: { brand: { green:'#4CAF50', greenDark:'#2E7D32', blue:'#1565C0', blueDark:'#0D47A1', teal:'#00838F', light:'#F0F7FF', dark:'#0A1628' } }, fontFamily: { sans:['Inter','Noto Sans SC','system-ui','sans-serif'] } } } };
</script>
<style>
:root { --primary:#1565C0; --primary-dark:#0D47A1; --accent:#4CAF50; --accent-dark:#2E7D32; }
* { scroll-behavior:smooth; }
body { font-family:'Inter','Noto Sans SC',system-ui,sans-serif; overflow-x:hidden; }
.gradient-text { background:linear-gradient(135deg,#1565C0 0%,#4CAF50 100%); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }
.gradient-bg { background:linear-gradient(135deg,#0A1628 0%,#1565C0 50%,#00838F 100%); }
.btn-primary { background:linear-gradient(135deg,#1565C0 0%,#0D47A1 100%); transition:all .3s ease; }
.btn-primary:hover { transform:translateY(-2px); box-shadow:0 10px 30px rgba(21,101,192,.35); }
.btn-accent { background:linear-gradient(135deg,#4CAF50 0%,#2E7D32 100%); transition:all .3s ease; }
.btn-accent:hover { transform:translateY(-2px); box-shadow:0 10px 30px rgba(76,175,80,.35); }
.input-field { transition:all .3s ease; }
.input-field:focus { border-color:#1565C0; box-shadow:0 0 0 4px rgba(21,101,192,.1); }
.auth-logo { display:block; filter:brightness(0) saturate(100%) invert(20%) sepia(84%) saturate(1926%) hue-rotate(202deg) brightness(94%) contrast(93%); }
.lang-switcher { display:flex; align-items:center; background:rgba(21,101,192,.08); border-radius:9999px; padding:3px; border:1px solid rgba(21,101,192,.15); }
.lang-switcher button { font-size:12px; font-weight:600; padding:4px 9px; border-radius:9999px; color:#0A1628; transition:all .25s ease; white-space:nowrap; }
.lang-switcher button:hover { color:#1565C0; }
.lang-switcher button.active { background:#1565C0; color:#fff; }
</style>
</head>
<body class="text-gray-800 antialiased bg-gray-50">
<!-- 顶部栏 -->
<header class="fixed top-0 inset-x-0 z-50 bg-white/95 backdrop-blur border-b border-gray-100">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
<a href="index.html" class="flex items-center" aria-label="返回官网">
<img src="assets/logo.png" alt="柏悦物流" class="auth-logo" style="width:161px;height:60px;object-fit:contain;">
</a>
<div class="lang-switcher" id="langSwitcher">
<button data-lang="zh" class="active" title="中文">中</button>
<button data-lang="en" title="English">EN</button>
<button data-lang="es" title="Español (México)">🇲🇽</button>
</div>
</div>
</header>
<section id="registerSection" class="min-h-screen pt-24 pb-12 bg-gray-50 flex items-center">
<div class="w-full max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="bg-white rounded-3xl shadow-2xl overflow-hidden">
<div class="grid lg:grid-cols-2">
<!-- 左侧视觉 -->
<div class="hidden lg:block relative">
<img src="https://images.unsplash.com/photo-1566576912321-d58ddd7a6088?ixlib=rb-4.0.3&auto=format&fit=crop&w=2070&q=80" alt="物流仓储" class="absolute inset-0 w-full h-full object-cover">
<div class="absolute inset-0 gradient-bg opacity-90"></div>
<div class="relative z-10 p-12 h-full flex flex-col justify-between text-white">
<div>
<img src="assets/logo.png" alt="虫虫出海" style="width:161px;height:60px;object-fit:contain;">
<h2 class="text-3xl font-bold mb-4" data-i18n="reg_welcome">加入我们</h2>
<p class="text-white/80" data-i18n="reg_welcome_desc">注册虫虫出海企业账号,开启高效跨境物流体验。</p>
</div>
<div class="space-y-4">
<div class="flex items-center space-x-3">
<i class="fas fa-check-circle text-brand-green"></i>
<span data-i18n="reg_feat1">新用户专属优惠</span>
</div>
<div class="flex items-center space-x-3">
<i class="fas fa-check-circle text-brand-green"></i>
<span data-i18n="reg_feat2">免费获取物流方案</span>
</div>
<div class="flex items-center space-x-3">
<i class="fas fa-check-circle text-brand-green"></i>
<span data-i18n="reg_feat3">专属客户经理对接</span>
</div>
</div>
</div>
</div>
<!-- 右侧表单 -->
<div class="p-8 sm:p-12 lg:p-16">
<div class="max-w-md mx-auto">
<h2 class="text-3xl font-bold text-gray-900 mb-2" data-i18n="reg_title">企业注册</h2>
<p class="text-gray-600 mb-8"><span data-i18n="reg_have_account">已有账号?</span><button onclick="window.location.href='login.html'" class="text-brand-blue font-semibold hover:underline" data-i18n="reg_login_link">立即登录</button></p>
<form id="registerForm" class="space-y-5">
<div class="grid sm:grid-cols-2 gap-5">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2" data-i18n="reg_company_label">企业名称</label>
<input type="text" name="company" required placeholder="请输入企业名称" data-i18n-ph="reg_company_ph" class="input-field w-full px-4 py-3.5 border border-gray-200 rounded-xl outline-none">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2" data-i18n="reg_contact_label">联系人</label>
<input type="text" name="contact" required placeholder="请输入联系人姓名" data-i18n-ph="reg_contact_ph" class="input-field w-full px-4 py-3.5 border border-gray-200 rounded-xl outline-none">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2" data-i18n="reg_email_label">邮箱</label>
<div class="relative">
<i class="fas fa-envelope absolute left-4 top-1/2 -translate-y-1/2 text-gray-400"></i>
<input type="email" name="email" required placeholder="请输入企业邮箱" data-i18n-ph="reg_email_ph" class="input-field w-full pl-11 pr-4 py-3.5 border border-gray-200 rounded-xl outline-none">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2" data-i18n="reg_phone_label">手机号</label>
<div class="relative">
<i class="fas fa-phone absolute left-4 top-1/2 -translate-y-1/2 text-gray-400"></i>
<input type="tel" name="phone" required placeholder="请输入手机号" data-i18n-ph="reg_phone_ph" class="input-field w-full pl-11 pr-4 py-3.5 border border-gray-200 rounded-xl outline-none">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2" data-i18n="reg_password_label">密码</label>
<div class="relative">
<i class="fas fa-lock absolute left-4 top-1/2 -translate-y-1/2 text-gray-400"></i>
<input type="password" name="password" required placeholder="请设置8-20位密码" data-i18n-ph="reg_password_ph" class="input-field w-full pl-11 pr-12 py-3.5 border border-gray-200 rounded-xl outline-none">
<button type="button" class="toggle-password absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600">
<i class="fas fa-eye"></i>
</button>
</div>
</div>
<div class="flex items-start">
<input type="checkbox" id="agree" required class="w-4 h-4 text-brand-blue rounded border-gray-300 mt-1 mr-2">
<label for="agree" class="text-sm text-gray-600">
<span data-i18n="reg_agree">我已阅读并同意</span> <button type="button" onclick="showToast(t('toast_tos'))" class="text-brand-blue hover:underline" data-i18n="reg_agree_tos">《用户服务协议》</button> 和 <button type="button" onclick="showToast(t('toast_pp'))" class="text-brand-blue hover:underline" data-i18n="reg_agree_pp">《隐私政策》</button>
</label>
</div>
<button type="submit" class="btn-accent w-full text-white font-bold py-4 rounded-xl text-lg" data-i18n="reg_submit">
立即注册
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</section> <!-- Toast 提示 -->
<div id="toast" class="fixed top-24 left-1/2 -translate-x-1/2 z-[60] opacity-0 pointer-events-none transition-all duration-300 transform -translate-y-4">
<div class="bg-gray-900 text-white px-6 py-3 rounded-full shadow-2xl flex items-center space-x-2">
<i class="fas fa-info-circle text-brand-green"></i>
<span id="toastMessage">提示信息</span>
</div>
</div>
<script>
let currentLang = localStorage.getItem('cc_lang') || 'zh';
function t(key) {
return (i18n[currentLang] && i18n[currentLang][key] !== undefined) ? i18n[currentLang][key] : key;
}
const i18n = {
zh: {
reg_welcome: "加入我们",
reg_welcome_desc: "注册虫虫出海企业账号,开启高效跨境物流体验。",
reg_feat1: "新用户专属优惠",
reg_feat2: "免费获取物流方案",
reg_feat3: "专属客户经理对接",
reg_title: "企业注册",
reg_have_account: "已有账号?",
reg_login_link: "立即登录",
reg_company_label: "企业名称",
reg_company_ph: "请输入企业名称",
reg_contact_label: "联系人",
reg_contact_ph: "请输入联系人姓名",
reg_email_label: "邮箱",
reg_email_ph: "请输入企业邮箱",
reg_phone_label: "手机号",
reg_phone_ph: "请输入手机号",
reg_password_label: "密码",
reg_password_ph: "请设置8-20位密码",
reg_agree: "我已阅读并同意",
reg_agree_tos: "《用户服务协议》",
reg_agree_pp: "《隐私政策》",
reg_submit: "立即注册",
toast_tos: "服务协议即将上线",
toast_pp: "隐私政策即将上线",
toast_register_ok: "注册成功!欢迎加入虫虫出海"
},
en: {
reg_welcome: "Join Us",
reg_welcome_desc: "Register a Chongchong Global business account and start efficient cross-border logistics.",
reg_feat1: "Exclusive New-User Offers",
reg_feat2: "Free Logistics Plan",
reg_feat3: "Dedicated Account Manager",
reg_title: "Business Registration",
reg_have_account: "Already have an account?",
reg_login_link: "Log In Now",
reg_company_label: "Company Name",
reg_company_ph: "Enter company name",
reg_contact_label: "Contact Person",
reg_contact_ph: "Enter contact name",
reg_email_label: "Email",
reg_email_ph: "Enter company email",
reg_phone_label: "Phone Number",
reg_phone_ph: "Enter phone number",
reg_password_label: "Password",
reg_password_ph: "Set an 8-20 char password",
reg_agree: "I have read and agree to the",
reg_agree_tos: "User Agreement",
reg_agree_pp: "Privacy Policy",
reg_submit: "Sign Up Now",
toast_tos: "Terms of Service coming soon",
toast_pp: "Privacy Policy coming soon",
toast_register_ok: "Registration successful! Welcome to Chongchong Global"
},
es: {
reg_welcome: "Únete a nosotros",
reg_welcome_desc: "Regístrate en la cuenta empresarial de Chongchong Global y comienza una logística eficiente.",
reg_feat1: "Ofertas exclusivas para nuevos usuarios",
reg_feat2: "Plan logístico gratuito",
reg_feat3: "Gerente de cuenta dedicado",
reg_title: "Registro empresarial",
reg_have_account: "¿Ya tienes una cuenta?",
reg_login_link: "Inicia sesión ahora",
reg_company_label: "Nombre de empresa",
reg_company_ph: "Ingresa el nombre de empresa",
reg_contact_label: "Persona de contacto",
reg_contact_ph: "Ingresa el nombre de contacto",
reg_email_label: "Correo electrónico",
reg_email_ph: "Ingresa el correo empresarial",
reg_phone_label: "Número de teléfono",
reg_phone_ph: "Ingresa tu número de teléfono",
reg_password_label: "Contraseña",
reg_password_ph: "Define una contraseña de 8-20 caracteres",
reg_agree: "He leído y acepto los",
reg_agree_tos: "Términos de servicio",
reg_agree_pp: "Política de privacidad",
reg_submit: "Regístrate ahora",
toast_tos: "Términos de servicio próximamente",
toast_pp: "Política de privacidad próximamente",
toast_register_ok: "¡Registro exitoso! Bienvenido a Chongchong Global"
}
};
function applyLanguage(lang) {
if (!i18n[lang]) lang = 'zh';
currentLang = lang;
const langAttr = lang === 'zh' ? 'zh-CN' : (lang === 'es' ? 'es-MX' : 'en');
document.documentElement.setAttribute('lang', langAttr);
document.querySelectorAll('[data-i18n]').forEach(el => {
const k = el.getAttribute('data-i18n');
const v = i18n[lang][k];
if (v !== undefined) { if (v.indexOf('<br>') !== -1) el.innerHTML = v; else el.textContent = v; }
});
document.querySelectorAll('[data-i18n-ph]').forEach(el => {
const k = el.getAttribute('data-i18n-ph');
if (i18n[lang][k] !== undefined) el.setAttribute('placeholder', i18n[lang][k]);
});
document.querySelectorAll('[data-lang]').forEach(b => {
b.classList.toggle('active', b.getAttribute('data-lang') === lang);
});
try { localStorage.setItem('cc_lang', lang); } catch (e) {}
}
document.querySelectorAll('#langSwitcher [data-lang]').forEach(b => {
b.addEventListener('click', () => applyLanguage(b.getAttribute('data-lang')));
});
function showToast(message) {
const toast = document.getElementById('toast');
const toastMessage = document.getElementById('toastMessage');
toastMessage.textContent = message;
toast.classList.remove('opacity-0', 'pointer-events-none', '-translate-y-4');
toast.classList.add('opacity-100', 'translate-y-0');
setTimeout(() => {
toast.classList.add('opacity-0', 'pointer-events-none', '-translate-y-4');
toast.classList.remove('opacity-100', 'translate-y-0');
}, 3000);
}
function updateAuthUI() {}
document.querySelectorAll('.toggle-password').forEach(button => {
button.addEventListener('click', function() {
const input = this.previousElementSibling;
const icon = this.querySelector('i');
if (input.type === 'password') { input.type = 'text'; icon.classList.remove('fa-eye'); icon.classList.add('fa-eye-slash'); }
else { input.type = 'password'; icon.classList.remove('fa-eye-slash'); icon.classList.add('fa-eye'); }
});
});
document.getElementById('registerForm').addEventListener('submit', function(e) {
e.preventDefault();
showToast(t('toast_register_ok'));
setTimeout(() => window.location.href = 'workbench.html', 900);
});
applyLanguage(currentLang);
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 764 KiB

File diff suppressed because it is too large Load Diff
+210
View File
@@ -0,0 +1,210 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 680 1170" width="100%" role="img" font-family="'PingFang SC','Microsoft YaHei','Hiragino Sans GB',sans-serif">
<title>头程与尾程物流流程图</title>
<desc>头程三大节点(下单、到仓、出仓)经装船发运进入尾程五大节点(到港前、到港、到仓、出仓、签收)的完整流程,左右两栏结构</desc>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</marker>
</defs>
<line x1="340" y1="12" x2="340" y2="1160" stroke="#5F5E5A" stroke-width="1" stroke-dasharray="4 4" opacity="0.5"/>
<rect x="20" y="20" width="310" height="40" rx="8" fill="#185FA5"/>
<text x="175" y="40" text-anchor="middle" dominant-baseline="central" font-size="14" font-weight="500" fill="#FFFFFF">头程物流</text>
<rect x="350" y="20" width="320" height="40" rx="8" fill="#444441"/>
<text x="510" y="40" text-anchor="middle" dominant-baseline="central" font-size="13" font-weight="500" fill="#F1EFE8">尾程物流(装船发运后)</text>
<line x1="332" y1="40" x2="347" y2="40" stroke="#7b8190" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="20" y="72" width="310" height="30" rx="6" fill="#185FA5"/>
<text x="175" y="87" text-anchor="middle" dominant-baseline="central" font-size="13" font-weight="500" fill="#E6F1FB">一、下单</text>
<g stroke="#85B7EB" stroke-width="1">
<line x1="175" y1="150" x2="175" y2="158"/>
<line x1="175" y1="198" x2="175" y2="206"/>
<line x1="175" y1="246" x2="175" y2="254"/>
</g>
<g>
<rect x="30" y="110" width="290" height="40" rx="8" fill="#042C53" stroke="#85B7EB" stroke-width="0.5"/>
<circle cx="52" cy="130" r="13" fill="#185FA5"/><text x="52" y="130" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#E6F1FB">1</text>
<text x="74" y="130" dominant-baseline="central" font-size="12" fill="#B5D4F4">客户下单</text>
<rect x="30" y="158" width="290" height="40" rx="8" fill="#042C53" stroke="#85B7EB" stroke-width="0.5"/>
<circle cx="52" cy="178" r="13" fill="#185FA5"/><text x="52" y="178" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#E6F1FB">2</text>
<text x="74" y="178" dominant-baseline="central" font-size="12" fill="#B5D4F4">委托我们下单</text>
<rect x="30" y="206" width="290" height="40" rx="8" fill="#042C53" stroke="#85B7EB" stroke-width="0.5"/>
<circle cx="52" cy="226" r="13" fill="#185FA5"/><text x="52" y="226" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#E6F1FB">3</text>
<text x="74" y="226" dominant-baseline="central" font-size="12" fill="#B5D4F4">快速下单(只有唛头)</text>
<rect x="30" y="254" width="290" height="40" rx="8" fill="#042C53" stroke="#85B7EB" stroke-width="0.5"/>
<circle cx="52" cy="274" r="13" fill="#185FA5"/><text x="52" y="274" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#E6F1FB">4</text>
<text x="74" y="274" dominant-baseline="central" font-size="12" fill="#B5D4F4">换标下单(中转、退货)</text>
</g>
<rect x="20" y="314" width="310" height="30" rx="6" fill="#3B6D11"/>
<text x="175" y="329" text-anchor="middle" dominant-baseline="central" font-size="13" font-weight="500" fill="#EAF3DE">二、到仓</text>
<g stroke="#97C459" stroke-width="1">
<line x1="175" y1="392" x2="175" y2="400"/>
<line x1="175" y1="440" x2="175" y2="448"/>
<line x1="175" y1="488" x2="175" y2="496"/>
<line x1="175" y1="536" x2="175" y2="544"/>
</g>
<g>
<rect x="30" y="352" width="290" height="40" rx="8" fill="#173404" stroke="#97C459" stroke-width="0.5"/>
<circle cx="52" cy="372" r="13" fill="#3B6D11"/><text x="52" y="372" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#EAF3DE">1</text>
<text x="74" y="372" dominant-baseline="central" font-size="12" fill="#C0DD97">预约取货</text>
<rect x="30" y="400" width="290" height="40" rx="8" fill="#173404" stroke="#97C459" stroke-width="0.5"/>
<circle cx="52" cy="420" r="13" fill="#3B6D11"/><text x="52" y="420" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#EAF3DE">2</text>
<text x="74" y="420" dominant-baseline="central" font-size="12" fill="#C0DD97">送货上门</text>
<rect x="30" y="448" width="290" height="40" rx="8" fill="#173404" stroke="#97C459" stroke-width="0.5"/>
<circle cx="52" cy="468" r="13" fill="#3B6D11"/><text x="52" y="468" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#EAF3DE">3</text>
<text x="74" y="468" dominant-baseline="central" font-size="12" fill="#C0DD97">入库测量(称重计费)</text>
<rect x="30" y="496" width="290" height="40" rx="8" fill="#173404" stroke="#97C459" stroke-width="0.5"/>
<circle cx="52" cy="516" r="13" fill="#3B6D11"/><text x="52" y="516" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#EAF3DE">4</text>
<text x="74" y="516" dominant-baseline="central" font-size="12" fill="#C0DD97">调整差异核对</text>
<rect x="30" y="544" width="290" height="40" rx="8" fill="#173404" stroke="#97C459" stroke-width="0.5"/>
<circle cx="52" cy="564" r="13" fill="#3B6D11"/><text x="52" y="564" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#EAF3DE">5</text>
<text x="74" y="564" dominant-baseline="central" font-size="12" fill="#C0DD97">仓库作业(指定库位)</text>
</g>
<rect x="20" y="604" width="310" height="30" rx="6" fill="#BA7517"/>
<text x="175" y="619" text-anchor="middle" dominant-baseline="central" font-size="13" font-weight="500" fill="#FAEEDA">三、出仓</text>
<g stroke="#EF9F27" stroke-width="1">
<line x1="175" y1="682" x2="175" y2="690"/>
<line x1="175" y1="730" x2="175" y2="738"/>
<line x1="175" y1="778" x2="175" y2="786"/>
<line x1="175" y1="826" x2="175" y2="834"/>
<line x1="175" y1="874" x2="175" y2="882"/>
<line x1="175" y1="922" x2="175" y2="930"/>
<line x1="175" y1="970" x2="175" y2="978"/>
<line x1="175" y1="1018" x2="175" y2="1026"/>
<line x1="175" y1="1066" x2="175" y2="1074"/>
</g>
<g>
<rect x="30" y="642" width="290" height="40" rx="8" fill="#412402" stroke="#EF9F27" stroke-width="0.5"/>
<circle cx="52" cy="662" r="13" fill="#BA7517"/><text x="52" y="662" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#FAEEDA">1</text>
<text x="74" y="662" dominant-baseline="central" font-size="12" fill="#FAC775">智能排柜</text>
<rect x="30" y="690" width="290" height="40" rx="8" fill="#412402" stroke="#EF9F27" stroke-width="0.5"/>
<circle cx="52" cy="710" r="13" fill="#BA7517"/><text x="52" y="710" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#FAEEDA">2</text>
<text x="74" y="710" dominant-baseline="central" font-size="12" fill="#FAC775">订舱</text>
<rect x="30" y="738" width="290" height="40" rx="8" fill="#412402" stroke="#EF9F27" stroke-width="0.5"/>
<circle cx="52" cy="758" r="13" fill="#BA7517"/><text x="52" y="758" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#FAEEDA">3</text>
<text x="74" y="758" dominant-baseline="central" font-size="12" fill="#FAC775">预约托车</text>
<rect x="30" y="786" width="290" height="40" rx="8" fill="#412402" stroke="#EF9F27" stroke-width="0.5"/>
<circle cx="52" cy="806" r="13" fill="#BA7517"/><text x="52" y="806" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#FAEEDA">4</text>
<text x="74" y="806" dominant-baseline="central" font-size="12" fill="#FAC775">PDA拣货</text>
<rect x="30" y="834" width="290" height="40" rx="8" fill="#412402" stroke="#EF9F27" stroke-width="0.5"/>
<circle cx="52" cy="854" r="13" fill="#BA7517"/><text x="52" y="854" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#FAEEDA">5</text>
<text x="74" y="854" dominant-baseline="central" font-size="12" fill="#FAC775">拍货柜及铅封照上传</text>
<rect x="30" y="882" width="290" height="40" rx="8" fill="#412402" stroke="#EF9F27" stroke-width="0.5"/>
<circle cx="52" cy="902" r="13" fill="#BA7517"/><text x="52" y="902" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#FAEEDA">6</text>
<text x="74" y="902" dominant-baseline="central" font-size="12" fill="#FAC775">客户上传报关资料</text>
<rect x="30" y="930" width="290" height="40" rx="8" fill="#412402" stroke="#EF9F27" stroke-width="0.5"/>
<circle cx="52" cy="950" r="13" fill="#BA7517"/><text x="52" y="950" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#FAEEDA">7</text>
<text x="74" y="950" dominant-baseline="central" font-size="12" fill="#FAC775">整理给到报关代理报关</text>
<rect x="30" y="978" width="290" height="40" rx="8" fill="#412402" stroke="#EF9F27" stroke-width="0.5"/>
<circle cx="52" cy="998" r="13" fill="#BA7517"/><text x="52" y="998" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#FAEEDA">8</text>
<text x="74" y="998" dominant-baseline="central" font-size="12" fill="#FAC775">回执上传到系统供客户下载</text>
<rect x="30" y="1026" width="290" height="40" rx="8" fill="#412402" stroke="#EF9F27" stroke-width="0.5"/>
<circle cx="52" cy="1046" r="13" fill="#BA7517"/><text x="52" y="1046" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#FAEEDA">9</text>
<text x="74" y="1046" dominant-baseline="central" font-size="12" fill="#FAC775">查港务信息(预计开船、到港等)</text>
</g>
<rect x="60" y="1074" width="230" height="28" rx="14" fill="none" stroke="#EF9F27" stroke-width="1"/>
<text x="175" y="1088" text-anchor="middle" dominant-baseline="central" font-size="13" fill="#EF9F27">出仓完成 · 装船发运</text>
<rect x="350" y="72" width="320" height="30" rx="6" fill="#0F6E56"/>
<text x="510" y="87" text-anchor="middle" dominant-baseline="central" font-size="13" font-weight="500" fill="#E1F5EE">一、到港前(船在海上)</text>
<g stroke="#5DCAA5" stroke-width="1">
<line x1="510" y1="150" x2="510" y2="158"/>
<line x1="510" y1="198" x2="510" y2="206"/>
<line x1="510" y1="246" x2="510" y2="254"/>
</g>
<g>
<rect x="360" y="110" width="300" height="40" rx="8" fill="#04342C" stroke="#5DCAA5" stroke-width="0.5"/>
<circle cx="382" cy="130" r="13" fill="#0F6E56"/><text x="382" y="130" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#E1F5EE">1</text>
<text x="404" y="130" dominant-baseline="central" font-size="12" fill="#E1F5EE">FBA 仓预约</text>
<rect x="360" y="158" width="300" height="40" rx="8" fill="#04342C" stroke="#5DCAA5" stroke-width="0.5"/>
<circle cx="382" cy="178" r="13" fill="#0F6E56"/><text x="382" y="178" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#E1F5EE">2</text>
<text x="404" y="178" dominant-baseline="central" font-size="12" fill="#E1F5EE">BOL 预清货(时间、费用、目的仓)</text>
<rect x="360" y="206" width="300" height="40" rx="8" fill="#04342C" stroke="#5DCAA5" stroke-width="0.5"/>
<circle cx="382" cy="226" r="13" fill="#0F6E56"/><text x="382" y="226" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#E1F5EE">3</text>
<text x="404" y="226" dominant-baseline="central" font-size="12" fill="#E1F5EE">清关资料上传到对应的货柜</text>
<rect x="360" y="254" width="300" height="40" rx="8" fill="#04342C" stroke="#5DCAA5" stroke-width="0.5"/>
<circle cx="382" cy="274" r="13" fill="#0F6E56"/><text x="382" y="274" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#E1F5EE">4</text>
<text x="404" y="274" dominant-baseline="central" font-size="12" fill="#E1F5EE">明细备注货物中转等</text>
</g>
<rect x="350" y="314" width="320" height="30" rx="6" fill="#A32D2D"/>
<text x="510" y="329" text-anchor="middle" dominant-baseline="central" font-size="13" font-weight="500" fill="#FCEBEB">二、到港</text>
<g stroke="#F09595" stroke-width="1">
<line x1="510" y1="392" x2="510" y2="400"/>
</g>
<g>
<rect x="360" y="352" width="300" height="40" rx="8" fill="#501313" stroke="#F09595" stroke-width="0.5"/>
<circle cx="382" cy="372" r="13" fill="#A32D2D"/><text x="382" y="372" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#FCEBEB">1</text>
<text x="404" y="372" dominant-baseline="central" font-size="12" fill="#FCEBEB">清关放行</text>
<rect x="360" y="400" width="300" height="40" rx="8" fill="#501313" stroke="#F09595" stroke-width="0.5"/>
<circle cx="382" cy="420" r="13" fill="#A32D2D"/><text x="382" y="420" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#FCEBEB">2</text>
<text x="404" y="420" dominant-baseline="central" font-size="12" fill="#FCEBEB">DO或PU邮发并通知码头车队提柜</text>
</g>
<rect x="350" y="460" width="320" height="30" rx="6" fill="#534AB7"/>
<text x="510" y="475" text-anchor="middle" dominant-baseline="central" font-size="13" font-weight="500" fill="#EEEDFE">三、到仓</text>
<g stroke="#AFA9EC" stroke-width="1">
<line x1="510" y1="538" x2="510" y2="546"/>
<line x1="510" y1="586" x2="510" y2="594"/>
</g>
<g>
<rect x="360" y="498" width="300" height="40" rx="8" fill="#26215C" stroke="#AFA9EC" stroke-width="0.5"/>
<circle cx="382" cy="518" r="13" fill="#534AB7"/><text x="382" y="518" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#EEEDFE">1</text>
<text x="404" y="518" dominant-baseline="central" font-size="12" fill="#EEEDFE">开柜拍照比对</text>
<rect x="360" y="546" width="300" height="40" rx="8" fill="#26215C" stroke="#AFA9EC" stroke-width="0.5"/>
<circle cx="382" cy="566" r="13" fill="#534AB7"/><text x="382" y="566" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#EEEDFE">2</text>
<text x="404" y="566" dominant-baseline="central" font-size="12" fill="#EEEDFE">根据 PDA 需求打托作业</text>
<rect x="360" y="594" width="300" height="40" rx="8" fill="#26215C" stroke="#AFA9EC" stroke-width="0.5"/>
<circle cx="382" cy="614" r="13" fill="#534AB7"/><text x="382" y="614" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#EEEDFE">3</text>
<text x="404" y="614" dominant-baseline="central" font-size="12" fill="#EEEDFE">派送车队筛选预约</text>
</g>
<rect x="350" y="654" width="320" height="30" rx="6" fill="#993556"/>
<text x="510" y="669" text-anchor="middle" dominant-baseline="central" font-size="13" font-weight="500" fill="#FBEAF0">四、出仓</text>
<g stroke="#ED93B1" stroke-width="1">
<line x1="510" y1="730" x2="510" y2="738"/>
<line x1="510" y1="778" x2="510" y2="786"/>
<line x1="510" y1="826" x2="510" y2="834"/>
<line x1="510" y1="874" x2="510" y2="882"/>
</g>
<g>
<rect x="360" y="692" width="300" height="40" rx="8" fill="#4B1528" stroke="#ED93B1" stroke-width="0.5"/>
<circle cx="382" cy="712" r="13" fill="#993556"/><text x="382" y="712" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#FBEAF0">1</text>
<text x="404" y="712" dominant-baseline="central" font-size="12" fill="#FBEAF0">预排车转 BOL 送货</text>
<rect x="360" y="740" width="300" height="40" rx="8" fill="#4B1528" stroke="#ED93B1" stroke-width="0.5"/>
<circle cx="382" cy="760" r="13" fill="#993556"/><text x="382" y="760" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#FBEAF0">2</text>
<text x="404" y="760" dominant-baseline="central" font-size="12" fill="#FBEAF0">PDA拣货装车</text>
<rect x="360" y="788" width="300" height="40" rx="8" fill="#4B1528" stroke="#ED93B1" stroke-width="0.5"/>
<circle cx="382" cy="808" r="13" fill="#993556"/><text x="382" y="808" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#FBEAF0">3</text>
<text x="404" y="808" dominant-baseline="central" font-size="12" fill="#FBEAF0">派送车队到仓装车</text>
<rect x="360" y="836" width="300" height="40" rx="8" fill="#4B1528" stroke="#ED93B1" stroke-width="0.5"/>
<circle cx="382" cy="856" r="13" fill="#993556"/><text x="382" y="856" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#FBEAF0">4</text>
<text x="404" y="856" dominant-baseline="central" font-size="12" fill="#FBEAF0">整柜派送</text>
<rect x="360" y="884" width="300" height="40" rx="8" fill="#4B1528" stroke="#ED93B1" stroke-width="0.5"/>
<circle cx="382" cy="904" r="13" fill="#993556"/><text x="382" y="904" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#FBEAF0">5</text>
<text x="404" y="904" dominant-baseline="central" font-size="12" fill="#FBEAF0">自提出仓(一件代发)</text>
</g>
<rect x="350" y="944" width="320" height="30" rx="6" fill="#B58900"/>
<text x="510" y="959" text-anchor="middle" dominant-baseline="central" font-size="13" font-weight="500" fill="#F5E6B8">五、签收</text>
<g stroke="#D9B94A" stroke-width="1">
<line x1="510" y1="1020" x2="510" y2="1028"/>
<line x1="510" y1="1068" x2="510" y2="1076"/>
</g>
<g>
<rect x="360" y="982" width="300" height="40" rx="8" fill="#3A2E00" stroke="#D9B94A" stroke-width="0.5"/>
<circle cx="382" cy="1002" r="13" fill="#B58900"/><text x="382" y="1002" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#F5E6B8">1</text>
<text x="404" y="1002" dominant-baseline="central" font-size="12" fill="#F5E6B8">私人仓签收回执上传</text>
<rect x="360" y="1030" width="300" height="40" rx="8" fill="#3A2E00" stroke="#D9B94A" stroke-width="0.5"/>
<circle cx="382" cy="1050" r="13" fill="#B58900"/><text x="382" y="1050" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#F5E6B8">2</text>
<text x="404" y="1050" dominant-baseline="central" font-size="12" fill="#F5E6B8">FBA仓签收回执上传</text>
<rect x="360" y="1078" width="300" height="40" rx="8" fill="#3A2E00" stroke="#D9B94A" stroke-width="0.5"/>
<circle cx="382" cy="1098" r="13" fill="#B58900"/><text x="382" y="1098" text-anchor="middle" dominant-baseline="central" font-size="11" font-weight="500" fill="#F5E6B8">3</text>
<text x="404" y="1098" dominant-baseline="central" font-size="12" fill="#F5E6B8">送仓异常</text>
</g>
<rect x="390" y="1126" width="240" height="28" rx="14" fill="none" stroke="#D9B94A" stroke-width="1"/>
<text x="510" y="1140" text-anchor="middle" dominant-baseline="central" font-size="13" fill="#D9B94A">签收完成</text>
</svg>

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because it is too large Load Diff
+266
View File
@@ -0,0 +1,266 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>%s</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;600;700;900&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = { theme: { extend: { colors: { brand: { green:'#4CAF50', greenDark:'#2E7D32', blue:'#1565C0', blueDark:'#0D47A1', teal:'#00838F', light:'#F0F7FF', dark:'#0A1628' } }, fontFamily: { sans:['Inter','Noto Sans SC','system-ui','sans-serif'] } } } };
</script>
<style>
:root { --primary:#1565C0; --primary-dark:#0D47A1; --accent:#4CAF50; --accent-dark:#2E7D32; }
* { scroll-behavior:smooth; }
body { font-family:'Inter','Noto Sans SC',system-ui,sans-serif; overflow-x:hidden; }
.gradient-text { background:linear-gradient(135deg,#1565C0 0%,#4CAF50 100%); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }
.gradient-bg { background:linear-gradient(135deg,#0A1628 0%,#1565C0 50%,#00838F 100%); }
.btn-primary { background:linear-gradient(135deg,#1565C0 0%,#0D47A1 100%); transition:all .3s ease; }
.btn-primary:hover { transform:translateY(-2px); box-shadow:0 10px 30px rgba(21,101,192,.35); }
.btn-accent { background:linear-gradient(135deg,#4CAF50 0%,#2E7D32 100%); transition:all .3s ease; }
.btn-accent:hover { transform:translateY(-2px); box-shadow:0 10px 30px rgba(76,175,80,.35); }
.input-field { transition:all .3s ease; }
.input-field:focus { border-color:#1565C0; box-shadow:0 0 0 4px rgba(21,101,192,.1); }
.auth-logo { display:block; filter:brightness(0) saturate(100%) invert(20%) sepia(84%) saturate(1926%) hue-rotate(202deg) brightness(94%) contrast(93%); }
.lang-switcher { display:flex; align-items:center; background:rgba(21,101,192,.08); border-radius:9999px; padding:3px; border:1px solid rgba(21,101,192,.15); }
.lang-switcher button { font-size:12px; font-weight:600; padding:4px 9px; border-radius:9999px; color:#0A1628; transition:all .25s ease; white-space:nowrap; }
.lang-switcher button:hover { color:#1565C0; }
.lang-switcher button.active { background:#1565C0; color:#fff; }
</style>
</head>
<body class="text-gray-800 antialiased bg-gray-50">
<!-- 顶部栏 -->
<header class="fixed top-0 inset-x-0 z-50 bg-white/95 backdrop-blur border-b border-gray-100">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
<a href="index.html" class="flex items-center" aria-label="返回官网">
<img src="assets/logo.png" alt="柏悦物流" class="auth-logo" style="width:161px;height:60px;object-fit:contain;">
</a>
<div class="lang-switcher" id="langSwitcher">
<button data-lang="zh" class="active" title="中文">中</button>
<button data-lang="en" title="English">EN</button>
<button data-lang="es" title="Español (México)">🇲🇽</button>
</div>
</div>
</header>
<section id="loginSection" class="min-h-screen pt-24 pb-12 bg-gray-50 flex items-center">
<div class="w-full max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="bg-white rounded-3xl shadow-2xl overflow-hidden">
<div class="grid lg:grid-cols-2">
<!-- 左侧视觉 -->
<div class="hidden lg:block relative">
<img src="https://images.unsplash.com/photo-1494412574643-ff11b0a5c1c3?ixlib=rb-4.0.3&auto=format&fit=crop&w=2070&q=80" alt="物流" class="absolute inset-0 w-full h-full object-cover">
<div class="absolute inset-0 gradient-bg opacity-90"></div>
<div class="relative z-10 p-12 h-full flex flex-col justify-between text-white">
<div>
<img src="assets/logo.png" alt="虫虫出海" style="width:161px;height:60px;object-fit:contain;">
<h2 class="text-3xl font-bold mb-4" data-i18n="login_welcome">欢迎回来</h2>
<p class="text-white/80" data-i18n="login_desc">登录虫虫出海平台,实时追踪货物轨迹,管理订单与库存。</p>
</div>
<div class="space-y-4">
<div class="flex items-center space-x-3">
<i class="fas fa-check-circle text-brand-green"></i>
<span data-i18n="login_feat1">全程可视化追踪</span>
</div>
<div class="flex items-center space-x-3">
<i class="fas fa-check-circle text-brand-green"></i>
<span data-i18n="login_feat2">智能订单管理</span>
</div>
<div class="flex items-center space-x-3">
<i class="fas fa-check-circle text-brand-green"></i>
<span data-i18n="login_feat3">在线询价下单</span>
</div>
</div>
</div>
</div>
<!-- 右侧表单 -->
<div class="p-8 sm:p-12 lg:p-16">
<div class="max-w-md mx-auto">
<h2 class="text-3xl font-bold text-gray-900 mb-2" data-i18n="login_title">账号登录</h2>
<p class="text-gray-600 mb-8"><span data-i18n="login_no_account">还没有账号?</span><button onclick="window.location.href='register.html'" class="text-brand-blue font-semibold hover:underline" data-i18n="login_register_link">立即注册</button></p>
<form id="loginForm" class="space-y-5">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2" data-i18n="login_account_label">邮箱 / 手机号</label>
<div class="relative">
<i class="fas fa-user absolute left-4 top-1/2 -translate-y-1/2 text-gray-400"></i>
<input type="text" name="account" required placeholder="请输入邮箱或手机号" data-i18n-ph="login_account_ph" class="input-field w-full pl-11 pr-4 py-3.5 border border-gray-200 rounded-xl outline-none">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2" data-i18n="login_password_label">密码</label>
<div class="relative">
<i class="fas fa-lock absolute left-4 top-1/2 -translate-y-1/2 text-gray-400"></i>
<input type="password" name="password" required placeholder="请输入密码" data-i18n-ph="login_password_ph" class="input-field w-full pl-11 pr-12 py-3.5 border border-gray-200 rounded-xl outline-none">
<button type="button" class="toggle-password absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600">
<i class="fas fa-eye"></i>
</button>
</div>
</div>
<div class="flex items-center justify-between text-sm">
<label class="flex items-center text-gray-600 cursor-pointer">
<input type="checkbox" class="w-4 h-4 text-brand-blue rounded border-gray-300 mr-2">
<span data-i18n="login_remember">记住我</span>
</label>
<button type="button" onclick="showToast(t('toast_resetpw'))" class="text-brand-blue hover:underline" data-i18n="login_forgot">忘记密码?</button>
</div>
<button type="submit" class="btn-primary w-full text-white font-bold py-4 rounded-xl text-lg" data-i18n="login_submit">
登 录
</button>
</form>
<div class="mt-8">
<div class="relative">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-200"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="px-2 bg-white text-gray-500" data-i18n="login_other">其他方式登录</span>
</div>
</div>
<div class="flex justify-center space-x-4 mt-6">
<button onclick="showToast('微信登录即将上线')" class="w-12 h-12 rounded-full border border-gray-200 flex items-center justify-center text-green-600 hover:bg-green-50 transition-colors">
<i class="fab fa-weixin text-xl"></i>
</button>
<button onclick="showToast('企业微信登录即将上线')" class="w-12 h-12 rounded-full border border-gray-200 flex items-center justify-center text-blue-600 hover:bg-blue-50 transition-colors">
<i class="fas fa-briefcase text-xl"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section> <!-- Toast 提示 -->
<div id="toast" class="fixed top-24 left-1/2 -translate-x-1/2 z-[60] opacity-0 pointer-events-none transition-all duration-300 transform -translate-y-4">
<div class="bg-gray-900 text-white px-6 py-3 rounded-full shadow-2xl flex items-center space-x-2">
<i class="fas fa-info-circle text-brand-green"></i>
<span id="toastMessage">提示信息</span>
</div>
</div>
<script>
let currentLang = localStorage.getItem('cc_lang') || 'zh';
function t(key) {
return (i18n[currentLang] && i18n[currentLang][key] !== undefined) ? i18n[currentLang][key] : key;
}
const i18n = {
zh: {
login_welcome: "欢迎回来",
login_desc: "登录虫虫出海平台,实时追踪货物轨迹,管理订单与库存。",
login_feat1: "全程可视化追踪",
login_feat2: "智能订单管理",
login_feat3: "在线询价下单",
login_title: "账号登录",
login_no_account: "还没有账号?",
login_register_link: "立即注册",
login_account_label: "邮箱 / 手机号",
login_account_ph: "请输入邮箱或手机号",
login_password_label: "密码",
login_password_ph: "请输入密码",
login_remember: "记住我",
login_forgot: "忘记密码?",
login_submit: "登 录",
login_other: "其他方式登录",
toast_resetpw: "请联系客服重置密码",
toast_login_ok: "登录成功!欢迎回来"
},
en: {
login_welcome: "Welcome Back",
login_desc: "Log in to track shipments in real time and manage orders and inventory.",
login_feat1: "Full Visual Tracking",
login_feat2: "Smart Order Management",
login_feat3: "Online Quoting & Ordering",
login_title: "Account Login",
login_no_account: "No account yet?",
login_register_link: "Sign Up Now",
login_account_label: "Email / Phone",
login_account_ph: "Enter email or phone",
login_password_label: "Password",
login_password_ph: "Enter password",
login_remember: "Remember Me",
login_forgot: "Forgot Password?",
login_submit: "Log In",
login_other: "Other Sign-In Methods",
toast_resetpw: "Please contact support to reset your password",
toast_login_ok: "Login successful! Welcome back"
},
es: {
login_welcome: "Bienvenido de nuevo",
login_desc: "Inicia sesión para rastrear envíos en tiempo real y gestionar pedidos e inventario.",
login_feat1: "Seguimiento visual completo",
login_feat2: "Gestión inteligente de pedidos",
login_feat3: "Cotización y pedidos en línea",
login_title: "Inicio de sesión",
login_no_account: "¿Aún no tienes cuenta?",
login_register_link: "Regístrate ahora",
login_account_label: "Correo / Teléfono",
login_account_ph: "Ingresa correo o teléfono",
login_password_label: "Contraseña",
login_password_ph: "Ingresa tu contraseña",
login_remember: "Recuérdame",
login_forgot: "¿Olvidó su contraseña?",
login_submit: "Iniciar sesión",
login_other: "Otros métodos de acceso",
toast_resetpw: "Contacta a soporte para restablecer tu contraseña",
toast_login_ok: "¡Inicio de sesión exitoso! Bienvenido"
}
};
function applyLanguage(lang) {
if (!i18n[lang]) lang = 'zh';
currentLang = lang;
const langAttr = lang === 'zh' ? 'zh-CN' : (lang === 'es' ? 'es-MX' : 'en');
document.documentElement.setAttribute('lang', langAttr);
document.querySelectorAll('[data-i18n]').forEach(el => {
const k = el.getAttribute('data-i18n');
const v = i18n[lang][k];
if (v !== undefined) { if (v.indexOf('<br>') !== -1) el.innerHTML = v; else el.textContent = v; }
});
document.querySelectorAll('[data-i18n-ph]').forEach(el => {
const k = el.getAttribute('data-i18n-ph');
if (i18n[lang][k] !== undefined) el.setAttribute('placeholder', i18n[lang][k]);
});
document.querySelectorAll('[data-lang]').forEach(b => {
b.classList.toggle('active', b.getAttribute('data-lang') === lang);
});
try { localStorage.setItem('cc_lang', lang); } catch (e) {}
}
document.querySelectorAll('#langSwitcher [data-lang]').forEach(b => {
b.addEventListener('click', () => applyLanguage(b.getAttribute('data-lang')));
});
function showToast(message) {
const toast = document.getElementById('toast');
const toastMessage = document.getElementById('toastMessage');
toastMessage.textContent = message;
toast.classList.remove('opacity-0', 'pointer-events-none', '-translate-y-4');
toast.classList.add('opacity-100', 'translate-y-0');
setTimeout(() => {
toast.classList.add('opacity-0', 'pointer-events-none', '-translate-y-4');
toast.classList.remove('opacity-100', 'translate-y-0');
}, 3000);
}
function updateAuthUI() {}
document.querySelectorAll('.toggle-password').forEach(button => {
button.addEventListener('click', function() {
const input = this.previousElementSibling;
const icon = this.querySelector('i');
if (input.type === 'password') { input.type = 'text'; icon.classList.remove('fa-eye'); icon.classList.add('fa-eye-slash'); }
else { input.type = 'password'; icon.classList.remove('fa-eye-slash'); icon.classList.add('fa-eye'); }
});
});
document.getElementById('loginForm').addEventListener('submit', function(e) {
e.preventDefault();
showToast(t('toast_login_ok'));
setTimeout(() => window.location.href = 'workbench.html', 900);
});
applyLanguage(currentLang);
</script>
</body>
</html>
+286
View File
@@ -0,0 +1,286 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>%s</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;600;700;900&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = { theme: { extend: { colors: { brand: { green:'#4CAF50', greenDark:'#2E7D32', blue:'#1565C0', blueDark:'#0D47A1', teal:'#00838F', light:'#F0F7FF', dark:'#0A1628' } }, fontFamily: { sans:['Inter','Noto Sans SC','system-ui','sans-serif'] } } } };
</script>
<style>
:root { --primary:#1565C0; --primary-dark:#0D47A1; --accent:#4CAF50; --accent-dark:#2E7D32; }
* { scroll-behavior:smooth; }
body { font-family:'Inter','Noto Sans SC',system-ui,sans-serif; overflow-x:hidden; }
.gradient-text { background:linear-gradient(135deg,#1565C0 0%,#4CAF50 100%); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }
.gradient-bg { background:linear-gradient(135deg,#0A1628 0%,#1565C0 50%,#00838F 100%); }
.btn-primary { background:linear-gradient(135deg,#1565C0 0%,#0D47A1 100%); transition:all .3s ease; }
.btn-primary:hover { transform:translateY(-2px); box-shadow:0 10px 30px rgba(21,101,192,.35); }
.btn-accent { background:linear-gradient(135deg,#4CAF50 0%,#2E7D32 100%); transition:all .3s ease; }
.btn-accent:hover { transform:translateY(-2px); box-shadow:0 10px 30px rgba(76,175,80,.35); }
.input-field { transition:all .3s ease; }
.input-field:focus { border-color:#1565C0; box-shadow:0 0 0 4px rgba(21,101,192,.1); }
.auth-logo { display:block; filter:brightness(0) saturate(100%) invert(20%) sepia(84%) saturate(1926%) hue-rotate(202deg) brightness(94%) contrast(93%); }
.lang-switcher { display:flex; align-items:center; background:rgba(21,101,192,.08); border-radius:9999px; padding:3px; border:1px solid rgba(21,101,192,.15); }
.lang-switcher button { font-size:12px; font-weight:600; padding:4px 9px; border-radius:9999px; color:#0A1628; transition:all .25s ease; white-space:nowrap; }
.lang-switcher button:hover { color:#1565C0; }
.lang-switcher button.active { background:#1565C0; color:#fff; }
</style>
</head>
<body class="text-gray-800 antialiased bg-gray-50">
<!-- 顶部栏 -->
<header class="fixed top-0 inset-x-0 z-50 bg-white/95 backdrop-blur border-b border-gray-100">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
<a href="index.html" class="flex items-center" aria-label="返回官网">
<img src="assets/logo.png" alt="柏悦物流" class="auth-logo" style="width:161px;height:60px;object-fit:contain;">
</a>
<div class="lang-switcher" id="langSwitcher">
<button data-lang="zh" class="active" title="中文">中</button>
<button data-lang="en" title="English">EN</button>
<button data-lang="es" title="Español (México)">🇲🇽</button>
</div>
</div>
</header>
<section id="registerSection" class="min-h-screen pt-24 pb-12 bg-gray-50 flex items-center">
<div class="w-full max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="bg-white rounded-3xl shadow-2xl overflow-hidden">
<div class="grid lg:grid-cols-2">
<!-- 左侧视觉 -->
<div class="hidden lg:block relative">
<img src="https://images.unsplash.com/photo-1566576912321-d58ddd7a6088?ixlib=rb-4.0.3&auto=format&fit=crop&w=2070&q=80" alt="物流仓储" class="absolute inset-0 w-full h-full object-cover">
<div class="absolute inset-0 gradient-bg opacity-90"></div>
<div class="relative z-10 p-12 h-full flex flex-col justify-between text-white">
<div>
<img src="assets/logo.png" alt="虫虫出海" style="width:161px;height:60px;object-fit:contain;">
<h2 class="text-3xl font-bold mb-4" data-i18n="reg_welcome">加入我们</h2>
<p class="text-white/80" data-i18n="reg_welcome_desc">注册虫虫出海企业账号,开启高效跨境物流体验。</p>
</div>
<div class="space-y-4">
<div class="flex items-center space-x-3">
<i class="fas fa-check-circle text-brand-green"></i>
<span data-i18n="reg_feat1">新用户专属优惠</span>
</div>
<div class="flex items-center space-x-3">
<i class="fas fa-check-circle text-brand-green"></i>
<span data-i18n="reg_feat2">免费获取物流方案</span>
</div>
<div class="flex items-center space-x-3">
<i class="fas fa-check-circle text-brand-green"></i>
<span data-i18n="reg_feat3">专属客户经理对接</span>
</div>
</div>
</div>
</div>
<!-- 右侧表单 -->
<div class="p-8 sm:p-12 lg:p-16">
<div class="max-w-md mx-auto">
<h2 class="text-3xl font-bold text-gray-900 mb-2" data-i18n="reg_title">企业注册</h2>
<p class="text-gray-600 mb-8"><span data-i18n="reg_have_account">已有账号?</span><button onclick="window.location.href='login.html'" class="text-brand-blue font-semibold hover:underline" data-i18n="reg_login_link">立即登录</button></p>
<form id="registerForm" class="space-y-5">
<div class="grid sm:grid-cols-2 gap-5">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2" data-i18n="reg_company_label">企业名称</label>
<input type="text" name="company" required placeholder="请输入企业名称" data-i18n-ph="reg_company_ph" class="input-field w-full px-4 py-3.5 border border-gray-200 rounded-xl outline-none">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2" data-i18n="reg_contact_label">联系人</label>
<input type="text" name="contact" required placeholder="请输入联系人姓名" data-i18n-ph="reg_contact_ph" class="input-field w-full px-4 py-3.5 border border-gray-200 rounded-xl outline-none">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2" data-i18n="reg_email_label">邮箱</label>
<div class="relative">
<i class="fas fa-envelope absolute left-4 top-1/2 -translate-y-1/2 text-gray-400"></i>
<input type="email" name="email" required placeholder="请输入企业邮箱" data-i18n-ph="reg_email_ph" class="input-field w-full pl-11 pr-4 py-3.5 border border-gray-200 rounded-xl outline-none">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2" data-i18n="reg_phone_label">手机号</label>
<div class="relative">
<i class="fas fa-phone absolute left-4 top-1/2 -translate-y-1/2 text-gray-400"></i>
<input type="tel" name="phone" required placeholder="请输入手机号" data-i18n-ph="reg_phone_ph" class="input-field w-full pl-11 pr-4 py-3.5 border border-gray-200 rounded-xl outline-none">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2" data-i18n="reg_password_label">密码</label>
<div class="relative">
<i class="fas fa-lock absolute left-4 top-1/2 -translate-y-1/2 text-gray-400"></i>
<input type="password" name="password" required placeholder="请设置8-20位密码" data-i18n-ph="reg_password_ph" class="input-field w-full pl-11 pr-12 py-3.5 border border-gray-200 rounded-xl outline-none">
<button type="button" class="toggle-password absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600">
<i class="fas fa-eye"></i>
</button>
</div>
</div>
<div class="flex items-start">
<input type="checkbox" id="agree" required class="w-4 h-4 text-brand-blue rounded border-gray-300 mt-1 mr-2">
<label for="agree" class="text-sm text-gray-600">
<span data-i18n="reg_agree">我已阅读并同意</span> <button type="button" onclick="showToast(t('toast_tos'))" class="text-brand-blue hover:underline" data-i18n="reg_agree_tos">《用户服务协议》</button> 和 <button type="button" onclick="showToast(t('toast_pp'))" class="text-brand-blue hover:underline" data-i18n="reg_agree_pp">《隐私政策》</button>
</label>
</div>
<button type="submit" class="btn-accent w-full text-white font-bold py-4 rounded-xl text-lg" data-i18n="reg_submit">
立即注册
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</section> <!-- Toast 提示 -->
<div id="toast" class="fixed top-24 left-1/2 -translate-x-1/2 z-[60] opacity-0 pointer-events-none transition-all duration-300 transform -translate-y-4">
<div class="bg-gray-900 text-white px-6 py-3 rounded-full shadow-2xl flex items-center space-x-2">
<i class="fas fa-info-circle text-brand-green"></i>
<span id="toastMessage">提示信息</span>
</div>
</div>
<script>
let currentLang = localStorage.getItem('cc_lang') || 'zh';
function t(key) {
return (i18n[currentLang] && i18n[currentLang][key] !== undefined) ? i18n[currentLang][key] : key;
}
const i18n = {
zh: {
reg_welcome: "加入我们",
reg_welcome_desc: "注册虫虫出海企业账号,开启高效跨境物流体验。",
reg_feat1: "新用户专属优惠",
reg_feat2: "免费获取物流方案",
reg_feat3: "专属客户经理对接",
reg_title: "企业注册",
reg_have_account: "已有账号?",
reg_login_link: "立即登录",
reg_company_label: "企业名称",
reg_company_ph: "请输入企业名称",
reg_contact_label: "联系人",
reg_contact_ph: "请输入联系人姓名",
reg_email_label: "邮箱",
reg_email_ph: "请输入企业邮箱",
reg_phone_label: "手机号",
reg_phone_ph: "请输入手机号",
reg_password_label: "密码",
reg_password_ph: "请设置8-20位密码",
reg_agree: "我已阅读并同意",
reg_agree_tos: "《用户服务协议》",
reg_agree_pp: "《隐私政策》",
reg_submit: "立即注册",
toast_tos: "服务协议即将上线",
toast_pp: "隐私政策即将上线",
toast_register_ok: "注册成功!欢迎加入虫虫出海"
},
en: {
reg_welcome: "Join Us",
reg_welcome_desc: "Register a Chongchong Global business account and start efficient cross-border logistics.",
reg_feat1: "Exclusive New-User Offers",
reg_feat2: "Free Logistics Plan",
reg_feat3: "Dedicated Account Manager",
reg_title: "Business Registration",
reg_have_account: "Already have an account?",
reg_login_link: "Log In Now",
reg_company_label: "Company Name",
reg_company_ph: "Enter company name",
reg_contact_label: "Contact Person",
reg_contact_ph: "Enter contact name",
reg_email_label: "Email",
reg_email_ph: "Enter company email",
reg_phone_label: "Phone Number",
reg_phone_ph: "Enter phone number",
reg_password_label: "Password",
reg_password_ph: "Set an 8-20 char password",
reg_agree: "I have read and agree to the",
reg_agree_tos: "User Agreement",
reg_agree_pp: "Privacy Policy",
reg_submit: "Sign Up Now",
toast_tos: "Terms of Service coming soon",
toast_pp: "Privacy Policy coming soon",
toast_register_ok: "Registration successful! Welcome to Chongchong Global"
},
es: {
reg_welcome: "Únete a nosotros",
reg_welcome_desc: "Regístrate en la cuenta empresarial de Chongchong Global y comienza una logística eficiente.",
reg_feat1: "Ofertas exclusivas para nuevos usuarios",
reg_feat2: "Plan logístico gratuito",
reg_feat3: "Gerente de cuenta dedicado",
reg_title: "Registro empresarial",
reg_have_account: "¿Ya tienes una cuenta?",
reg_login_link: "Inicia sesión ahora",
reg_company_label: "Nombre de empresa",
reg_company_ph: "Ingresa el nombre de empresa",
reg_contact_label: "Persona de contacto",
reg_contact_ph: "Ingresa el nombre de contacto",
reg_email_label: "Correo electrónico",
reg_email_ph: "Ingresa el correo empresarial",
reg_phone_label: "Número de teléfono",
reg_phone_ph: "Ingresa tu número de teléfono",
reg_password_label: "Contraseña",
reg_password_ph: "Define una contraseña de 8-20 caracteres",
reg_agree: "He leído y acepto los",
reg_agree_tos: "Términos de servicio",
reg_agree_pp: "Política de privacidad",
reg_submit: "Regístrate ahora",
toast_tos: "Términos de servicio próximamente",
toast_pp: "Política de privacidad próximamente",
toast_register_ok: "¡Registro exitoso! Bienvenido a Chongchong Global"
}
};
function applyLanguage(lang) {
if (!i18n[lang]) lang = 'zh';
currentLang = lang;
const langAttr = lang === 'zh' ? 'zh-CN' : (lang === 'es' ? 'es-MX' : 'en');
document.documentElement.setAttribute('lang', langAttr);
document.querySelectorAll('[data-i18n]').forEach(el => {
const k = el.getAttribute('data-i18n');
const v = i18n[lang][k];
if (v !== undefined) { if (v.indexOf('<br>') !== -1) el.innerHTML = v; else el.textContent = v; }
});
document.querySelectorAll('[data-i18n-ph]').forEach(el => {
const k = el.getAttribute('data-i18n-ph');
if (i18n[lang][k] !== undefined) el.setAttribute('placeholder', i18n[lang][k]);
});
document.querySelectorAll('[data-lang]').forEach(b => {
b.classList.toggle('active', b.getAttribute('data-lang') === lang);
});
try { localStorage.setItem('cc_lang', lang); } catch (e) {}
}
document.querySelectorAll('#langSwitcher [data-lang]').forEach(b => {
b.addEventListener('click', () => applyLanguage(b.getAttribute('data-lang')));
});
function showToast(message) {
const toast = document.getElementById('toast');
const toastMessage = document.getElementById('toastMessage');
toastMessage.textContent = message;
toast.classList.remove('opacity-0', 'pointer-events-none', '-translate-y-4');
toast.classList.add('opacity-100', 'translate-y-0');
setTimeout(() => {
toast.classList.add('opacity-0', 'pointer-events-none', '-translate-y-4');
toast.classList.remove('opacity-100', 'translate-y-0');
}, 3000);
}
function updateAuthUI() {}
document.querySelectorAll('.toggle-password').forEach(button => {
button.addEventListener('click', function() {
const input = this.previousElementSibling;
const icon = this.querySelector('i');
if (input.type === 'password') { input.type = 'text'; icon.classList.remove('fa-eye'); icon.classList.add('fa-eye-slash'); }
else { input.type = 'password'; icon.classList.remove('fa-eye-slash'); icon.classList.add('fa-eye'); }
});
});
document.getElementById('registerForm').addEventListener('submit', function(e) {
e.preventDefault();
showToast(t('toast_register_ok'));
setTimeout(() => window.location.href = 'workbench.html', 900);
});
applyLanguage(currentLang);
</script>
</body>
</html>
+880
View File
@@ -0,0 +1,880 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>虫虫出海 CHONGCHONG GLOBAL | 工作台</title>
<meta name="description" content="虫虫出海工作台 - 海外采购、美国本土卡车、美本土散板一键下单。">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;600;700&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
brand: {
green: '#4CAF50',
greenDark: '#2E7D32',
blue: '#1565C0',
blueDark: '#0D47A1',
teal: '#00838F',
light: '#F0F7FF',
dark: '#0A1628'
}
},
fontFamily: {
sans: ['Inter', 'Noto Sans SC', 'system-ui', 'sans-serif'],
},
boxShadow: {
'card': '0 10px 40px rgba(10, 22, 40, 0.08)',
'card-hover': '0 20px 60px rgba(10, 22, 40, 0.15)',
}
}
}
}
</script>
<style>
* { scroll-behavior: smooth; }
body { font-family: 'Inter', 'Noto Sans SC', system-ui, sans-serif; overflow-x: hidden; }
.btn-primary {
background: linear-gradient(135deg, #1565C0 0%, #0D47A1 100%);
transition: all 0.3s ease;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px rgba(21, 101, 192, 0.35);
}
.btn-accent {
background: linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%);
transition: all 0.3s ease;
}
.btn-accent:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px rgba(76, 175, 80, 0.35);
}
/* 语言切换器 */
.lang-switcher {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.15);
border-radius: 9999px;
padding: 3px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.lang-switcher button {
font-size: 12px;
font-weight: 600;
padding: 4px 9px;
border-radius: 9999px;
color: rgba(255, 255, 255, 0.9);
transition: all 0.25s ease;
white-space: nowrap;
}
.lang-switcher button:hover { color: #fff; }
.lang-switcher button.active { background: #fff; color: #0D47A1; }
/* ===== 下单工作台 Dashboard ===== */
.dash-view { animation: fadeIn 0.4s ease; }
.dash-card {
background: #fff;
border: 1px solid #eef2f7;
border-radius: 1.25rem;
box-shadow: 0 10px 40px rgba(10, 22, 40, 0.06);
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
}
.dash-card:hover {
transform: translateY(-8px);
box-shadow: 0 24px 60px rgba(10, 22, 40, 0.14);
border-color: rgba(21, 101, 192, 0.35);
}
.dash-form-input {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid #e2e8f0;
border-radius: 0.75rem;
outline: none;
font-size: 0.95rem;
color: #1f2937;
background: #fff;
transition: all 0.2s ease;
}
.dash-form-input:focus {
border-color: #1565C0;
box-shadow: 0 0 0 3px rgba(21, 101, 192, 0.12);
}
.dash-form-input::placeholder { color: #9ca3af; }
.dash-label {
display: block;
font-size: 0.875rem;
font-weight: 600;
color: #374151;
margin-bottom: 0.5rem;
}
.dash-label .req { color: #ef4444; margin-left: 2px; }
.seg-input { position: absolute; opacity: 0; width: 0; height: 0; }
.seg-pill {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 0.65rem 1.1rem;
border: 1px solid #e2e8f0;
border-radius: 0.75rem;
cursor: pointer;
font-size: 0.9rem;
color: #475569;
background: #fff;
transition: all 0.2s ease;
user-select: none;
}
.seg-pill:hover { border-color: #cbd5e1; }
.seg-input:checked + .seg-pill {
border-color: #1565C0;
background: #EAF2FD;
color: #0D47A1;
font-weight: 600;
box-shadow: 0 0 0 1px #1565C0 inset;
}
.dash-topbar {
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(12px);
border-bottom: 1px solid #eef2f7;
}
.dash-sidebar {
width: 250px;
flex-shrink: 0;
background: linear-gradient(180deg, #0B1F3A 0%, #0C2A52 100%);
color: #fff;
display: flex;
flex-direction: column;
position: sticky;
top: 0;
height: 100vh;
padding: 1.25rem 0.875rem;
}
.dash-sidebar-brand {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.4rem 0.55rem 1.1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
margin-bottom: 0.6rem;
}
.dash-sidebar-brand .brand-name { font-weight: 700; font-size: 1rem; line-height: 1.15; }
.dash-sidebar-brand .brand-sub { font-size: 0.62rem; letter-spacing: 0.12em; color: rgba(255, 255, 255, 0.45); }
.dash-nav { flex: 1; display: flex; flex-direction: column; gap: 0.25rem; overflow-y: auto; }
.dash-nav-group {
font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.1em;
color: rgba(255, 255, 255, 0.4); padding: 0.85rem 0.75rem 0.35rem;
}
.dash-nav-link {
display: flex; align-items: center; gap: 0.75rem;
padding: 0.72rem 0.85rem; border-radius: 0.75rem;
color: rgba(255, 255, 255, 0.72); font-size: 0.92rem; font-weight: 500;
cursor: pointer; transition: all 0.2s ease; white-space: nowrap;
}
.dash-nav-link i { width: 1.2rem; text-align: center; font-size: 0.95rem; }
.dash-nav-link:hover { background: rgba(255, 255, 255, 0.07); color: #fff; }
.dash-nav-link.active {
background: linear-gradient(90deg, #1565C0, #1E88E5); color: #fff;
box-shadow: 0 6px 18px rgba(21, 101, 192, 0.4);
}
.dash-sidebar-bottom {
border-top: 1px solid rgba(255, 255, 255, 0.08); padding-top: 0.9rem;
display: flex; flex-direction: column; gap: 0.7rem; align-items: stretch;
}
.dash-sidebar-bottom #langSwitcherDash { align-self: center; }
.dash-logout-btn {
display: flex; align-items: center; justify-content: center; gap: 0.5rem;
padding: 0.6rem; border-radius: 0.7rem; font-size: 0.88rem; font-weight: 600;
background: rgba(239, 68, 68, 0.12); color: #fca5a5; cursor: pointer;
transition: all 0.2s ease;
}
.dash-logout-btn:hover { background: rgba(239, 68, 68, 0.22); color: #fff; }
.dash-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.dash-menu-btn {
display: none; width: 2.25rem; height: 2.25rem; border-radius: 0.6rem;
align-items: center; justify-content: center; color: #374151;
background: #f1f5f9; cursor: pointer;
}
.dash-overlay { display: none; position: fixed; inset: 0; background: rgba(0, 0, 0, 0.45); z-index: 45; }
.dash-overlay.show { display: block; }
.dash-right {
width: 320px; flex-shrink: 0;
background: #fff; border-left: 1px solid #eef2f7;
padding: 1.5rem 1.25rem;
position: sticky; top: 4rem; align-self: flex-start;
max-height: calc(100vh - 4rem); overflow-y: auto;
}
.dash-right-card {
background: #f8fafc; border: 1px solid #eef2f7;
border-radius: 1rem; padding: 1.25rem; margin-bottom: 1rem;
}
.dash-right-card h4 {
font-size: 0.72rem; font-weight: 700; letter-spacing: 0.06em;
text-transform: uppercase; color: #94a3b8; margin-bottom: 0.9rem;
}
.dash-account { display: flex; align-items: center; gap: 0.75rem; }
.dash-account-avatar {
width: 2.75rem; height: 2.75rem; border-radius: 0.8rem;
background: linear-gradient(135deg, #1565C0, #00BFA5); color: #fff;
display: flex; align-items: center; justify-content: center;
font-weight: 700; font-size: 1.1rem; flex-shrink: 0;
}
.dash-account-name { font-weight: 700; color: #0f172a; font-size: 0.95rem; line-height: 1.2; }
.dash-account-type { font-size: 0.75rem; color: #64748b; margin-top: 0.15rem; }
.dash-stat-row { display: flex; justify-content: space-between; align-items: center; padding: 0.6rem 0; border-bottom: 1px dashed #e2e8f0; }
.dash-stat-row:last-child { border-bottom: none; }
.dash-stat-label { font-size: 0.85rem; color: #475569; }
.dash-stat-value { font-weight: 700; color: #0f172a; font-size: 0.95rem; }
.dash-help-item {
display: flex; align-items: center; gap: 0.6rem;
padding: 0.6rem 0.55rem; border-radius: 0.6rem; cursor: pointer;
color: #334155; font-size: 0.9rem; transition: background 0.2s, color 0.2s;
}
.dash-help-item:hover { background: #eef2f7; color: #1565C0; }
.dash-help-item i { color: #00BFA5; width: 1.15rem; text-align: center; }
.dash-tip {
background: linear-gradient(135deg, rgba(0,191,165,0.08), rgba(21,101,192,0.08));
border: 1px solid rgba(0,191,165,0.22); border-radius: 1rem;
padding: 1.1rem; font-size: 0.82rem; color: #475569; line-height: 1.65;
}
.dash-tip i { color: #00BFA5; margin-right: 0.4rem; }
.dash-empty { padding: 1rem; color: #94a3b8; }
.dash-empty i { font-size: 3rem; color: #cbd5e1; margin-bottom: 1rem; display: block; }
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 768px) {
.dash-sidebar {
position: fixed; left: 0; top: 0; z-index: 50;
transform: translateX(-100%); transition: transform 0.3s ease;
}
.dash-sidebar.open { transform: translateX(0); }
.dash-menu-btn { display: inline-flex; }
}
</style>
</head>
<body class="text-gray-800 antialiased bg-gray-50">
<!-- Toast 提示 -->
<div id="toast" class="fixed top-24 left-1/2 -translate-x-1/2 z-[60] opacity-0 pointer-events-none transition-all duration-300 transform -translate-y-4">
<div class="bg-gray-900 text-white px-6 py-3 rounded-full shadow-2xl flex items-center space-x-2">
<i class="fas fa-info-circle text-brand-green"></i>
<span id="toastMessage">提示信息</span>
</div>
</div>
<!-- 工作台 -->
<section id="workbench" class="min-h-screen flex">
<aside id="dashSidebar" class="dash-sidebar">
<div class="dash-sidebar-brand">
<img src="assets/logo.png" alt="虫虫出海" style="width:161px;height:60px;object-fit:contain;">
</div>
<nav class="dash-nav">
<div class="dash-nav-group" data-i18n="dash_menu_main">主导航</div>
<a class="dash-nav-link active" data-view="Home" onclick="goDash('Home')"><i class="fas fa-th-large"></i><span data-i18n="dash_menu_overview">业务总览</span></a>
<a class="dash-nav-link" data-view="Procurement" onclick="goDash('Procurement')"><i class="fas fa-shopping-cart"></i><span data-i18n="dash_menu_proc">海外采购</span></a>
<a class="dash-nav-link" data-view="Truck" onclick="goDash('Truck')"><i class="fas fa-truck-moving"></i><span data-i18n="dash_menu_truck">美国本土卡</span></a>
<a class="dash-nav-link" data-view="Pallet" onclick="goDash('Pallet')"><i class="fas fa-pallet"></i><span data-i18n="dash_menu_pallet">美本土散板</span></a>
<a class="dash-nav-link" data-view="Orders" onclick="goDash('Orders')"><i class="fas fa-clipboard-list"></i><span data-i18n="dash_menu_orders">我的订单</span></a>
</nav>
<div class="dash-sidebar-bottom">
<div id="langSwitcherDash" class="lang-switcher flex">
<button data-lang="zh">中</button>
<button data-lang="en">EN</button>
<button data-lang="es">🇲🇽</button>
</div>
<button class="dash-logout-btn" onclick="doLogout()"><i class="fas fa-sign-out-alt"></i><span data-i18n="dash_logout">退出登录</span></button>
</div>
</aside>
<!-- 移动端遮罩 -->
<div id="dashOverlay" class="dash-overlay" onclick="toggleDashSidebar()"></div>
<!-- 主内容区 -->
<div class="dash-main">
<header class="dash-topbar sticky top-0 z-30">
<div class="px-4 sm:px-6 lg:px-10 h-16 flex items-center justify-between">
<div class="flex items-center space-x-3">
<button class="dash-menu-btn" onclick="toggleDashSidebar()"><i class="fas fa-bars"></i></button>
<span class="text-sm text-gray-500" id="dashUserName">Hi, 客户</span>
</div>
<a href="index.html" class="text-sm text-gray-600 hover:text-brand-blue font-medium px-3 py-2 transition-colors" data-i18n="dash_home_link">返回官网</a>
</div>
</header>
<div class="flex flex-1 min-h-0">
<main class="flex-1 px-4 sm:px-6 lg:px-10 py-8 min-w-0">
<!-- 视图1:选择业务 -->
<div id="dashViewHome" class="dash-view">
<div class="mb-10">
<h1 class="text-3xl lg:text-4xl font-bold text-gray-900 mb-3" data-i18n="dash_welcome_title">选择业务,快速下单</h1>
<p class="text-gray-500" data-i18n="dash_welcome_sub">请选择您需要的业务类型,填写信息后提交订单,我们将尽快为您安排。</p>
</div>
<div class="grid md:grid-cols-3 gap-6">
<div class="dash-card p-8" onclick="openOrder('Procurement')">
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-brand-blue to-brand-blueDark flex items-center justify-center text-white text-2xl mb-6">
<i class="fas fa-shopping-cart"></i>
</div>
<h3 class="text-xl font-bold text-gray-900 mb-2" data-i18n="dash_biz1_title">海外采购</h3>
<p class="text-sm text-gray-500 mb-6" data-i18n="dash_biz1_desc">采购代购 · 验货质检 · 集中发货</p>
<span class="inline-flex items-center text-brand-blue font-semibold text-sm">
<span data-i18n="dash_start">开始下单</span><i class="fas fa-arrow-right ml-2"></i>
</span>
</div>
<div class="dash-card p-8" onclick="openOrder('Truck')">
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-brand-green to-brand-greenDark flex items-center justify-center text-white text-2xl mb-6">
<i class="fas fa-truck-moving"></i>
</div>
<h3 class="text-xl font-bold text-gray-900 mb-2" data-i18n="dash_biz2_title">美国本土卡</h3>
<p class="text-sm text-gray-500 mb-6" data-i18n="dash_biz2_desc">港口提柜 · 仓到仓 · 仓到门</p>
<span class="inline-flex items-center text-brand-green font-semibold text-sm">
<span data-i18n="dash_start">开始下单</span><i class="fas fa-arrow-right ml-2"></i>
</span>
</div>
<div class="dash-card p-8" onclick="openOrder('Pallet')">
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-brand-teal to-cyan-700 flex items-center justify-center text-white text-2xl mb-6">
<i class="fas fa-pallet"></i>
</div>
<h3 class="text-xl font-bold text-gray-900 mb-2" data-i18n="dash_biz3_title">美本土散板</h3>
<p class="text-sm text-gray-500 mb-6" data-i18n="dash_biz3_desc">拼车拼载 · 打盘服务 · 灵活派送</p>
<span class="inline-flex items-center text-brand-teal font-semibold text-sm">
<span data-i18n="dash_start">开始下单</span><i class="fas fa-arrow-right ml-2"></i>
</span>
</div>
</div>
</div>
<!-- 视图2:海外采购下单 -->
<div id="dashViewProcurement" class="dash-view hidden">
<button onclick="showDashView('Home')" class="inline-flex items-center text-gray-500 hover:text-brand-blue mb-6 text-sm font-medium">
<i class="fas fa-arrow-left mr-2"></i><span data-i18n="dash_back">返回业务选择</span>
</button>
<div class="bg-white rounded-2xl shadow-card border border-gray-100 p-8 lg:p-10 max-w-3xl">
<div class="flex items-center space-x-3 mb-8">
<div class="w-11 h-11 rounded-xl bg-gradient-to-br from-brand-blue to-brand-blueDark flex items-center justify-center text-white text-lg"><i class="fas fa-shopping-cart"></i></div>
<h2 class="text-2xl font-bold text-gray-900" data-i18n="po_title">海外采购下单</h2>
</div>
<form class="order-form space-y-5" data-order="海外采购 Overseas Sourcing">
<div class="grid sm:grid-cols-2 gap-5">
<div>
<label class="dash-label"><span data-i18n="po_category">采购品类</span><span class="req">*</span></label>
<input type="text" name="采购品类" required class="dash-form-input" placeholder="如:家居用品 / 3C 配件" data-i18n-ph="po_category_ph">
</div>
<div>
<label class="dash-label"><span data-i18n="po_spec">商品规格</span></label>
<input type="text" name="商品规格" class="dash-form-input" placeholder="如:尺寸 / 颜色 / 材质" data-i18n-ph="po_spec_ph">
</div>
</div>
<div>
<label class="dash-label"><span data-i18n="po_link">商品链接</span></label>
<input type="url" name="商品链接" class="dash-form-input" placeholder="https://" data-i18n-ph="po_link_ph">
</div>
<div class="grid sm:grid-cols-2 gap-5">
<div>
<label class="dash-label"><span data-i18n="po_qty">采购数量</span><span class="req">*</span></label>
<input type="number" min="1" name="采购数量" required class="dash-form-input" placeholder="0" data-i18n-ph="po_qty_ph">
</div>
<div>
<label class="dash-label"><span data-i18n="po_price">预算单价</span></label>
<div class="relative">
<span class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 font-medium">$</span>
<input type="number" min="0" step="0.01" name="预算单价($)" class="dash-form-input pl-8" placeholder="0.00" data-i18n-ph="po_price_ph">
</div>
</div>
</div>
<div>
<label class="dash-label"><span data-i18n="po_address">收货地址</span></label>
<input type="text" name="收货地址" class="dash-form-input" placeholder="请输入收货地址" data-i18n-ph="po_address_ph">
</div>
<div class="grid sm:grid-cols-2 gap-5">
<div>
<label class="dash-label"><span data-i18n="po_contact">联系人</span><span class="req">*</span></label>
<input type="text" name="联系人" required class="dash-form-input" placeholder="请输入联系人姓名" data-i18n-ph="po_contact_ph">
</div>
<div>
<label class="dash-label"><span data-i18n="po_phone">联系电话</span><span class="req">*</span></label>
<input type="tel" name="联系电话" required class="dash-form-input" placeholder="请输入联系电话" data-i18n-ph="po_phone_ph">
</div>
</div>
<div>
<label class="dash-label"><span data-i18n="po_note">订单备注</span></label>
<textarea name="订单备注" rows="3" class="dash-form-input" placeholder="其他需求说明(选填)" data-i18n-ph="po_note_ph"></textarea>
</div>
<div class="flex gap-3 pt-2">
<button type="submit" class="btn-primary text-white font-bold px-8 py-3.5 rounded-xl" data-i18n="dash_submit">提交订单</button>
<button type="button" onclick="showDashView('Home')" class="px-8 py-3.5 rounded-xl border border-gray-200 text-gray-600 font-medium hover:bg-gray-50" data-i18n="dash_cancel">取消</button>
</div>
</form>
</div>
</div>
<!-- 视图3:美国本土卡下单 -->
<div id="dashViewTruck" class="dash-view hidden">
<button onclick="showDashView('Home')" class="inline-flex items-center text-gray-500 hover:text-brand-blue mb-6 text-sm font-medium">
<i class="fas fa-arrow-left mr-2"></i><span data-i18n="dash_back">返回业务选择</span>
</button>
<div class="bg-white rounded-2xl shadow-card border border-gray-100 p-8 lg:p-10 max-w-3xl">
<div class="flex items-center space-x-3 mb-8">
<div class="w-11 h-11 rounded-xl bg-gradient-to-br from-brand-green to-brand-greenDark flex items-center justify-center text-white text-lg"><i class="fas fa-truck-moving"></i></div>
<h2 class="text-2xl font-bold text-gray-900" data-i18n="tk_title">美国本土卡下单</h2>
</div>
<form class="order-form space-y-6" data-order="美国本土卡 US Trucking">
<div>
<label class="dash-label"><span data-i18n="ts_type">运输类型</span><span class="req">*</span></label>
<div class="flex flex-wrap gap-3">
<label><input type="radio" name="运输类型" value="港口提柜" class="seg-input" checked><span class="seg-pill"><i class="fas fa-anchor text-xs"></i><span data-i18n="ts_type_port">港口提柜</span></span></label>
<label><input type="radio" name="运输类型" value="仓到仓" class="seg-input"><span class="seg-pill"><i class="fas fa-warehouse text-xs"></i><span data-i18n="ts_type_w2w">仓到仓</span></span></label>
<label><input type="radio" name="运输类型" value="仓到门" class="seg-input"><span class="seg-pill"><i class="fas fa-door-open text-xs"></i><span data-i18n="ts_type_w2d">仓到门</span></span></label>
</div>
</div>
<div class="grid sm:grid-cols-2 gap-5">
<div>
<label class="dash-label"><span data-i18n="ts_origin">起运地</span><span class="req">*</span></label>
<input type="text" name="起运地" required class="dash-form-input" placeholder="城市 / 港口 / 州" data-i18n-ph="ts_origin_ph">
</div>
<div>
<label class="dash-label"><span data-i18n="ts_dest">目的地</span><span class="req">*</span></label>
<input type="text" name="目的地" required class="dash-form-input" placeholder="城市 / 州" data-i18n-ph="ts_dest_ph">
</div>
<div>
<label class="dash-label"><span data-i18n="ts_origin_addr">详细地址</span></label>
<input type="text" name="起运详细地址" class="dash-form-input" placeholder="街道 / 门牌" data-i18n-ph="ts_origin_addr_ph">
</div>
<div>
<label class="dash-label"><span data-i18n="ts_dest_addr">详细地址 / 码头 / 仓号</span></label>
<input type="text" name="目的详细地址/码头/仓号" class="dash-form-input" placeholder="地址 / 码头 / 仓号" data-i18n-ph="ts_dest_addr_ph">
</div>
</div>
<div>
<label class="dash-label mb-3"><i class="fas fa-box mr-1 text-gray-400"></i><span data-i18n="cargo_title">货物信息</span></label>
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div>
<label class="text-xs text-gray-500 mb-1 block" data-i18n="cargo_container">柜型</label>
<select name="柜型" class="dash-form-input">
<option value="20GP">20GP</option>
<option value="40GP">40GP</option>
<option value="40HQ">40HQ</option>
<option value="45HQ">45HQ</option>
<option value="LTL" data-i18n="cargo_container_ltl">散货/托盘</option>
</select>
</div>
<div>
<label class="text-xs text-gray-500 mb-1 block" data-i18n="cargo_pieces">件数</label>
<input type="number" min="0" name="件数" class="dash-form-input" placeholder="0">
</div>
<div>
<label class="text-xs text-gray-500 mb-1 block" data-i18n="cargo_weight">总重量 (kg)</label>
<input type="number" min="0" step="0.01" name="总重量(kg)" class="dash-form-input" placeholder="0">
</div>
<div>
<label class="text-xs text-gray-500 mb-1 block" data-i18n="cargo_volume">总体积 (m³)</label>
<input type="number" min="0" step="0.01" name="总体积(m³)" class="dash-form-input" placeholder="0">
</div>
</div>
</div>
<div>
<label class="dash-label mb-3"><i class="fas fa-user mr-1 text-gray-400"></i><span data-i18n="contact_title">联系人信息</span></label>
<div class="grid sm:grid-cols-2 gap-5">
<div>
<label class="dash-label"><span data-i18n="contact_name">联系人</span><span class="req">*</span></label>
<input type="text" name="联系人" required class="dash-form-input" placeholder="请输入联系人姓名" data-i18n-ph="contact_name_ph">
</div>
<div>
<label class="dash-label"><span data-i18n="contact_phone">联系方式</span><span class="req">*</span></label>
<input type="text" name="联系方式" required class="dash-form-input" placeholder="电话 / 微信 / 邮箱" data-i18n-ph="contact_phone_ph">
</div>
</div>
</div>
<div class="max-w-xs">
<label class="dash-label"><span data-i18n="req_time">要求时间</span></label>
<input type="date" name="要求时间" class="dash-form-input">
</div>
<div class="flex gap-3 pt-2">
<button type="submit" class="btn-primary text-white font-bold px-8 py-3.5 rounded-xl" data-i18n="dash_submit">提交订单</button>
<button type="button" onclick="showDashView('Home')" class="px-8 py-3.5 rounded-xl border border-gray-200 text-gray-600 font-medium hover:bg-gray-50" data-i18n="dash_cancel">取消</button>
</div>
</form>
</div>
</div>
<!-- 视图4:美本土散板下单 -->
<div id="dashViewPallet" class="dash-view hidden">
<button onclick="showDashView('Home')" class="inline-flex items-center text-gray-500 hover:text-brand-blue mb-6 text-sm font-medium">
<i class="fas fa-arrow-left mr-2"></i><span data-i18n="dash_back">返回业务选择</span>
</button>
<div class="bg-white rounded-2xl shadow-card border border-gray-100 p-8 lg:p-10 max-w-3xl">
<div class="flex items-center space-x-3 mb-8">
<div class="w-11 h-11 rounded-xl bg-gradient-to-br from-brand-teal to-cyan-700 flex items-center justify-center text-white text-lg"><i class="fas fa-pallet"></i></div>
<h2 class="text-2xl font-bold text-gray-900" data-i18n="pl_title">美本土散板下单</h2>
</div>
<form class="order-form space-y-6" data-order="美本土散板 US Pallet">
<div class="grid sm:grid-cols-2 gap-5">
<div>
<label class="dash-label"><span data-i18n="pl_method">运输方式</span><span class="req">*</span></label>
<div class="flex flex-wrap gap-3">
<label><input type="radio" name="运输方式" value="拼车" class="seg-input" checked><span class="seg-pill"><span data-i18n="pl_method_pool">拼车</span></span></label>
<label><input type="radio" name="运输方式" value="优先拼载" class="seg-input"><span class="seg-pill"><span data-i18n="pl_method_priority">优先拼载</span></span></label>
</div>
</div>
<div>
<label class="dash-label"><span data-i18n="pl_palletize">是否打盘</span><span class="req">*</span></label>
<div class="flex flex-wrap gap-3">
<label><input type="radio" name="是否打盘" value="是" class="seg-input" checked><span class="seg-pill"><span data-i18n="pl_yes">是</span></span></label>
<label><input type="radio" name="是否打盘" value="否" class="seg-input"><span class="seg-pill"><span data-i18n="pl_no">否</span></span></label>
</div>
</div>
</div>
<div>
<label class="dash-label"><span data-i18n="ts_type">运输类型</span><span class="req">*</span></label>
<div class="flex flex-wrap gap-3">
<label><input type="radio" name="运输类型" value="港口提柜" class="seg-input" checked><span class="seg-pill"><i class="fas fa-anchor text-xs"></i><span data-i18n="ts_type_port">港口提柜</span></span></label>
<label><input type="radio" name="运输类型" value="仓到仓" class="seg-input"><span class="seg-pill"><i class="fas fa-warehouse text-xs"></i><span data-i18n="ts_type_w2w">仓到仓</span></span></label>
<label><input type="radio" name="运输类型" value="仓到门" class="seg-input"><span class="seg-pill"><i class="fas fa-door-open text-xs"></i><span data-i18n="ts_type_w2d">仓到门</span></span></label>
</div>
</div>
<div class="grid sm:grid-cols-2 gap-5">
<div>
<label class="dash-label"><span data-i18n="ts_origin">起运地</span><span class="req">*</span></label>
<input type="text" name="起运地" required class="dash-form-input" placeholder="城市 / 港口 / 州" data-i18n-ph="ts_origin_ph">
</div>
<div>
<label class="dash-label"><span data-i18n="ts_dest">目的地</span><span class="req">*</span></label>
<input type="text" name="目的地" required class="dash-form-input" placeholder="城市 / 州" data-i18n-ph="ts_dest_ph">
</div>
<div>
<label class="dash-label"><span data-i18n="ts_origin_addr">详细地址</span></label>
<input type="text" name="起运详细地址" class="dash-form-input" placeholder="街道 / 门牌" data-i18n-ph="ts_origin_addr_ph">
</div>
<div>
<label class="dash-label"><span data-i18n="ts_dest_addr">详细地址 / 码头 / 仓号</span></label>
<input type="text" name="目的详细地址/码头/仓号" class="dash-form-input" placeholder="地址 / 码头 / 仓号" data-i18n-ph="ts_dest_addr_ph">
</div>
</div>
<div>
<label class="dash-label mb-3"><i class="fas fa-box mr-1 text-gray-400"></i><span data-i18n="cargo_title">货物信息</span></label>
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div>
<label class="text-xs text-gray-500 mb-1 block" data-i18n="cargo_container">柜型</label>
<select name="柜型" class="dash-form-input">
<option value="20GP">20GP</option>
<option value="40GP">40GP</option>
<option value="40HQ">40HQ</option>
<option value="45HQ">45HQ</option>
<option value="LTL" data-i18n="cargo_container_ltl">散货/托盘</option>
</select>
</div>
<div>
<label class="text-xs text-gray-500 mb-1 block" data-i18n="cargo_pieces">件数</label>
<input type="number" min="0" name="件数" class="dash-form-input" placeholder="0">
</div>
<div>
<label class="text-xs text-gray-500 mb-1 block" data-i18n="cargo_weight">总重量 (kg)</label>
<input type="number" min="0" step="0.01" name="总重量(kg)" class="dash-form-input" placeholder="0">
</div>
<div>
<label class="text-xs text-gray-500 mb-1 block" data-i18n="cargo_volume">总体积 (m³)</label>
<input type="number" min="0" step="0.01" name="总体积(m³)" class="dash-form-input" placeholder="0">
</div>
</div>
</div>
<div>
<label class="dash-label mb-3"><i class="fas fa-user mr-1 text-gray-400"></i><span data-i18n="contact_title">联系人信息</span></label>
<div class="grid sm:grid-cols-2 gap-5">
<div>
<label class="dash-label"><span data-i18n="contact_name">联系人</span><span class="req">*</span></label>
<input type="text" name="联系人" required class="dash-form-input" placeholder="请输入联系人姓名" data-i18n-ph="contact_name_ph">
</div>
<div>
<label class="dash-label"><span data-i18n="contact_phone">联系方式</span><span class="req">*</span></label>
<input type="text" name="联系方式" required class="dash-form-input" placeholder="电话 / 微信 / 邮箱" data-i18n-ph="contact_phone_ph">
</div>
</div>
</div>
<div class="max-w-xs">
<label class="dash-label"><span data-i18n="req_time">要求时间</span></label>
<input type="date" name="要求时间" class="dash-form-input">
</div>
<div class="flex gap-3 pt-2">
<button type="submit" class="btn-primary text-white font-bold px-8 py-3.5 rounded-xl" data-i18n="dash_submit">提交订单</button>
<button type="button" onclick="showDashView('Home')" class="px-8 py-3.5 rounded-xl border border-gray-200 text-gray-600 font-medium hover:bg-gray-50" data-i18n="dash_cancel">取消</button>
</div>
</form>
</div>
</div>
<!-- 视图5:我的订单 -->
<div id="dashViewOrders" class="dash-view hidden">
<div class="bg-white rounded-2xl shadow-card border border-gray-100 p-10 lg:p-14 max-w-3xl mx-auto text-center">
<div class="dash-empty">
<i class="fas fa-clipboard-list"></i>
<h3 class="text-xl font-bold text-gray-700 mb-2" data-i18n="dash_orders_empty">暂无订单</h3>
<p class="text-gray-500" data-i18n="dash_orders_hint">您还没有提交任何订单,前往「业务总览」选择服务开始下单吧。</p>
</div>
</div>
</div>
</main>
<!-- 右侧信息栏 -->
<aside id="dashRight" class="dash-right hidden lg:flex flex-col">
<div class="dash-right-card">
<h4 data-i18n="right_account_title">账户信息</h4>
<div class="dash-account">
<div class="dash-account-avatar" id="dashAvatar">C</div>
<div>
<div class="dash-account-name" id="dashAccountName">虫虫出海客户</div>
<div class="dash-account-type" data-i18n="right_account_type">企业客户</div>
</div>
</div>
</div>
<div class="dash-right-card">
<h4 data-i18n="right_stat_title">数据概览</h4>
<div class="dash-stat-row">
<span class="dash-stat-label" data-i18n="right_stat_progress">进行中订单</span>
<span class="dash-stat-value" id="statProgress">0</span>
</div>
<div class="dash-stat-row">
<span class="dash-stat-label" data-i18n="right_stat_done">已完成订单</span>
<span class="dash-stat-value" id="statDone">0</span>
</div>
<div class="dash-stat-row">
<span class="dash-stat-label" data-i18n="right_stat_month">本月下单</span>
<span class="dash-stat-value" id="statMonth">0</span>
</div>
</div>
<div class="dash-right-card">
<h4 data-i18n="right_help_title">帮助与支持</h4>
<div class="dash-help-item" onclick="showToast(t('right_help_cs_toast'))">
<i class="fas fa-headset"></i><span data-i18n="right_help_cs">联系客服</span>
</div>
<div class="dash-help-item" onclick="showToast(t('right_help_faq_toast'))">
<i class="fas fa-question-circle"></i><span data-i18n="right_help_faq">常见问题</span>
</div>
<div class="dash-help-item" onclick="showToast(t('right_help_quote_toast'))">
<i class="fas fa-tag"></i><span data-i18n="right_help_quote">获取报价</span>
</div>
</div>
<div class="dash-tip">
<i class="fas fa-lightbulb"></i>
<span data-i18n="right_tip_text">提交订单后,专属客户经理将在 24 小时内与您联系,确认运输方案与报价。</span>
</div>
</aside>
</div>
</div>
</section>
<script>
// 工作台交互
function setDashUser(name) {
const display = name ? name : '虫虫出海客户';
const topEl = document.getElementById('dashUserName');
if (topEl) topEl.textContent = 'Hi, ' + display;
const accEl = document.getElementById('dashAccountName');
if (accEl) accEl.textContent = display;
const avEl = document.getElementById('dashAvatar');
if (avEl) avEl.textContent = display.charAt(0).toUpperCase();
}
function showDashView(name) {
document.querySelectorAll('#workbench .dash-view').forEach(v => v.classList.add('hidden'));
const view = document.getElementById('dashView' + name);
if (view) view.classList.remove('hidden');
document.querySelectorAll('.dash-nav-link').forEach(l => l.classList.toggle('active', l.getAttribute('data-view') === name));
window.scrollTo(0, 0);
}
function goDash(name) {
showDashView(name);
closeDashSidebar();
}
function toggleDashSidebar() {
const sb = document.getElementById('dashSidebar');
const ov = document.getElementById('dashOverlay');
if (!sb) return;
const open = sb.classList.toggle('open');
if (ov) ov.classList.toggle('show', open);
document.body.style.overflow = open ? 'hidden' : '';
}
function closeDashSidebar() {
const sb = document.getElementById('dashSidebar');
const ov = document.getElementById('dashOverlay');
if (sb) sb.classList.remove('open');
if (ov) ov.classList.remove('show');
document.body.style.overflow = '';
}
function openOrder(type) {
showDashView(type);
}
function doLogout() {
showToast(t('toast_logout'));
setTimeout(() => { window.location.href = 'index.html'; }, 700);
}
// 订单表单提交
document.querySelectorAll('.order-form').forEach(form => {
form.addEventListener('submit', function(e) {
e.preventDefault();
showToast(t('toast_order_ok'));
this.reset();
setTimeout(() => showDashView('Home'), 1200);
});
});
// ====== 多语言 i18n ======
const i18n = {
zh: {
dash_home_link: "返回官网", dash_logout: "退出登录",
dash_welcome_title: "选择业务,快速下单", dash_welcome_sub: "请选择您需要的业务类型,填写信息后提交订单,我们将尽快为您安排。",
dash_biz1_title: "海外采购", dash_biz1_desc: "采购代购 · 验货质检 · 集中发货",
dash_biz2_title: "美国本土卡", dash_biz2_desc: "港口提柜 · 仓到仓 · 仓到门",
dash_biz3_title: "美本土散板", dash_biz3_desc: "拼车拼载 · 打盘服务 · 灵活派送",
dash_start: "开始下单", dash_back: "返回业务选择", dash_submit: "提交订单", dash_cancel: "取消",
po_title: "海外采购下单", po_category: "采购品类", po_category_ph: "如:家居用品 / 3C 配件",
po_spec: "商品规格", po_spec_ph: "如:尺寸 / 颜色 / 材质", po_link: "商品链接", po_link_ph: "https://",
po_qty: "采购数量", po_qty_ph: "0", po_price: "预算单价", po_price_ph: "0.00",
po_address: "收货地址", po_address_ph: "请输入收货地址", po_contact: "联系人", po_contact_ph: "请输入联系人姓名",
po_phone: "联系电话", po_phone_ph: "请输入联系电话", po_note: "订单备注", po_note_ph: "其他需求说明(选填)",
tk_title: "美国本土卡下单",
pl_title: "美本土散板下单", pl_method: "运输方式", pl_method_pool: "拼车", pl_method_priority: "优先拼载",
pl_palletize: "是否打盘", pl_yes: "是", pl_no: "否",
ts_type: "运输类型", ts_type_port: "港口提柜", ts_type_w2w: "仓到仓", ts_type_w2d: "仓到门",
ts_origin: "起运地", ts_origin_ph: "城市 / 港口 / 州", ts_dest: "目的地", ts_dest_ph: "城市 / 州",
ts_origin_addr: "详细地址", ts_origin_addr_ph: "街道 / 门牌",
ts_dest_addr: "详细地址 / 码头 / 仓号", ts_dest_addr_ph: "地址 / 码头 / 仓号",
cargo_title: "货物信息", cargo_container: "柜型", cargo_container_ltl: "散货/托盘",
cargo_pieces: "件数", cargo_weight: "总重量 (kg)", cargo_volume: "总体积 (m³)",
req_time: "要求时间", contact_title: "联系人信息", contact_name: "联系人", contact_name_ph: "请输入联系人姓名", contact_phone: "联系方式", contact_phone_ph: "电话 / 微信 / 邮箱",
toast_logout: "已退出登录", toast_order_ok: "订单提交成功,我们将尽快为您安排!",
dash_menu_main: "主导航", dash_menu_overview: "业务总览", dash_menu_proc: "海外采购",
dash_menu_truck: "美国本土卡", dash_menu_pallet: "美本土散板", dash_menu_orders: "我的订单",
dash_orders_empty: "暂无订单", dash_orders_hint: "您还没有提交任何订单,前往「业务总览」选择服务开始下单吧。",
right_account_title: "账户信息", right_account_type: "企业客户",
right_stat_title: "数据概览", right_stat_progress: "进行中订单", right_stat_done: "已完成订单", right_stat_month: "本月下单",
right_help_title: "帮助与支持", right_help_cs: "联系客服", right_help_faq: "常见问题", right_help_quote: "获取报价",
right_help_cs_toast: "客服热线:400-XXX-XXXX(工作日 9:00-18:00)", right_help_faq_toast: "常见问题页面即将上线", right_help_quote_toast: "报价顾问将尽快与您联系",
right_tip_text: "提交订单后,专属客户经理将在 24 小时内与您联系,确认运输方案与报价。"
},
en: {
dash_home_link: "Back to Site", dash_logout: "Log Out",
dash_welcome_title: "Choose a Service to Order", dash_welcome_sub: "Select the service you need, fill in the details, and submit your order — we will arrange it for you shortly.",
dash_biz1_title: "Overseas Sourcing", dash_biz1_desc: "Sourcing · Inspection · Consolidation",
dash_biz2_title: "US Domestic Trucking", dash_biz2_desc: "Port Pickup · Warehouse-to-Warehouse · Door-to-Door",
dash_biz3_title: "US Pallet Break", dash_biz3_desc: "Pooled Freight · Palletizing · Flexible Delivery",
dash_start: "Start Order", dash_back: "Back to Services", dash_submit: "Submit Order", dash_cancel: "Cancel",
po_title: "Overseas Sourcing Order", po_category: "Product Category", po_category_ph: "e.g. Home goods / 3C accessories",
po_spec: "Specs", po_spec_ph: "e.g. Size / Color / Material", po_link: "Product Link", po_link_ph: "https://",
po_qty: "Quantity", po_qty_ph: "0", po_price: "Budget Unit Price", po_price_ph: "0.00",
po_address: "Shipping Address", po_address_ph: "Enter shipping address", po_contact: "Contact Name", po_contact_ph: "Enter contact name",
po_phone: "Contact Phone", po_phone_ph: "Enter phone number", po_note: "Order Notes", po_note_ph: "Other requirements (optional)",
tk_title: "US Domestic Trucking Order",
pl_title: "US Pallet Break Order", pl_method: "Transport Mode", pl_method_pool: "Pooled", pl_method_priority: "Priority Load",
pl_palletize: "Palletize?", pl_yes: "Yes", pl_no: "No",
ts_type: "Transport Type", ts_type_port: "Port Pickup", ts_type_w2w: "Warehouse to Warehouse", ts_type_w2d: "Warehouse to Door",
ts_origin: "Origin", ts_origin_ph: "City / Port / State", ts_dest: "Destination", ts_dest_ph: "City / State",
ts_origin_addr: "Detailed Address", ts_origin_addr_ph: "Street / Number",
ts_dest_addr: "Detailed Address / Pier / WH No.", ts_dest_addr_ph: "Address / Pier / WH No.",
cargo_title: "Cargo Info", cargo_container: "Container Type", cargo_container_ltl: "Loose / Pallet",
cargo_pieces: "Pieces", cargo_weight: "Total Weight (kg)", cargo_volume: "Total Volume (m³)",
req_time: "Required By", contact_title: "Contact Info", contact_name: "Contact Person", contact_name_ph: "Enter contact name", contact_phone: "Contact Number", contact_phone_ph: "Phone / WeChat / Email",
toast_logout: "Logged out", toast_order_ok: "Order submitted! We will arrange it for you shortly.",
dash_menu_main: "Main Menu", dash_menu_overview: "Overview", dash_menu_proc: "Overseas Sourcing",
dash_menu_truck: "US Trucking", dash_menu_pallet: "US Pallet Break", dash_menu_orders: "My Orders",
dash_orders_empty: "No Orders Yet", dash_orders_hint: "You haven't placed any orders yet. Go to Overview to start a new order.",
right_account_title: "Account", right_account_type: "Enterprise Client",
right_stat_title: "Overview", right_stat_progress: "In Progress", right_stat_done: "Completed", right_stat_month: "This Month",
right_help_title: "Help & Support", right_help_cs: "Contact Support", right_help_faq: "FAQ", right_help_quote: "Get a Quote",
right_help_cs_toast: "Support Hotline: 400-XXX-XXXX (Mon-Fri 9:00-18:00)", right_help_faq_toast: "FAQ page coming soon", right_help_quote_toast: "A quoting specialist will contact you shortly",
right_tip_text: "After you submit an order, your dedicated account manager will contact you within 24 hours to confirm the shipping plan and quote."
},
es: {
dash_home_link: "Volver al sitio", dash_logout: "Cerrar sesión",
dash_welcome_title: "Elige un servicio para pedir", dash_welcome_sub: "Selecciona el servicio que necesitas, completa los datos y envía tu pedido; lo gestionaremos lo antes posible.",
dash_biz1_title: "Abastecimiento en el extranjero", dash_biz1_desc: "Compras · Inspección · Consolidación",
dash_biz2_title: "Transporte por camión en EE.UU.", dash_biz2_desc: "Recogida en puerto · Almacén a almacén · Puerta a puerta",
dash_biz3_title: "Despaletizado en EE.UU.", dash_biz3_desc: "Carga compartida · Paletizado · Entrega flexible",
dash_start: "Iniciar pedido", dash_back: "Volver a servicios", dash_submit: "Enviar pedido", dash_cancel: "Cancelar",
po_title: "Pedido de abastecimiento", po_category: "Categoría del producto", po_category_ph: "ej. Artículos para el hogar / Accesorios 3C",
po_spec: "Especificaciones", po_spec_ph: "ej. Tamaño / Color / Material", po_link: "Enlace del producto", po_link_ph: "https://",
po_qty: "Cantidad", po_qty_ph: "0", po_price: "Precio unitario estimado", po_price_ph: "0.00",
po_address: "Dirección de envío", po_address_ph: "Ingresa la dirección de envío", po_contact: "Persona de contacto", po_contact_ph: "Ingresa el nombre de contacto",
po_phone: "Teléfono de contacto", po_phone_ph: "Ingresa el teléfono", po_note: "Notas del pedido", po_note_ph: "Otros requisitos (opcional)",
tk_title: "Pedido de camión en EE.UU.",
pl_title: "Pedido de despaletizado en EE.UU.", pl_method: "Modo de transporte", pl_method_pool: "Carga compartida", pl_method_priority: "Carga prioritaria",
pl_palletize: "¿Paletizar?", pl_yes: "Sí", pl_no: "No",
ts_type: "Tipo de transporte", ts_type_port: "Recogida en puerto", ts_type_w2w: "Almacén a almacén", ts_type_w2d: "Almacén a puerta",
ts_origin: "Origen", ts_origin_ph: "Ciudad / Puerto / Estado", ts_dest: "Destino", ts_dest_ph: "Ciudad / Estado",
ts_origin_addr: "Dirección detallada", ts_origin_addr_ph: "Calle / Número",
ts_dest_addr: "Dirección / Muelle / N.º almacén", ts_dest_addr_ph: "Dirección / Muelle / N.º almacén",
cargo_title: "Información de carga", cargo_container: "Tipo de contenedor", cargo_container_ltl: "Carga suelta / Paleta",
cargo_pieces: "Piezas", cargo_weight: "Peso total (kg)", cargo_volume: "Volumen total (m³)",
req_time: "Fecha requerida", contact_title: "Información de contacto", contact_name: "Persona de contacto", contact_name_ph: "Ingrese el nombre de contacto", contact_phone: "Número de contacto", contact_phone_ph: "Teléfono / WeChat / Email",
toast_logout: "Sesión cerrada", toast_order_ok: "¡Pedido enviado! Lo gestionaremos lo antes posible.",
dash_menu_main: "Menú principal", dash_menu_overview: "Resumen", dash_menu_proc: "Abastecimiento",
dash_menu_truck: "Camión en EE.UU.", dash_menu_pallet: "Despaletizado", dash_menu_orders: "Mis pedidos",
dash_orders_empty: "Sin pedidos", dash_orders_hint: "Aún no has enviado pedidos. Ve a Resumen para empezar uno nuevo.",
right_account_title: "Cuenta", right_account_type: "Cliente Empresarial",
right_stat_title: "Resumen", right_stat_progress: "En Progreso", right_stat_done: "Completados", right_stat_month: "Este Mes",
right_help_title: "Ayuda y Soporte", right_help_cs: "Contactar Soporte", right_help_faq: "Preguntas Frecuentes", right_help_quote: "Obtener Cotización",
right_help_cs_toast: "Línea de soporte: 400-XXX-XXXX (Lun-Vie 9:00-18:00)", right_help_faq_toast: "Página de preguntas próximamente", right_help_quote_toast: "Un especialista en cotización te contactará pronto",
right_tip_text: "Después de enviar el pedido, tu ejecutivo de cuenta te contactará en 24 horas para confirmar el plan de envío y la cotización."
}
};
let currentLang = localStorage.getItem('cc_lang') || 'zh';
function t(key) {
return (i18n[currentLang] && i18n[currentLang][key] !== undefined) ? i18n[currentLang][key] : key;
}
function applyLanguage(lang) {
if (!i18n[lang]) lang = 'zh';
currentLang = lang;
const langAttr = lang === 'zh' ? 'zh-CN' : (lang === 'es' ? 'es-MX' : 'en');
document.documentElement.setAttribute('lang', langAttr);
document.querySelectorAll('[data-i18n]').forEach(el => {
const k = el.getAttribute('data-i18n');
if (i18n[lang][k] !== undefined) el.textContent = i18n[lang][k];
});
document.querySelectorAll('[data-i18n-ph]').forEach(el => {
const k = el.getAttribute('data-i18n-ph');
if (i18n[lang][k] !== undefined) el.setAttribute('placeholder', i18n[lang][k]);
});
document.querySelectorAll('#langSwitcherDash [data-lang]').forEach(b => {
b.classList.toggle('active', b.getAttribute('data-lang') === lang);
});
try { localStorage.setItem('cc_lang', lang); } catch (e) {}
}
document.querySelectorAll('#langSwitcherDash [data-lang]').forEach(b => {
b.addEventListener('click', () => applyLanguage(b.getAttribute('data-lang')));
});
// Toast 提示
function showToast(message) {
const toast = document.getElementById('toast');
const toastMessage = document.getElementById('toastMessage');
toastMessage.textContent = message;
toast.classList.remove('opacity-0', 'pointer-events-none', '-translate-y-4');
toast.classList.add('opacity-100', 'translate-y-0');
setTimeout(() => {
toast.classList.add('opacity-0', 'pointer-events-none', '-translate-y-4');
toast.classList.remove('opacity-100', 'translate-y-0');
}, 3000);
}
// 初始化
document.addEventListener('DOMContentLoaded', function() {
applyLanguage(currentLang);
// 尝试从官网登录带入用户名(URL ?user=xxx)
const params = new URLSearchParams(window.location.search);
const userParam = params.get('user');
if (userParam) {
setDashUser(decodeURIComponent(userParam));
}
});
applyLanguage(currentLang);
</script>
</body>
</html>
+455
View File
@@ -0,0 +1,455 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>虫虫出海 - 用户登录</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', sans-serif;
display: flex;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
background-size: 400% 400%;
animation: gradientShift 15s ease infinite;
}
@keyframes gradientShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* ===== 左侧品牌区 ===== */
.brand-side {
width: 50%;
min-width: 420px;
position: relative;
background: linear-gradient(155deg, rgba(30,64,175,0.85) 0%, rgba(37,99,235,0.75) 40%, rgba(59,130,246,0.65) 100%);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overflow: hidden;
}
/* 浮动装饰圆圈 */
.brand-side::before {
content: '';
position: absolute;
top: -20%; right: -20%;
width: 80%; height: 80%;
background: radial-gradient(ellipse, rgba(255,255,255,.2) 0%, transparent 70%);
animation: float1 8s ease-in-out infinite;
pointer-events: none;
}
.brand-side::after {
content: '';
position: absolute;
bottom: -15%; left: -15%;
width: 70%; height: 70%;
background: radial-gradient(ellipse, rgba(147,197,253,.3) 0%, transparent 65%);
animation: float2 10s ease-in-out infinite;
pointer-events: none;
}
@keyframes float1 {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate(-30px, 30px) scale(1.05); }
}
@keyframes float2 {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate(20px, -20px) scale(1.1); }
}
.brand-content {
position: relative;
z-index: 1;
text-align: center;
padding: 40px;
animation: fadeInUp 0.8s ease-out;
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
.logo-wrap {
width: 100px; height: 100px;
margin: 0 auto 24px;
border-radius: 26px;
overflow: hidden;
background: rgba(255,255,255,.95);
padding: 12px;
box-shadow: 0 12px 40px rgba(0,0,0,.15), 0 0 0 1px rgba(255,255,255,.5) inset;
animation: logoFloat 3s ease-in-out infinite;
}
@keyframes logoFloat {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
.logo-wrap img { width: 100%; height: 100%; object-fit: contain; }
.brand-title { font-size: 36px; font-weight: 800; color: #fff; letter-spacing: 4px; text-shadow: 0 4px 20px rgba(0,0,0,.15); }
.brand-subtitle { font-size: 15px; color: rgba(255,255,255,.88); margin-top: 12px; letter-spacing: 2px; }
.feature-list { margin-top: 56px; text-align: left; display: inline-block; }
.feature-item {
color: rgba(255,255,255,.9);
font-size: 15px;
line-height: 2.6;
padding-left: 24px;
position: relative;
opacity: 0;
animation: fadeInLeft 0.6s ease-out forwards;
}
.feature-item:nth-child(1) { animation-delay: 0.2s; }
.feature-item:nth-child(2) { animation-delay: 0.4s; }
.feature-item:nth-child(3) { animation-delay: 0.6s; }
@keyframes fadeInLeft {
from { opacity: 0; transform: translateX(-20px); }
to { opacity: 1; transform: translateX(0); }
}
.feature-item::before {
content: '✓';
position: absolute;
left: 0; top: 2px;
width: 18px; height: 18px;
border-radius: 50%;
background: rgba(255,255,255,.25);
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 700;
}
/* ===== 右侧登录区 ===== */
.login-side {
flex: 1;
min-width: 400px;
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
}
.login-card {
width: 100%;
max-width: 420px;
padding: 52px 44px;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 24px;
box-shadow: 0 25px 80px rgba(0,0,0,.15), 0 0 0 1px rgba(255,255,255,.5) inset;
animation: cardFloat 0.8s ease-out;
}
@keyframes cardFloat {
from { opacity: 0; transform: translateY(40px) scale(0.95); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
/* 标题区 */
.card-header { margin-bottom: 36px; }
.card-title {
font-size: 30px;
font-weight: 800;
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
letter-spacing: 1px;
}
.card-desc {
font-size: 15px;
color: #64748b;
margin-top: 10px;
}
/* 表单 */
.form-group { margin-bottom: 22px; }
.form-input {
width: 100%;
height: 52px;
border: 2px solid #e2e8f0;
border-radius: 14px;
padding: 0 18px;
font-size: 15px;
color: #1e293b;
outline: none;
transition: all .3s cubic-bezier(0.4, 0, 0.2, 1);
background: #f8fafc;
}
.form-input:focus {
border-color: #667eea;
background: #fff;
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
transform: translateY(-2px);
}
.form-input::placeholder { color: #94a3b8; }
/* 密码框 */
.password-wrap { position: relative; }
.password-toggle {
position: absolute; right: 14px; top: 50%; transform: translateY(-50%);
border: none; background: none; cursor: pointer;
padding: 6px; color: #64748b; font-size: 18px; line-height: 1;
transition: all .2s;
}
.password-toggle:hover { color: #667eea; transform: translateY(-50%) scale(1.1); }
/* 验证码 */
.captcha-row {
display: flex;
gap: 14px;
align-items: center;
}
.captcha-row .form-input { flex: 1; }
.captcha-img {
width: 120px; height: 52px;
border: 2px solid #e2e8f0;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
user-select: none;
font-size: 22px;
font-weight: 700;
letter-spacing: 6px;
transition: all .3s cubic-bezier(0.4, 0, 0.2, 1);
flex-shrink: 0;
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
}
.captcha-img:hover {
border-color: #667eea;
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
}
/* 按钮 */
.submit-btn {
width: 100%;
height: 54px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
background-size: 200% 200%;
color: #fff;
border: none;
border-radius: 14px;
font-size: 16px;
font-weight: 700;
cursor: pointer;
transition: all .3s cubic-bezier(0.4, 0, 0.2, 1);
letter-spacing: 4px;
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.4);
}
.submit-btn:hover {
background-position: 100% 0;
transform: translateY(-3px);
box-shadow: 0 12px 32px rgba(102, 126, 234, 0.5);
}
.submit-btn:active { transform: translateY(-1px); }
/* 底部辅助行 */
.form-options {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 28px;
}
.remember-me {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #64748b;
cursor: pointer;
user-select: none;
transition: color .2s;
}
.remember-me:hover { color: #475569; }
.remember-me input { accent-color: #667eea; width: 18px; height: 18px; cursor: pointer; }
.forgot-link {
font-size: 14px;
color: #667eea;
text-decoration: none;
font-weight: 500;
transition: all .2s;
}
.forgot-link:hover { color: #764ba2; text-decoration: underline; }
/* 底部注册链接 */
.register-tip {
text-align: center;
margin-top: 28px;
font-size: 14px;
color: #94a3b8;
}
.register-tip a {
color: #667eea;
text-decoration: none;
font-weight: 600;
transition: all .2s;
}
.register-tip a:hover { color: #764ba2; text-decoration: underline; }
/* 错误提示 */
.error-msg {
font-size: 13px;
color: #ef4444;
margin-top: 6px;
display: none;
padding-left: 4px;
animation: shake 0.3s ease-out;
}
.error-msg.show { display: block; }
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-4px); }
75% { transform: translateX(4px); }
}
/* 响应式 */
@media (max-width: 900px) {
body { flex-direction: column; }
.brand-side { min-width: auto; min-height: 240px; width: 100%; }
.brand-content { padding: 32px; }
.feature-list { display: none; }
.login-side { min-width: auto; padding: 24px 16px; }
.login-card { max-width: 420px; padding: 40px 28px; }
}
</style>
</head>
<body>
<!-- 左侧品牌 -->
<div class="brand-side">
<div class="brand-content">
<div class="logo-wrap">
<img src="logo.png" alt="Logo">
</div>
<h1 class="brand-title">虫虫出海</h1>
<p class="brand-subtitle">一站式跨境物流服务平台</p>
<div class="feature-list">
<div class="feature-item">安全可靠 · 全程可追踪</div>
<div class="feature-item">专业团队 · 快速响应</div>
<div class="feature-item">透明报价 · 无隐藏费用</div>
</div>
</div>
</div>
<!-- 右侧登录 -->
<div class="login-side">
<div class="login-card">
<div class="card-header">
<h2 class="card-title">欢迎回来</h2>
<p class="card-desc">请登录您的账户以继续</p>
</div>
<form id="loginForm" autocomplete="off">
<div class="form-group">
<input type="text" class="form-input" id="loginUsername" placeholder="用户名">
<div class="error-msg" id="loginUserError">请输入用户名</div>
</div>
<div class="form-group">
<div class="password-wrap">
<input type="password" class="form-input" id="loginPassword" placeholder="密码">
<button type="button" class="password-toggle" onclick="togglePassword()">👁</button>
</div>
<div class="error-msg" id="loginPassError">请输入密码</div>
</div>
<div class="form-group">
<div class="captcha-row">
<input type="text" class="form-input" id="loginCaptcha" placeholder="验证码" maxlength="4">
<div class="captcha-img" id="loginCaptchaImg" onclick="refreshCaptcha('login')"></div>
</div>
<div class="error-msg" id="loginCaptchaError">验证码错误</div>
</div>
<div class="form-options">
<label class="remember-me">
<input type="checkbox"> 记住我
</label>
<a href="javascript:void(0)" class="forgot-link">忘记密码?</a>
</div>
<button type="submit" class="submit-btn">登 录</button>
</form>
<div class="register-tip">没有账号?<a href="register.html">立即注册</a></div>
</div>
</div>
<script>
// 验证码
let currentCaptcha = {};
function generateCaptcha() {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789';
let c = '';
for (let i = 0; i < 4; i++) c += chars.charAt(Math.floor(Math.random() * chars.length));
return c;
}
function refreshCaptcha(type) {
const c = generateCaptcha();
currentCaptcha[type] = c;
const el = document.getElementById(type + 'CaptchaImg');
if (!el) return;
el.textContent = c;
el.style.transform = 'rotate(' + (Math.random() * 8 - 4) + 'deg)';
el.style.color = ['#2563eb','#3b82f6','#1d4ed8','#1e40af'][Math.floor(Math.random()*4)];
}
refreshCaptcha('login');
// 密码显隐
function togglePassword() {
const pw = document.getElementById('loginPassword');
pw.type = pw.type === 'password' ? 'text' : 'password';
}
// 表单提交
document.getElementById('loginForm').addEventListener('submit', function(e) {
e.preventDefault();
var username = document.getElementById('loginUsername').value.trim();
var password = document.getElementById('loginPassword').value;
var captcha = document.getElementById('loginCaptcha').value.trim();
document.querySelectorAll('#loginForm .error-msg').forEach(function(el) { el.classList.remove('show'); });
var hasError = false;
if (!username) { document.getElementById('loginUserError').classList.add('show'); hasError = true; }
if (!password) { document.getElementById('loginPassError').classList.add('show'); hasError = true; }
if (captcha.toUpperCase() !== currentCaptcha['login'].toUpperCase()) {
document.getElementById('loginCaptchaError').classList.add('show');
refreshCaptcha('login');
hasError = true;
}
if (hasError) return;
alert('登录成功!欢迎回来,' + username + '!');
});
// 实时清除错误
document.querySelectorAll('.form-input').forEach(function(input) {
input.addEventListener('input', function() {
var group = this.closest('.form-group');
if (!group) return;
var err = group.querySelector('.error-msg');
if (err) err.classList.remove('show');
});
});
</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 764 KiB

+511
View File
@@ -0,0 +1,511 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>虫虫出海 - 企业注册</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
background-size: 400% 400%;
animation: gradientShift 15s ease infinite;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 40px 20px;
position: relative;
overflow-x: hidden;
}
@keyframes gradientShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* 装饰性浮动元素 */
body::before {
content: '';
position: fixed;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 60%);
animation: rotate 30s linear infinite;
pointer-events: none;
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* ====== 主容器:居中卡片式 ====== */
.container {
width: 100%;
max-width: 580px;
position: relative;
z-index: 1;
animation: fadeInUp 0.8s ease-out;
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(40px); }
to { opacity: 1; transform: translateY(0); }
}
/* 顶部品牌条 */
.top-bar {
text-align: center;
margin-bottom: 32px;
}
.logo-wrap {
width: 120px; height: 120px;
margin: 0 auto 18px;
border-radius: 32px;
background: rgba(255,255,255,0.95);
padding: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,.2), 0 0 0 1px rgba(255,255,255,.5) inset;
overflow: hidden;
animation: logoFloat 3s ease-in-out infinite;
}
@keyframes logoFloat {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.logo-wrap img { width: 100%; height: 100%; object-fit: contain; filter: none; }
.logo-wrap img.no-filter { filter: none; }
.brand-name {
font-size: 30px;
font-weight: 800;
color: #fff;
letter-spacing: 3px;
margin-bottom: 6px;
text-shadow: 0 4px 20px rgba(0,0,0,.2);
}
.brand-sub { font-size: 14px; color: rgba(255,255,255,0.85); letter-spacing: 1.5px; }
/* 卡片 */
.form-card {
background: rgba(255,255,255,0.96);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 28px;
padding: 44px 40px;
box-shadow: 0 25px 80px rgba(0,0,0,.15), 0 0 0 1px rgba(255,255,255,.5) inset;
border: none;
}
/* 标题行 */
.card-header {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 30px;
}
.card-title {
font-size: 24px; font-weight: 800;
background: linear-gradient(135deg, #1e293b 0%, #475569 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
position: relative;
padding-left: 16px;
}
.card-title::before {
content: '';
position: absolute; left: 0; top: 2px; bottom: 2px;
width: 4px; border-radius: 3px;
background: linear-gradient(180deg, #667eea, #764ba2);
}
.back-link {
font-size: 14px; color: #64748b; text-decoration: none;
transition: all .2s;
font-weight: 500;
padding: 6px 12px;
border-radius: 8px;
}
.back-link:hover { color: #667eea; background: rgba(102,126,234,0.08); }
.back-link::before { content: '← '; font-size: 14px; }
/* 分组标题 */
.section-label {
font-size: 12px; font-weight: 700; color: #94a3b8;
text-transform: uppercase; letter-spacing: 2.5px;
margin-bottom: 16px; margin-top: 24px;
}
.section-label:first-of-type { margin-top: 0; }
/* 表单项 - 单列 */
.form-group { margin-bottom: 20px; position: relative; }
.form-label {
display: block; font-size: 14px; font-weight: 600;
color: #475569; margin-bottom: 8px;
}
.form-label .required { color: #ef4444; margin-right: 3px; font-size: 15px; }
.form-input, .form-select {
width: 100%; padding: 14px 18px; font-size: 15px;
border: 2px solid #e2e8f0; border-radius: 14px; outline: none;
transition: all .3s cubic-bezier(0.4, 0, 0.2, 1);
background: #f8fafc; color: #1e293b;
appearance: none;
height: 54px;
}
.form-input:hover, .form-select:hover {
border-color: #cbd5e1;
background: #fff;
}
.form-input:focus, .form-select:focus {
border-color: #667eea;
background: #fff;
box-shadow: 0 0 0 5px rgba(102,126,234,.12);
transform: translateY(-2px);
}
.form-input::placeholder { color: #94a3b8; }
.form-select {
cursor: pointer;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat; background-position: right 18px center; padding-right: 42px;
}
/* 营业执照上传区 */
.license-upload-area {
border: 2px dashed #cbd5e1;
border-radius: 16px; padding: 28px;
text-align: center; cursor: pointer;
transition: all .3s cubic-bezier(0.4, 0, 0.2, 1);
background: linear-gradient(135deg, #fafbfc 0%, #f8fafc 100%);
position: relative;
overflow: hidden;
}
.license-upload-area:hover {
border-color: #667eea;
background: linear-gradient(135deg, #eff6ff 0%, #f0f9ff 100%);
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(102,126,234,0.15);
}
.license-upload-area.has-image {
border-style: solid; border-color: #a78bfa; padding: 6px; background: #fff;
}
.license-upload-area.has-image .upload-placeholder { display: none; }
.upload-placeholder .upload-icon-big {
font-size: 40px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
line-height: 1;
margin-bottom: 8px;
}
.upload-placeholder .upload-hint {
font-size: 14px; color: #64748b; font-weight: 600;
}
.upload-placeholder .upload-desc {
font-size: 12px; color: #94a3b8; margin-top: 6px;
}
.license-preview-img {
width: 100%; max-height: 180px; object-fit: cover;
border-radius: 12px; display: none;
}
.has-image .license-preview-img { display: block; }
.license-upload-area input[type="file"] { display: none; }
/* 验证码 */
.captcha-group { display: flex; gap: 14px; align-items: stretch; }
.captcha-group .form-input { flex: 1; }
.captcha-img {
flex-shrink: 0; width: 130px;
height: auto; min-height: 54px;
border-radius: 14px; cursor: pointer;
border: 2px solid #e2e8f0;
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
display: flex; align-items: center; justify-content: center;
padding: 0 16px; font-size: 22px; font-weight: 800;
letter-spacing: 6px; color: #667eea; user-select: none;
transition: all .3s cubic-bezier(0.4, 0, 0.2, 1);
}
.captcha-img:hover {
border-color: #667eea;
background: linear-gradient(135deg, #dbeafe 0%, #cffafe 100%);
transform: scale(1.05);
box-shadow: 0 6px 20px rgba(102,126,234,0.2);
}
/* 错误提示 */
.error-msg {
color: #ef4444;
font-size: 13px;
margin-top: 6px;
display: none;
padding-left: 4px;
animation: shake 0.3s ease-out;
}
.error-msg.show { display: block; }
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-4px); }
75% { transform: translateX(4px); }
}
/* 提交按钮 */
.submit-btn {
width: 100%; padding: 16px; font-size: 16px; font-weight: 700;
color: white;
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
background-size: 200% 200%;
border: none; border-radius: 14px;
cursor: pointer;
transition: all .3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 10px 30px rgba(102,126,234,.4);
letter-spacing: 1.5px;
margin-top: 10px;
}
.submit-btn:hover {
background-position: 100% 0;
transform: translateY(-3px);
box-shadow: 0 16px 40px rgba(102,126,234,.5);
}
.submit-btn:active { transform: translateY(-1px); }
/* 底部链接 */
.form-footer { text-align: center; margin-top: 22px; font-size: 14px; color: #64748b; }
.form-footer a {
color: #667eea;
text-decoration: none;
font-weight: 700;
transition: all .2s;
}
.form-footer a:hover { color: #764ba2; text-decoration: underline; }
/* 分割线装饰 */
.divider {
height: 1px;
background: linear-gradient(90deg, transparent, #e2e8f0, #cbd5e1, #e2e8f0, transparent);
margin: 26px 0;
}
@keyframes cardShake {
0%,100%{transform:translateX(0)} 25%{transform:translateX(-8px)} 75%{transform:translateX(8px)}
}
.shake { animation: cardShake .45s; }
/* 响应式 */
@media (max-width: 580px) {
body { padding: 24px 16px; }
.form-card { padding: 32px 26px; border-radius: 22px; }
.brand-name { font-size: 26px; }
.logo-wrap { width: 100px; height: 100px; border-radius: 26px; padding: 16px; }
.captcha-group { flex-direction: column; }
.captcha-img { width: 100%; min-height: 52px; }
}
</style>
</head>
<body>
<div class="container">
<!-- 品牌头部 -->
<div class="top-bar">
<div class="logo-wrap">
<img src="logo.png" alt="虫虫出海" class="no-filter">
</div>
<h1 class="brand-name">虫虫出海</h1>
<p class="brand-sub">一站式跨境物流服务平台</p>
</div>
<!-- 注册表单 -->
<div class="form-card">
<div class="card-header">
<h2 class="card-title">企业注册</h2>
<a href="login-register.html" class="back-link">返回登录</a>
</div>
<form id="registerForm">
<!-- 基本信息 -->
<div class="section-label">基本信息</div>
<div class="form-group">
<label class="form-label"><span class="required">*</span>公司名称</label>
<input type="text" class="form-input" id="regCompany" placeholder="请输入完整公司名称" required autocomplete="organization">
<div class="error-msg" id="regCompanyError">请输入公司名称</div>
</div>
<div class="form-group">
<label class="form-label"><span class="required">*</span>法人姓名</label>
<input type="text" class="form-input" id="regLegalPerson" placeholder="法人代表姓名" required>
<div class="error-msg" id="regLegalPersonError">请输入法人姓名</div>
</div>
<div class="form-group">
<label class="form-label"><span class="required">*</span>法人电话</label>
<input type="tel" class="form-input" id="regLegalPhone" placeholder="用于业务联系的手机号码" required>
<div class="error-msg" id="regLegalPhoneError">请输入法人电话</div>
</div>
<div class="divider"></div>
<!-- 证件信息 -->
<div class="section-label">证件信息</div>
<div class="form-group">
<label class="form-label"><span class="required">*</span>营业执照 / 证件照</label>
<div class="license-upload-area" id="licenseUploadArea" onclick="document.getElementById('regLicenseFile').click()">
<div class="upload-placeholder">
<div class="upload-icon-big">📄</div>
<div class="upload-hint">点击上传证件图片</div>
<div class="upload-desc">支持 JPG、PNG 格式,不超过 5MB</div>
</div>
<img class="license-preview-img" id="licensePreview" alt="">
<input type="file" id="regLicenseFile" accept="image/*" onchange="handleLicenseUpload(this)">
</div>
<div class="error-msg" id="regLicenseError">请上传营业执照或相关证件</div>
</div>
<div class="form-group">
<label class="form-label">法人身份证号</label>
<input type="text" class="form-input" id="regIdCard" placeholder="18位身份证号">
</div>
<div class="divider"></div>
<!-- 地址与发票 -->
<div class="section-label">地址与发票</div>
<div class="form-group">
<label class="form-label">国家/地区</label>
<select class="form-select" id="regCountry">
<option value="中国">中国</option>
<option value="中国香港">中国香港</option>
<option value="中国台湾">中国台湾</option>
<option value="中国澳门">中国澳门</option>
<option value="美国">美国</option>
<option value="英国">英国</option>
<option value="德国">德国</option>
<option value="法国">法国</option>
<option value="日本">日本</option>
<option value="韩国">韩国</option>
<option value="澳大利亚">澳大利亚</option>
<option value="加拿大">加拿大</option>
<option value="其他">其他</option>
</select>
</div>
<div class="form-group">
<label class="form-label">详细地址</label>
<input type="text" class="form-input" id="regAddress" placeholder="公司注册地址或经营地址">
</div>
<div class="form-group">
<label class="form-label">发票抬头</label>
<input type="text" class="form-input" id="regInvoiceTitle" placeholder="用于开具发票的名称(选填)">
</div>
<div class="divider"></div>
<!-- 验证码 -->
<div class="form-group">
<label class="form-label"><span class="required">*</span>验证码</label>
<div class="captcha-group">
<input type="text" class="form-input" id="regCaptcha" placeholder="输入下方验证码" required maxlength="4">
<div class="captcha-img" id="regCaptchaImg" onclick="refreshCaptcha()" title="点击刷新验证码"></div>
</div>
<div class="error-msg" id="regCaptchaError">验证码错误,请重新输入</div>
</div>
<button type="submit" class="submit-btn">提交注册申请</button>
<div class="form-footer">
已有账号?<a href="login-register.html">立即登录 →</a>
</div>
</form>
</div>
</div>
<script>
let currentCaptcha = '';
let licenseUploaded = false;
function generateCaptcha() {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789';
let c = '';
for (let i = 0; i < 4; i++) c += chars.charAt(Math.floor(Math.random() * chars.length));
return c;
}
function refreshCaptcha() {
currentCaptcha = generateCaptcha();
const img = document.getElementById('regCaptchaImg');
img.textContent = currentCaptcha;
img.style.transform = 'rotate(' + (Math.random() * 8 - 4) + 'deg)';
img.style.color = ['#667eea','#764ba2','#818cf8','#a78bfa'][Math.floor(Math.random()*4)];
}
refreshCaptcha();
// 营业执照上传
function handleLicenseUpload(input) {
var file = input.files[0];
if (!file) return;
if (file.size > 5*1024*1024) { alert('图片大小不能超过 5MB'); input.value=''; return; }
var reader = new FileReader();
reader.onload = function(e) {
var preview = document.getElementById('licensePreview');
preview.src = e.target.result;
var area = document.getElementById('licenseUploadArea');
area.classList.add('has-image');
licenseUploaded = true;
document.getElementById('regLicenseError').classList.remove('show');
};
reader.readAsDataURL(file);
}
// 表单提交
document.getElementById('registerForm').addEventListener('submit', function(e) {
e.preventDefault();
var company = document.getElementById('regCompany').value.trim();
var legalPerson = document.getElementById('regLegalPerson').value.trim();
var legalPhone = document.getElementById('regLegalPhone').value.trim();
var captcha = document.getElementById('regCaptcha').value.trim();
document.querySelectorAll('.error-msg').forEach(function(el){ el.classList.remove('show'); });
var hasError = false;
if (!company) { document.getElementById('regCompanyError').classList.add('show'); hasError=true; }
if (!legalPerson) { document.getElementById('regLegalPersonError').classList.add('show'); hasError=true; }
if (!legalPhone) { document.getElementById('regLegalPhoneError').classList.add('show'); hasError=true; }
if (!licenseUploaded) { document.getElementById('regLicenseError').classList.add('show'); hasError=true; }
if (captcha.toUpperCase() !== currentCaptcha.toUpperCase()) {
document.getElementById('regCaptchaError').classList.add('show');
refreshCaptcha(); hasError=true;
}
if (hasError) {
this.querySelector('.submit-btn').classList.add('shake');
setTimeout(() => this.querySelector('.submit-btn').classList.remove('shake'), 450);
return;
}
alert('注册申请已提交!\n\n公司名称:' + company +
'\n\n我们将尽快审核您的资料。');
window.location.href = 'login-register.html';
});
// 实时清除错误
document.querySelectorAll('.form-input,.form-select').forEach(function(input){
input.addEventListener('input', function(){
var group = this.closest('.form-group');
if (group) { var err = group.querySelector('.error-msg'); if (err) err.classList.remove('show'); }
});
});
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 764 KiB

+49
View File
@@ -0,0 +1,49 @@
const http = require('http');
const fs = require('fs');
const path = require('path');
const PORT = 8081;
const mimeTypes = {
'.html': 'text/html',
'.js': 'text/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon'
};
const server = http.createServer((req, res) => {
console.log(`${req.method} ${req.url}`);
let filePath = '.' + req.url;
if (filePath === './') {
filePath = './index.html';
}
const extname = String(path.extname(filePath)).toLowerCase();
const contentType = mimeTypes[extname] || 'application/octet-stream';
fs.readFile(filePath, (error, content) => {
if (error) {
if (error.code === 'ENOENT') {
res.writeHead(404, { 'Content-Type': 'text/html' });
res.end('<h1>404 Not Found</h1>', 'utf-8');
} else {
res.writeHead(500);
res.end('Server Error: ' + error.code, 'utf-8');
}
} else {
res.writeHead(200, { 'Content-Type': contentType });
res.end(content, 'utf-8');
}
});
});
server.listen(PORT, () => {
console.log(`[TraePreviewUrl]: http://localhost:${PORT}`);
console.log(`Server running at http://localhost:${PORT}/`);
});
@@ -0,0 +1,290 @@
#!/usr/bin/env python3
"""
柏悦WMS - 分阶段脚本
Phase 1: 登录并保存cookies (需要可见浏览器+手动验证码)
Phase 2: 加载cookies, headless模式抓取所有页面
用法:
python3 scrape_wms_final.py login # 阶段1: 登录保存cookie
python3 scrape_wms_final.py scrape # 阶段2: 用cookie抓取
"""
import asyncio, json, sys, os, time
from playwright.async_api import async_playwright
BASE = "http://39.101.149.173"
API_BASE = "https://adtest.bylogistic.cn"
USERNAME = "admin"
PASSWORD = "ndui#Emdfj23123"
COOKIE_FILE = "/tmp/wms_cookies.json"
OUTPUT_DIR = os.path.expanduser("~/Documents/柏悦")
os.makedirs(OUTPUT_DIR, exist_ok=True)
async def phase_login():
"""打开浏览器让用户登录,然后保存cookies"""
async with async_playwright() as p:
browser = await p.chromium.launch(headless=False, args=["--start-maximized"])
context = await browser.new_context(viewport={"width": 1440, "height": 900})
page = await context.new_page()
print("=== 打开登录页 ===")
await page.goto(f"{BASE}/user/login?redirect=%2Fwms%2FByInstoreList", timeout=30000)
await page.wait_for_timeout(2000)
# 自动填充
inputs_text = page.locator("input[type='text']")
input_pwd = page.locator("input[type='password']")
await inputs_text.nth(0).fill(USERNAME)
await input_pwd.fill(PASSWORD)
print("✅ 账号密码已填写")
print("👉 请在浏览器中输入验证码并点击'确定'")
print("⏳ 等待登录成功...")
try:
await page.wait_for_function(
"() => !window.location.pathname.includes('login')",
timeout=300000 # 等5分钟
)
print(f"✅ 登录成功! URL: {page.url}")
except Exception as e:
print(f"❌ 等待超时: {e}")
try:
await browser.close()
except:
pass
return False
# 立即保存cookies(不等页面加载完)
try:
await page.wait_for_timeout(3000)
except:
pass
try:
cookies = await context.cookies()
with open(COOKIE_FILE, "w") as f:
json.dump(cookies, f, indent=2)
print(f"✅ Cookies已保存到 {COOKIE_FILE} ({len(cookies)} 个)")
except Exception as ce:
print(f"⚠️ Cookie保存出错: {ce}")
# 截图当前页
try:
await page.screenshot(path="/tmp/wms_logged_in.png", full_page=True)
print("📸 当前页面截图: /tmp/wms_logged_in.png")
except Exception as se:
print(f"⚠️ 截图出错: {se}")
# 保持浏览器打开
print("\n浏览器将保持10秒后关闭...")
try:
await asyncio.sleep(10)
except:
pass
try:
await browser.close()
except:
pass
return True
async def phase_scrape():
"""加载cookies并headless抓取所有页面"""
# 读取cookies
if not os.path.exists(COOKIE_FILE):
print(f"❌ Cookie文件不存在: {COOKIE_FILE}")
print("请先运行: python3 scrape_wms_final.py login")
return
with open(COOKIE_FILE) as f:
saved_cookies = json.load(f)
print(f"🍪 加载了 {len(saved_cookies)} 个cookies")
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
context = await browser.new_context(viewport={"width": 1440, "height": 900})
await context.add_cookies(saved_cookies)
page = await context.new_page()
# 监听API
all_apis = []
page.on("response", lambda r: all_apis.append({
"url": r.url, "status": r.status, "method": r.request.method
}))
# 先访问入库单列表(默认首页)
print("\n=== 访问系统首页 ===")
await page.goto(f"{BASE}/wms/ByInstoreList", timeout=20000)
await page.wait_for_timeout(4000)
if "login" in page.url.lower():
print("❌ Cookies已失效,请重新运行 login 命令")
await browser.close()
return
print(f"✅ 已进入系统: {page.url}")
# ===== 抓取菜单结构 =====
print("\n=== 抓取菜单结构 ===")
menu_structure = {}
# 一级菜单
top_menus = page.locator(".ant-menu-submenu-title")
top_count = await top_menus.count()
for i in range(top_count):
try:
name = (await top_menus.nth(i).inner_text()).strip().split("\n")[0].strip()
if not name or len(name) > 20:
continue
# 展开菜单
await top_menus.nth(i).click()
await page.wait_for_timeout(600)
# 获取子菜单
sub_items = page.locator(".ant-menu-submenu-open .ant-menu-item")
subs = []
for j in range(await sub_items.count()):
try:
sn = (await sub_items.nth(j).inner_text()).strip().split("\n")[0]
if sn and len(sn) < 30:
subs.append(sn)
except:
pass
menu_structure[name] = subs
print(f" 📂 {name}: {subs}")
# 收起
await top_menus.nth(i).click()
await page.wait_for_timeout(300)
except Exception as e:
pass
# 所有要访问的页面路径
pages_to_visit = [
("首页", "/wms/Dashboard"),
("入库单列表", "/wms/ByInstoreList"),
("入库单测量", "/wms/ByInstoreList4Measured"),
("测量数据审核", "/wms/ByInstoreList4MeasuredAudit"),
("接收审核", "/wms/ByInstoreList4ReceiveAudit"),
("客户管理", "/wms/ByCustomerList"),
("出库单列表", "/wms/ByOutstoreList"),
("派送单列表", "/wms/ByDispatchList"),
("货柜管理", "/wms/ByContainerList"),
("库存管理", "/wms/ByInventoryList"),
("财务结算", "/wms/ByFinanceList"),
("托盘管理", "/wms/ByTrayList"),
("BOL管理", "/wms/ByBolList"),
("用户管理", "/system/UserList"),
("角色管理", "/system/RoleList"),
("菜单管理", "/system/MenuList"),
("字典管理", "/system/DictList"),
("参数配置", "/system/ConfigList"),
]
# ===== 逐页抓取 =====
print(f"\n=== 逐页抓取 (共{len(pages_to_visit)}页) ===\n")
pages_data = {}
for idx, (name, path) in enumerate(pages_to_visit, 1):
try:
url = BASE + path
print(f"[{idx}/{len(pages_to_visit)}] {name} -> {path}", end=" ... ")
await page.goto(url, timeout=15000)
await page.wait_for_timeout(2500)
if "login" in page.url.lower():
print("❌ 会话过期!")
break
entry = {
"title": await page.title(),
"url": page.url,
}
# 表格列头
headers = []
ths = page.locator(".ant-table-thead th, thead th, [class*='table'] th")
for i in range(min(await ths.count(), 25)):
h = (await ths.nth(i).inner_text()).strip()
if h and h not in headers:
headers.append(h)
# 搜索条件字段
fields = []
labels = page.locator(".ant-form-item-label label, .query-group label, label[for]")
for i in range(min(await labels.count(), 20)):
t = (await labels.nth(i).inner_text()).strip()
if t and t not in fields and len(t) < 30:
fields.append(t)
# 操作按钮
buttons = []
btns = page.locator(".ant-btn:not(.ant-pagination-item):not(.ant-pagination-options-quick-jumper):not([class*='link'])")
for i in range(min(await btns.count(), 15)):
b = (await btns.nth(i).inner_text()).strip()
if b and b not in buttons and len(b) < 20 and b not in ["刷 新","关 闭"]:
buttons.append(b)
# 统计卡片数字
stat_vals = []
stats = page.locator("[class*='statistic'] [class*='value'], [class*='number-card'] [class*='value']")
for i in range(min(await stats.count(), 10)):
s = (await stats.nth(i).inner_text()).strip()
if s:
stat_vals.append(s)
entry["table_headers"] = headers
entry["search_fields"] = fields
entry["buttons"] = buttons
entry["statistics"] = stat_vals
pages_data[name] = entry
print(f"✅ 表头:{len(headers)} 字段:{len(fields)} 按钮:{len(buttons)}")
# 截图每个页面
safe_name = name.replace("/", "_").replace("\\", "_")
await page.screenshot(path=f"/tmp/wms_page_{safe_name}.png")
except Exception as e:
print(f"❌ {e}")
pages_data[name] = {"error": str(e)}
# 保存完整数据
full_data = {
"system_name": "柏悦物流管理系统 (Baiyue WMS)",
"base_url": BASE,
"api_base": API_BASE,
"scrape_time": time.strftime("%Y-%m-%d %H:%M:%S"),
"menu_structure": menu_structure,
"pages": pages_data,
"api_calls": all_apis[-50:], # 最后50个API调用
}
output_file = os.path.join(OUTPUT_DIR, "wms_system_data.json")
with open(output_file, "w", encoding="utf-8") as f:
json.dump(full_data, f, indent=2, ensure_ascii=False)
print(f"\n{'='*60}")
print(f"✅ 全部完成!")
print(f" - 菜单: {len(menu_structure)} 个一级模块")
print(f" - 页面: {len(pages_data)} 个页面")
print(f" - 输出: {output_file}")
print(f"{'='*60}")
await browser.close()
if __name__ == "__main__":
cmd = sys.argv[1] if len(sys.argv) > 1 else "login"
if cmd == "login":
asyncio.run(phase_login())
elif cmd == "scrape":
asyncio.run(phase_scrape())
else:
print(f"Usage: python3 {sys.argv[0]} [login|scrape]")
+207
View File
@@ -0,0 +1,207 @@
#!/usr/bin/env python3
"""
从可见浏览器中提取token,然后用API方式抓取所有数据
"""
import asyncio, json, os, time
from playwright.async_api import async_playwright
BASE = "http://39.101.149.173"
USERNAME = "admin"
PASSWORD = "ndui#Emdfj23123"
TOKEN_FILE = "/tmp/wms_token.json"
OUTPUT_DIR = os.path.expanduser("~/Documents/柏悦")
async def main():
async with async_playwright() as p:
browser = await p.chromium.launch(headless=False, args=["--start-maximized"])
context = await browser.new_context(viewport={"width": 1440, "height": 900})
page = await context.new_page()
print("=== 打开登录页 ===")
await page.goto(f"{BASE}/user/login?redirect=%2Fwms%2FByInstoreList", timeout=30000)
await page.wait_for_timeout(2000)
# 填充
inputs = page.locator("input[type='text']")
pwd = page.locator("input[type='password']")
await inputs.nth(0).fill(USERNAME)
await pwd.fill(PASSWORD)
print("👉 请在浏览器中输入验证码并点击确定...")
try:
await page.wait_for_function(
"() => !window.location.pathname.includes('login')",
timeout=300000
)
print("✅ 登录成功!")
except:
print("❌ 超时")
await browser.close()
return
await page.wait_for_timeout(3000)
# ========== 抓取完整菜单结构 ==========
print("\n=== 抓取菜单 ===")
menu_structure = {}
page_list = [] # [(display_name, url_path)]
top_menus = page.locator(".ant-menu-submenu-title")
for i in range(await top_menus.count()):
try:
name = (await top_menus.nth(i).inner_text()).strip().split("\n")[0].strip()
if not name or len(name) > 20:
continue
await top_menus.nth(i).click()
await page.wait_for_timeout(500)
subs = []
sub_items = page.locator(".ant-menu-submenu-open .ant-menu-item")
for j in range(await sub_items.count()):
sn = (await sub_items.nth(j).inner_text()).strip().split("\n")[0]
if sn and len(sn) < 30:
subs.append(sn)
menu_structure[name] = subs
print(f" {name}: {subs}")
await top_menus.nth(i).click()
await page.wait_for_timeout(300)
except:
pass
# ========== 逐页截图抓取 ==========
print(f"\n=== 逐页抓取 ===")
# 构建所有要访问的页面
all_pages = [("首页", "/wms/Dashboard")]
for menu_name, subs in menu_structure.items():
for sub in subs:
# 推断URL路径
path_map = {
"入库单列表": "/wms/ByInstoreList",
"入库单测量": "/wms/ByInstoreList4Measured",
"测量数据审核": "/wms/ByInstoreList4MeasuredAudit",
"接收审核": "/wms/ByInstoreList4ReceiveAudit",
"客户管理": "/wms/ByCustomerList",
"出库单列表": "/wms/ByOutstoreList",
"派送单列表": "/wms/ByDispatchList",
"BOL管理": "/wms/ByBolList",
"货柜管理": "/wms/ByContainerList",
"库存管理": "/wms/ByInventoryList",
"财务结算": "/wms/ByFinanceList",
"托盘管理": "/wms/ByTrayList",
"用户管理": "/system/UserList",
"角色管理": "/system/RoleList",
"菜单管理": "/system/MenuList",
"字典管理": "/system/DictList",
"参数配置": "/system/ConfigList",
"通用配置": "/system/ConfigList",
"报表设计": "/wms/Dashboard", # 待确认
"消息中心": "/wms/Dashboard",
"常见案例": "/wms/Dashboard",
"在线开发": "/wms/Dashboard",
"系统监控": "/wms/Dashboard",
"统计报表": "/wms/Dashboard",
}
path = path_map.get(sub, "")
if path:
all_pages.append((f"{menu_name} > {sub}", path))
# 去重
seen = set()
unique_pages = []
for name, path in all_pages:
if path not in seen:
seen.add(path)
unique_pages.append((name, path))
pages_data = {}
success_count = 0
for idx, (name, path) in enumerate(unique_pages, 1):
try:
url = BASE + path
print(f"[{idx}/{len(unique_pages)}] {name}", end=" ... ")
await page.goto(url, timeout=15000)
await page.wait_for_timeout(2500)
if "login" in page.url.lower():
print("会话过期!")
break
# 抓表格列头
headers = []
th_selectors = [".ant-table-thead th", "thead th", "table th"]
for ths in th_selectors:
els = page.locator(ths)
for i in range(min(await els.count(), 30)):
try:
h = (await els.nth(i).inner_text()).strip()
if h and h not in headers:
headers.append(h)
except:
pass
# 搜索字段
fields = []
labels = page.locator("label, .ant-form-item-label")
for i in range(min(await labels.count(), 25)):
try:
t = (await labels.nth(i).inner_text()).strip()
if t and t not in fields and len(t) < 30:
fields.append(t)
except:
pass
# 操作按钮
buttons = []
btns = page.locator("button:not([class*='pagination']):not([class*='quick-jumper'])")
for i in range(min(await btns.count(), 20)):
try:
b = (await btns.nth(i).inner_text()).strip()
if b and b not in buttons and len(b) < 25:
buttons.append(b)
except:
pass
pages_data[name] = {
"url": url,
"title": await page.title(),
"table_headers": headers,
"search_fields": fields,
"buttons": buttons,
}
print(f"✅ headers:{len(headers)} fields:{len(fields)} btns:{len(buttons)}")
success_count += 1
except Exception as e:
print(f"❌ {str(e)[:80]}")
pages_data[name] = {"error": str(e)[:200]}
# 保存数据
output = {
"system_name": "柏悦物流管理系统 (Baiyue WMS)",
"scrape_time": time.strftime("%Y-%m-%d %H:%M:%S"),
"menu_structure": menu_structure,
"pages": pages_data,
"pages_count": success_count,
}
out_path = os.path.join(OUTPUT_DIR, "wms_full_data.json")
with open(out_path, "w", encoding="utf-8") as f:
json.dump(output, f, indent=2, ensure_ascii=False)
print(f"\n✅ 完成! {success_count}/{len(unique_pages)} 页面成功抓取")
print(f"输出: {out_path}")
print("\n浏览器10秒后关闭...")
await asyncio.sleep(10)
await browser.close()
asyncio.run(main())
@@ -0,0 +1,322 @@
{
"system_name": "柏悦物流管理系统 (Baiyue WMS)",
"scrape_time": "2026-06-01 12:06:00",
"menu_structure": {
"客户管理": [
"客户公司管理",
"用户资料",
"海外仓阶梯费用",
"客户账户流水"
],
"入库": [
"入库单列表",
"入库单测量",
"测量数据审核",
"接收审核"
],
"出库": [
"派送单列表",
"BOL",
"自提单"
],
"货柜管理": [
"货柜列表",
"装柜清单"
],
"库存": [
"海外仓库存查询",
"理货管理",
"报损单"
],
"财务结算": [
"客户应收应付",
"客户对账单",
"结算单位应付",
"结算单位对账单",
"洛杉矶仓储成本"
],
"托盘管理": [
"托盘列表",
"生成托盘"
],
"通用": [
"产品信息",
"费用项目",
"FBA编码地址",
"托盘管理",
"库位/货架管理",
"订单流转路径",
"快递公司",
"往来单位"
],
"常见案例": [
"自定义组件",
"对象存储"
],
"在线开发": [
"Online表单开发"
],
"报表设计": [
"积木报表设计",
"生产销售监控",
"智慧物流监控"
],
"消息中心": [
"模板管理",
"消息管理",
"我的消息"
],
"系统管理": [
"用户管理",
"角色管理",
"菜单管理",
"租户管理",
"部门管理",
"我的部门",
"职务管理",
"通讯录",
"数据字典",
"分类字典",
"系统通告",
"菜单管理2"
],
"系统监控": [
"在线用户",
"路由网关",
"定时任务",
"日志管理",
"数据日志",
"SQL监控",
"在线文档"
]
},
"pages": {
"首页": {
"url": "http://39.101.149.173/wms/Dashboard",
"title": "柏悦物流管理系统",
"table_headers": [],
"search_fields": [],
"buttons": [
"返回首页"
]
},
"入库 > 入库单列表": {
"url": "http://39.101.149.173/wms/ByInstoreList",
"title": "入库单列表 · 柏悦物流管理系统",
"table_headers": [
"仓库",
"交货仓",
"配载状态",
"是否扣货",
"扣货理由",
"集装箱号",
"柜号",
"公司",
"业务类型",
"提货方式",
"提货时间",
"提货联系人电话",
"提货地址",
"提货邮编",
"提货联系人邮箱",
"渠道",
"买单/退税",
"是否整柜",
"商检验",
"开船时间",
"到港时间",
"总箱数",
"总毛重(T.K.GW)"
],
"search_fields": [
"定位仓库",
"交货仓",
"入仓码",
"订单状态",
"费用审核状态"
],
"buttons": [
"查询",
"重置",
"国内仓下单",
"海外分仓下单",
"整托下单",
"高级查询",
"导出报表",
"导出入库清单总表",
"导出唛头",
"导出FBA流转单ZIP",
"下载"
]
},
"入库 > 入库单测量": {
"url": "http://39.101.149.173/wms/ByInstoreList4Measured",
"title": "入库单测量 · 柏悦物流管理系统",
"table_headers": [
"入仓码",
"仓库",
"订单状态",
"配载状态",
"测量数据审核状态",
"唛头文件",
"是否扣货",
"扣货理由",
"公司",
"业务类型",
"渠道",
"买单/退税",
"是否整柜",
"商检验",
"总箱数",
"总毛重(T.K.GW)",
"总体积(m³)",
"总材积重",
"总净重(T.K.NW)",
"目的仓类型",
"目的仓",
"货件编号",
"测量完成时间",
"操作"
],
"search_fields": [
"仓库",
"入仓码",
"公司",
"FBA货件编号"
],
"buttons": [
"查询",
"重置",
"下载"
]
},
"入库 > 测量数据审核": {
"url": "http://39.101.149.173/wms/ByInstoreList4MeasuredAudit",
"title": "测量数据审核 · 柏悦物流管理系统",
"table_headers": [
"入仓码",
"仓库",
"订单状态",
"配载状态",
"测量数据审核状态",
"唛头文件",
"是否扣货",
"扣货理由",
"公司",
"业务类型",
"渠道",
"买单/退税",
"是否整柜",
"商检验",
"总箱数",
"总毛重(T.K.GW)",
"总体积(m³)",
"总材积重",
"总净重(T.K.NW)",
"目的仓类型",
"目的仓",
"货件编号",
"测量完成时间",
"操作"
],
"search_fields": [
"仓库",
"入仓码",
"公司",
"FBA货件编号"
],
"buttons": [
"查询",
"重置",
"下载"
]
},
"入库 > 接收审核": {
"url": "http://39.101.149.173/wms/ByInstoreList4ReceiveAudit",
"title": "接收审核 · 柏悦物流管理系统",
"table_headers": [
"入仓码",
"仓库",
"订单状态",
"配载状态",
"测量数据审核状态",
"唛头文件",
"是否扣货",
"扣货理由",
"公司",
"业务类型",
"渠道",
"买单/退税",
"是否整柜",
"商检验",
"总箱数",
"总毛重(T.K.GW)",
"总体积(m³)",
"总材积重",
"总净重(T.K.NW)",
"目的仓类型",
"目的仓",
"货件编号",
"测量完成时间",
"操作"
],
"search_fields": [
"仓库",
"入仓码",
"公司",
"FBA货件编号"
],
"buttons": [
"查询",
"重置",
"下载"
]
},
"出库 > 派送单列表": {
"url": "http://39.101.149.173/wms/ByDispatchList",
"title": "柏悦物流管理系统",
"table_headers": [],
"search_fields": [],
"buttons": [
"返回首页"
]
},
"通用 > 托盘管理": {
"url": "http://39.101.149.173/wms/ByTrayList",
"title": "柏悦物流管理系统",
"table_headers": [],
"search_fields": [],
"buttons": [
"返回首页"
]
},
"系统管理 > 用户管理": {
"url": "http://39.101.149.173/system/UserList",
"title": "柏悦物流管理系统",
"table_headers": [],
"search_fields": [],
"buttons": [
"返回首页"
]
},
"系统管理 > 角色管理": {
"url": "http://39.101.149.173/system/RoleList",
"title": "柏悦物流管理系统",
"table_headers": [],
"search_fields": [],
"buttons": [
"返回首页"
]
},
"系统管理 > 菜单管理": {
"url": "http://39.101.149.173/system/MenuList",
"title": "柏悦物流管理系统",
"table_headers": [],
"search_fields": [],
"buttons": [
"返回首页"
]
}
},
"pages_count": 10
}
@@ -0,0 +1,393 @@
{
"menu_items": [
{
"text": "首页",
"selector": ".ant-menu-item",
"href": ""
},
{
"text": "入库单列表",
"selector": ".ant-menu-item",
"href": ""
},
{
"text": "入库单测量",
"selector": ".ant-menu-item",
"href": ""
},
{
"text": "测量数据审核",
"selector": ".ant-menu-item",
"href": ""
},
{
"text": "接收审核",
"selector": ".ant-menu-item",
"href": ""
},
{
"text": "刷 新",
"selector": ".ant-menu-item",
"href": ""
},
{
"text": "关闭左侧",
"selector": ".ant-menu-item",
"href": ""
},
{
"text": "关闭右侧",
"selector": ".ant-menu-item",
"href": ""
},
{
"text": "关闭其它",
"selector": ".ant-menu-item",
"href": ""
},
{
"text": "客户管理",
"selector": ".ant-menu-submenu-title",
"href": ""
},
{
"text": "入库",
"selector": ".ant-menu-submenu-title",
"href": ""
},
{
"text": "出库",
"selector": ".ant-menu-submenu-title",
"href": ""
},
{
"text": "货柜管理",
"selector": ".ant-menu-submenu-title",
"href": ""
},
{
"text": "库存",
"selector": ".ant-menu-submenu-title",
"href": ""
},
{
"text": "财务结算",
"selector": ".ant-menu-submenu-title",
"href": ""
},
{
"text": "托盘管理",
"selector": ".ant-menu-submenu-title",
"href": ""
},
{
"text": "通用",
"selector": ".ant-menu-submenu-title",
"href": ""
},
{
"text": "常见案例",
"selector": ".ant-menu-submenu-title",
"href": ""
},
{
"text": "在线开发",
"selector": ".ant-menu-submenu-title",
"href": ""
},
{
"text": "报表设计",
"selector": ".ant-menu-submenu-title",
"href": ""
},
{
"text": "消息中心",
"selector": ".ant-menu-submenu-title",
"href": ""
},
{
"text": "系统管理",
"selector": ".ant-menu-submenu-title",
"href": ""
},
{
"text": "系统监控",
"selector": ".ant-menu-submenu-title",
"href": ""
},
{
"text": "统计报表",
"selector": ".ant-menu-submenu-title",
"href": ""
},
{
"text": "入库 入库单列表 入库单测量 测量数据审核 接收审核",
"selector": "li[role='menuitem']",
"href": ""
},
{
"text": "定位仓库 请选择仓库",
"selector": "[class*='menu'] [class*='item']",
"href": ""
},
{
"text": "定位仓库",
"selector": "[class*='menu'] [class*='item']",
"href": ""
},
{
"text": "请选择仓库",
"selector": "[class*='menu'] [class*='item']",
"href": ""
},
{
"text": "交货仓 请选择交货仓",
"selector": "[class*='menu'] [class*='item']",
"href": ""
},
{
"text": "交货仓",
"selector": "[class*='menu'] [class*='item']",
"href": ""
},
{
"text": "请选择交货仓",
"selector": "[class*='menu'] [class*='item']",
"href": ""
},
{
"text": "入仓码",
"selector": "[class*='menu'] [class*='item']",
"href": ""
},
{
"text": "订单状态 请选择订单状态",
"selector": "[class*='menu'] [class*='item']",
"href": ""
},
{
"text": "订单状态",
"selector": "[class*='menu'] [class*='item']",
"href": ""
},
{
"text": "请选择订单状态",
"selector": "[class*='menu'] [class*='item']",
"href": ""
},
{
"text": "费用审核状态 请选择费用审核状态",
"selector": "[class*='menu'] [class*='item']",
"href": ""
},
{
"text": "费用审核状态",
"selector": "[class*='menu'] [class*='item']",
"href": ""
},
{
"text": "请选择费用审核状态",
"selector": "[class*='menu'] [class*='item']",
"href": ""
},
{
"text": "1",
"selector": "[class*='menu'] [class*='item']",
"href": ""
},
{
"text": "2",
"selector": "[class*='menu'] [class*='item']",
"href": ""
},
{
"text": "3",
"selector": "[class*='menu'] [class*='item']",
"href": ""
},
{
"text": "4",
"selector": "[class*='menu'] [class*='item']",
"href": ""
},
{
"text": "5",
"selector": "[class*='menu'] [class*='item']",
"href": ""
},
{
"text": "•••",
"selector": "[class*='menu'] [class*='item']",
"href": ""
},
{
"text": "36",
"selector": "[class*='menu'] [class*='item']",
"href": ""
}
],
"all_links": [
"/system/ConfigList",
"/system/DictList",
"/system/MenuList",
"/system/RoleList",
"/system/UserList",
"/wms/ByBolList",
"/wms/ByContainerList",
"/wms/ByCustomerList",
"/wms/ByDispatchList",
"/wms/ByFinanceList",
"/wms/ByInstoreList",
"/wms/ByInstoreList4Measured",
"/wms/ByInstoreList4MeasuredAudit",
"/wms/ByInstoreList4ReceiveAudit",
"/wms/ByInventoryList",
"/wms/ByOutstoreList",
"/wms/ByTrayList",
"/wms/Dashboard",
"/wms/home",
"/wms/index"
],
"api_calls": [
{
"url": "http://39.101.149.173/user/login?redirect=%2Fwms%2FByInstoreList",
"status": 200,
"method": "GET"
},
{
"url": "https://adtest.bylogistic.cn/sys/randomImage/1780285671792?_t=1780285671",
"status": 200,
"method": "GET"
},
{
"url": "https://adtest.bylogistic.cn/sys/randomImage/1780285691766?_t=1780285691",
"status": 200,
"method": "GET"
},
{
"url": "https://adtest.bylogistic.cn/sys/randomImage/1780285694400?_t=1780285694",
"status": 200,
"method": "GET"
},
{
"url": "https://adtest.bylogistic.cn/sys/login",
"status": 200,
"method": "POST"
},
{
"url": "https://adtest.bylogistic.cn/sys/randomImage/1780285703728?_t=1780285703",
"status": 200,
"method": "GET"
},
{
"url": "https://adtest.bylogistic.cn/sys/login",
"status": 200,
"method": "POST"
},
{
"url": "https://adtest.bylogistic.cn/sys/randomImage/1780285711155?_t=1780285711",
"status": 200,
"method": "GET"
},
{
"url": "https://adtest.bylogistic.cn/sys/login",
"status": 200,
"method": "POST"
},
{
"url": "http://39.101.149.173/user/login?redirect=%2Fwms%2FByInstoreList",
"status": 200,
"method": "GET"
},
{
"url": "https://adtest.bylogistic.cn/sys/randomImage/1780285725968?_t=1780285725",
"status": 200,
"method": "GET"
},
{
"url": "https://adtest.bylogistic.cn/sys/randomImage/1780285750913?_t=1780285750",
"status": 200,
"method": "GET"
},
{
"url": "https://adtest.bylogistic.cn/sys/login",
"status": 200,
"method": "POST"
},
{
"url": "https://adtest.bylogistic.cn/sys/permission/getUserPermissionByToken?_t=1780285758",
"status": 200,
"method": "GET"
},
{
"url": "https://adtest.bylogistic.cn/sys/dict/getDictItems/by_company,company_name,id",
"status": 200,
"method": "GET"
},
{
"url": "https://adtest.bylogistic.cn/sys/dict/getDictItems/by_order_status,name,value,value%20not%20in%20(1001%5E%5E1002)",
"status": 200,
"method": "GET"
},
{
"url": "https://adtest.bylogistic.cn/sys/dict/getDictItems/by_customer,realname,id",
"status": 200,
"method": "GET"
},
{
"url": "https://adtest.bylogistic.cn/sys/dict/getDictItems/by_store,store_name,store_no",
"status": 200,
"method": "GET"
},
{
"url": "https://adtest.bylogistic.cn/sys/dict/getDictItems/by_order_status,name,value",
"status": 200,
"method": "GET"
},
{
"url": "https://adtest.bylogistic.cn/sys/dict/getDictItems/by_store,store_name,store_no,parent_store_id!=''",
"status": 200,
"method": "GET"
},
{
"url": "https://adtest.bylogistic.cn/sys/dict/getDictItems/by_order_path,path_name,id",
"status": 200,
"method": "GET"
},
{
"url": "https://adtest.bylogistic.cn/sys/dict/getDictItems/by_fba,fba_no,fba_no",
"status": 200,
"method": "GET"
},
{
"url": "https://adtest.bylogistic.cn/sys/annountCement/listByUser?_t=1780285758",
"status": 200,
"method": "GET"
},
{
"url": "https://adtest.bylogistic.cn/sys/permission/getUserPermissionByToken?_t=1780285762",
"status": 200,
"method": "GET"
},
{
"url": "https://adtest.bylogistic.cn/sys/loginfo?_t=1780285763",
"status": 200,
"method": "GET"
},
{
"url": "https://adtest.bylogistic.cn/sys/visitInfo?_t=1780285763",
"status": 200,
"method": "GET"
},
{
"url": "https://adtest.bylogistic.cn/sys/annountCement/listByUser?_t=1780285763",
"status": 200,
"method": "GET"
},
{
"url": "https://adtest.bylogistic.cn/sys/permission/getUserPermissionByToken?_t=1780285764",
"status": 200,
"method": "GET"
}
]
}
@@ -0,0 +1,67 @@
{
"/system/ConfigList": {
"title": "柏悦物流管理系统",
"url": "http://39.101.149.173/dashboard/analysis",
"headers": [],
"buttons": [],
"form_fields": [],
"content_preview": "欢迎进入 柏悦物流管理系统\n欢迎您,管理员 退出登录\n首页\nCopyright 2022 BaiYue Logisitcs"
},
"/system/DictList": {
"error": "Page.wait_for_timeout: Target page, context or browser has been closed"
},
"/system/MenuList": {
"error": "Page.goto: Target page, context or browser has been closed"
},
"/system/RoleList": {
"error": "Page.goto: Target page, context or browser has been closed"
},
"/system/UserList": {
"error": "Page.goto: Target page, context or browser has been closed"
},
"/wms/ByBolList": {
"error": "Page.goto: Target page, context or browser has been closed"
},
"/wms/ByContainerList": {
"error": "Page.goto: Target page, context or browser has been closed"
},
"/wms/ByCustomerList": {
"error": "Page.goto: Target page, context or browser has been closed"
},
"/wms/ByDispatchList": {
"error": "Page.goto: Target page, context or browser has been closed"
},
"/wms/ByFinanceList": {
"error": "Page.goto: Target page, context or browser has been closed"
},
"/wms/ByInstoreList": {
"error": "Page.goto: Target page, context or browser has been closed"
},
"/wms/ByInstoreList4Measured": {
"error": "Page.goto: Target page, context or browser has been closed"
},
"/wms/ByInstoreList4MeasuredAudit": {
"error": "Page.goto: Target page, context or browser has been closed"
},
"/wms/ByInstoreList4ReceiveAudit": {
"error": "Page.goto: Target page, context or browser has been closed"
},
"/wms/ByInventoryList": {
"error": "Page.goto: Target page, context or browser has been closed"
},
"/wms/ByOutstoreList": {
"error": "Page.goto: Target page, context or browser has been closed"
},
"/wms/ByTrayList": {
"error": "Page.goto: Target page, context or browser has been closed"
},
"/wms/Dashboard": {
"error": "Page.goto: Target page, context or browser has been closed"
},
"/wms/home": {
"error": "Page.goto: Target page, context or browser has been closed"
},
"/wms/index": {
"error": "Page.goto: Target page, context or browser has been closed"
}
}
@@ -0,0 +1,510 @@
# 柏悦物流管理系统(Baiyue WMS)系统文档
> **版本**: 基于测试系统 2026-06-01 抓取分析
> **访问地址**: http://39.101.149.173
> **后端API**: https://adtest.bylogistic.cn
> **技术栈**: Ant Design Vue + Spring Boot + MySQL + Redis
> **测试账号**: admin / ndui#Emdfj23123
---
## 目录
1. [系统概述](#1-系统概述)
2. [技术架构](#2-技术架构)
3. [首页仪表盘](#3-首页仪表盘)
4. [客户管理模块](#4-客户管理模块)
5. [入库管理模块](#5-入库管理模块)
6. [出库管理模块](#6-出库管理模块)
7. [货柜管理模块](#7-货柜管理模块)
8. [库存管理模块](#8-库存管理模块)
9. [财务结算模块](#9-财务结算模块)
10. [托盘管理模块](#10-托盘管理模块)
11. [通用配置模块](#11-通用配置模块)
12. [常见案例模块](#12-常见案例模块)
13. [在线开发模块](#13-在线开发模块)
14. [报表设计模块](#14-报表设计模块)
15. [消息中心模块](#15-消息中心模块)
16. [系统管理模块](#16-系统管理模块)
17. [系统监控模块](#17-系统监控模块)
18. [API接口清单](#18-api接口清单)
---
## 1. 系统概述
柏悦物流管理系统(Baiyue WMS)是一套面向跨境物流仓储行业的综合管理系统,覆盖从**客户管理 → 入库 → 出库 → 货柜管理 → 库存 → 托盘 → 财务结算**的全链路业务流程。
### 1.1 核心业务流程
```
客户下单 → 入库单 → 入库测量 → 测量审核 → 接收审核 → 库存管理
↓
出库单 → BOL/派送 → 托盘管理 → 货柜管理 → 财务结算
```
### 1.2 系统模块总览
| 序号 | 模块 | 子页面数 | 说明 |
|------|------|----------|------|
| 1 | 首页 | 1 | 数据看板/仪表盘 |
| 2 | 客户管理 | 4 | 客户公司、用户、阶梯费用、流水 |
| 3 | 入库 | 4 | 入库单、测量、审核、接收 |
| 4 | 出库 | 3 | 派送单、BOL、自提单 |
| 5 | 货柜管理 | 2 | 货柜列表、装柜清单 |
| 6 | 库存 | 3 | 海外仓库存查询、理货、报损 |
| 7 | 财务结算 | 5 | 应收应付、对账单、仓储成本 |
| 8 | 托盘管理 | 2 | 托盘列表、生成托盘 |
| 9 | 通用 | 8 | 产品、费用、FBA地址、库位等 |
| 10 | 常见案例 | 2 | 自定义组件、对象存储 |
| 11 | 在线开发 | 1 | Online表单开发 |
| 12 | 报表设计 | 3 | 积木报表、生产监控、物流监控 |
| 13 | 消息中心 | 3 | 模板、消息管理、我的消息 |
| 14 | 系统管理 | 12 | 用户、角色、菜单、租户等 |
| 15 | 系统监控 | 7 | 在线用户、日志、定时任务等 |
| 16 | 统计报表 | - | 报表设计入口 |
---
## 2. 技术架构
### 2.1 前端技术
- **框架**: Vue.js(SPA 单页应用)
- **UI 组件库**: Ant Design Vue
- **路由**: Vue Router(Hash 模式)
- **状态管理**: Vuex
- **图表**: 集成数据可视化组件
### 2.2 后端技术
- **框架**: Spring Boot(推测,基于 Spring 生态)
- **数据库**: MySQL
- **缓存**: Redis
- **认证**: Token-based(JWT)
- **API 风格**: RESTful
### 2.3 认证流程
```
用户登录 → POST /sys/login → 返回 Token
后续请求 → Header: Authorization: Bearer {token}
权限验证 → GET /sys/permission/getUserPermissionByToken
```
---
## 3. 首页仪表盘
- **URL**: `/wms/Dashboard`
- **页面标题**: 柏悦物流管理系统
首页提供系统总览数据看板,展示关键业务指标(订单数、库存状态、财务概况等)。
---
## 4. 客户管理模块
### 4.1 客户公司管理
管理客户公司的基本信息,包括公司名称、联系方式、合作状态等。
### 4.2 用户资料
管理客户下的用户账号资料,包括登录账号、角色权限等。
### 4.3 海外仓阶梯费用
配置客户在海外仓的阶梯式费用规则,按不同量级设置不同单价。
### 4.4 客户账户流水
查看客户账户的资金流水明细,包括充值、消费、退款等记录。
---
## 5. 入库管理模块
入库是系统的核心业务流程模块,包含四个阶段:入库单创建 → 测量 → 测量审核 → 接收审核。
### 5.1 入库单列表
- **URL**: `/wms/ByInstoreList`
- **页面标题**: 入库单列表 · 柏悦物流管理系统
#### 搜索条件
| 字段 | 说明 |
|------|------|
| 定位仓库 | 筛选指定仓库的入库单 |
| 交货仓 | 筛选指定交货仓的入库单 |
| 入仓码 | 按入仓码精确查询 |
| 订单状态 | 按订单状态筛选(如:待处理、处理中、已完成) |
| 费用审核状态 | 按费用审核状态筛选 |
#### 表格列头(23列)
| 列名 | 说明 |
|------|------|
| 仓库 | 入库目标仓库 |
| 交货仓 | 货物来源交货仓 |
| 配载状态 | 货物配载状态 |
| 是否扣货 | 是否被扣货标记 |
| 扣货理由 | 扣货原因说明 |
| 集装箱号 | 集装箱编号 |
| 柜号 | 货柜编号 |
| 公司 | 客户公司名称 |
| 业务类型 | 业务分类 |
| 提货方式 | 提货方式(自提/送货等) |
| 提货时间 | 预约提货时间 |
| 提货联系人电话 | 提货联系人电话 |
| 提货地址 | 提货地址详情 |
| 提货邮编 | 提货地邮编 |
| 提货联系人邮箱 | 提货联系人邮箱 |
| 渠道 | 业务渠道来源 |
| 买单/退税 | 贸易方式(买单/退税) |
| 是否整柜 | 整柜/拼柜标记 |
| 商检验 | 商检检验标记 |
| 开船时间 | 预计开船时间 |
| 到港时间 | 预计到港时间 |
| 总箱数 | 货物总箱数 |
| 总毛重(T.K.GW) | 总毛重(吨/公斤) |
#### 操作按钮
| 按钮 | 功能 |
|------|------|
| 查询 | 根据搜索条件查询数据 |
| 重置 | 清空搜索条件 |
| 国内仓下单 | 创建国内仓入库单 |
| 海外分仓下单 | 创建海外分仓入库单 |
| 整托下单 | 以整托方式创建入库单 |
| 高级查询 | 打开高级筛选面板 |
| 导出报表 | 导出当前查询结果为Excel |
| 导出入库清单总表 | 导出完整的入库清单 |
| 导出唛头 | 导出唛头标签文件 |
| 导出FBA流转单ZIP | 导出FBA流转单压缩包 |
| 下载 | 下载选中记录的相关文件 |
### 5.2 入库单测量
- **URL**: `/wms/ByInstoreList4Measured`
- **页面标题**: 入库单测量 · 柏悦物流管理系统
对入库货物进行实际测量,记录测量数据。
#### 搜索条件
| 字段 | 说明 |
|------|------|
| 仓库 | 筛选仓库 |
| 入仓码 | 按入仓码查询 |
| 公司 | 按客户公司筛选 |
| FBA货件编号 | 按FBA货件编号查询 |
#### 表格列头(24列)
| 列名 | 说明 |
|------|------|
| 入仓码 | 入仓编码 |
| 仓库 | 目标仓库 |
| 订单状态 | 订单处理状态 |
| 配载状态 | 配载情况 |
| 测量数据审核状态 | 测量审核进度 |
| 唛头文件 | 唛头标签文件 |
| 是否扣货 | 扣货标记 |
| 扣货理由 | 扣货原因 |
| 公司 | 客户公司 |
| 业务类型 | 业务分类 |
| 渠道 | 业务渠道 |
| 买单/退税 | 贸易方式 |
| 是否整柜 | 整柜/拼柜 |
| 商检验 | 商检状态 |
| 总箱数 | 货物总箱数 |
| 总毛重(T.K.GW) | 总毛重 |
| 总体积(m³) | 货物总体积 |
| 总材积重 | 材积重量 |
| 总净重(T.K.NW) | 总净重 |
| 目的仓类型 | 目的仓库类型 |
| 目的仓 | 目的仓库 |
| 货件编号 | 货件编号 |
| 测量完成时间 | 测量完成时间 |
| 操作 | 操作按钮列 |
#### 操作按钮
| 按钮 | 功能 |
|------|------|
| 查询 | 根据条件查询 |
| 重置 | 清空条件 |
| 下载 | 下载数据 |
### 5.3 测量数据审核
- **URL**: `/wms/ByInstoreList4MeasuredAudit`
- **页面标题**: 测量数据审核 · 柏悦物流管理系统
对测量数据进行审核确认。表头和搜索条件与入库单测量一致。
### 5.4 接收审核
- **URL**: `/wms/ByInstoreList4ReceiveAudit`
- **页面标题**: 接收审核 · 柏悦物流管理系统
最终接收审核环节,确认货物入库完成。表头和搜索条件与入库单测量一致。
---
## 6. 出库管理模块
### 6.1 派送单列表
- **URL**: `/wms/ByDispatchList`
管理出库派送订单,包括派送地址、物流方式、派送状态等。
### 6.2 BOL(提单管理)
- **URL**: `/wms/ByBolList`
管理 Bill of Lading(提单),是国际物流中的重要单证。包括提单号、船名航次、发货人、收货人等。
### 6.3 自提单
管理客户自提订单,记录自提预约、提货人信息等。
---
## 7. 货柜管理模块
### 7.1 货柜列表
- **URL**: `/wms/ByContainerList`
管理所有货柜信息,包括柜号、类型、尺寸、状态、当前位置等。
### 7.2 装柜清单
记录每个货柜的装柜明细,展示柜内货物列表和装载方案。
---
## 8. 库存管理模块
### 8.1 海外仓库存查询
- **URL**: `/wms/ByInventoryList`
查询海外仓当前库存情况,支持按仓库、产品、SKU等维度查询。
### 8.2 理货管理
对库存货物进行盘点、整理、移位等操作。
### 8.3 报损单
管理货物损耗/损坏的报损申请和审批流程。
---
## 9. 财务结算模块
### 9.1 客户应收应付
- **URL**: `/wms/ByFinanceList`
管理每个客户的应收账款和应付账款明细。
### 9.2 客户对账单
生成和查看客户对账单,用于与客户进行账务核对。
### 9.3 结算单位应付
按结算单位(如承运商、仓库方)管理应付账款。
### 9.4 结算单位对账单
生成结算单位的对账单。
### 9.5 洛杉矶仓储成本
专门针对洛杉矶仓库的仓储成本核算和分析。
---
## 10. 托盘管理模块
### 10.1 托盘列表
- **URL**: `/wms/ByTrayList`
管理所有托盘信息,包括托盘编号、规格、状态、关联货物等。
### 10.2 生成托盘
根据入库货物自动/手动生成托盘,分配货物到托盘。
---
## 11. 通用配置模块
系统基础数据和配置管理。
| 子模块 | 说明 |
|--------|------|
| 产品信息 | 管理产品/货品基础信息(名称、规格、HS编码等) |
| 费用项目 | 定义各类费用项目(仓储费、操作费、运输费等) |
| FBA编码地址 | 管理Amazon FBA仓库编码和地址信息 |
| 托盘管理 | 托盘基础配置 |
| 库位/货架管理 | 仓库库位和货架的编号和配置 |
| 订单流转路径 | 定义订单在不同节点之间的流转规则 |
| 快递公司 | 合作的快递/物流公司信息 |
| 往来单位 | 管理供应商、承运商等往来单位信息 |
---
## 12. 常见案例模块
### 12.1 自定义组件
展示系统自定义组件的使用示例和开发指南。
### 12.2 对象存储
文件/对象存储功能的配置和使用说明。
---
## 13. 在线开发模块
### 13.1 Online表单开发
低代码表单开发工具,允许管理员在线设计和配置业务表单,无需编写代码。
---
## 14. 报表设计模块
### 14.1 积木报表设计
可视化报表设计器,支持拖拽式设计各类业务报表。
### 14.2 生产销售监控
展示生产和销售的监控数据看板。
### 14.3 智慧物流监控
物流全链路监控大屏,展示实时物流数据。
---
## 15. 消息中心模块
### 15.1 模板管理
管理系统通知的消息模板,支持短信、邮件、站内信等类型。
### 15.2 消息管理
查看和管理所有已发送的系统消息。
### 15.3 我的消息
当前登录用户收到的个人消息列表。
---
## 16. 系统管理模块
系统后台管理功能。
| 子模块 | 说明 |
|--------|------|
| 用户管理 | 管理系统用户账号(创建、编辑、禁用、重置密码) |
| 角色管理 | 定义角色并分配权限 |
| 菜单管理 | 配置系统导航菜单结构 |
| 租户管理 | 多租户隔离管理(适用于SaaS模式) |
| 部门管理 | 组织架构部门管理 |
| 我的部门 | 当前用户所属部门信息 |
| 职务管理 | 定义系统内职务 |
| 通讯录 | 系统用户通讯录 |
| 数据字典 | 系统枚举值和字典数据维护 |
| 分类字典 | 分类树形字典维护 |
| 系统通告 | 发布系统级公告通知 |
| 菜单管理2 | 菜单管理的增强版/备用入口 |
---
## 17. 系统监控模块
系统运行状态监控和运维管理。
| 子模块 | 说明 |
|--------|------|
| 在线用户 | 查看当前在线用户列表,支持强制下线 |
| 路由网关 | 查看系统路由配置和网关状态 |
| 定时任务 | 管理系统的定时任务(Cron Job) |
| 日志管理 | 查看系统操作日志 |
| 数据日志 | 数据变更日志记录 |
| SQL监控 | SQL执行监控(基于Druid连接池) |
| 在线文档 | 系统API文档(Swagger/Knife4j) |
---
## 18. API接口清单
系统后端API部署在 `https://adtest.bylogistic.cn`,以下为已发现的API端点:
### 认证相关
| 接口 | 方法 | 说明 |
|------|------|------|
| `/sys/login` | POST | 用户登录 |
| `/sys/randomImage/{timestamp}` | GET | 获取验证码图片 |
| `/sys/permission/getUserPermissionByToken` | GET | 获取用户权限 |
### 数据字典
| 接口 | 方法 | 说明 |
|------|------|------|
| `/sys/dict/getDictItems/by_company` | GET | 获取公司字典 |
| `/sys/dict/getDictItems/by_order_status` | GET | 获取订单状态字典 |
| `/sys/dict/getDictItems/by_customer` | GET | 获取客户字典 |
| `/sys/dict/getDictItems/by_store` | GET | 获取仓库字典 |
| `/sys/dict/getDictItems/by_order_path` | GET | 获取订单路径字典 |
| `/sys/dict/getDictItems/by_fba` | GET | 获取FBA编码字典 |
### 系统功能
| 接口 | 方法 | 说明 |
|------|------|------|
| `/sys/annountCement/listByUser` | GET | 获取用户公告列表 |
| `/sys/loginfo` | GET | 系统日志信息 |
| `/sys/visitInfo` | GET | 访问统计信息 |
---
## 附录:SKU明细区域列头
在入库单详情页面,SKU明细表格包含以下列:
| 列名 | 说明 |
|------|------|
| 操作 | 编辑/删除操作 |
| # | 序号 |
| 报关HS_CODE | 报关用HS海关编码 |
| 货品图片 | 货品图片预览 |
| 仓/储/微(快递)码/纱 | 仓储编码/微信码/快递码 |
| 中品名 | 中文品名 |
| 英品名 | 英文品名 |
| 清关HS_CODE | 清关用HS海关编码 |
| 箱规长(cm) | 外箱长度 |
| 箱规宽(cm) | 外箱宽度 |
---
> **文档生成时间**: 2026-06-01
> **数据来源**: 测试系统 http://39.101.149.173 实际登录抓取
> **说明**: 部分页面因SPA路由限制未能获取完整表头信息,已根据菜单结构推断标注。如需补充具体页面详情,请登录系统确认后更新。
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff