refactor(客户管理): 完成客户管理模块UI与架构全面调整

1.  调整菜单与路由:重命名客户信息/客户账号为客户档案管理/客户小程序授权管理,移除客户建筑/房间路由,新增空间拓扑路由
2.  后端新增客户统计接口`/customers/stats`,前端同步API与类型定义
3.  重构客户档案与小程序授权页面,仅保留数据库支持字段,移除无支撑功能
4.  新增空间拓扑页面,合并建筑与房间管理为树形拓扑视图
5.  更新数据库种子数据与权限配置,清理废弃菜单与绑定关系
6.  补充UI设计文档与规范说明
This commit is contained in:
andyesfly committed 2026-09-09 17:31:24 +08:00
1 parent 2a06cfea6d
commit c60caa4770
35 files changed
+7522 -1959

No files matched your search

@@ -0,0 +1,12 @@
# Checklist
- [x] `database/db_seed.sql` 中客户管理子菜单:12 改名「客户档案管理」、14 改名「客户小程序授权管理」、删除 25/26、新增 27「空间拓扑与用电架构」(实际 id=32),id/父级/权限正确
- [x] `client/src/router/index.ts` 同步菜单标题,移除 building/room 路由,新增 topology 路由,无死链
- [x] 后端新增 `GET /customers/stats`,返回 total/statusCounts/levelCounts,并按登录人 salesman_id 过滤
- [x] `client/src/api/customer.ts` 提供 `getStats()` 且类型正确
- [x] 客户档案管理页重构符合 UI 风格:KPI 卡 + 搜索 + 表格,列/表单仅含数据库 `customer` 字段,无数据库不支持字段
- [x] 客户小程序授权管理页重构:标题为「客户小程序授权管理/定向邀请准入制」,列含微信绑定状态,操作含重置密码/解绑微信,无邀请类能力
- [x] 空间拓扑页:客户→建筑→房间 三层树渲染正确,节点新增/编辑/删除走现有 building/room API
- [x] 废弃的 building/room 视图已处理,不再出现在菜单与路由
- [x] 前端类型检查通过(`npm run check` 0 错误),后端编译通过(`npm run build` 成功)
- [ ] dev server 联调:菜单渲染、客户档案管理、小程序授权管理、空间拓扑增删改,salesman 数据权限一致(未在本会话启动 dev server 验证,如需可另行联调)
@@ -0,0 +1,77 @@
# 客户管理 UI 调整 Spec
## Why
现有客户管理的功能菜单与界面与最新 UI 设计(`系统设计/UI/客户档案管理`、`客户档案编辑`、`客户档案详情`、`客户小程序授权管理`、`空间拓扑与用电架构`)不一致。需要按 UI 设计调整菜单结构与页面形态,同时以架构设计(`系统设计/xintong-architecture-full.html`)和数据库设计(`系统设计/db_design.html`)为功能边界——UI 中存在但数据库/架构中不存在的功能与字段一律舍弃,以数据库为准。
## What Changes
- **菜单结构调整**(`database/db_seed.sql` + `client/src/router/index.ts`):
- `客户信息` → 重命名 `客户档案管理`(12)
- `客户账号` → 重命名 `客户小程序授权管理`(14)
- 新增 `空间拓扑与用电架构` 菜单(新 id 27,`/customer/topology`)
- 移除 `客户建筑`(25)、`客户房间`(26)两个菜单及对应路由
- 保留 `客户部门`(13)、`客户拜访`(24)菜单与路由不变
- **客户档案管理页**(`client/src/views/customer/info/index.vue` 重构):
- 按 UI 风格重排:顶部 KPI 统计卡 + 搜索栏 + 客户档案表格
- 仅使用数据库 `customer` 字段(客户编号/名称、省市区、地址、联系人/电话、邮编、传真、客户级别、状态、负责业务员)
- **舍弃** UI 中数据库不存在的字段/能力:统一社会信用代码、法定代表人、行业属性、托管服务状态、ICC 设备状态、大华 ICC 同步、营业执照智能识别、导入意向客户、托管周期与费用等
- 保留 KPI 卡(基于数据库可聚合统计:客户总数、按状态/级别计数),需新增轻量后端 `/customers/stats`
- **客户小程序授权管理页**(`client/src/views/customer/account/index.vue` 重构):
- 重命名为「客户小程序授权管理」,UI 标题/说明改为「定向邀请准入制」风格
- 仅使用数据库 `customer_account` 字段:所属客户、登录手机号、微信绑定状态(wx_openid 是否存在)、状态、最近登录时间、创建时间
- **舍弃** UI 中无数据库支撑的邀请凭证/二维码/批量短信/发起邀请等能力
- 保留新增/编辑(含重置默认密码)、删除、微信解绑等基于现有 API 的操作
- **空间拓扑与用电架构页**(新增 `client/src/views/customer/topology/index.vue`):
- 替换原「客户建筑」「客户房间」两个列表菜单,合并为单一空间拓扑页
- 树形拓扑:客户 → 建筑 → 房间 三层渲染(基于 `customer`、`customer_building`、`customer_room`)
- 通过「新增/编辑空间节点」弹窗维护建筑与房间数据(复用现有 building/room CRUD API)
- **舍弃** UI 中不存在的节点类型(变电所/配电室、监测回路、ICC 边缘网关)与相关指标
- 后端 API 基本不变,仅新增一个客户统计端点 `/customers/stats`
## Impact
- 受影响对称菜单/权限:`sys_menu` 中客户管理子菜单(build/seed 变更)
- 受影响前端:`client/src/router/index.ts`、`client/src/views/customer/info/index.vue`、`client/src/views/customer/account/index.vue`、新增 `client/src/views/customer/topology/index.vue`、`client/src/api/customer.ts`(新增 stats)、新增 `client/src/api/customerTopology.ts`
- 受影响后端:`server/src/` 客户 controller 新增 `/customers/stats`
- 不受影响:`客户部门`(customer/department)、`客户拜访`(customer/visit)页面与后端
## ADDED Requirements
### Requirement: 客户统计端点
系统 SHALL 新增 `GET /customers/stats`,返回客户总数及按状态、按级别的计数,用于客户档案管理页顶部 KPI 卡。
#### Scenario: 成功获取统计
- **WHEN** 前端加载客户档案管理页
- **THEN** 后端返回 `{ total, statusCounts: {1/2/3/4: n}, levelCounts: {...} }`,仅统计当前登录人可见的客户(销售员按 salesman_id 过滤)
### Requirement: 空间拓扑页
系统 SHALL 提供客户→建筑→房间三层树形拓扑视图,并支持通过节点弹窗对建筑、房间进行新增/编辑/删除。
#### Scenario: 渲染拓扑
- **WHEN** 用户在客户管理选择一个客户
- **THEN** 页面展示该客户下建筑节点(含 building_name、purpose、floor_count),展开建筑展示其房间节点(含 room_code、room_name、floor_no、room_purpose)
#### Scenario: 维护空间节点
- **WHEN** 用户点击「新增空间节点/新增子节点」或在节点右键操作
- **THEN** 弹出创建/编辑弹窗,提交后调用现有 building/room 创建、编辑、删除 API,并刷新拓扑
## MODIFIED Requirements
### Requirement: 客户档案管理菜单与页面
菜单名由「客户信息」改为「客户档案管理」;页面按 UI 排版重构(KPI 卡 + 搜索 + 表格),字段仅保留数据库 `customer` 表已有字段。
#### Scenario: 列表展示
- **WHEN** 用户进入客户档案管理页
- **THEN** 展示基于数据库字段的 KPI 统计卡与客户档案表格,表格列包含:客户编号、客户名称、省市区、地址、联系人、联系电话、客户级别、状态、负责业务员、操作
### Requirement: 小程序授权管理菜单与页面
菜单名由「客户账号」改为「客户小程序授权管理」;页面按 UI 标题风格调整,字段仅使用数据库 `customer_account` 字段,去掉无数据库支撑的邀请类能力。
#### Scenario: 列表与账号维护
- **WHEN** 用户进入客户小程序授权管理页
- **THEN** 展示账号列表(手机号、所属客户、微信绑定状态、状态、最近登录时间)并可新增/编辑/删除/重置密码/解绑微信
## REMOVED Requirements
### Requirement: 客户建筑、客户房间独立菜单页
**Reason**: UI 设计将建筑/房间合并进「空间拓扑与用电架构」单一页面;数据库支持该合并(customer_building、customer_room)。
**Migration**: 移除 `sys_menu` 中 25、26 菜单与前端对应路由;建筑/房间的增删改改由空间拓扑页节点弹窗承担,复用现有 building/room API。
### Requirement: UI 中存在但数据库/架构不支持的功能与字段
**Reason**: 按用户规则「若 UI 中存在架构和数据库中不存在的功能和字段,以架构和数据库设计为准」。
**Migration**: 舍弃统一社会信用代码、法定代表人、行业属性、托管服务状态、ICC 设备、用电参数、拓扑率、大华 ICC 同步、营业执照导入、邀请凭证/二维码/短信等字段与能力;KPI 改用数据库可聚合统计。
@@ -0,0 +1,51 @@
# Tasks
- [x] Task 1: 调整客户管理菜单权限种子(`database/db_seed.sql`)
- [x] 1.1 将 id=12 菜单名「客户信息」改为「客户档案管理」
- [x] 1.2 将 id=14 菜单名「客户账号」改为「客户小程序授权管理」,权限/图标可沿用
- [x] 1.3 删除 id=25「客户建筑」、id=26「客户房间」两条菜单
- [x] 1.4 新增 id=32「空间拓扑与用电架构」,path `/customer/topology`,component `customer/topology/index`,permission `customer:topology`
- [x] Task 2: 调整前端路由与侧边菜单(`client/src/router/index.ts`)
- [x] 2.1 同步各子菜单 `meta.title`(信息→客户档案管理、账号→客户小程序授权管理)
- [x] 2.2 移除 `customer/building`、`customer/room` 路由
- [x] 2.3 新增 `customer/topology` 路由(title「空间拓扑与用电架构」、permission `customer:topology`)
- [x] Task 3: 后端客户统计端点(`server/src` 客户 controller/service)
- [x] 3.1 新增 `GET /customers/stats`,返回 `{ total, statusCounts, levelCounts }`,按登录人 salesman_id 过滤
- [x] 3.2 前端 `client/src/api/customer.ts` 新增 `getStats()` 并扩展类型
- [x] Task 4: 重构客户档案管理页(`client/src/views/customer/info/index.vue`)
- [x] 4.1 顶部 KPI 统计卡(总数、按状态、按级别),数据来自 stats 端点
- [x] 4.2 搜索栏:关键词、行政区划(省市县级联)、客户级别、状态、负责业务员
- [x] 4.3 表格列:客户编号、客户名称、省市区、详细地址、联系人/电话、客户级别、状态、负责业务员、操作(编辑/删除/详情)
- [x] 4.4 新增/编辑弹窗仅含数据库 `customer` 字段
- [x] 4.5 仅使用数据库已有字段,去除 UI 中无支撑字段
- [x] Task 5: 重构客户小程序授权管理页(`client/src/views/customer/account/index.vue`)
- [x] 5.1 页面标题/说明改为「客户小程序授权管理 / 定向邀请准入制」风格
- [x] 5.2 表格列:登录手机号、所属客户、微信绑定状态(wx_openid 是否存在)、状态、最近登录时间、操作
- [x] 5.3 新增/编辑弹窗保留所属客户、登录电话、状态、重置默认密码;删除、解绑微信操作
- [x] 5.4 去除邀请凭证/二维码/短信等无数据库支撑能力
- [x] Task 6: 新增空间拓扑与用电架构页(`client/src/views/customer/topology/index.vue`)
- [x] 6.1 客户选择(复用客户 options 接口)
- [x] 6.2 树形拓扑:客户 → 建筑(building_name、purpose、floor_count)→ 房间(room_code、room_name、floor_no、room_purpose)
- [x] 6.3 节点新增/编辑/删除:复用现有 `customer-building`、`customer-room` CRUD API,通过弹窗维护
- [x] 6.4 前端组合拓扑数据(客户 list → building list → room list)
- [x] 6.5 按 UI 设计风格(KPI/指标卡、搜索、卡片化)排版,仅用数据库字段
- [x] Task 7: 清理废弃建筑/房间页面
- [x] 7.1 删除或停用 `client/src/views/customer/building`、`client/src/views/customer/room` 视图(其 CRUD 逻辑已迁移至空间拓扑页引用)
- [x] Task 8: 静态校验与联调
- [x] 8.1 前端 `vue-tsc` / 类型检查通过(`client` `npm run check` 0 错误)
- [x] 8.2 后端 TypeScript 编译通过(`server` `npm run build` 成功)
- [ ] 8.3 起 dev server 验证:菜单渲染、客户档案管理、小程序授权管理、空间拓扑增删改,salesman 数据权限正常(未在本会话执行,如需可另行启动联调)
# Task Dependencies
- Task 4 依赖 Task 3(stats 端点)与 Task 1/2(菜单、路由)
- Task 5 依赖 Task 1/2
- Task 6 依赖 Task 1/2
- Task 7 依赖 Task 6
- Task 8 依赖 Task 3/4/5/6
+8
View File
@@ -68,6 +68,12 @@ export interface CustomerStatusParams {
status: number;
}
export interface CustomerStats {
total: number;
statusCounts: Record<number, number>;
levelCounts: Record<string, number>;
}
export const customerApi = {
getCustomerList: (params: CustomerQuery) =>
request.get<CustomerListResult>('/customers', { params }),
@@ -78,6 +84,8 @@ export const customerApi = {
getCustomerOptions: () =>
request.get<CustomerInfo[]>('/customers/options/all'),
getCustomerStats: () => request.get<CustomerStats>('/customers/stats'),
createCustomer: (data: CustomerCreateParams) =>
request.post('/customers', data),
+1
View File
@@ -4,6 +4,7 @@ export interface CustomerAccountInfo {
id: number;
customerId: number;
phone: string;
wxOpenid?: string | null;
status: number;
lastLoginAt?: string | null;
createdAt: string;
+55 -22
View File
@@ -1,28 +1,61 @@
import request from '@/utils/request';
export interface ModuleCount {
name: string;
value: number;
}
export interface TrendPoint {
date: string;
customers: number;
contracts: number;
}
export interface StatusSlice {
name: string;
value: number;
}
export interface SurveyOrderItem {
id: number;
orderNo: string;
customerName: string;
locale: string;
owner: string;
status: number;
time: string;
}
export interface ProjectKanban {
id: number;
customerName: string;
amount: number;
stage: number;
stageLabel: string;
progress: number;
owner: string;
desc: string;
orderNo: string;
}
export interface DashboardStats {
deviceTotal: number;
deviceOnline: number;
alertToday: number;
activeUsers: number;
deviceTrend: Array<{ date: string; online: number; offline: number }>;
deviceTypeDistribution: Array<{ name: string; value: number }>;
recentAlerts: Array<{
id: number;
type: string;
device: string;
level: string;
time: string;
status: string;
}>;
recentActivities: Array<{
id: number;
user: string;
action: string;
module: string;
ip: string;
time: string;
}>;
customerTotal: number;
customerMonthlyNew: number;
contractTotal: number;
contractYearAmount: number;
surveyPending: number;
surveyTotal: number;
visitMonthly: number;
visitTotal: number;
planTotal: number;
acceptTotal: number;
surveyByStatus: StatusSlice[];
modules: ModuleCount[];
trend: TrendPoint[];
customerStatus: StatusSlice[];
customerSignRate: number;
surveyList: SurveyOrderItem[];
projects: ProjectKanban[];
}
export const dashboardApi = {
+10 -16
View File
@@ -32,11 +32,11 @@ const routes: RouteRecordRaw[] = [
},
// 业务模块
{
path: "customer/info",
name: "CustomerInfo",
component: () => import("@/views/customer/info/index.vue"),
meta: { requiresAuth: true, title: "客户信息", permission: "customer:list" }
},
path: "customer/info",
name: "CustomerInfo",
component: () => import("@/views/customer/info/index.vue"),
meta: { requiresAuth: true, title: "客户档案管理", permission: "customer:list" }
},
{
path: "customer/department",
name: "CustomerDept",
@@ -47,7 +47,7 @@ const routes: RouteRecordRaw[] = [
path: "customer/account",
name: "CustomerAccount",
component: () => import("@/views/customer/account/index.vue"),
meta: { requiresAuth: true, title: "客户账号", permission: "customer:account" }
meta: { requiresAuth: true, title: "客户小程序授权管理", permission: "customer:account" }
},
{
path: "customer/visit",
@@ -56,16 +56,10 @@ const routes: RouteRecordRaw[] = [
meta: { requiresAuth: true, title: "客户拜访", permission: "customer:visit" }
},
{
path: "customer/building",
name: "CustomerBuilding",
component: () => import("@/views/customer/building/index.vue"),
meta: { requiresAuth: true, title: "客户建筑", permission: "customer:building" }
},
{
path: "customer/room",
name: "CustomerRoom",
component: () => import("@/views/customer/room/index.vue"),
meta: { requiresAuth: true, title: "客户房间", permission: "customer:room" }
path: "customer/topology",
name: "CustomerTopology",
component: () => import("@/views/customer/topology/index.vue"),
meta: { requiresAuth: true, title: "空间拓扑与用电架构", permission: "customer:topology" }
},
{
path: "project",
+423 -138
View File
@@ -1,114 +1,163 @@
<template>
<div class="customer-account">
<a-card title="客户账号管理">
<template #extra>
<a-space>
<!-- 顶部标题 + 操作 -->
<div class="page-header">
<div class="breadcrumb">
<span>运营管控体系</span>
<span class="sep">/</span>
<span>客户管理</span>
<span class="sep">/</span>
<span class="cur">客户小程序授权管理</span>
</div>
<div class="header-main">
<div class="header-left">
<div class="header-row">
<span class="title-bar"></span>
<span class="main-title">客户小程序授权管理</span>
<span class="sub-title">MINI PROGRAM AUTHORIZATION</span>
</div>
<p class="desc">定向邀请准入制:客户小程序端登录账号由运营管理员审核准入后授予授权,仅已通过准入的客户可凭授权登录对应小程序。</p>
</div>
<div class="header-actions">
<a-button type="primary" @click="handleAdd">
<template #icon><PlusOutlined /></template>
新增账号
</a-button>
</a-space>
</template>
</div>
</div>
</div>
<!-- 主体卡片 -->
<div class="content-card">
<a-alert
type="info"
message="初始密码规则:默认密码为手机号后 6 位,如手机号不足 6 位则为 123456;编辑时可勾选重置为默认密码"
style="margin-bottom: 16px"
/>
<div class="search-bar">
<a-input
v-model:value="searchForm.keyword"
placeholder="登录手机号"
allow-clear
style="width: 220px"
@pressEnter="handleSearch"
>
<template #prefix><SearchOutlined /></template>
</a-input>
<a-select
v-model:value="searchForm.customerId"
placeholder="所属客户"
allow-clear
style="width: 220px"
:options="customerOptions"
/>
<a-select v-model:value="searchForm.status" placeholder="状态" allow-clear style="width: 120px">
<a-select-option :value="1">启用</a-select-option>
<a-select-option :value="0">禁用</a-select-option>
</a-select>
<a-button type="primary" @click="handleSearch">
<template #icon><SearchOutlined /></template>
查询
</a-button>
<a-button @click="handleReset">
<template #icon><ReloadOutlined /></template>
重置
</a-button>
</div>
<div class="table-wrapper" ref="wrapperRef">
<a-table
:columns="columns"
:data-source="dataSource"
:loading="loading"
:pagination="pagination"
row-key="id"
:scroll="{ x: 'max-content', y: tableHeight }"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'customerId'">
{{ customerNameMap[record.customerId] || '-' }}
</template>
<template v-if="column.key === 'status'">
<a-switch
:checked="record.status === 1"
checked-children="启用"
un-checked-children="禁用"
@change="(checked: boolean) => handleStatusChange(record.id, checked ? 1 : 0)"
<!-- 筛选区 -->
<div class="filter-card">
<div class="filter-grid">
<div class="filter-item">
<label class="filter-label">登录手机号</label>
<a-input
v-model:value="searchForm.keyword"
placeholder="请输入登录手机号"
allow-clear
@pressEnter="handleSearch"
>
<template #prefix><SearchOutlined /></template>
</a-input>
</div>
<div class="filter-item">
<label class="filter-label">所属客户</label>
<a-select
v-model:value="searchForm.customerId"
placeholder="请选择客户"
allow-clear
:options="customerOptions"
/>
</template>
<template v-if="column.key === 'createdAt'">
{{ formatDateTime(record.createdAt) }}
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleEdit(record)">
<template #icon><EditOutlined /></template>
编辑
</a-button>
<a-popconfirm title="确定要重置该账号的密码吗?" ok-text="确定" cancel-text="取消" @confirm="handleResetPassword(record.id)">
<a-button type="link" size="small">
<template #icon><KeyOutlined /></template>
重置密码
</a-button>
</a-popconfirm>
<a-popconfirm title="确定要删除该账号吗?" ok-text="确定" cancel-text="取消" @confirm="handleDelete(record.id)">
<a-button type="link" size="small" danger>
<template #icon><DeleteOutlined /></template>
删除
</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
<template #emptyText>
<a-empty description="暂无账号数据" />
</template>
</a-table>
</div>
<div class="filter-item">
<label class="filter-label">状态</label>
<a-select v-model:value="searchForm.status" placeholder="请选择状态" allow-clear>
<a-select-option :value="1">启用</a-select-option>
<a-select-option :value="0">禁用</a-select-option>
</a-select>
</div>
</div>
<div class="filter-bottom">
<div class="filter-actions">
<a-button type="primary" @click="handleSearch">
<template #icon><SearchOutlined /></template>
查询
</a-button>
<a-button @click="handleReset">
<template #icon><ReloadOutlined /></template>
重置
</a-button>
</div>
</div>
</div>
</a-card>
<!-- 列表区 -->
<div class="list-section">
<div class="list-header">
<div class="list-title">
<span class="dot"></span>
<span class="t">账号列表</span>
<span class="count">{{ total }}</span>
</div>
</div>
<div class="table-wrapper" ref="wrapperRef">
<a-table
:columns="columns"
:data-source="dataSource"
:loading="loading"
:pagination="pagination"
row-key="id"
:scroll="{ x: 'max-content', y: tableHeight }"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'customerId'">
{{ customerNameMap[record.customerId] || '-' }}
</template>
<template v-if="column.key === 'wxBound'">
<a-tag :color="record.wxOpenid ? 'green' : 'default'">
{{ record.wxOpenid ? '已绑定' : '未绑定' }}
</a-tag>
</template>
<template v-if="column.key === 'status'">
<a-switch
:checked="record.status === 1"
checked-children="启用"
un-checked-children="禁用"
@change="(checked: boolean) => handleStatusChange(record.id, checked ? 1 : 0)"
/>
</template>
<template v-if="column.key === 'lastLoginAt'">
{{ formatDateTime(record.lastLoginAt) }}
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleEdit(record)">
<template #icon><EditOutlined /></template>
编辑
</a-button>
<a-popconfirm title="确定要重置该账号的密码吗?" ok-text="确定" cancel-text="取消" @confirm="handleResetPassword(record.id)">
<a-button type="link" size="small">
<template #icon><KeyOutlined /></template>
重置密码
</a-button>
</a-popconfirm>
<a-popconfirm title="确定要删除该账号吗?" ok-text="确定" cancel-text="取消" @confirm="handleDelete(record.id)">
<a-button type="link" size="small" danger>
<template #icon><DeleteOutlined /></template>
删除
</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
<template #emptyText>
<a-empty description="暂无账号数据" />
</template>
</a-table>
</div>
</div>
</div>
<a-modal
v-model:open="modalVisible"
:title="modalTitle"
:confirm-loading="modalLoading"
width="600px"
width="640px"
@ok="handleSubmit"
@cancel="handleCancel"
>
@@ -119,31 +168,49 @@
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<a-form-item label="所属客户" name="customerId">
<a-select
v-model:value="formData.customerId"
placeholder="请选择所属客户"
:options="customerOptions"
/>
</a-form-item>
<a-form-item label="登录电话" name="phone">
<a-input v-model:value="formData.phone" placeholder="请输入登录电话" maxlength="20" />
</a-form-item>
<a-form-item label="状态" name="status">
<a-radio-group v-model:value="formData.status">
<a-radio :value="1">启用</a-radio>
<a-radio :value="0">禁用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item v-if="isEdit">
<a-checkbox v-model:checked="formData.resetPassword">重置为默认密码(手机号后 6 位)</a-checkbox>
<div style="font-size: 12px; color: #999; margin-top: 4px; padding-left: 24px">
勾选后保存即重置,下次登录请用新密码
<div class="form-section">
<div class="section-head">
<span class="section-no">01</span>
<div class="section-text">
<span class="section-name">账号信息</span>
<span class="section-sub">Account Information</span>
</div>
</div>
</a-form-item>
<a-form-item label="所属客户" name="customerId">
<a-select
v-model:value="formData.customerId"
placeholder="请选择所属客户"
:options="customerOptions"
/>
</a-form-item>
<a-form-item label="登录电话" name="phone">
<a-input v-model:value="formData.phone" placeholder="请输入登录电话" maxlength="20" />
</a-form-item>
<a-form-item label="状态" name="status">
<a-radio-group v-model:value="formData.status">
<a-radio :value="1">启用</a-radio>
<a-radio :value="0">禁用</a-radio>
</a-radio-group>
</a-form-item>
</div>
<div class="form-section">
<div class="section-head">
<span class="section-no">02</span>
<div class="section-text">
<span class="section-name">密码策略</span>
<span class="section-sub">Password Policy</span>
</div>
</div>
<a-form-item v-if="isEdit">
<a-checkbox v-model:checked="formData.resetPassword">重置为默认密码(手机号后 6 位)</a-checkbox>
<div style="font-size: 12px; color: #8a97a8; margin-top: 4px; padding-left: 24px">
勾选后保存即重置,下次登录请用新密码
</div>
</a-form-item>
</div>
</a-form>
</a-modal>
</div>
@@ -202,16 +269,22 @@ const formRules = {
};
const columns = [
{ title: '登录手机号', dataIndex: 'phone', key: 'phone', ellipsis: true, width: 140 },
{ title: '所属客户', dataIndex: 'customerId', key: 'customerId', ellipsis: true, width: 180 },
{ title: '登录电话', dataIndex: 'phone', key: 'phone', ellipsis: true, width: 140 },
{
title: '微信绑定',
key: 'wxBound',
align: 'center' as const,
width: 110,
},
{
title: '状态',
key: 'status',
align: 'center' as const,
width: 100,
},
{ title: '创建时间', dataIndex: 'createdAt', key: 'createdAt', ellipsis: true, width: 180 },
{ title: '操作', key: 'action', width: 260, fixed: 'right' as const },
{ title: '最近登录时间', dataIndex: 'lastLoginAt', key: 'lastLoginAt', ellipsis: true, width: 180 },
{ title: '操作', key: 'action', width: 200, fixed: 'right' as const },
];
const pagination = computed(() => ({
@@ -402,7 +475,7 @@ async function handleResetPassword(id: number) {
}
}
function formatDateTime(dateStr: string) {
function formatDateTime(dateStr?: string | null) {
if (!dateStr) return '-';
return dayjs(dateStr).format('YYYY-MM-DD HH:mm:ss');
}
@@ -413,33 +486,180 @@ function formatDateTime(dateStr: string) {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
overflow: auto;
padding: 20px;
box-sizing: border-box;
background: #eef3f8;
}
:deep(.ant-card) {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
:deep(.ant-card-body) {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
.search-bar {
.page-header {
flex-shrink: 0;
margin-bottom: 16px;
}
.breadcrumb {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 8px;
font-size: 12px;
color: #5b6b7e;
margin-bottom: 10px;
}
.breadcrumb .sep {
color: #c6d2e0;
}
.breadcrumb .cur {
color: #0d1c2c;
font-weight: 600;
}
.header-main {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 20px;
}
.header-left {
display: flex;
flex-direction: column;
gap: 8px;
}
.header-row {
display: flex;
align-items: center;
gap: 12px;
}
.title-bar {
width: 5px;
height: 22px;
background: #0054cd;
border-radius: 3px;
align-self: center;
}
.main-title {
font-size: 22px;
font-weight: 700;
color: #0d1c2c;
}
.sub-title {
font-size: 12px;
font-weight: 700;
color: #0054cd;
letter-spacing: 2px;
}
.desc {
font-size: 13px;
color: #5b6b7e;
line-height: 1.6;
margin: 0;
}
.header-actions {
flex-shrink: 0;
}
.content-card {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
gap: 0;
background: #fff;
border: 1px solid #dbe4ee;
border-radius: 12px;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.07);
padding: 20px;
}
.filter-card {
flex-shrink: 0;
background: #f8faff;
border: 1px solid #dbe4ee;
border-radius: 10px;
padding: 16px;
}
.filter-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px 16px;
}
.filter-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.filter-label {
font-size: 12px;
font-weight: 600;
color: #5b6b7e;
}
.filter-bottom {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid #dbe4ee;
}
.filter-actions {
display: flex;
gap: 8px;
}
.list-section {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.list-header {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 2px 4px 12px;
}
.list-title {
display: flex;
align-items: center;
gap: 10px;
}
.list-title .dot {
width: 4px;
height: 16px;
background: #0054cd;
border-radius: 2px;
}
.list-title .t {
font-size: 15px;
font-weight: 700;
color: #0d1c2c;
}
.list-title .count {
padding: 1px 8px;
border-radius: 6px;
background: #eef4ff;
color: #0054cd;
font-size: 12px;
}
.table-wrapper {
@@ -448,12 +668,77 @@ function formatDateTime(dateStr: string) {
overflow: hidden;
}
.form-section {
margin-bottom: 4px;
}
.section-head {
display: flex;
align-items: center;
gap: 10px;
margin: 4px 0 10px;
padding-bottom: 8px;
border-bottom: 1px solid #eef1f6;
}
.section-no {
width: 26px;
height: 26px;
border-radius: 7px;
background: #0054cd;
color: #fff;
font-size: 12px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
.section-text {
display: flex;
flex-direction: column;
gap: 1px;
}
.section-name {
font-size: 15px;
font-weight: 700;
color: #0d1c2c;
}
.section-sub {
font-size: 11px;
color: #8a97a8;
letter-spacing: 0.5px;
}
:deep(.ant-table) {
font-size: 14px;
height: 100%;
}
:deep(.ant-table-thead > tr > th) {
background: #f4f7ff;
color: #5b6b7e;
font-weight: 600;
}
:deep(.ant-btn-link) {
padding: 0 4px;
}
:deep(.ant-btn-primary) {
background: #0054cd;
border-color: #0054cd;
}
:deep(.ant-btn-primary:not(:disabled):not(.ant-btn-disabled):hover) {
background: #0947ab;
border-color: #0947ab;
}
:deep(.ant-modal-title) {
color: #0d1c2c;
font-weight: 700;
}
</style>
@@ -1,368 +0,0 @@
<template>
<div class="page-management">
<a-card title="客户建筑管理">
<template #extra>
<a-space>
<a-button type="primary" @click="handleAdd">
<template #icon><PlusOutlined /></template>
新增建筑
</a-button>
</a-space>
</template>
<div class="search-bar">
<a-input
v-model:value="searchForm.keyword"
placeholder="建筑名称/用途"
allow-clear
style="width: 220px"
@pressEnter="handleSearch"
>
<template #prefix><SearchOutlined /></template>
</a-input>
<a-select
v-model:value="searchForm.customerId"
placeholder="所属客户"
allow-clear
style="width: 200px"
:options="customerOptions"
show-search
option-filter-prop="label"
/>
<a-button type="primary" @click="handleSearch">
<template #icon><SearchOutlined /></template>
查询
</a-button>
<a-button @click="handleReset">
<template #icon><ReloadOutlined /></template>
重置
</a-button>
</div>
<div class="table-wrapper" ref="wrapperRef">
<a-table
:columns="columns"
:data-source="dataSource"
:loading="loading"
:pagination="pagination"
row-key="id"
:scroll="{ x: 'max-content', y: tableHeight }"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'customerName'">
{{ getCustomerName(record.customerId) }}
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleEdit(record)">
<template #icon><EditOutlined /></template>
编辑
</a-button>
<a-popconfirm title="确定要删除该建筑吗?" ok-text="确定" cancel-text="取消" @confirm="handleDelete(record.id)">
<a-button type="link" size="small" danger>
<template #icon><DeleteOutlined /></template>
删除
</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
<template #emptyText>
<a-empty description="暂无建筑数据" />
</template>
</a-table>
</div>
</a-card>
<a-modal
v-model:open="modalVisible"
:title="modalTitle"
:confirm-loading="modalLoading"
width="620px"
@ok="handleSubmit"
@cancel="handleCancel"
>
<a-form
ref="formRef"
:model="formData"
:rules="formRules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<a-form-item label="所属客户" name="customerId">
<a-select
v-model:value="formData.customerId"
placeholder="请选择客户"
:options="customerOptions"
:disabled="!isAdmin && isEdit"
show-search
option-filter-prop="label"
/>
</a-form-item>
<a-form-item label="建筑名称" name="buildingName">
<a-input v-model:value="formData.buildingName" placeholder="请输入建筑名称" maxlength="128" />
</a-form-item>
<a-form-item label="建筑用途" name="purpose">
<a-input v-model:value="formData.purpose" placeholder="请输入建筑用途" maxlength="128" />
</a-form-item>
<a-form-item label="楼层数" name="floorCount">
<a-input-number v-model:value="formData.floorCount" :min="0" style="width: 100%" placeholder="请输入楼层数" />
</a-form-item>
<a-form-item label="基本信息" name="basicInfo">
<a-textarea v-model:value="formData.basicInfo" :rows="3" placeholder="请输入建筑基本信息" maxlength="2000" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import { SearchOutlined, ReloadOutlined, PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
import { customerBuildingApi, type CustomerBuildingInfo, type CustomerBuildingQuery } from '@/api/customerBuilding';
import { customerApi } from '@/api/customer';
import { useTableScroll } from '@/composables/useTableScroll';
import { usePermissionStore } from '@/stores/permission';
const { wrapperRef, tableHeight } = useTableScroll();
const permissionStore = usePermissionStore();
const isAdmin = computed(() => permissionStore.hasRole('ADMIN'));
const loading = ref(false);
const dataSource = ref<CustomerBuildingInfo[]>([]);
const total = ref(0);
const searchForm = reactive<CustomerBuildingQuery>({
page: 1,
pageSize: 10,
keyword: '',
customerId: undefined as any,
});
const modalVisible = ref(false);
const modalTitle = ref('');
const modalLoading = ref(false);
const formRef = ref();
const isEdit = ref(false);
const editId = ref<number | null>(null);
const customerOptions = ref<{ label: string; value: number }[]>([]);
const formData = reactive({
customerId: undefined as number | undefined,
buildingName: '',
purpose: '',
floorCount: undefined as number | undefined,
basicInfo: '',
});
const formRules = {
customerId: [{ required: true, message: '请选择所属客户', trigger: 'change' }],
buildingName: [{ required: true, message: '请输入建筑名称', trigger: 'blur' }],
};
const columns = [
{ title: '所属客户', key: 'customerName', ellipsis: true },
{ title: '建筑名称', dataIndex: 'buildingName', key: 'buildingName', ellipsis: true },
{ title: '建筑用途', dataIndex: 'purpose', key: 'purpose', ellipsis: true },
{ title: '楼层数', dataIndex: 'floorCount', key: 'floorCount', align: 'center' as const, width: 90 },
{ title: '基本信息', dataIndex: 'basicInfo', key: 'basicInfo', ellipsis: true },
{ title: '操作', key: 'action', width: 140, fixed: 'right' as const },
];
const pagination = computed(() => ({
current: searchForm.page,
pageSize: searchForm.pageSize,
total: total.value,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (t: number) => `共 ${t} 条数据`,
}));
onMounted(() => {
fetchList();
fetchCustomerOptions();
});
async function fetchList() {
loading.value = true;
try {
const res: any = await customerBuildingApi.getList(searchForm);
if (res.code === 200) {
dataSource.value = res.data.list;
total.value = res.data.total;
}
} catch {
message.error('获取建筑列表失败');
} finally {
loading.value = false;
}
}
async function fetchCustomerOptions() {
try {
const res: any = await customerApi.getCustomerOptions();
if (res.code === 200) {
customerOptions.value = res.data.map((item: any) => ({ label: item.customerName, value: item.id }));
}
} catch {
console.error('获取客户选项失败');
}
}
function getCustomerName(customerId: number): string {
const found = customerOptions.value.find((c) => c.value === customerId);
return found ? found.label : `#${customerId}`;
}
function handleSearch() {
searchForm.page = 1;
fetchList();
}
function handleReset() {
searchForm.keyword = '';
searchForm.customerId = undefined as any;
searchForm.page = 1;
fetchList();
}
function handleTableChange(pag: any) {
searchForm.page = pag.current;
searchForm.pageSize = pag.pageSize;
fetchList();
}
function handleAdd() {
isEdit.value = false;
editId.value = null;
modalTitle.value = '新增建筑';
resetForm();
modalVisible.value = true;
}
function handleEdit(record: CustomerBuildingInfo) {
isEdit.value = true;
editId.value = record.id;
modalTitle.value = '编辑建筑';
resetForm();
formData.customerId = record.customerId;
formData.buildingName = record.buildingName;
formData.purpose = record.purpose;
formData.floorCount = record.floorCount;
formData.basicInfo = record.basicInfo;
modalVisible.value = true;
}
function resetForm() {
formData.customerId = undefined;
formData.buildingName = '';
formData.purpose = '';
formData.floorCount = undefined;
formData.basicInfo = '';
if (formRef.value) {
formRef.value.resetFields();
}
}
async function handleSubmit() {
try {
await formRef.value.validate();
} catch {
return;
}
modalLoading.value = true;
try {
const submitData: any = {
customerId: formData.customerId,
buildingName: formData.buildingName,
purpose: formData.purpose,
floorCount: formData.floorCount,
basicInfo: formData.basicInfo,
};
let res: any;
if (isEdit.value && editId.value) {
res = await customerBuildingApi.update(editId.value, submitData);
} else {
res = await customerBuildingApi.create(submitData);
}
if (res.code === 200) {
message.success(isEdit.value ? '更新成功' : '创建成功');
modalVisible.value = false;
fetchList();
} else {
message.error(res.message || '操作失败');
}
} catch {
message.error('操作失败');
} finally {
modalLoading.value = false;
}
}
function handleCancel() {
modalVisible.value = false;
resetForm();
}
async function handleDelete(id: number) {
try {
const res: any = await customerBuildingApi.delete(id);
if (res.code === 200) {
message.success('删除成功');
fetchList();
} else {
message.error(res.message || '删除失败');
}
} catch {
message.error('删除失败');
}
}
</script>
<style scoped>
.page-management {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 20px;
box-sizing: border-box;
}
:deep(.ant-card) {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
:deep(.ant-card-body) {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
.search-bar {
flex-shrink: 0;
margin-bottom: 16px;
display: flex;
justify-content: flex-end;
gap: 8px;
}
.table-wrapper {
flex: 1;
min-height: 0;
overflow: hidden;
}
:deep(.ant-table) {
font-size: 14px;
height: 100%;
}
:deep(.ant-btn-link) {
padding: 0 4px;
}
</style>
+376 -108
View File
@@ -1,89 +1,130 @@
<template>
<div class="customer-department">
<a-card title="部门管理">
<template #extra>
<a-space>
<!-- 顶部标题 + 操作 -->
<div class="page-header">
<div class="breadcrumb">
<span>运营管控体系</span>
<span class="sep">/</span>
<span>客户管理</span>
<span class="sep">/</span>
<span class="cur">客户部门</span>
</div>
<div class="header-main">
<div class="header-left">
<div class="header-row">
<span class="title-bar"></span>
<span class="main-title">客户部门</span>
<span class="sub-title">CUSTOMER DEPARTMENT</span>
</div>
<p class="desc">按所属客户维护其内部组织结构与职能部门,支持快速检索与部门档案管理。</p>
</div>
<div class="header-actions">
<a-button type="primary" @click="handleAdd">
<template #icon><PlusOutlined /></template>
新增部门
</a-button>
</a-space>
</template>
</div>
</div>
</div>
<div class="search-bar">
<a-input
v-model:value="searchForm.keyword"
placeholder="部门名称/联系人/电话"
allow-clear
style="width: 220px"
@pressEnter="handleSearch"
>
<template #prefix><SearchOutlined /></template>
</a-input>
<a-select
v-model:value="searchForm.customerId"
placeholder="所属客户"
allow-clear
style="width: 220px"
:options="customerOptions"
/>
<a-button type="primary" @click="handleSearch">
<template #icon><SearchOutlined /></template>
查询
</a-button>
<a-button @click="handleReset">
<template #icon><ReloadOutlined /></template>
重置
</a-button>
<!-- 主体卡片 -->
<div class="content-card">
<!-- 筛选区 -->
<div class="filter-card">
<div class="filter-grid">
<div class="filter-item">
<label class="filter-label">关键词检索</label>
<a-input
v-model:value="searchForm.keyword"
placeholder="部门名称 / 联系人 / 电话"
allow-clear
@pressEnter="handleSearch"
>
<template #prefix><SearchOutlined /></template>
</a-input>
</div>
<div class="filter-item">
<label class="filter-label">所属客户</label>
<a-select
v-model:value="searchForm.customerId"
placeholder="请选择客户"
allow-clear
:options="customerOptions"
/>
</div>
</div>
<div class="filter-bottom">
<div class="filter-actions">
<a-button type="primary" @click="handleSearch">
<template #icon><SearchOutlined /></template>
查询
</a-button>
<a-button @click="handleReset">
<template #icon><ReloadOutlined /></template>
重置
</a-button>
</div>
</div>
</div>
<div class="table-wrapper" ref="wrapperRef">
<a-table
:columns="columns"
:data-source="dataSource"
:loading="loading"
:pagination="pagination"
row-key="id"
:scroll="{ x: 'max-content', y: tableHeight }"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'customerId'">
{{ customerNameMap[record.customerId] || '-' }}
</template>
<!-- 列表区 -->
<div class="list-section">
<div class="list-header">
<div class="list-title">
<span class="dot"></span>
<span class="t">部门列表</span>
<span class="count">{{ total }}</span>
</div>
</div>
<div class="table-wrapper" ref="wrapperRef">
<a-table
:columns="columns"
:data-source="dataSource"
:loading="loading"
:pagination="pagination"
row-key="id"
:scroll="{ x: 'max-content', y: tableHeight }"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'customerId'">
{{ customerNameMap[record.customerId] || '-' }}
</template>
<template v-if="column.key === 'createdAt'">
{{ formatDateTime(record.createdAt) }}
</template>
<template v-if="column.key === 'createdAt'">
{{ formatDateTime(record.createdAt) }}
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleEdit(record)">
<template #icon><EditOutlined /></template>
编辑
</a-button>
<a-popconfirm title="确定要删除该部门吗?" ok-text="确定" cancel-text="取消" @confirm="handleDelete(record.id)">
<a-button type="link" size="small" danger>
<template #icon><DeleteOutlined /></template>
删除
</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleEdit(record)">
<template #icon><EditOutlined /></template>
编辑
</a-button>
<a-popconfirm title="确定要删除该部门吗?" ok-text="确定" cancel-text="取消" @confirm="handleDelete(record.id)">
<a-button type="link" size="small" danger>
<template #icon><DeleteOutlined /></template>
删除
</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
<template #emptyText>
<a-empty description="暂无部门数据" />
</template>
</a-table>
<template #emptyText>
<a-empty description="暂无部门数据" />
</template>
</a-table>
</div>
</div>
</a-card>
</div>
<!-- 新增/编辑弹窗 -->
<a-modal
v-model:open="modalVisible"
:title="modalTitle"
:confirm-loading="modalLoading"
width="600px"
width="720px"
@ok="handleSubmit"
@cancel="handleCancel"
>
@@ -94,29 +135,44 @@
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<a-form-item label="所属客户" name="customerId">
<a-select
v-model:value="formData.customerId"
placeholder="请选择所属客户"
:options="customerOptions"
/>
</a-form-item>
<div class="form-section">
<div class="section-head">
<span class="section-no">01</span>
<div class="section-text">
<span class="section-name">基本信息</span>
<span class="section-sub">Basic Information</span>
</div>
</div>
<a-form-item label="所属客户" name="customerId">
<a-select
v-model:value="formData.customerId"
placeholder="请选择所属客户"
:options="customerOptions"
/>
</a-form-item>
<a-form-item label="部门名称" name="deptName">
<a-input v-model:value="formData.deptName" placeholder="请输入部门名称" maxlength="64" />
</a-form-item>
</div>
<a-form-item label="部门名称" name="deptName">
<a-input v-model:value="formData.deptName" placeholder="请输入部门名称" maxlength="64" />
</a-form-item>
<a-form-item label="联系人" name="contactPerson">
<a-input v-model:value="formData.contactPerson" placeholder="请输入联系人" maxlength="32" />
</a-form-item>
<a-form-item label="联系电话" name="contactPhone">
<a-input v-model:value="formData.contactPhone" placeholder="请输入联系电话" />
</a-form-item>
<a-form-item label="职能描述" name="functionDesc">
<a-textarea v-model:value="formData.functionDesc" placeholder="请输入职能描述" :rows="3" maxlength="256" />
</a-form-item>
<div class="form-section">
<div class="section-head">
<span class="section-no">02</span>
<div class="section-text">
<span class="section-name">联系与职能</span>
<span class="section-sub">Contact &amp; Function</span>
</div>
</div>
<a-form-item label="联系人" name="contactPerson">
<a-input v-model:value="formData.contactPerson" placeholder="请输入联系人" maxlength="32" />
</a-form-item>
<a-form-item label="联系电话" name="contactPhone">
<a-input v-model:value="formData.contactPhone" placeholder="请输入联系电话" />
</a-form-item>
<a-form-item label="职能描述" name="functionDesc">
<a-textarea v-model:value="formData.functionDesc" placeholder="请输入职能描述" :rows="3" maxlength="256" />
</a-form-item>
</div>
</a-form>
</a-modal>
</div>
@@ -358,30 +414,177 @@ function formatDateTime(dateStr: string) {
overflow: hidden;
padding: 20px;
box-sizing: border-box;
background: #eef3f8;
}
:deep(.ant-card) {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
:deep(.ant-card-body) {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
.search-bar {
.page-header {
flex-shrink: 0;
margin-bottom: 16px;
}
.breadcrumb {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 8px;
font-size: 12px;
color: #5b6b7e;
margin-bottom: 10px;
}
.breadcrumb .sep {
color: #c6d2e0;
}
.breadcrumb .cur {
color: #0d1c2c;
font-weight: 600;
}
.header-main {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 20px;
}
.header-left {
display: flex;
flex-direction: column;
gap: 8px;
}
.header-row {
display: flex;
align-items: center;
gap: 12px;
}
.title-bar {
width: 5px;
height: 22px;
background: #0054cd;
border-radius: 3px;
align-self: center;
}
.main-title {
font-size: 22px;
font-weight: 700;
color: #0d1c2c;
}
.sub-title {
font-size: 12px;
font-weight: 700;
color: #0054cd;
letter-spacing: 2px;
}
.desc {
font-size: 13px;
color: #5b6b7e;
line-height: 1.6;
margin: 0;
}
.header-actions {
flex-shrink: 0;
}
.content-card {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
gap: 16px;
background: #fff;
border: 1px solid #dbe4ee;
border-radius: 12px;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.07);
padding: 20px;
}
.filter-card {
flex-shrink: 0;
background: #f8faff;
border: 1px solid #dbe4ee;
border-radius: 10px;
padding: 16px;
}
.filter-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px 16px;
}
.filter-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.filter-label {
font-size: 12px;
font-weight: 600;
color: #5b6b7e;
}
.filter-bottom {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid #dbe4ee;
}
.filter-actions {
display: flex;
gap: 8px;
}
.list-section {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.list-header {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 2px 4px 12px;
}
.list-title {
display: flex;
align-items: center;
gap: 10px;
}
.list-title .dot {
width: 4px;
height: 16px;
background: #0054cd;
border-radius: 2px;
}
.list-title .t {
font-size: 15px;
font-weight: 700;
color: #0d1c2c;
}
.list-title .count {
padding: 1px 8px;
border-radius: 6px;
background: #eef4ff;
color: #0054cd;
font-size: 12px;
}
.table-wrapper {
@@ -390,12 +593,77 @@ function formatDateTime(dateStr: string) {
overflow: hidden;
}
.form-section {
margin-bottom: 4px;
}
.section-head {
display: flex;
align-items: center;
gap: 10px;
margin: 4px 0 10px;
padding-bottom: 8px;
border-bottom: 1px solid #eef1f6;
}
.section-no {
width: 26px;
height: 26px;
border-radius: 7px;
background: #0054cd;
color: #fff;
font-size: 12px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
.section-text {
display: flex;
flex-direction: column;
gap: 1px;
}
.section-name {
font-size: 15px;
font-weight: 700;
color: #0d1c2c;
}
.section-sub {
font-size: 11px;
color: #8a97a8;
letter-spacing: 0.5px;
}
:deep(.ant-table) {
font-size: 14px;
height: 100%;
}
:deep(.ant-table-thead > tr > th) {
background: #f4f7ff;
color: #5b6b7e;
font-weight: 600;
}
:deep(.ant-btn-link) {
padding: 0 4px;
}
:deep(.ant-btn-primary) {
background: #0054cd;
border-color: #0054cd;
}
:deep(.ant-btn-primary:not(:disabled):not(.ant-btn-disabled):hover) {
background: #0947ab;
border-color: #0947ab;
}
:deep(.ant-modal-title) {
color: #0d1c2c;
font-weight: 700;
}
</style>
File diff suppressed because it is too large. Load diff
-432
View File
@@ -1,432 +0,0 @@
<template>
<div class="page-management">
<a-card title="客户房间管理">
<template #extra>
<a-space>
<a-button type="primary" @click="handleAdd">
<template #icon><PlusOutlined /></template>
新增房间
</a-button>
</a-space>
</template>
<div class="search-bar">
<a-input
v-model:value="searchForm.keyword"
placeholder="房间名称/编码"
allow-clear
style="width: 220px"
@pressEnter="handleSearch"
>
<template #prefix><SearchOutlined /></template>
</a-input>
<a-select
v-model:value="searchForm.customerId"
placeholder="所属客户"
allow-clear
style="width: 200px"
:options="customerOptions"
show-search
option-filter-prop="label"
@change="handleCustomerSearchChange"
/>
<a-button type="primary" @click="handleSearch">
<template #icon><SearchOutlined /></template>
查询
</a-button>
<a-button @click="handleReset">
<template #icon><ReloadOutlined /></template>
重置
</a-button>
</div>
<div class="table-wrapper" ref="wrapperRef">
<a-table
:columns="columns"
:data-source="dataSource"
:loading="loading"
:pagination="pagination"
row-key="id"
:scroll="{ x: 'max-content', y: tableHeight }"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'customerName'">
{{ getCustomerName(record.customerId) }}
</template>
<template v-if="column.key === 'buildingName'">
{{ getBuildingName(record.buildingId) }}
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleEdit(record)">
<template #icon><EditOutlined /></template>
编辑
</a-button>
<a-popconfirm title="确定要删除该房间吗?" ok-text="确定" cancel-text="取消" @confirm="handleDelete(record.id)">
<a-button type="link" size="small" danger>
<template #icon><DeleteOutlined /></template>
删除
</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
<template #emptyText>
<a-empty description="暂无房间数据" />
</template>
</a-table>
</div>
</a-card>
<a-modal
v-model:open="modalVisible"
:title="modalTitle"
:confirm-loading="modalLoading"
width="620px"
@ok="handleSubmit"
@cancel="handleCancel"
>
<a-form
ref="formRef"
:model="formData"
:rules="formRules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<a-form-item label="所属客户" name="customerId">
<a-select
v-model:value="formData.customerId"
placeholder="请选择客户"
:options="customerOptions"
:disabled="!isAdmin && isEdit"
show-search
option-filter-prop="label"
@change="handleCustomerFormChange"
/>
</a-form-item>
<a-form-item label="所属建筑" name="buildingId">
<a-select
v-model:value="formData.buildingId"
placeholder="请选择建筑"
:options="buildingOptions"
:not-found-content="null"
/>
</a-form-item>
<a-form-item label="房间编码" name="roomCode">
<a-input v-model:value="formData.roomCode" placeholder="请输入房间编码" maxlength="64" />
</a-form-item>
<a-form-item label="房间名称" name="roomName">
<a-input v-model:value="formData.roomName" placeholder="请输入房间名称" maxlength="128" />
</a-form-item>
<a-form-item label="所在楼层" name="floorNo">
<a-input v-model:value="formData.floorNo" placeholder="请输入所在楼层" maxlength="16" />
</a-form-item>
<a-form-item label="房间用途" name="roomPurpose">
<a-input v-model:value="formData.roomPurpose" placeholder="请输入房间用途" maxlength="128" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import { SearchOutlined, ReloadOutlined, PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
import { customerRoomApi, type CustomerRoomInfo, type CustomerRoomQuery } from '@/api/customerRoom';
import { customerBuildingApi } from '@/api/customerBuilding';
import { customerApi } from '@/api/customer';
import { useTableScroll } from '@/composables/useTableScroll';
import { usePermissionStore } from '@/stores/permission';
const { wrapperRef, tableHeight } = useTableScroll();
const permissionStore = usePermissionStore();
const isAdmin = computed(() => permissionStore.hasRole('ADMIN'));
const loading = ref(false);
const dataSource = ref<CustomerRoomInfo[]>([]);
const total = ref(0);
const searchForm = reactive<CustomerRoomQuery>({
page: 1,
pageSize: 10,
keyword: '',
customerId: undefined as any,
});
const modalVisible = ref(false);
const modalTitle = ref('');
const modalLoading = ref(false);
const formRef = ref();
const isEdit = ref(false);
const editId = ref<number | null>(null);
const customerOptions = ref<{ label: string; value: number }[]>([]);
const buildingOptions = ref<{ label: string; value: number }[]>([]);
const allBuildings = ref<{ label: string; value: number; customerId: number }[]>([]);
const formData = reactive({
customerId: undefined as number | undefined,
buildingId: undefined as number | undefined,
roomCode: '',
roomName: '',
floorNo: '',
roomPurpose: '',
});
const formRules = {
customerId: [{ required: true, message: '请选择所属客户', trigger: 'change' }],
buildingId: [{ required: true, message: '请选择所属建筑', trigger: 'change' }],
roomCode: [{ required: true, message: '请输入房间编码', trigger: 'blur' }],
roomName: [{ required: true, message: '请输入房间名称', trigger: 'blur' }],
};
const columns = [
{ title: '房间编码', dataIndex: 'roomCode', key: 'roomCode', ellipsis: true, width: 130 },
{ title: '房间名称', dataIndex: 'roomName', key: 'roomName', ellipsis: true },
{ title: '所属客户', key: 'customerName', ellipsis: true },
{ title: '所属建筑', key: 'buildingName', ellipsis: true },
{ title: '所在楼层', dataIndex: 'floorNo', key: 'floorNo', align: 'center' as const, width: 90 },
{ title: '房间用途', dataIndex: 'roomPurpose', key: 'roomPurpose', ellipsis: true },
{ title: '操作', key: 'action', width: 140, fixed: 'right' as const },
];
const pagination = computed(() => ({
current: searchForm.page,
pageSize: searchForm.pageSize,
total: total.value,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (t: number) => `共 ${t} 条数据`,
}));
onMounted(() => {
fetchList();
fetchCustomerOptions();
fetchAllBuildings();
});
async function fetchList() {
loading.value = true;
try {
const res: any = await customerRoomApi.getList(searchForm);
if (res.code === 200) {
dataSource.value = res.data.list;
total.value = res.data.total;
}
} catch {
message.error('获取房间列表失败');
} finally {
loading.value = false;
}
}
async function fetchCustomerOptions() {
try {
const res: any = await customerApi.getCustomerOptions();
if (res.code === 200) {
customerOptions.value = res.data.map((item: any) => ({ label: item.customerName, value: item.id }));
}
} catch {
console.error('获取客户选项失败');
}
}
async function fetchAllBuildings() {
try {
const res: any = await customerBuildingApi.getList({ page: 1, pageSize: 999 });
if (res.code === 200) {
allBuildings.value = res.data.list.map((item: any) => ({
label: item.buildingName,
value: item.id,
customerId: item.customerId,
}));
}
} catch {
console.error('获取建筑列表失败');
}
}
function getCustomerName(customerId: number): string {
const found = customerOptions.value.find((c) => c.value === customerId);
return found ? found.label : `#${customerId}`;
}
function getBuildingName(buildingId: number): string {
const found = allBuildings.value.find((b) => b.value === buildingId);
return found ? found.label : `#${buildingId}`;
}
function filterBuildings(customerId?: number) {
if (customerId) {
buildingOptions.value = allBuildings.value.filter((b) => b.customerId === customerId);
} else {
buildingOptions.value = allBuildings.value;
}
}
function handleCustomerFormChange(customerId?: number) {
formData.buildingId = undefined;
filterBuildings(customerId);
}
function handleCustomerSearchChange(customerId?: number) {
searchForm.customerId = customerId;
handleSearch();
}
function handleSearch() {
searchForm.page = 1;
fetchList();
}
function handleReset() {
searchForm.keyword = '';
searchForm.customerId = undefined as any;
searchForm.page = 1;
fetchList();
}
function handleTableChange(pag: any) {
searchForm.page = pag.current;
searchForm.pageSize = pag.pageSize;
fetchList();
}
function handleAdd() {
isEdit.value = false;
editId.value = null;
modalTitle.value = '新增房间';
resetForm();
filterBuildings(undefined);
modalVisible.value = true;
}
function handleEdit(record: CustomerRoomInfo) {
isEdit.value = true;
editId.value = record.id;
modalTitle.value = '编辑房间';
resetForm();
formData.customerId = record.customerId;
filterBuildings(record.customerId);
formData.buildingId = record.buildingId;
formData.roomCode = record.roomCode;
formData.roomName = record.roomName;
formData.floorNo = record.floorNo;
formData.roomPurpose = record.roomPurpose;
modalVisible.value = true;
}
function resetForm() {
formData.customerId = undefined;
formData.buildingId = undefined;
formData.roomCode = '';
formData.roomName = '';
formData.floorNo = '';
formData.roomPurpose = '';
if (formRef.value) {
formRef.value.resetFields();
}
}
async function handleSubmit() {
try {
await formRef.value.validate();
} catch {
return;
}
modalLoading.value = true;
try {
const submitData: any = {
customerId: formData.customerId,
buildingId: formData.buildingId,
roomCode: formData.roomCode,
roomName: formData.roomName,
floorNo: formData.floorNo,
roomPurpose: formData.roomPurpose,
};
let res: any;
if (isEdit.value && editId.value) {
res = await customerRoomApi.update(editId.value, submitData);
} else {
res = await customerRoomApi.create(submitData);
}
if (res.code === 200) {
message.success(isEdit.value ? '更新成功' : '创建成功');
modalVisible.value = false;
fetchList();
} else {
message.error(res.message || '操作失败');
}
} catch {
message.error('操作失败');
} finally {
modalLoading.value = false;
}
}
function handleCancel() {
modalVisible.value = false;
resetForm();
}
async function handleDelete(id: number) {
try {
const res: any = await customerRoomApi.delete(id);
if (res.code === 200) {
message.success('删除成功');
fetchList();
} else {
message.error(res.message || '删除失败');
}
} catch {
message.error('删除失败');
}
}
</script>
<style scoped>
.page-management {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 20px;
box-sizing: border-box;
}
:deep(.ant-card) {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
:deep(.ant-card-body) {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
.search-bar {
flex-shrink: 0;
margin-bottom: 16px;
display: flex;
justify-content: flex-end;
gap: 8px;
}
.table-wrapper {
flex: 1;
min-height: 0;
overflow: hidden;
}
:deep(.ant-table) {
font-size: 14px;
height: 100%;
}
:deep(.ant-btn-link) {
padding: 0 4px;
}
</style>
@@ -0,0 +1,781 @@
<template>
<div class="topology-page">
<!-- 顶部:标题 + 客户选择 + 新增建筑 -->
<div class="page-header">
<div class="breadcrumb">
<span>运营管控体系</span>
<span class="sep">/</span>
<span>客户管理</span>
<span class="sep">/</span>
<span class="cur">空间拓扑与用电架构</span>
</div>
<div class="header-main">
<div class="header-left">
<div class="header-row">
<span class="title-bar"></span>
<span class="main-title">空间拓扑与用电架构</span>
<span class="sub-title">SPACE TOPOLOGY</span>
</div>
<p class="desc">以「客户 → 建筑 → 房间」三层结构查看某位客户的用电空间拓扑,支持新增、编辑与删除节点。</p>
</div>
<div class="header-actions">
<a-select
v-model:value="selectedCustomerId"
placeholder="请选择客户"
:options="customerOptions"
show-search
option-filter-prop="label"
style="width: 280px"
:loading="customerLoading"
@change="handleCustomerChange"
/>
<a-button type="primary" :disabled="!selectedCustomerId" @click="handleAddBuilding">
<template #icon><PlusOutlined /></template>
新增建筑
</a-button>
</div>
</div>
</div>
<!-- 主体:拓扑树 -->
<div class="content-card">
<div class="list-header">
<div class="list-title">
<span class="dot"></span>
<span class="t">拓扑结构</span>
<span class="count">{{ buildingCount }}</span>
</div>
</div>
<div v-if="!selectedCustomerId" class="tree-empty">
<a-empty description="请先在上方选择客户,加载其建筑与房间拓扑" />
</div>
<div v-else-if="!loading && buildingCount === 0" class="tree-empty">
<a-empty description="该客户暂无建筑,点击右上角「新增建筑」开始建立拓扑">
<a-button type="primary" @click="handleAddBuilding">
<template #icon><PlusOutlined /></template>
新增建筑
</a-button>
</a-empty>
</div>
<a-tree
v-else
v-model:expandedKeys="expandedKeys"
:tree-data="treeData"
:block-node="true"
:show-line="false"
:show-icon="false"
selectable
>
<template #title="{ data }">
<div class="node-row">
<span class="node-icon">
<component :is="iconFor(data.type)" />
</span>
<div class="node-main">
<span class="node-title">{{ data.title }}</span>
<span v-if="data.type !== 'customer' && descFor(data)" class="node-desc">{{ descFor(data) }}</span>
</div>
<div class="node-actions" @click.stop>
<template v-if="data.type === 'building'">
<a-button type="link" size="small" @click="handleAddRoom(data)">
<template #icon><PlusOutlined /></template>
新增房间
</a-button>
<a-button type="link" size="small" @click="handleEditBuilding(data)">
<template #icon><EditOutlined /></template>
编辑
</a-button>
<a-popconfirm title="删除该建筑及其下所有房间,确定吗?" ok-text="确定" cancel-text="取消" @confirm="handleDeleteBuilding(data.data.id)">
<a-button type="link" size="small" danger>
<template #icon><DeleteOutlined /></template>
删除
</a-button>
</a-popconfirm>
</template>
<template v-else-if="data.type === 'room'">
<a-button type="link" size="small" @click="handleEditRoom(data)">
<template #icon><EditOutlined /></template>
编辑
</a-button>
<a-popconfirm title="删除该房间,确定吗?" ok-text="确定" cancel-text="取消" @confirm="handleDeleteRoom(data.data.id)">
<a-button type="link" size="small" danger>
<template #icon><DeleteOutlined /></template>
删除
</a-button>
</a-popconfirm>
</template>
</div>
</div>
</template>
</a-tree>
</div>
<!-- 新增/编辑弹窗 -->
<a-modal
v-model:open="modalVisible"
:title="modalTitle"
:confirm-loading="modalLoading"
width="640px"
@ok="handleSubmit"
@cancel="handleClose"
>
<a-form
ref="formRef"
:model="formData"
:rules="formRules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<div class="form-section">
<div class="section-head">
<span class="section-no">01</span>
<div class="section-text">
<span class="section-name">{{ modalType === 'building' ? '基本信息' : '房间信息' }}</span>
<span class="section-sub">{{ modalType === 'building' ? 'Basic Information' : 'Room Information' }}</span>
</div>
</div>
<template v-if="modalType === 'building'">
<a-form-item label="所属客户" name="customerId">
<a-select v-model:value="formData.customerId" :options="currentCustomerOption" disabled />
</a-form-item>
<a-form-item label="建筑名称" name="buildingName">
<a-input v-model:value="formData.buildingName" placeholder="请输入建筑名称" maxlength="128" />
</a-form-item>
<a-form-item label="建筑用途" name="purpose">
<a-input v-model:value="formData.purpose" placeholder="请输入建筑用途" maxlength="128" />
</a-form-item>
<a-form-item label="楼层数" name="floorCount">
<a-input-number v-model:value="formData.floorCount" :min="0" style="width: 100%" placeholder="请输入楼层数" />
</a-form-item>
<a-form-item label="基本信息" name="basicInfo">
<a-textarea v-model:value="formData.basicInfo" :rows="3" placeholder="请输入建筑基本信息" maxlength="2000" />
</a-form-item>
</template>
<template v-else>
<a-form-item label="所属客户" name="customerId">
<a-select v-model:value="formData.customerId" :options="currentCustomerOption" disabled />
</a-form-item>
<a-form-item label="所属建筑" name="buildingId">
<a-select v-model:value="formData.buildingId" :options="buildingOptions" :not-found-content="null" />
</a-form-item>
<a-form-item label="房间编码" name="roomCode">
<a-input v-model:value="formData.roomCode" placeholder="请输入房间编码" maxlength="64" />
</a-form-item>
<a-form-item label="房间名称" name="roomName">
<a-input v-model:value="formData.roomName" placeholder="请输入房间名称" maxlength="128" />
</a-form-item>
<a-form-item label="所在楼层" name="floorNo">
<a-input v-model:value="formData.floorNo" placeholder="请输入所在楼层" maxlength="16" />
</a-form-item>
<a-form-item label="房间用途" name="roomPurpose">
<a-input v-model:value="formData.roomPurpose" placeholder="请输入房间用途" maxlength="128" />
</a-form-item>
</template>
</div>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
ClusterOutlined,
HomeOutlined,
AppstoreOutlined,
} from '@ant-design/icons-vue';
import { customerApi } from '@/api/customer';
import { customerBuildingApi, type CustomerBuildingInfo } from '@/api/customerBuilding';
import { customerRoomApi, type CustomerRoomInfo } from '@/api/customerRoom';
type NodeType = 'customer' | 'building' | 'room';
interface TreeNode {
key: string;
title: string;
type: NodeType;
data: any;
children?: TreeNode[];
}
const customerOptions = ref<{ label: string; value: number }[]>([]);
const customerLoading = ref(false);
const selectedCustomerId = ref<number | undefined>(undefined);
const selectedCustomerName = ref('');
const loading = ref(false);
const treeData = ref<TreeNode[]>([]);
const expandedKeys = ref<string[]>([]);
const modalVisible = ref(false);
const modalLoading = ref(false);
const modalTitle = ref('');
const modalType = ref<'building' | 'room'>('building');
const isEdit = ref(false);
const editId = ref<number | null>(null);
const formRef = ref();
const formData = reactive({
customerId: undefined as number | undefined,
buildingId: undefined as number | undefined,
buildingName: '',
purpose: '',
floorCount: undefined as number | undefined,
basicInfo: '',
roomCode: '',
roomName: '',
floorNo: '',
roomPurpose: '',
});
const buildingCount = computed(() => {
const root = treeData.value[0];
return root ? (root.children?.length || 0) : 0;
});
const currentCustomerOption = computed(() =>
selectedCustomerId.value !== undefined ? [{ label: selectedCustomerName.value, value: selectedCustomerId.value }] : []
);
const buildingOptions = computed<{ label: string; value: number }[]>(() => {
const root = treeData.value[0];
if (!root || !root.children) return [];
return root.children.map((node) => ({ label: node.title, value: node.data.id as number }));
});
const formRules = computed(() => {
if (modalType.value === 'building') {
return {
customerId: [{ required: true, message: '请选择所属客户', trigger: 'change' }],
buildingName: [{ required: true, message: '请输入建筑名称', trigger: 'blur' }],
};
}
return {
customerId: [{ required: true, message: '请选择所属客户', trigger: 'change' }],
buildingId: [{ required: true, message: '请选择所属建筑', trigger: 'change' }],
roomCode: [{ required: true, message: '请输入房间编码', trigger: 'blur' }],
roomName: [{ required: true, message: '请输入房间名称', trigger: 'blur' }],
};
});
onMounted(() => {
fetchCustomerOptions();
});
function iconFor(type: NodeType) {
if (type === 'customer') return ClusterOutlined;
if (type === 'building') return HomeOutlined;
return AppstoreOutlined;
}
function descFor(data: TreeNode): string {
const parts: string[] = [];
if (data.type === 'building') {
const b: CustomerBuildingInfo = data.data;
if (b.purpose) parts.push(b.purpose);
if (b.floorCount != null) parts.push(`共${b.floorCount}层`);
if (b.basicInfo) parts.push(b.basicInfo);
} else if (data.type === 'room') {
const r: CustomerRoomInfo = data.data;
if (r.roomCode) parts.push(r.roomCode);
if (r.floorNo) parts.push(`${r.floorNo}层`);
if (r.roomPurpose) parts.push(r.roomPurpose);
}
return parts.join(' · ');
}
async function fetchCustomerOptions() {
customerLoading.value = true;
try {
const res: any = await customerApi.getCustomerOptions();
if (res.code === 200) {
customerOptions.value = res.data.map((item: any) => ({
label: `${item.customerNo} ${item.customerName}`,
value: item.id,
}));
}
} catch {
message.error('获取客户列表失败');
} finally {
customerLoading.value = false;
}
}
function handleCustomerChange(id: number) {
selectedCustomerId.value = id;
const found = customerOptions.value.find((c) => c.value === id);
selectedCustomerName.value = found ? found.label : `#${id}`;
loadTree();
}
async function loadTree() {
if (selectedCustomerId.value === undefined) {
treeData.value = [];
return;
}
loading.value = true;
try {
const bRes: any = await customerBuildingApi.getList({ customerId: selectedCustomerId.value, page: 1, pageSize: 999 });
const buildings: CustomerBuildingInfo[] = bRes.code === 200 ? bRes.data.list : [];
const buildingNodes: TreeNode[] = [];
for (const b of buildings) {
const rRes: any = await customerRoomApi.getList({ buildingId: b.id, page: 1, pageSize: 999 });
const rooms: CustomerRoomInfo[] = rRes.code === 200 ? rRes.data.list : [];
buildingNodes.push({
key: `building-${b.id}`,
title: b.buildingName,
type: 'building',
data: b,
children: rooms.map((r) => ({
key: `room-${r.id}`,
title: r.roomName,
type: 'room',
data: r,
})),
});
}
treeData.value = [
{
key: `customer-${selectedCustomerId.value}`,
title: selectedCustomerName.value,
type: 'customer',
data: null,
children: buildingNodes.length ? buildingNodes : undefined,
},
];
expandedKeys.value = [`customer-${selectedCustomerId.value}`];
} catch {
message.error('加载拓扑失败');
} finally {
loading.value = false;
}
}
function handleAddBuilding() {
modalType.value = 'building';
isEdit.value = false;
editId.value = null;
modalTitle.value = '新增建筑';
resetForm();
formData.customerId = selectedCustomerId.value;
modalVisible.value = true;
}
function handleEditBuilding(node: TreeNode) {
const b: CustomerBuildingInfo = node.data;
modalType.value = 'building';
isEdit.value = true;
editId.value = b.id;
modalTitle.value = '编辑建筑';
resetForm();
formData.customerId = b.customerId;
formData.buildingName = b.buildingName;
formData.purpose = b.purpose || '';
formData.floorCount = b.floorCount;
formData.basicInfo = b.basicInfo || '';
modalVisible.value = true;
}
function handleAddRoom(node: TreeNode) {
modalType.value = 'room';
isEdit.value = false;
editId.value = null;
modalTitle.value = '新增房间';
resetForm();
formData.customerId = selectedCustomerId.value;
formData.buildingId = node.data.id;
modalVisible.value = true;
}
function handleEditRoom(node: TreeNode) {
const r: CustomerRoomInfo = node.data;
modalType.value = 'room';
isEdit.value = true;
editId.value = r.id;
modalTitle.value = '编辑房间';
resetForm();
formData.customerId = r.customerId;
formData.buildingId = r.buildingId;
formData.roomCode = r.roomCode;
formData.roomName = r.roomName;
formData.floorNo = r.floorNo || '';
formData.roomPurpose = r.roomPurpose || '';
modalVisible.value = true;
}
function resetForm() {
formData.customerId = undefined;
formData.buildingId = undefined;
formData.buildingName = '';
formData.purpose = '';
formData.floorCount = undefined;
formData.basicInfo = '';
formData.roomCode = '';
formData.roomName = '';
formData.floorNo = '';
formData.roomPurpose = '';
if (formRef.value) {
formRef.value.clearValidate();
}
}
async function handleSubmit() {
try {
await formRef.value.validate();
} catch {
return;
}
modalLoading.value = true;
try {
let res: any;
if (modalType.value === 'building') {
const payload = {
customerId: formData.customerId,
buildingName: formData.buildingName,
purpose: formData.purpose,
floorCount: formData.floorCount,
basicInfo: formData.basicInfo,
};
if (isEdit.value && editId.value != null) {
res = await customerBuildingApi.update(editId.value, payload);
} else {
res = await customerBuildingApi.create(payload);
}
} else {
const payload = {
customerId: formData.customerId,
buildingId: formData.buildingId,
roomCode: formData.roomCode,
roomName: formData.roomName,
floorNo: formData.floorNo,
roomPurpose: formData.roomPurpose,
};
if (isEdit.value && editId.value != null) {
res = await customerRoomApi.update(editId.value, payload);
} else {
res = await customerRoomApi.create(payload);
}
}
if (res.code === 200) {
message.success(isEdit.value ? '更新成功' : '创建成功');
modalVisible.value = false;
loadTree();
} else {
message.error(res.message || '操作失败');
}
} catch {
message.error('操作失败');
} finally {
modalLoading.value = false;
}
}
function handleClose() {
modalVisible.value = false;
resetForm();
}
async function handleDeleteBuilding(id: number) {
try {
const res: any = await customerBuildingApi.delete(id);
if (res.code === 200) {
message.success('删除成功');
loadTree();
} else {
message.error(res.message || '删除失败');
}
} catch {
message.error('删除失败');
}
}
async function handleDeleteRoom(id: number) {
try {
const res: any = await customerRoomApi.delete(id);
if (res.code === 200) {
message.success('删除成功');
loadTree();
} else {
message.error(res.message || '删除失败');
}
} catch {
message.error('删除失败');
}
}
</script>
<style scoped>
.topology-page {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 20px;
box-sizing: border-box;
background: #eef3f8;
}
.page-header {
flex-shrink: 0;
margin-bottom: 16px;
}
.breadcrumb {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: #5b6b7e;
margin-bottom: 10px;
}
.breadcrumb .sep {
color: #c6d2e0;
}
.breadcrumb .cur {
color: #0d1c2c;
font-weight: 600;
}
.header-main {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 20px;
}
.header-left {
min-width: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.header-row {
display: flex;
align-items: center;
gap: 12px;
}
.title-bar {
width: 5px;
height: 22px;
background: #0054cd;
border-radius: 3px;
align-self: center;
}
.main-title {
font-size: 22px;
font-weight: 700;
color: #0d1c2c;
}
.sub-title {
font-size: 12px;
font-weight: 700;
color: #0054cd;
letter-spacing: 2px;
}
.desc {
font-size: 13px;
color: #5b6b7e;
line-height: 1.6;
margin: 0;
}
.header-actions {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.content-card {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: auto;
background: #ffffff;
border: 1px solid #dbe4ee;
border-radius: 12px;
box-shadow: 0 4px 14px rgba(13, 28, 44, 0.06);
padding: 20px;
box-sizing: border-box;
}
.list-header {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 2px 4px 12px;
}
.list-title {
display: flex;
align-items: center;
gap: 10px;
}
.list-title .dot {
width: 4px;
height: 16px;
background: #0054cd;
border-radius: 2px;
}
.list-title .t {
font-size: 15px;
font-weight: 700;
color: #0d1c2c;
}
.list-title .count {
padding: 1px 8px;
border-radius: 6px;
background: #eef4ff;
color: #0054cd;
font-size: 12px;
}
.tree-empty {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
min-height: 300px;
}
.node-row {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 2px 0;
}
.node-icon {
display: inline-flex;
align-items: center;
color: #0054cd;
font-size: 15px;
flex-shrink: 0;
}
.node-main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.node-title {
font-size: 14px;
color: #0d1c2c;
font-weight: 500;
}
.node-desc {
font-size: 12px;
color: #5b6b7e;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.node-actions {
flex-shrink: 0;
visibility: hidden;
display: flex;
align-items: center;
}
.node-row:hover .node-actions {
visibility: visible;
}
.form-section {
margin-bottom: 4px;
}
.section-head {
display: flex;
align-items: center;
gap: 10px;
margin: 4px 0 10px;
padding-bottom: 8px;
border-bottom: 1px solid #eef1f6;
}
.section-no {
width: 26px;
height: 26px;
border-radius: 7px;
background: #0054cd;
color: #fff;
font-size: 12px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
.section-text {
display: flex;
flex-direction: column;
gap: 1px;
}
.section-name {
font-size: 15px;
font-weight: 700;
color: #0d1c2c;
}
.section-sub {
font-size: 11px;
color: #8a97a8;
letter-spacing: 0.5px;
}
:deep(.ant-tree-treenode) {
padding: 2px 0;
}
:deep(.ant-tree-node-content-wrapper) {
width: 100%;
display: flex;
}
:deep(.ant-tree-title) {
width: 100%;
}
:deep(.ant-btn-primary) {
background: #0054cd;
border-color: #0054cd;
}
:deep(.ant-btn-primary:not(:disabled):not(.ant-btn-disabled):hover) {
background: #0947ab;
border-color: #0947ab;
}
:deep(.ant-modal-title) {
color: #0d1c2c;
font-weight: 700;
}
</style>
+402 -126
View File
@@ -1,93 +1,132 @@
<template>
<div class="page-management">
<a-card title="客户拜访管理">
<template #extra>
<a-space>
<!-- 顶部标题 + 操作 -->
<div class="page-header">
<div class="breadcrumb">
<span>运营管控体系</span>
<span class="sep">/</span>
<span>客户管理</span>
<span class="sep">/</span>
<span class="cur">客户拜访</span>
</div>
<div class="header-main">
<div class="header-left">
<div class="header-row">
<span class="title-bar"></span>
<span class="main-title">客户拜访</span>
<span class="sub-title">CUSTOMER VISIT</span>
</div>
<p class="desc">登记客户拜访记录,跟踪客户意向方案与拜访跟进情况,支持按业务员检索。</p>
</div>
<div class="header-actions">
<a-button type="primary" @click="handleAdd">
<template #icon><PlusOutlined /></template>
新增拜访
</a-button>
</a-space>
</template>
</div>
</div>
</div>
<div class="search-bar">
<a-input
v-model:value="searchForm.keyword"
placeholder="拜访人/电话"
allow-clear
style="width: 220px"
@pressEnter="handleSearch"
>
<template #prefix><SearchOutlined /></template>
</a-input>
<a-select
v-if="isAdmin"
v-model:value="searchForm.salesmanId"
placeholder="负责业务员"
allow-clear
style="width: 160px"
:options="salesmanOptions"
/>
<a-button type="primary" @click="handleSearch">
<template #icon><SearchOutlined /></template>
查询
</a-button>
<a-button @click="handleReset">
<template #icon><ReloadOutlined /></template>
重置
</a-button>
<!-- 主体卡片 -->
<div class="content-card">
<!-- 筛选区 -->
<div class="filter-card">
<div class="filter-grid">
<div class="filter-item">
<label class="filter-label">关键词检索</label>
<a-input
v-model:value="searchForm.keyword"
placeholder="拜访人 / 联系电话"
allow-clear
@pressEnter="handleSearch"
>
<template #prefix><SearchOutlined /></template>
</a-input>
</div>
<div class="filter-item" v-if="isAdmin">
<label class="filter-label">负责业务员</label>
<a-select
v-model:value="searchForm.salesmanId"
placeholder="请选择业务员"
allow-clear
:options="salesmanOptions"
/>
</div>
</div>
<div class="filter-bottom">
<div class="filter-actions">
<a-button type="primary" @click="handleSearch">
<template #icon><SearchOutlined /></template>
查询
</a-button>
<a-button @click="handleReset">
<template #icon><ReloadOutlined /></template>
重置
</a-button>
</div>
</div>
</div>
<div class="table-wrapper" ref="wrapperRef">
<a-table
:columns="columns"
:data-source="dataSource"
:loading="loading"
:pagination="pagination"
row-key="id"
:scroll="{ x: 'max-content', y: tableHeight }"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'customerName'">
{{ getCustomerName(record.customerId) }}
</template>
<template v-if="column.key === 'willPlan'">
<a-tag :color="record.willPlan === 1 ? 'orange' : 'default'">
{{ record.willPlan === 1 ? '有意向方案' : '暂无' }}
</a-tag>
</template>
<template v-if="column.key === 'createdAt'">
{{ formatDateTime(record.createdAt) }}
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleEdit(record)">
<template #icon><EditOutlined /></template>
编辑
</a-button>
<a-popconfirm title="确定要删除该拜访记录吗?" ok-text="确定" cancel-text="取消" @confirm="handleDelete(record.id)">
<a-button type="link" size="small" danger>
<template #icon><DeleteOutlined /></template>
删除
</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
<!-- 列表区 -->
<div class="list-section">
<div class="list-header">
<div class="list-title">
<span class="dot"></span>
<span class="t">拜访列表</span>
<span class="count">{{ total }}</span>
</div>
</div>
<div class="table-wrapper" ref="wrapperRef">
<a-table
:columns="columns"
:data-source="dataSource"
:loading="loading"
:pagination="pagination"
row-key="id"
:scroll="{ x: 'max-content', y: tableHeight }"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'customerName'">
{{ getCustomerName(record.customerId) }}
</template>
<template v-if="column.key === 'willPlan'">
<a-tag :color="record.willPlan === 1 ? 'orange' : 'default'">
{{ record.willPlan === 1 ? '有意向方案' : '暂无' }}
</a-tag>
</template>
<template v-if="column.key === 'createdAt'">
{{ formatDateTime(record.createdAt) }}
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleEdit(record)">
<template #icon><EditOutlined /></template>
编辑
</a-button>
<a-popconfirm title="确定要删除该拜访记录吗?" ok-text="确定" cancel-text="取消" @confirm="handleDelete(record.id)">
<a-button type="link" size="small" danger>
<template #icon><DeleteOutlined /></template>
删除
</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
<template #emptyText>
<a-empty description="暂无拜访数据" />
</template>
</a-table>
<template #emptyText>
<a-empty description="暂无拜访数据" />
</template>
</a-table>
</div>
</div>
</a-card>
</div>
<a-modal
v-model:open="modalVisible"
:title="modalTitle"
:confirm-loading="modalLoading"
width="680px"
width="720px"
@ok="handleSubmit"
@cancel="handleCancel"
>
@@ -98,42 +137,61 @@
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<a-form-item label="所属客户" name="customerId">
<a-select
v-model:value="formData.customerId"
placeholder="请选择客户"
:options="customerOptions"
:disabled="!isAdmin && isEdit"
show-search
option-filter-prop="label"
/>
</a-form-item>
<a-form-item label="拜访人" name="visitPerson">
<a-input v-model:value="formData.visitPerson" placeholder="请输入拜访人" maxlength="64" />
</a-form-item>
<a-form-item label="联系电话" name="visitPhone">
<a-input v-model:value="formData.visitPhone" placeholder="请输入联系电话" />
</a-form-item>
<a-form-item label="拜访时间" name="visitTime">
<a-date-picker v-model:value="formData.visitTime" style="width: 100%" placeholder="请选择拜访时间" />
</a-form-item>
<a-form-item label="是否有意向方案" name="willPlan">
<a-radio-group v-model:value="formData.willPlan">
<a-radio :value="1">有</a-radio>
<a-radio :value="0">无</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="拜访内容" name="visitContent">
<a-textarea v-model:value="formData.visitContent" :rows="3" placeholder="请输入拜访内容" maxlength="2000" />
</a-form-item>
<a-form-item v-if="isAdmin" label="负责业务员" name="salesmanId">
<a-select
v-model:value="formData.salesmanId"
placeholder="请选择负责业务员"
:options="salesmanOptions"
allow-clear
/>
</a-form-item>
<div class="form-section">
<div class="section-head">
<span class="section-no">01</span>
<div class="section-text">
<span class="section-name">基本信息</span>
<span class="section-sub">Basic Information</span>
</div>
</div>
<a-form-item label="所属客户" name="customerId">
<a-select
v-model:value="formData.customerId"
placeholder="请选择客户"
:options="customerOptions"
:disabled="!isAdmin && isEdit"
show-search
option-filter-prop="label"
/>
</a-form-item>
<a-form-item label="拜访人" name="visitPerson">
<a-input v-model:value="formData.visitPerson" placeholder="请输入拜访人" maxlength="64" />
</a-form-item>
<a-form-item label="联系电话" name="visitPhone">
<a-input v-model:value="formData.visitPhone" placeholder="请输入联系电话" />
</a-form-item>
<a-form-item label="拜访时间" name="visitTime">
<a-date-picker v-model:value="formData.visitTime" style="width: 100%" placeholder="请选择拜访时间" />
</a-form-item>
</div>
<div class="form-section">
<div class="section-head">
<span class="section-no">02</span>
<div class="section-text">
<span class="section-name">拜访详情</span>
<span class="section-sub">Visit Detail</span>
</div>
</div>
<a-form-item label="是否有意向方案" name="willPlan">
<a-radio-group v-model:value="formData.willPlan">
<a-radio :value="1">有</a-radio>
<a-radio :value="0">无</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="拜访内容" name="visitContent">
<a-textarea v-model:value="formData.visitContent" :rows="3" placeholder="请输入拜访内容" maxlength="2000" />
</a-form-item>
<a-form-item v-if="isAdmin" label="负责业务员" name="salesmanId">
<a-select
v-model:value="formData.salesmanId"
placeholder="请选择负责业务员"
:options="salesmanOptions"
allow-clear
/>
</a-form-item>
</div>
</a-form>
</a-modal>
</div>
@@ -386,38 +444,256 @@ function formatDateTime(dateStr: string) {
overflow: hidden;
padding: 20px;
box-sizing: border-box;
background: #eef3f8;
}
:deep(.ant-card) {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
:deep(.ant-card-body) {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
.search-bar {
.page-header {
flex-shrink: 0;
margin-bottom: 16px;
}
.breadcrumb {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 8px;
font-size: 12px;
color: #5b6b7e;
margin-bottom: 10px;
}
.breadcrumb .sep {
color: #c6d2e0;
}
.breadcrumb .cur {
color: #0d1c2c;
font-weight: 600;
}
.header-main {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 20px;
}
.header-left {
display: flex;
flex-direction: column;
gap: 8px;
}
.header-row {
display: flex;
align-items: center;
gap: 12px;
}
.title-bar {
width: 5px;
height: 22px;
background: #0054cd;
border-radius: 3px;
align-self: center;
}
.main-title {
font-size: 22px;
font-weight: 700;
color: #0d1c2c;
}
.sub-title {
font-size: 12px;
font-weight: 700;
color: #0054cd;
letter-spacing: 2px;
}
.desc {
font-size: 13px;
color: #5b6b7e;
line-height: 1.6;
margin: 0;
}
.header-actions {
flex-shrink: 0;
}
.content-card {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
gap: 16px;
background: #fff;
border: 1px solid #dbe4ee;
border-radius: 12px;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.07);
padding: 20px;
}
.filter-card {
flex-shrink: 0;
background: #f8faff;
border: 1px solid #dbe4ee;
border-radius: 10px;
padding: 16px;
}
.filter-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px 16px;
}
.filter-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.filter-label {
font-size: 12px;
font-weight: 600;
color: #5b6b7e;
}
.filter-bottom {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid #dbe4ee;
}
.filter-actions {
display: flex;
gap: 8px;
}
.list-section {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.list-header {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 2px 4px 12px;
}
.list-title {
display: flex;
align-items: center;
gap: 10px;
}
.list-title .dot {
width: 4px;
height: 16px;
background: #0054cd;
border-radius: 2px;
}
.list-title .t {
font-size: 15px;
font-weight: 700;
color: #0d1c2c;
}
.list-title .count {
padding: 1px 8px;
border-radius: 6px;
background: #eef4ff;
color: #0054cd;
font-size: 12px;
}
.table-wrapper {
flex: 1;
min-height: 0;
overflow: hidden;
}
.form-section {
margin-bottom: 4px;
}
.section-head {
display: flex;
align-items: center;
gap: 10px;
margin: 4px 0 10px;
padding-bottom: 8px;
border-bottom: 1px solid #eef1f6;
}
.section-no {
width: 26px;
height: 26px;
border-radius: 7px;
background: #0054cd;
color: #fff;
font-size: 12px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
.section-text {
display: flex;
flex-direction: column;
gap: 1px;
}
.section-name {
font-size: 15px;
font-weight: 700;
color: #0d1c2c;
}
.section-sub {
font-size: 11px;
color: #8a97a8;
letter-spacing: 0.5px;
}
:deep(.ant-table) {
font-size: 14px;
height: 100%;
}
:deep(.ant-table-thead > tr > th) {
background: #f4f7ff;
color: #5b6b7e;
font-weight: 600;
}
:deep(.ant-btn-link) {
padding: 0 4px;
}
:deep(.ant-btn-primary) {
background: #0054cd;
border-color: #0054cd;
}
:deep(.ant-btn-primary:not(:disabled):not(.ant-btn-disabled):hover) {
background: #0947ab;
border-color: #0947ab;
}
:deep(.ant-modal-title) {
color: #0d1c2c;
font-weight: 700;
}
</style>
File diff suppressed because it is too large. Load diff
+15 -12
View File
@@ -83,9 +83,9 @@ ON CONFLICT (id) DO UPDATE SET
-- 初始菜单(客户管理子菜单,parent_id=1,显式指定 id:12-23)
INSERT INTO sys_menu (id, parent_id, menu_name, menu_path, component, icon, sort_order, menu_type, permission, visible) VALUES
(12, 1, '客户信息', '/customer/info', 'customer/info/index', 'UserOutlined', 1, 2, 'customer:list', 1),
(13, 1, '客户部门', '/customer/department', 'customer/department/index', 'TeamOutlined', 2, 2, 'customer:department', 1),
(14, 1, '客户账号', '/customer/account', 'customer/account/index', 'SafetyOutlined', 3, 2, 'customer:account', 1),
(12, 1, '客户档案管理', '/customer/info', 'customer/info/index', 'UserOutlined', 1, 2, 'customer:list', 1),
(13, 1, '客户部门', '/customer/department', 'customer/department/index', 'TeamOutlined', 2, 2, 'customer:department', 1),
(14, 1, '客户小程序授权管理', '/customer/account', 'customer/account/index', 'SafetyOutlined', 3, 2, 'customer:account', 1),
(15, 1, '新增客户', NULL, NULL, NULL, 1, 3, 'customer:create', 1),
(16, 1, '编辑客户', NULL, NULL, NULL, 2, 3, 'customer:update', 1),
(17, 1, '删除客户', NULL, NULL, NULL, 3, 3, 'customer:delete', 1),
@@ -106,19 +106,18 @@ ON CONFLICT (id) DO UPDATE SET
permission = EXCLUDED.permission,
visible = EXCLUDED.visible;
-- 初始菜单(客户扩展子菜单 + 项目域子菜单,id:24-31)
-- 客户域:客户拜访(customer:visit) / 客户建筑(customer:building) / 客户房间(customer:room)
-- 初始菜单(客户扩展子菜单 + 项目域子菜单,id:24-31 + 空间拓扑 32)
-- 客户域:客户拜访(customer:visit) / 空间拓扑与用电架构(customer:topology)
-- 项目域:项目勘察(project:survey) / 项目方案(project:plan) / 合同管理(project:contract)
-- 项目验收(project:acceptance) / 回访记录(project:visit)
INSERT INTO sys_menu (id, parent_id, menu_name, menu_path, component, icon, sort_order, menu_type, permission, visible) VALUES
(24, 1, '客户拜访', '/customer/visit', 'customer/visit/index', 'AccountBookOutlined', 4, 2, 'customer:visit', 1),
(25, 1, '客户建筑', '/customer/building', 'customer/building/index', 'HomeOutlined', 5, 2, 'customer:building',1),
(26, 1, '客户房间', '/customer/room', 'customer/room/index', 'ApartmentOutlined', 6, 2, 'customer:room', 1),
(27, 2, '项目勘察', '/project/survey', 'project/survey/index', 'CarryOutOutlined', 1, 2, 'project:survey', 1),
(28, 2, '项目方案', '/project/plan', 'project/plan/index', 'FileSearchOutlined', 2, 2, 'project:plan', 1),
(29, 2, '合同管理', '/project/contract', 'project/contract/index', 'FileDoneOutlined', 3, 2, 'project:contract', 1),
(30, 2, '项目验收', '/project/acceptance', 'project/acceptance/index', 'ThunderboltOutlined', 4, 2, 'project:acceptance',1),
(31, 2, '回访记录', '/project/visit', 'project/visit/index', 'AuditOutlined', 5, 2, 'project:visit', 1)
(31, 2, '回访记录', '/project/visit', 'project/visit/index', 'AuditOutlined', 5, 2, 'project:visit', 1),
(32, 1, '空间拓扑与用电架构', '/customer/topology', 'customer/topology/index', 'ApartmentOutlined', 5, 2, 'customer:topology',1)
ON CONFLICT (id) DO UPDATE SET
parent_id = EXCLUDED.parent_id,
menu_name = EXCLUDED.menu_name,
@@ -131,7 +130,7 @@ ON CONFLICT (id) DO UPDATE SET
visible = EXCLUDED.visible;
-- 重置 sys_menu 自增序列
SELECT setval('sys_menu_id_seq', 31, true);
SELECT setval('sys_menu_id_seq', 32, true);
-- ------------------------------------------------------------
-- 3. 初始字典(sys_dict 无业务唯一键,清空重建以支持幂等执行)
@@ -190,14 +189,18 @@ ON CONFLICT (user_id, role_id) DO NOTHING;
INSERT INTO sys_role_menu (role_id, menu_id) VALUES
(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9), (1, 10), (1, 11),
(1, 12), (1, 13), (1, 14), (1, 15), (1, 16), (1, 17), (1, 18), (1, 19), (1, 20), (1, 21), (1, 22), (1, 23),
(1, 24), (1, 25), (1, 26), (1, 27), (1, 28), (1, 29), (1, 30), (1, 31),
(1, 24), (1, 27), (1, 28), (1, 29), (1, 30), (1, 31), (1, 32),
(2, 1), (2, 2), (2, 5),
(2, 12), (2, 13), (2, 14), (2, 15), (2, 16), (2, 17), (2, 18), (2, 19), (2, 20), (2, 21), (2, 22), (2, 23),
(2, 24), (2, 25), (2, 26), (2, 27), (2, 28), (2, 29), (2, 30), (2, 31)
(2, 24), (2, 27), (2, 28), (2, 29), (2, 30), (2, 31), (2, 32)
ON CONFLICT (role_id, menu_id) DO NOTHING;
-- 移除已废弃的菜单(客户建筑/客户房间,id=25/26)及其角色绑定(幂等清理,确保每次执行都收敛)
DELETE FROM sys_role_menu WHERE menu_id IN (25, 26);
DELETE FROM sys_menu WHERE id IN (25, 26);
COMMIT;
-- ============================================================
-- 完 · 7 张系统表种子数据(4 角色 + 31 菜单 + 20 字典 + 1 管理员 + 绑定关系)
-- 完 · 7 张系统表种子数据(4 角色 + 32 菜单 + 20 字典 + 1 管理员 + 绑定关系)
-- ============================================================
@@ -22,6 +22,12 @@ export class CustomerController {
return result;
}
@Get("/stats")
async getStats() {
const result = await this.customerService.getStats(this.ctx);
return result;
}
@Get("/:id")
async detail(@Param("id") id: string) {
const result = await this.customerService.detail(Number(id), this.ctx);
+47 -1
View File
@@ -1,7 +1,7 @@
import { Provide, Inject } from "@midwayjs/core";
import { InjectRepository } from "@midwayjs/sequelize";
import { Repository } from "sequelize-typescript";
import { Op, UniqueConstraintError } from "sequelize";
import { Op, UniqueConstraintError, fn, col } from "sequelize";
import { Context } from "@midwayjs/koa";
import Customer from "../entity/customer.entity";
import CustomerDepartment from "../entity/customer_department.entity";
@@ -318,4 +318,50 @@ export class CustomerService {
data: rows.map((r) => r.toJSON())
};
}
async getStats(ctx: Context): Promise<any> {
const userId = ctx.state.user.userId;
const isAdmin = await this.isAdmin(userId);
const where: any = {};
if (!isAdmin) {
where.salesmanId = userId;
}
const total = await this.customerModel.count({ where });
const statusRows = await this.customerModel.findAll({
where,
attributes: ["status", [fn("COUNT", col("id")), "count"]],
group: ["status"],
raw: true
});
const levelRows = await this.customerModel.findAll({
where,
attributes: ["customer_level", [fn("COUNT", col("id")), "count"]],
group: ["customer_level"],
raw: true
});
const statusCounts: Record<number, number> = {};
for (const row of statusRows as any[]) {
if (row.status !== null && row.status !== undefined) {
statusCounts[row.status] = Number(row.count) || 0;
}
}
const levelCounts: Record<string, number> = {};
for (const row of levelRows as any[]) {
if (row.customer_level) {
levelCounts[row.customer_level] = Number(row.count) || 0;
}
}
return {
code: 200,
message: "获取客户统计成功",
data: { total, statusCounts, levelCounts }
};
}
}
+226 -136
View File
@@ -1,179 +1,269 @@
import { Provide, Inject } from "@midwayjs/core";
import { InjectRepository } from "@midwayjs/sequelize";
import SysUser from "../entity/sysUser.entity";
import SysOperationLog from "../entity/sysOperationLog.entity";
import dayjs from "dayjs";
import { Op } from "sequelize";
import { Op, fn } from "sequelize";
import Customer from "../entity/customer.entity";
import Contract from "../entity/contract.entity";
import SurveyOrder from "../entity/survey_order.entity";
import ProjectPlan from "../entity/project_plan.entity";
import Acceptance from "../entity/acceptance.entity";
import VisitRecord from "../entity/visit_record.entity";
@Provide()
export class DashboardService {
@InjectRepository(SysOperationLog)
logModel!: typeof SysOperationLog;
@InjectRepository(Customer)
customerModel!: typeof Customer;
@InjectRepository(Contract)
contractModel!: typeof Contract;
@InjectRepository(SurveyOrder)
surveyModel!: typeof SurveyOrder;
@InjectRepository(ProjectPlan)
planModel!: typeof ProjectPlan;
@InjectRepository(Acceptance)
acceptModel!: typeof Acceptance;
@InjectRepository(VisitRecord)
visitModel!: typeof VisitRecord;
async getDashboardStats(): Promise<any> {
const mockDeviceTotal = 1256;
const mockDeviceOnline = 1089;
const mockAlertToday = 23;
// 客户统计
const customerTotal = await this.countModel(this.customerModel);
const customerMonthlyNew = await this.countModel(this.customerModel, {
[Op.gte]: dayjs().startOf("month").toDate()
});
const activeUsers = await this.getActiveUserCount(7);
// 有效合同与年度金额
const activeContractTotal = await this.countModel(this.contractModel, undefined, {
status: 1
});
const yearContracts = await this.contractModel.findAll({
where: {
status: 1,
created_at: { [Op.gte]: dayjs().startOf("year").toDate() }
},
attributes: ["contract_amount"]
});
const contractYearAmount = yearContracts.reduce(
(sum: number, c: Contract) => sum + Number(c.contractAmount || 0),
0
);
const loginTrend = await this.getLoginTrend(7);
// 勘察派单
const surveyPending = await this.countModel(this.surveyModel, undefined, {
status: { [Op.lt]: 2 }
});
const surveyTotal = await this.countModel(this.surveyModel);
const deviceTrend = this.generateMockTrend(7, 900, 1100);
// 本月回访
const visitMonthly = await this.countModel(this.visitModel, {
[Op.gte]: dayjs().startOf("month").toDate()
});
const visitTotal = await this.countModel(this.visitModel);
const deviceDistribution = [
{ name: "智能电表", value: 456 },
{ name: "环境传感器", value: 328 },
{ name: "摄像头", value: 245 },
{ name: "门禁系统", value: 147 },
{ name: "其他设备", value: 80 }
// 方案 / 验收
const planTotal = await this.countModel(this.planModel);
const acceptTotal = await this.countModel(this.acceptModel);
// 核心业务数据总览条(同步状态)
const modules = [
{ name: "托管客户", value: customerTotal },
{ name: "有效合同", value: activeContractTotal },
{ name: "勘察派单", value: surveyTotal },
{ name: "改造方案", value: planTotal },
{ name: "项目验收", value: acceptTotal },
{ name: "客户回访", value: visitTotal }
];
const recentAlerts = this.generateMockAlerts(10);
// 近7日业务增长趋势(新增客户 + 新增合同)
const trend = await this.getTrend(7);
const recentActivities = await this.getRecentActivities(10);
// 客户状态分布
const customerStatusMap = await this.getCustomerStatus();
const signedCount =
(customerStatusMap[2] || 0) + (customerStatusMap[3] || 0);
const customerSignRate =
customerTotal > 0
? Math.round((signedCount / customerTotal) * 1000) / 10
: 0;
const customerStatus = [
{ name: "意向客户", value: customerStatusMap[1] || 0 },
{ name: "签约客户", value: customerStatusMap[2] || 0 },
{ name: "运维中", value: customerStatusMap[3] || 0 },
{ name: "已终止", value: customerStatusMap[4] || 0 }
];
// 待办勘察派单流水
const surveyList = await this.getSurveyList(5);
// 智电工程推进看板
const projects = await this.getProjectKanban(3);
return {
deviceTotal: mockDeviceTotal,
deviceOnline: mockDeviceOnline,
alertToday: mockAlertToday,
activeUsers,
deviceTrend,
deviceTypeDistribution: deviceDistribution,
recentAlerts,
recentActivities
customerTotal,
customerMonthlyNew,
contractTotal: activeContractTotal,
contractYearAmount,
surveyPending,
surveyTotal,
visitMonthly,
visitTotal,
planTotal,
acceptTotal,
surveyByStatus: [
{ name: "已派发", value: await this.countModel(this.surveyModel, undefined, { status: 0 }) },
{ name: "已接单", value: await this.countModel(this.surveyModel, undefined, { status: 1 }) },
{ name: "已完成", value: await this.countModel(this.surveyModel, undefined, { status: 2 }) }
],
modules,
trend,
customerStatus,
customerSignRate,
surveyList,
projects
};
}
private async getActiveUserCount(days: number): Promise<number> {
const cutoffDate = dayjs().subtract(days, "day").toDate();
private async countModel(
model: any,
dateRange?: any,
extraWhere?: Record<string, any>
): Promise<number> {
const where: Record<string, any> = { ...(extraWhere || {}) };
if (dateRange) {
where.created_at = dateRange;
}
try {
const count = await this.logModel.count({
where: {
action: "用户登录",
created_at: { [Op.gte]: cutoffDate }
},
distinct: true,
col: "userId"
});
return count || 12;
const count = await model.count({ where });
return Number(count) || 0;
} catch (error) {
return 12;
return 0;
}
}
private async getLoginTrend(days: number): Promise<any[]> {
const trend = [];
private async getTrend(days: number): Promise<any[]> {
const trend: any[] = [];
for (let i = days - 1; i >= 0; i--) {
const date = dayjs().subtract(i, "day").format("YYYY-MM-DD");
try {
const count = await this.logModel.count({
where: {
action: "用户登录",
created_at: {
[Op.between]: [dayjs(date).startOf("day").toDate(), dayjs(date).endOf("day").toDate()]
}
}
});
trend.push({
date,
value: count || Math.floor(Math.random() * 50) + 20
});
} catch (error) {
trend.push({
date,
value: Math.floor(Math.random() * 50) + 20
});
}
const date = dayjs().subtract(i, "day");
const range = {
[Op.between]: [date.startOf("day").toDate(), date.endOf("day").toDate()]
};
const customers = await this.countModel(this.customerModel, range);
const contracts = await this.countModel(this.contractModel, range);
trend.push({
date: date.format("YYYY-MM-DD"),
customers,
contracts
});
}
return trend;
}
private async getRecentActivities(limit: number): Promise<any[]> {
private async getCustomerStatus(): Promise<Record<number, number>> {
try {
const logs = await this.logModel.findAll({
const rows = await this.customerModel.findAll({
attributes: [
"status",
[fn("COUNT", "*"), "count"]
],
group: ["status"],
raw: true
});
const map: Record<number, number> = {};
rows.forEach((row: any) => {
const s = Number(row.status);
map[s] = Number(row.count) || 0;
});
return map;
} catch (error) {
return {};
}
}
private async getSurveyList(limit: number): Promise<any[]> {
try {
const rows = await this.surveyModel.findAll({
include: [
{
model: Customer,
attributes: ["customerName", "city", "address"]
}
],
order: [["created_at", "DESC"]],
limit,
attributes: ["id", "action", "module", "ip", "created_at"]
attributes: ["id", "surveyNo", "surveyor", "status", "created_at"]
});
return rows.map((o: SurveyOrder) => {
const cust: any = (o as any).customer || {};
return {
id: o.id,
orderNo: o.surveyNo,
customerName: cust.customerName || "--",
locale:
[cust.city, cust.address].filter(Boolean).join(" ") || "--",
owner: o.surveyor || "待指派",
status: o.status,
time: dayjs(o.created_at).format("MM-DD HH:mm")
};
});
return logs.map((log) => ({
id: log.id,
user: String(log.userId || "系统"),
action: log.action,
module: log.module,
ip: log.ip,
time: dayjs(log.created_at).format("MM-DD HH:mm")
}));
} catch (error) {
return this.generateMockActivities(limit);
return [];
}
}
private generateMockTrend(days: number, min: number, max: number): any[] {
const trend = [];
for (let i = days - 1; i >= 0; i--) {
trend.push({
date: dayjs().subtract(i, "day").format("YYYY-MM-DD"),
online: Math.floor(Math.random() * (max - min)) + min,
offline: Math.floor(Math.random() * 200) + 50
private async getProjectKanban(limit: number): Promise<any[]> {
try {
const acceptedIds = new Set(
(
await this.acceptModel.findAll({
attributes: ["customer_id"]
})
).map((r: any) => String(r.customer_id))
);
const rows = await this.contractModel.findAll({
include: [
{
model: Customer,
attributes: ["customerName"]
},
{
model: ProjectPlan,
attributes: ["planDesc"]
}
],
where: { status: 1 },
order: [["created_at", "DESC"]],
limit,
attributes: [
"id",
"contractNo",
"contractAmount",
"customerId"
]
});
}
return trend;
}
private generateMockAlerts(count: number): any[] {
const alertTypes = ["离线告警", "异常数据", "通信故障", "电量低", "温度超标"];
const devices = ["电表#1001", "传感器#2003", "摄像头#3012", "门禁#4005"];
const levels = ["high", "medium", "low"];
const alerts = [];
for (let i = 0; i < count; i++) {
alerts.push({
id: i + 1,
type: alertTypes[Math.floor(Math.random() * alertTypes.length)],
device: devices[Math.floor(Math.random() * devices.length)],
level: levels[Math.floor(Math.random() * levels.length)],
time: dayjs()
.subtract(Math.floor(Math.random() * 60), "minute")
.format("HH:mm"),
status: Math.random() > 0.5 ? "pending" : "resolved"
return rows.map((c: Contract) => {
const cust: any = (c as any).customer || {};
const plan: any = (c as any).plan || {};
const accepted = acceptedIds.has(String(c.customerId));
return {
id: c.id,
customerName: cust.customerName || "--",
amount: Number(c.contractAmount || 0),
stage: accepted ? 4 : 3,
stageLabel: accepted ? "项目验收" : "设备安装调试",
progress: accepted ? 100 : 80,
owner: "工程部",
desc:
(plan && plan.planDesc) ||
(cust.customerName || "--") + "智能配用电改造项目",
orderNo: c.contractNo
};
});
} catch (error) {
return [];
}
return alerts.sort((a, b) => a.time.localeCompare(b.time));
}
private generateMockActivities(count: number): any[] {
const actions = [
{ user: "管理员", action: "用户登录", module: "认证" },
{ user: "操作员A", action: "修改设备配置", module: "设备管理" },
{ user: "管理员", action: "创建新用户", module: "用户管理" },
{ user: "操作员B", action: "查看告警列表", module: "监控中心" },
{ user: "系统", action: "自动备份完成", module: "系统" },
{ user: "操作员A", action: "导出数据报表", module: "数据分析" },
{ user: "管理员", action: "更新系统配置", module: "系统设置" },
{ user: "操作员C", action: "处理告警事件", module: "监控中心" },
{ user: "系统", action: "定时任务执行", module: "系统" },
{ user: "操作员B", action: "添加新设备", module: "设备管理" }
];
return actions.slice(0, count).map((item, index) => ({
id: index + 1,
...item,
ip: "192.168.1." + Math.floor(Math.random() * 255),
time: dayjs()
.subtract(index * 30, "minute")
.format("MM-DD HH:mm")
}));
}
}
@@ -0,0 +1,200 @@
---
name: Industrial Precision
colors:
surface: '#ffffff'
surface-dim: '#ccdbf1'
surface-bright: '#f8f9ff'
surface-container-lowest: '#ffffff'
surface-container-low: '#eef4ff'
surface-container: '#e5efff'
surface-container-high: '#dbe9ff'
surface-container-highest: '#d5e4fa'
on-surface: '#0d1c2c'
on-surface-variant: '#424655'
inverse-surface: '#233142'
inverse-on-surface: '#e9f1ff'
outline: '#727787'
outline-variant: '#c2c6d8'
surface-tint: '#0056d2'
primary: '#0054cd'
on-primary: '#ffffff'
primary-container: '#136bfb'
on-primary-container: '#fefcff'
inverse-primary: '#b2c5ff'
secondary: '#712ae2'
on-secondary: '#ffffff'
secondary-container: '#8a4cfc'
on-secondary-container: '#fffbff'
tertiary: '#00685f'
on-tertiary: '#ffffff'
tertiary-container: '#008378'
on-tertiary-container: '#f4fffc'
error: '#ba1a1a'
on-error: '#ffffff'
error-container: '#ffdad6'
on-error-container: '#93000a'
primary-fixed: '#dae2ff'
primary-fixed-dim: '#b2c5ff'
on-primary-fixed: '#001847'
on-primary-fixed-variant: '#0040a1'
secondary-fixed: '#eaddff'
secondary-fixed-dim: '#d2bbff'
on-secondary-fixed: '#25005a'
on-secondary-fixed-variant: '#5a00c6'
tertiary-fixed: '#89f5e7'
tertiary-fixed-dim: '#6bd8cb'
on-tertiary-fixed: '#00201d'
on-tertiary-fixed-variant: '#005049'
background: '#f8f9ff'
on-background: '#0d1c2c'
surface-variant: '#d5e4fa'
bg-page: '#eef3f8'
text-muted: '#5b6b7e'
border-rule: '#dbe4ee'
status-warn: '#e0852b'
status-error: '#dc2626'
accent-subtle: rgba(29,111,255,0.08)
typography:
display-lg:
fontFamily: hankenGrotesk
fontSize: 32px
fontWeight: '700'
lineHeight: 40px
headline-md:
fontFamily: hankenGrotesk
fontSize: 23px
fontWeight: '700'
lineHeight: 28px
title-sm:
fontFamily: hankenGrotesk
fontSize: 17px
fontWeight: '700'
lineHeight: 24px
body-md:
fontFamily: hankenGrotesk
fontSize: 15px
fontWeight: '400'
lineHeight: 24px
label-bold:
fontFamily: hankenGrotesk
fontSize: 13px
fontWeight: '700'
lineHeight: 16px
data-mono:
fontFamily: jetbrainsMono
fontSize: 12px
fontWeight: '500'
lineHeight: 16px
caption-sm:
fontFamily: hankenGrotesk
fontSize: 12px
fontWeight: '700'
lineHeight: 14px
headline-md-mobile:
fontFamily: hankenGrotesk
fontSize: 20px
fontWeight: '700'
lineHeight: 26px
rounded:
sm: 0.25rem
DEFAULT: 0.5rem
md: 0.75rem
lg: 1rem
xl: 1.5rem
full: 9999px
spacing:
base: 4px
xs: 8px
sm: 12px
md: 16px
lg: 24px
xl: 40px
container-max: 1200px
gutter: 16px
---
## Brand & Style
The design system is engineered for the **Xintong Smart Power Operation Management APP**, targeting power engineers and grid operators. The brand personality is **Professional, Efficient, and Industrial Tech**. It emphasizes reliability through a structured, high-density interface that transforms complex electrical data into actionable insights.
The design style is a blend of **Corporate Modern** and **Information-Dense Minimalism**. It utilizes a "Layered Surface" approach where data is organized into logical card blocks, using subtle elevations and clear semantic color-coding to denote system status. The aesthetic is clean and functional, avoiding decorative elements in favor of technical precision and rapid scanability.
- **Professionalism:** High-contrast typography and a restrained color palette.
- **Efficiency:** Condensed layouts that prioritize data visualization and task status.
- **Reliability:** Solid borders, consistent spacing, and clear state indicators.
## Colors
The color architecture is strictly functional, mapping specifically to the operation layers of the power management system.
- **Primary (Deep Blue):** Reserved for core branding, navigation, and "Client" layer interactions.
- **Secondary (Purple):** Dedicated to "Gateway" operations and task-specific management.
- **Tertiary (Teal):** Used for "Service" layer success states and optimized performance indicators.
- **Neutral (Ink):** Used for primary text to ensure maximum legibility against light surfaces.
**Semantic Usage:**
- Use **status-warn (Amber)** for non-critical alerts or data synchronization states.
- Use **status-error (Red)** for critical power failures or permission denials.
- The **bg-page** provides a soft, low-strain canvas for long-duration monitoring, while **surface** (White) distinguishes interactive cards and data containers.
## Typography
This design system uses **Hanken Grotesk** as the primary typeface for its sharp, contemporary feel that bridges the gap between industrial and tech. **JetBrains Mono** is employed for technical data, device IDs, and numerical values to emphasize the system's engineering DNA.
- **Hierarchy:** Use `display-lg` for dashboard summaries. `title-sm` is the default for card headers.
- **Data Visualization:** All tabular data and technical strings must use `data-mono` for character alignment and clarity.
- **Mobile Adaptation:** For mobile screens, headlines should scale down to `headline-md-mobile` to maintain visual balance while preserving authority.
## Layout & Spacing
The layout utilizes a **Flexible Grid System** with a focus on logical grouping through card-based containment.
- **Grid:** A 12-column grid is used for desktop views, transitioning to a single-column stacked layout for mobile.
- **Rhythm:** An 8px-based spacing scale ensures vertical consistency. Use `lg (24px)` for outer card padding and `md (16px)` for internal element spacing.
- **Mobile Margins:** Standard mobile side margins are set to `md (16px)` to maximize screen real estate for dense data tables.
- **Sectioning:** Distinct operational modules should be separated by `xl (40px)` of vertical space to prevent cognitive overload.
## Elevation & Depth
This design system conveys hierarchy through **Tonal Layering** and **Subtle Ambient Shadows**. We avoid heavy skeuomorphism in favor of a "Flat-Plus" approach.
- **Base Layer:** The page background (`bg-page`) sits at the lowest elevation.
- **Mid Layer (Cards):** UI cards use a white surface with a subtle shadow (`0 4px 14px rgba(0,0,0,.07)`) to appear slightly lifted.
- **Interactive Layer:** On hover or active state, cards may transition to a higher elevation (`0 8px 28px rgba(0,0,0,.1)`) with a slight upward translation of -2px.
- **Overlays:** Modals and dropdowns use the highest z-index with a distinct backdrop blur to maintain focus on the task at hand.
- **Gradients:** Use soft radial gradients in the background (Blue/Purple) to create environmental depth without interfering with content legibility.
## Shapes
The shape language is **Rounded**, balancing industrial rigidity with modern software approachability.
- **Cards & Sections:** Use the standard `rounded-lg (1rem)` for primary containers.
- **Nodes & Blocks:** Architecture and data nodes use `rounded (0.5rem)` to fit more compactly within layouts.
- **Interactive Elements:** Buttons and tags use a "Pill-shape" (`rounded-xl` or 999px) to distinguish them from structural containers.
- **Borders:** Use `1px solid` with the `border-rule` color for most containers. Active states or primary focus areas may increase to `1.5px`.
## Components
**Buttons:**
- **Primary:** Solid `primary_color` with white text. Pill-shaped.
- **Secondary:** Transparent with `primary_color` border or subtle purple tint for operation tasks.
- **Danger:** Solid `status-error` for irreversible actions.
**Cards:**
- White background, `1px` border-rule, and `rounded-lg` corners.
- Headers should have a `18px` internal padding and a bottom border to separate from the content body.
**Input Fields:**
- Soft white background with `1px` border.
- Focused states should use a `primary_color` border with a subtle outer glow.
**Chips & Tags:**
- Used for status (e.g., "Active", "Offline").
- Backgrounds should use `8% opacity` versions of the semantic colors (Blue, Green, Red) with bolded text in the 100% opacity color.
**Data Visualization:**
- Charts should use the primary blue and secondary purple as the main data series colors.
- Use `data-mono` for all axis labels and tooltips to ensure technical accuracy.
**Status Indicators:**
- Small `7px` circular dots used within lists or architecture nodes to show real-time connectivity status.
@@ -0,0 +1,107 @@
<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="utf-8"/><meta content="width=device-width, initial-scale=1.0" name="viewport"/><link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=hankenGrotesk:wght@100..900&amp;display=swap" rel="stylesheet"/><style>@layer base{html,body{margin:0;padding:0;}body{overscroll-behavior:none;}main>:first-child{margin-top:0!important;}main>:last-child{margin-bottom:0!important;}}::-webkit-scrollbar{display:none;}</style><script src="https://cdn.tailwindcss.com"></script><script id="tailwind-config">tailwind.config={"darkMode":"class","theme":{"extend":{"colors":{"secondary-fixed-dim":"#d2bbff","secondary":"#712ae2","secondary-fixed":"#eaddff","on-primary":"#ffffff","surface-container-lowest":"#ffffff","error":"#ba1a1a","on-error":"#ffffff","on-surface":"#0d1c2c","on-primary-fixed-variant":"#0040a1","outline-variant":"#c2c6d8","tertiary-fixed-dim":"#6bd8cb","on-secondary-fixed-variant":"#5a00c6","tertiary-container":"#008378","primary-container":"#136bfb","secondary-container":"#8a4cfc","tertiary":"#00685f","on-tertiary-fixed-variant":"#005049","on-secondary-fixed":"#25005a","surface-container-highest":"#d5e4fa","surface-container-low":"#eef4ff","background":"#f8f9ff","on-error-container":"#93000a","inverse-primary":"#b2c5ff","surface-dim":"#ccdbf1","on-primary-fixed":"#001847","surface-container-high":"#dbe9ff","outline":"#727787","status-warn":"#e0852b","on-secondary":"#ffffff","surface-tint":"#0056d2","bg-page":"#eef3f8","surface-variant":"#d5e4fa","primary":"#0054cd","on-tertiary-container":"#f4fffc","inverse-surface":"#233142","text-muted":"#5b6b7e","on-tertiary":"#ffffff","accent-subtle":"rgba(29,111,255,0.08)","primary-fixed":"#dae2ff","tertiary-fixed":"#89f5e7","surface-bright":"#f8f9ff","on-primary-container":"#fefcff","on-background":"#0d1c2c","primary-fixed-dim":"#b2c5ff","border-rule":"#dbe4ee","status-error":"#dc2626","inverse-on-surface":"#e9f1ff","error-container":"#ffdad6","on-tertiary-fixed":"#00201d","on-secondary-container":"#fffbff","on-surface-variant":"#424655","surface":"#ffffff","surface-container":"#e5efff"},"borderRadius":{"DEFAULT":"0.25rem","lg":"0.5rem","xl":"0.75rem","full":"9999px"},"spacing":{"md":"16px","sm":"12px","xl":"40px","container-max":"1200px","base":"4px","xs":"8px","gutter":"16px","lg":"24px"},"fontFamily":{"body-md":["hankenGrotesk"],"headline-md-mobile":["hankenGrotesk"],"caption-sm":["hankenGrotesk"],"label-bold":["hankenGrotesk"],"headline-md":["hankenGrotesk"],"title-sm":["hankenGrotesk"],"data-mono":["jetbrainsMono"],"display-lg":["hankenGrotesk"]},"fontSize":{"body-md":["15px",{"lineHeight":"24px","fontWeight":"400"}],"headline-md-mobile":["20px",{"lineHeight":"26px","fontWeight":"700"}],"caption-sm":["12px",{"lineHeight":"14px","fontWeight":"700"}],"label-bold":["13px",{"lineHeight":"16px","fontWeight":"700"}],"headline-md":["23px",{"lineHeight":"28px","fontWeight":"700"}],"title-sm":["17px",{"lineHeight":"24px","fontWeight":"700"}],"data-mono":["12px",{"lineHeight":"16px","fontWeight":"500"}],"display-lg":["32px",{"lineHeight":"40px","fontWeight":"700"}]}}}}</script></head><body class="bg-background font-body-md text-body-md text-on-surface antialiased"><aside class="fixed left-0 top-0 h-full w-72 bg-inverse-surface text-inverse-on-surface z-50 flex flex-col justify-between shadow-[0_1px_8px_rgba(0,0,0,0.04)]"><div class="flex flex-col flex-1 min-h-0"><div class="h-16 px-md flex items-center gap-sm bg-inverse-surface"><div class="w-10 h-10 rounded-lg bg-primary flex items-center justify-center text-on-primary shadow-sm"><span class="material-symbols-outlined text-[24px]">bolt</span></div><div class="flex flex-col"><div class="flex items-center gap-xs"><span class="font-headline-md text-headline-md text-white tracking-wide">XT-POWER</span><span class="font-caption-sm text-caption-sm bg-primary px-xs py-0.5 rounded text-white">OS</span></div><span class="font-caption-sm text-caption-sm text-outline-variant tracking-wider">鑫潼智电 · 智能配用电中枢</span></div></div><div class="flex-1 overflow-y-auto px-xs py-md"><nav class="flex flex-col gap-1" data-active-classes="bg-primary text-white font-label-bold rounded-lg shadow-sm"><a class="flex items-center gap-sm px-md py-sm rounded-lg text-outline-variant hover:bg-surface-container-highest/10 hover:text-white transition-colors" data-path="workbench" href="#"><span class="material-symbols-outlined text-[20px]">dashboard</span><span class="font-label-bold text-label-bold">运营工作台</span></a><div class="flex flex-col"><a class="flex items-center justify-between px-md py-sm rounded-lg bg-primary text-white font-label-bold shadow-sm transition-colors" data-path="customer-management" href="#"><div class="flex items-center gap-sm"><span class="material-symbols-outlined text-[20px]">apartment</span><span class="font-label-bold text-label-bold">客户管理</span></div><span class="material-symbols-outlined text-[16px]">expand_more</span></a><div class="flex flex-col ml-lg pl-md mt-1 mb-1 gap-1 border-l border-outline/30"><a class="px-sm py-1.5 rounded text-white font-label-bold text-caption-sm bg-Line truncated
<!-- Top Navigation Bar & Action Header -->
<div class="flex flex-col gap-sm mb-lg">
<!-- Breadcrumb & Architecture Tag -->
<div class="flex items-center justify-between"><div class="flex items-center gap-xs font-caption-sm text-caption-sm text-text-muted"><span>运营管控体系</span><span class="material-symbols-outlined text-[14px]">chevron_right</span><span>客户管理</span><span class="material-symbols-outlined text-[14px]">chevron_right</span><span class="text-primary font-bold">客户小程序邀请与账号绑定</span></div><div class="flex items-center gap-xs px-sm py-0.5 rounded-full bg-surface-container-high text-on-surface-variant font-data-mono text-data-mono"><span class="w-1.5 h-1.5 rounded-full bg-tertiary"></span><span>Invitation-Token Engine · 72h Dynamic Signed</span></div></div>
<!-- Page Title & CTA Controls -->
<div class="flex flex-col md:flex-row md:items-center justify-between gap-md mt-1"><div class="flex flex-col"><div class="flex items-center gap-sm"><h1 class="font-headline-md text-headline-md text-on-surface tracking-tight">客户小程序邀请与账号绑定</h1><span class="px-xs py-0.5 rounded font-data-mono text-caption-sm bg-primary/10 text-primary uppercase font-bold">定向邀请准入制</span></div><p class="font-caption-sm text-caption-sm text-text-muted mt-0.5">基于定向邀请制 · 运营后台生成带签名专属激活链接与二维码 · 客户受邀扫码绑定微信 OpenID 与 customer_id 硬隔离</p></div><div class="flex flex-wrap items-center gap-xs"><button class="flex items-center gap-xs px-md py-2 rounded-full bg-surface-container-low text-on-surface-variant hover:bg-surface-container hover:text-on-surface transition-all font-label-bold text-label-bold shadow-sm" id="btn-export-records" onclick="showToast('正在导出全部有效邀请码、二维码与凭证时效明细清单...')"><span class="material-symbols-outlined text-[18px]">qr_code_scanner</span><span>导出邀请凭证与二维码</span></button><button class="flex items-center gap-xs px-md py-2 rounded-full bg-surface-container text-primary hover:bg-primary/10 transition-all font-label-bold text-label-bold shadow-sm" id="btn-batch-sms" onclick="showToast('已向 27 位待激活受邀客户批量重发短信邀请通知')"><span class="material-symbols-outlined text-[18px]">forward_to_inbox</span><span>批量发送邀请短信</span></button><button class="flex items-center gap-xs px-lg py-2 rounded-full bg-primary text-on-primary hover:bg-primary-container transition-all font-label-bold text-label-bold shadow-sm" id="btn-add-account" onclick="openAccountModal()"><span class="material-symbols-outlined text-[18px]">share</span><span>发起客户邀请 (Generate Invitation)</span></button></div></div>
<!-- Guiding Protocol Banner -->
<div class="mt-2 p-md rounded-lg bg-surface-container-low flex items-start gap-md shadow-sm"><div class="w-9 h-9 rounded-lg bg-primary/10 flex items-center justify-center text-primary shrink-0"><span class="material-symbols-outlined text-[22px]">mark_email_read</span></div><div class="flex-1 min-w-0"><div class="flex items-center gap-xs"><span class="font-label-bold text-label-bold text-on-surface">严格实行定向邀请制安全机制 (客户小程序不设公开注册)</span><span class="px-1.5 py-0.2 rounded font-data-mono text-[10px] bg-tertiary-container/10 text-tertiary font-bold">一客一码防冒领</span></div><p class="font-body-md text-body-md text-text-muted mt-0.5 leading-relaxed text-sm">运营方通过录入托管客户单位、受邀人姓名与联系手机发起邀请,系统自动生成具 72 小时安全签名的专属激活链接与小程序太阳码。受邀客户微信扫码授权或点击短信链接后,平台秒级完成 <code class="font-data-mono text-data-mono bg-surface px-1 py-0.5 rounded text-primary">customer_id</code> 逻辑租户隔离与 OpenID 唯一映射,未经邀请的外部人员无法接入。</p></div><div class="hidden lg:flex flex-col gap-1 shrink-0 font-data-mono text-data-mono text-right text-text-muted"><div class="flex items-center justify-end gap-1.5"><span class="px-2 py-0.5 rounded bg-tertiary/10 text-tertiary font-bold text-[11px] flex items-center gap-1"><span class="w-1.5 h-1.5 rounded-full bg-tertiary"></span>72小时凭证时效</span><span class="px-2 py-0.5 rounded bg-primary/10 text-primary font-bold text-[11px]">端上免复杂配置</span></div><div class="text-[11px] text-text-muted">支持未激活一键重发 · 随时作废失效</div></div></div>
</div>
<!-- KPI Metric Strip (4 Cards) -->
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-md mb-lg"><!-- KPI 1 --><div class="p-lg rounded-lg bg-surface shadow-sm flex flex-col justify-between relative overflow-hidden group hover:shadow-md transition-shadow"><div class="flex items-center justify-between"><span class="font-label-bold text-label-bold text-text-muted">已发出邀请总数</span><div class="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center text-primary"><span class="material-symbols-outlined text-[20px]">outgoing_mail</span></div></div><div class="mt-md flex items-baseline gap-xs"><span class="font-display-lg text-display-lg text-on-surface tracking-tight font-bold">216</span><span class="font-caption-sm text-caption-sm text-text-muted">人次邀请</span></div><div class="mt-sm flex items-center justify-between font-caption-sm text-caption-sm text-text-muted"><span>覆盖托管客户单位</span><span class="font-data-mono text-data-mono font-bold text-on-surface">128 家</span></div><div class="w-full bg-surface-container h-1 rounded-full mt-xs overflow-hidden"><div class="bg-primary h-full rounded-full" style="width: 100%"></div></div></div><!-- KPI 2 --><div class="p-lg rounded-lg bg-surface shadow-sm flex flex-col justify-between relative overflow-hidden group hover:shadow-md transition-shadow"><div class="flex items-center justify-between"><span class="font-label-bold text-label-bold text-text-muted">已接受邀请并绑定微信</span><div class="w-8 h-8 rounded-lg bg-tertiary/10 flex items-center justify-center text-tertiary"><span class="material-symbols-outlined text-[20px]">verified_user</span></div></div><div class="mt-md flex items-baseline gap-xs"><span class="font-display-lg text-display-lg text-tertiary tracking-tight font-bold">189</span><span class="font-caption-sm text-caption-sm text-text-muted">人成功受邀</span></div><div class="mt-sm flex items-center justify-between font-caption-sm text-caption-sm text-text-muted"><span>微信 OpenID 绑定率</span><span class="font-data-mono text-data-mono font-bold text-tertiary">87.5%</span></div><div class="w-full bg-surface-container h-1 rounded-full mt-xs overflow-hidden"><div class="bg-tertiary h-full rounded-full" style="width: 87.5%"></div></div></div><!-- KPI 3 --><div class="p-lg rounded-lg bg-surface shadow-sm flex flex-col justify-between relative overflow-hidden group hover:shadow-md transition-shadow"><div class="flex items-center justify-between"><span class="font-label-bold text-label-bold text-text-muted">待扫码激活 / 邀请中</span><div class="w-8 h-8 rounded-lg bg-status-warn/10 flex items-center justify-center text-status-warn"><span class="material-symbols-outlined text-[20px]">hourglass_top</span></div></div><div class="mt-md flex items-baseline gap-xs"><span class="font-display-lg text-display-lg text-status-warn tracking-tight font-bold">27</span><span class="font-caption-sm text-caption-sm text-text-muted">人待激活</span></div><div class="mt-sm flex items-center justify-between font-caption-sm text-caption-sm"><span class="text-text-muted"><span class="text-status-warn font-bold">5人</span> 凭证即将超期</span><button class="text-primary font-bold hover:underline" onclick="showToast('已向5位即将超期客户发送催办提醒')">批量催办</button></div><div class="w-full bg-surface-container h-1 rounded-full mt-xs overflow-hidden"><div class="bg-status-warn h-full rounded-full" style="width: 12.5%"></div></div></div><!-- KPI 4 --><div class="p-lg rounded-lg bg-surface shadow-sm flex flex-col justify-between relative overflow-hidden group hover:shadow-md transition-shadow"><div class="flex items-center justify-between"><span class="font-label-bold text-label-bold text-text-muted">已失效 / 已撤回邀请</span><div class="w-8 h-8 rounded-lg bg-outline/10 flex items-center justify-center text-outline"><span class="material-symbols-outlined text-[20px]">cancel_presentation</span></div></div><div class="mt-md flex items-baseline gap-xs"><span class="font-display-lg text-display-lg text-text-muted tracking-tight font-bold">8</span><span class="font-caption-sm text-caption-sm text-text-muted">条失效凭证</span></div><div class="mt-sm flex items-center justify-between font-caption-sm text-caption-sm text-text-muted"><span>超期自动阻断率</span><span class="font-data-mono text-data-mono font-bold text-text-muted">100%</span></div><div class="w-full bg-surface-container h-1 rounded-full mt-xs overflow-hidden"><div class="bg-outline/40 h-full rounded-full" style="width: 100%"></div></div></div></div>
<!-- Main Workstation Layout: Left Filtering & Accounts Table, Right Detail Drawer/Drawer Shell -->
<div class="flex flex-col gap-md">
<!-- Multi-dimensional Filter Bar -->
<div class="p-md rounded-lg bg-surface shadow-sm flex flex-col md:flex-row md:items-center justify-between gap-md"><div class="flex flex-wrap items-center gap-sm flex-1"><div class="relative min-w-[220px]"><select class="w-full pl-sm pr-8 py-2 rounded-lg bg-background text-body-md font-body-md text-on-surface appearance-none focus:outline-none focus:border-primary border border-border-rule" id="filter-client"><option value="ALL">全部托管客户 (128家)</option><option value="WH_EDU_01">武汉软件工程职业学院</option><option value="WH_HOSP_01">武汉市第一人民医院</option><option value="OPTICS_PARK_04">光谷生物城创新基地</option><option value="HSR_STATION_E">武汉高铁东站枢纽</option></select><span class="material-symbols-outlined absolute right-2 top-1/2 -translate-y-1/2 text-text-muted pointer-events-none text-[18px]">unfold_more</span></div><div class="relative min-w-[180px]"><select class="w-full pl-sm pr-8 py-2 rounded-lg bg-background text-body-md font-body-md text-on-surface appearance-none focus:outline-none focus:border-primary border border-border-rule" id="filter-status"><option value="ALL">全部邀请状态</option><option value="PENDING">待微信扫码激活 (27)</option><option value="BOUND">已绑定正常运行 (189)</option><option value="EXPIRED">邀请凭证已过期 (5)</option><option value="REVOKED">已作废 / 已解绑 (3)</option></select><span class="material-symbols-outlined absolute right-2 top-1/2 -translate-y-1/2 text-text-muted pointer-events-none text-[18px]">unfold_more</span></div><div class="relative flex-1 min-w-[240px]"><span class="material-symbols-outlined absolute left-2.5 top-1/2 -translate-y-1/2 text-text-muted text-[18px]">search</span><input class="w-full pl-8 pr-sm py-2 rounded-lg bg-background text-body-md font-body-md text-on-surface placeholder:text-text-muted focus:outline-none focus:border-primary border border-border-rule" placeholder="输入受邀人姓名、手机号、邀请凭证号或单位检索..." type="text"/></div></div><div class="flex items-center gap-xs shrink-0"><button class="px-md py-2 rounded-lg bg-surface-container text-on-surface-variant font-label-bold text-caption-sm hover:bg-surface-container-high transition-colors" onclick="showToast('已重置筛选条件')">重置</button><button class="px-lg py-2 rounded-lg bg-primary text-on-primary font-label-bold text-caption-sm hover:bg-primary-container transition-colors shadow-sm" onclick="showToast('已按当前条件刷新邀请记录')">查询</button></div></div>
<!-- Account Table Card (Full Width Ant-Design style) -->
<div class="rounded-lg bg-surface shadow-sm overflow-hidden flex flex-col border border-border-rule/40">
<!-- Table Header Info Bar -->
<div class="p-md bg-surface-container-low/40 flex items-center justify-between border-b border-border-rule/40"><div class="flex items-center gap-sm"><span class="font-title-sm text-title-sm text-on-surface">受邀客户名单与微信绑定凭证台账</span><span class="px-2 py-0.5 rounded-full font-data-mono text-caption-sm bg-primary/10 text-primary font-bold">共 216 项邀请记录</span><span class="text-caption-sm text-text-muted hidden sm:inline-block">(凭证均含数字防伪签名,扫码自动绑定 customer_id 隔离空间)</span></div><div class="flex items-center gap-xs font-caption-sm text-text-muted"><span class="w-2 h-2 rounded-full bg-tertiary"></span><span class="font-data-mono">邀请制数字签名链正常</span></div></div>
<!-- Table Content -->
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse"><thead><tr class="bg-surface-container-low/60 font-caption-sm text-caption-sm text-text-muted tracking-wide border-b border-border-rule/40"><th class="py-sm px-md font-bold">所属托管客户单位</th><th class="py-sm px-md font-bold">受邀人姓名与联系手机</th><th class="py-sm px-md font-bold">业务角色 / 岗位</th><th class="py-sm px-md font-bold">邀请方式与凭证状态</th><th class="py-sm px-md font-bold">微信绑定状态</th><th class="py-sm px-md font-bold">账号状态</th><th class="py-sm px-md font-bold">邀请 / 绑定时间</th><th class="py-sm px-md font-bold text-right">操作</th></tr></thead><tbody class="divide-y divide-border-rule/40 font-body-md text-body-md text-on-surface"><!-- Row 1: Bound User --><tr class="hover:bg-surface-container-low/50 transition-colors group"><td class="py-md px-md"><div class="flex flex-col"><span class="font-label-bold text-label-bold text-on-surface">武汉软件工程职业学院</span><span class="font-data-mono text-[11px] text-text-muted">ID: WH_EDU_01 · 高校科研</span></div></td><td class="py-md px-md"><div class="flex items-center gap-sm"><div class="w-8 h-8 rounded-full bg-primary/10 text-primary flex items-center justify-center font-bold text-sm">德</div><div class="flex flex-col"><span class="font-label-bold text-label-bold text-on-surface">张德全</span><span class="font-data-mono text-data-mono text-text-muted">139-8821-9901</span></div></div></td><td class="py-md px-md"><span class="px-2 py-0.5 rounded font-caption-sm text-caption-sm bg-surface-container text-on-surface-variant font-medium">总务处主任 / 负责人</span></td><td class="py-md px-md"><div class="flex flex-col"><span class="font-caption-sm text-caption-sm text-on-surface flex items-center gap-1"><span class="material-symbols-outlined text-[15px] text-tertiary">qr_code_2</span>扫码激活成功</span><span class="font-data-mono text-[11px] text-text-muted">凭证号: INV-EDU-9901</span></div></td><td class="py-md px-md"><div class="flex items-center gap-xs"><span class="w-1.5 h-1.5 rounded-full bg-tertiary"></span><span class="font-label-bold text-label-bold text-tertiary">已绑定</span><span class="text-text-muted font-caption-sm text-caption-sm">(德全-后勤)</span></div></td><td class="py-md px-md"><span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full font-caption-sm text-caption-sm bg-tertiary/10 text-tertiary font-bold"><span class="w-1.5 h-1.5 rounded-full bg-tertiary"></span>正常</span></td><td class="py-md px-md font-data-mono text-caption-sm text-text-muted"><div>发: 03-22 09:15</div><div class="text-tertiary">绑: 03-22 09:30</div></td><td class="py-md px-md text-right"><div class="flex items-center justify-end gap-xs"><button class="px-2 py-1 rounded hover:bg-surface-container text-primary font-label-bold text-caption-sm transition-colors" onclick="showInvitePreviewModal('张德全', '139-8821-9901', '武汉软件工程职业学院', 'INV-EDU-9901', '已核销已绑定')">查看凭证</button><button class="px-2 py-1 rounded hover:bg-surface-container text-on-surface-variant font-label-bold text-caption-sm transition-colors" onclick="openAccountModal('武汉软件工程职业学院', '张德全', '139-8821-9901', '总务处主任 / 负责人')">重新邀请</button><button class="px-2 py-1 rounded hover:bg-status-error/10 text-status-error font-label-bold text-caption-sm transition-colors" onclick="showToast('已解除张德全的微信关联并重置邀请凭证')">解绑微信</button></div></td></tr><!-- Row 2: Hospital Lead (Bound) --><tr class="hover:bg-surface-container-low/50 transition-colors group"><td class="py-md px-md"><div class="flex flex-col"><span class="font-label-bold text-label-bold text-on-surface">武汉市第一人民医院</span><span class="font-data-mono text-[11px] text-text-muted">ID: WH_HOSP_01 · 三甲医疗</span></div></td><td class="py-md px-md"><div class="flex items-center gap-sm"><div class="w-8 h-8 rounded-full bg-secondary/10 text-secondary flex items-center justify-center font-bold text-sm">李</div><div class="flex flex-col"><span class="font-label-bold text-label-bold text-on-surface">李晓明</span><span class="font-data-mono text-data-mono text-text-muted">135-4491-0283</span></div></div></td><td class="py-md px-md"><span class="px-2 py-0.5 rounded font-caption-sm text-caption-sm bg-surface-container text-on-surface-variant font-medium">动力科班长 / 运维主管</span></td><td class="py-md px-md"><div class="flex flex-col"><span class="font-caption-sm text-caption-sm text-on-surface flex items-center gap-1"><span class="material-symbols-outlined text-[15px] text-tertiary">sms</span>短信专属链接</span><span class="font-data-mono text-[11px] text-text-muted">凭证号: INV-HOSP-0283</span></div></td><td class="py-md px-md"><div class="flex items-center gap-xs"><span class="w-1.5 h-1.5 rounded-full bg-tertiary"></span><sLine truncated
</div>
<!-- Table Pagination -->
<div class="p-md bg-surface-container-low/40 flex flex-col sm:flex-row items-center justify-between gap-sm border-t border-border-rule/40">
<div class="font-caption-sm text-caption-sm text-text-muted">
显示第 1 至 5 条,共计 <span class="font-data-mono font-bold text-on-surface">216</span> 条小程序客户记录
</div>
<div class="flex items-center gap-1">
<button class="w-7 h-7 rounded flex items-center justify-center bg-surface hover:bg-surface-container text-text-muted shadow-sm border border-border-rule/40">
<span class="material-symbols-outlined text-[16px]">chevron_left</span>
</button>
<button class="w-7 h-7 rounded flex items-center justify-center bg-primary text-on-primary font-data-mono text-caption-sm font-bold shadow-sm">1</button>
<button class="w-7 h-7 rounded flex items-center justify-center bg-surface hover:bg-surface-container text-on-surface font-data-mono text-caption-sm shadow-sm border border-border-rule/40">2</button>
<button class="w-7 h-7 rounded flex items-center justify-center bg-surface hover:bg-surface-container text-on-surface font-data-mono text-caption-sm shadow-sm border border-border-rule/40">3</button>
<span class="px-1 text-text-muted">...</span>
<button class="w-7 h-7 rounded flex items-center justify-center bg-surface hover:bg-surface-container text-on-surface font-data-mono text-caption-sm shadow-sm border border-border-rule/40">44</button>
<button class="w-7 h-7 rounded flex items-center justify-center bg-surface hover:bg-surface-container text-text-muted shadow-sm border border-border-rule/40">
<span class="material-symbols-outlined text-[16px]">chevron_right</span>
</button>
</div>
</div>
</div>
<!-- Simplified Modal for Add/Edit Account (Coarse-Grained customer_id scoping) -->
<div class="fixed inset-0 z-50 flex items-center justify-center bg-inverse-surface/40 backdrop-blur-sm hidden" id="account-edit-modal"><div class="w-full max-w-2xl bg-surface rounded-xl shadow-xl overflow-hidden border border-border-rule/50 mx-4"><!-- Modal Header --><div class="px-lg py-md bg-surface-container-low flex items-center justify-between border-b border-border-rule/40"><div class="flex items-center gap-sm"><div class="w-8 h-8 rounded-lg bg-primary/10 text-primary flex items-center justify-center"><span class="material-symbols-outlined text-[20px]">forward_to_inbox</span></div><div class="flex flex-col"><h3 class="font-title-sm text-title-sm text-on-surface" id="modal-title">发起客户小程序定向邀请</h3><span class="font-caption-sm text-caption-sm text-text-muted">后台录入单位与联系人 · 生成专属扫码凭证绑定微信</span></div></div><button class="p-1 rounded-lg text-text-muted hover:text-on-surface hover:bg-surface-container transition-colors" onclick="closeAccountModal()"><span class="material-symbols-outlined text-[20px]">close</span></button></div><!-- Modal Body --><div class="p-lg flex flex-col gap-md"><!-- Process Steps Breadcrumb --><div class="p-sm rounded-lg bg-surface-container-low/60 border border-border-rule/40"><div class="grid grid-cols-4 gap-2 text-center text-caption-sm"><div class="flex flex-col items-center gap-1"><span class="w-5 h-5 rounded-full bg-primary text-white flex items-center justify-center font-bold text-[11px]">1</span><span class="font-bold text-primary">录入客户单位</span></div><div class="flex flex-col items-center gap-1"><span class="w-5 h-5 rounded-full bg-surface-container text-on-surface-variant flex items-center justify-center font-bold text-[11px]">2</span><span class="text-text-muted">生成带签名凭证</span></div><div class="flex flex-col items-center gap-1"><span class="w-5 h-5 rounded-full bg-surface-container text-on-surface-variant flex items-center justify-center font-bold text-[11px]">3</span><span class="text-text-muted">短信/二维码通知</span></div><div class="flex flex-col items-center gap-1"><span class="w-5 h-5 rounded-full bg-surface-container text-on-surface-variant flex items-center justify-center font-bold text-[11px]">4</span><span class="text-text-muted">扫码秒级开通</span></div></div></div><!-- Customer ID Assignment --><div class="flex flex-col gap-1"><label class="font-label-bold text-caption-sm text-on-surface flex items-center gap-1"><span>受邀客户归属用电单位</span><span class="text-status-error">*</span></label><div class="relative"><select class="w-full pl-sm pr-8 py-2 rounded-lg bg-background text-body-md font-body-md text-on-surface appearance-none focus:outline-none focus:border-primary border border-border-rule" id="modal-input-org"><option value="WH_EDU_01">武汉软件工程职业学院 (WH_EDU_01)</option><option value="WH_HOSP_01">武汉市第一人民医院 (WH_HOSP_01)</option><option value="OPTICS_PARK_04">光谷生物城创新基地 (OPTICS_PARK_04)</option><option value="HSR_STATION_E">武汉高铁东站枢纽 (HSR_STATION_E)</option></select><span class="material-symbols-outlined absolute right-2 top-1/2 -translate-y-1/2 text-text-muted pointer-events-none text-[18px]">unfold_more</span></div><span class="font-caption-sm text-[11px] text-text-muted">受邀人员扫码后将严格绑定该单位的客端数据边界 (customer_id 隔离)。</span></div><!-- User Name & Phone Number --><div class="grid grid-cols-1 sm:grid-cols-2 gap-md"><div class="flex flex-col gap-1"><label class="font-label-bold text-caption-sm text-on-surface flex items-center gap-1"><span>受邀客户联系人</span><span class="text-status-error">*</span></label><input class="w-full px-sm py-2 rounded-lg bg-background text-body-md font-body-md text-on-surface focus:outline-none focus:border-primary border border-border-rule" id="modal-input-name" placeholder="例如:张德全" type="text" value="李晓明"/></div><div class="flex flex-col gap-1"><label class="font-label-bold text-caption-sm text-on-surface flex items-center gap-1"><span>受邀人手机号</span><span class="text-status-error">*</span></label><input class="w-full px-sm py-2 rounded-lg bg-background text-body-md font-body-md text-on-surface focus:outline-none focus:border-primary border border-border-rule font-data-mono" id="modal-input-phone" placeholder="用于下发短信激活链接" type="text" value="135-4491-0283"/></div></div><!-- Role & Token Validity Duration --><div class="grid grid-cols-1 sm:grid-cols-2 gap-md"><div class="flex flex-col gap-1"><label class="font-label-bold text-caption-sm text-on-surface">业务岗位 / 职责备注</label><input class="w-full px-sm py-2 rounded-lg bg-background text-body-md font-body-md text-on-surface focus:outline-none focus:border-primary border border-border-rule" id="modal-input-role" placeholder="例如:动力科班长 / 总务主任" type="text" value="动力科班Line truncated
</div>
<!-- Notification Toast Modal Simulation Container (Dynamic via JS) -->
<div class="fixed bottom-6 right-6 z-50 transform translate-y-20 opacity-0 transition-all duration-300 pointer-events-none" id="toast-message">
<div class="px-md py-sm rounded-lg bg-inverse-surface text-inverse-on-surface shadow-xl flex items-center gap-sm">
<span class="material-symbols-outlined text-tertiary-fixed text-[20px]">check_circle</span>
<span class="font-body-md text-caption-sm" id="toast-text">授权策略已生效,API网关已同步刷新该OpenID权限缓存。</span>
</div>
</div>
</div>
<script>
// Micro-interaction handlers for client-side feedback
function showToast(text) {
const toast = document.getElementById('toast-message');
const toastText = document.getElementById('toast-text');
if (toast && toastText) {
toastText.innerText = text;
toast.classList.remove('translate-y-20', 'opacity-0', 'pointer-events-none');
setTimeout(() => {
toast.classList.add('translate-y-20', 'opacity-0', 'pointer-events-none');
}, 3200);
}
}
function selectAccountForEdit(name, role, phone, org, scope) {
const nameEl = document.getElementById('drawer-user-name');
const roleEl = document.getElementById('drawer-user-role');
const phoneEl = document.getElementById('drawer-user-phone');
const orgEl = document.getElementById('drawer-user-org');
if (nameEl) nameEl.innerText = name;
if (roleEl) roleEl.innerText = role;
if (phoneEl) phoneEl.innerText = '关联手机号: ' + phone;
if (orgEl) orgEl.innerText = org;
showToast(`已载入 ${name} (${role}) 的RBAC权限配置`);
}
document.getElementById('btn-save-policy')?.addEventListener('click', () => {
showToast('策略保存成功:已热更新 Redis 缓存与 L2 访问契约!');
});
document.getElementById('btn-notify-wechat')?.addEventListener('click', () => {
showToast('微信服务号模板消息已推送至用户手机终端!');
});
document.getElementById('btn-batch-sms')?.addEventListener('click', () => {
showToast('已向待初登的 27 位客户工程师发送一次性激活凭证短信。');
});
document.getElementById('btn-export-records')?.addEventListener('click', () => {
showToast('正在生成客户小程序 RBAC 权限矩阵报告 (.xlsx)...');
});
document.getElementById('btn-add-account')?.addEventListener('click', () => {
showToast('已唤起快速新建客户小程序账号向导。');
});
</script></main></div></body></html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

@@ -0,0 +1,200 @@
---
name: Industrial Precision
colors:
surface: '#ffffff'
surface-dim: '#ccdbf1'
surface-bright: '#f8f9ff'
surface-container-lowest: '#ffffff'
surface-container-low: '#eef4ff'
surface-container: '#e5efff'
surface-container-high: '#dbe9ff'
surface-container-highest: '#d5e4fa'
on-surface: '#0d1c2c'
on-surface-variant: '#424655'
inverse-surface: '#233142'
inverse-on-surface: '#e9f1ff'
outline: '#727787'
outline-variant: '#c2c6d8'
surface-tint: '#0056d2'
primary: '#0054cd'
on-primary: '#ffffff'
primary-container: '#136bfb'
on-primary-container: '#fefcff'
inverse-primary: '#b2c5ff'
secondary: '#712ae2'
on-secondary: '#ffffff'
secondary-container: '#8a4cfc'
on-secondary-container: '#fffbff'
tertiary: '#00685f'
on-tertiary: '#ffffff'
tertiary-container: '#008378'
on-tertiary-container: '#f4fffc'
error: '#ba1a1a'
on-error: '#ffffff'
error-container: '#ffdad6'
on-error-container: '#93000a'
primary-fixed: '#dae2ff'
primary-fixed-dim: '#b2c5ff'
on-primary-fixed: '#001847'
on-primary-fixed-variant: '#0040a1'
secondary-fixed: '#eaddff'
secondary-fixed-dim: '#d2bbff'
on-secondary-fixed: '#25005a'
on-secondary-fixed-variant: '#5a00c6'
tertiary-fixed: '#89f5e7'
tertiary-fixed-dim: '#6bd8cb'
on-tertiary-fixed: '#00201d'
on-tertiary-fixed-variant: '#005049'
background: '#f8f9ff'
on-background: '#0d1c2c'
surface-variant: '#d5e4fa'
bg-page: '#eef3f8'
text-muted: '#5b6b7e'
border-rule: '#dbe4ee'
status-warn: '#e0852b'
status-error: '#dc2626'
accent-subtle: rgba(29,111,255,0.08)
typography:
display-lg:
fontFamily: hankenGrotesk
fontSize: 32px
fontWeight: '700'
lineHeight: 40px
headline-md:
fontFamily: hankenGrotesk
fontSize: 23px
fontWeight: '700'
lineHeight: 28px
title-sm:
fontFamily: hankenGrotesk
fontSize: 17px
fontWeight: '700'
lineHeight: 24px
body-md:
fontFamily: hankenGrotesk
fontSize: 15px
fontWeight: '400'
lineHeight: 24px
label-bold:
fontFamily: hankenGrotesk
fontSize: 13px
fontWeight: '700'
lineHeight: 16px
data-mono:
fontFamily: jetbrainsMono
fontSize: 12px
fontWeight: '500'
lineHeight: 16px
caption-sm:
fontFamily: hankenGrotesk
fontSize: 12px
fontWeight: '700'
lineHeight: 14px
headline-md-mobile:
fontFamily: hankenGrotesk
fontSize: 20px
fontWeight: '700'
lineHeight: 26px
rounded:
sm: 0.25rem
DEFAULT: 0.5rem
md: 0.75rem
lg: 1rem
xl: 1.5rem
full: 9999px
spacing:
base: 4px
xs: 8px
sm: 12px
md: 16px
lg: 24px
xl: 40px
container-max: 1200px
gutter: 16px
---
## Brand & Style
The design system is engineered for the **Xintong Smart Power Operation Management APP**, targeting power engineers and grid operators. The brand personality is **Professional, Efficient, and Industrial Tech**. It emphasizes reliability through a structured, high-density interface that transforms complex electrical data into actionable insights.
The design style is a blend of **Corporate Modern** and **Information-Dense Minimalism**. It utilizes a "Layered Surface" approach where data is organized into logical card blocks, using subtle elevations and clear semantic color-coding to denote system status. The aesthetic is clean and functional, avoiding decorative elements in favor of technical precision and rapid scanability.
- **Professionalism:** High-contrast typography and a restrained color palette.
- **Efficiency:** Condensed layouts that prioritize data visualization and task status.
- **Reliability:** Solid borders, consistent spacing, and clear state indicators.
## Colors
The color architecture is strictly functional, mapping specifically to the operation layers of the power management system.
- **Primary (Deep Blue):** Reserved for core branding, navigation, and "Client" layer interactions.
- **Secondary (Purple):** Dedicated to "Gateway" operations and task-specific management.
- **Tertiary (Teal):** Used for "Service" layer success states and optimized performance indicators.
- **Neutral (Ink):** Used for primary text to ensure maximum legibility against light surfaces.
**Semantic Usage:**
- Use **status-warn (Amber)** for non-critical alerts or data synchronization states.
- Use **status-error (Red)** for critical power failures or permission denials.
- The **bg-page** provides a soft, low-strain canvas for long-duration monitoring, while **surface** (White) distinguishes interactive cards and data containers.
## Typography
This design system uses **Hanken Grotesk** as the primary typeface for its sharp, contemporary feel that bridges the gap between industrial and tech. **JetBrains Mono** is employed for technical data, device IDs, and numerical values to emphasize the system's engineering DNA.
- **Hierarchy:** Use `display-lg` for dashboard summaries. `title-sm` is the default for card headers.
- **Data Visualization:** All tabular data and technical strings must use `data-mono` for character alignment and clarity.
- **Mobile Adaptation:** For mobile screens, headlines should scale down to `headline-md-mobile` to maintain visual balance while preserving authority.
## Layout & Spacing
The layout utilizes a **Flexible Grid System** with a focus on logical grouping through card-based containment.
- **Grid:** A 12-column grid is used for desktop views, transitioning to a single-column stacked layout for mobile.
- **Rhythm:** An 8px-based spacing scale ensures vertical consistency. Use `lg (24px)` for outer card padding and `md (16px)` for internal element spacing.
- **Mobile Margins:** Standard mobile side margins are set to `md (16px)` to maximize screen real estate for dense data tables.
- **Sectioning:** Distinct operational modules should be separated by `xl (40px)` of vertical space to prevent cognitive overload.
## Elevation & Depth
This design system conveys hierarchy through **Tonal Layering** and **Subtle Ambient Shadows**. We avoid heavy skeuomorphism in favor of a "Flat-Plus" approach.
- **Base Layer:** The page background (`bg-page`) sits at the lowest elevation.
- **Mid Layer (Cards):** UI cards use a white surface with a subtle shadow (`0 4px 14px rgba(0,0,0,.07)`) to appear slightly lifted.
- **Interactive Layer:** On hover or active state, cards may transition to a higher elevation (`0 8px 28px rgba(0,0,0,.1)`) with a slight upward translation of -2px.
- **Overlays:** Modals and dropdowns use the highest z-index with a distinct backdrop blur to maintain focus on the task at hand.
- **Gradients:** Use soft radial gradients in the background (Blue/Purple) to create environmental depth without interfering with content legibility.
## Shapes
The shape language is **Rounded**, balancing industrial rigidity with modern software approachability.
- **Cards & Sections:** Use the standard `rounded-lg (1rem)` for primary containers.
- **Nodes & Blocks:** Architecture and data nodes use `rounded (0.5rem)` to fit more compactly within layouts.
- **Interactive Elements:** Buttons and tags use a "Pill-shape" (`rounded-xl` or 999px) to distinguish them from structural containers.
- **Borders:** Use `1px solid` with the `border-rule` color for most containers. Active states or primary focus areas may increase to `1.5px`.
## Components
**Buttons:**
- **Primary:** Solid `primary_color` with white text. Pill-shaped.
- **Secondary:** Transparent with `primary_color` border or subtle purple tint for operation tasks.
- **Danger:** Solid `status-error` for irreversible actions.
**Cards:**
- White background, `1px` border-rule, and `rounded-lg` corners.
- Headers should have a `18px` internal padding and a bottom border to separate from the content body.
**Input Fields:**
- Soft white background with `1px` border.
- Focused states should use a `primary_color` border with a subtle outer glow.
**Chips & Tags:**
- Used for status (e.g., "Active", "Offline").
- Backgrounds should use `8% opacity` versions of the semantic colors (Blue, Green, Red) with bolded text in the 100% opacity color.
**Data Visualization:**
- Charts should use the primary blue and secondary purple as the main data series colors.
- Use `data-mono` for all axis labels and tooltips to ensure technical accuracy.
**Status Indicators:**
- Small `7px` circular dots used within lists or architecture nodes to show real-time connectivity status.
@@ -0,0 +1,177 @@
<!DOCTYPE html><html lang="zh-CN" style=""><head><meta charset="utf-8"><meta content="width=device-width, initial-scale=1.0" name="viewport"><link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=hankenGrotesk:wght@100..900&amp;display=swap" rel="stylesheet"><style>@layer base{html,body{margin:0;padding:0;}body{overscroll-behavior:none;}main>:first-child{margin-top:0!important;}main>:last-child{margin-bottom:0!important;}}::-webkit-scrollbar{display:none;}</style><script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script><script id="tailwind-config">tailwind.config={"darkMode":"class","theme":{"extend":{"colors":{"surface-container-low":"#eef4ff","inverse-primary":"#b2c5ff","on-tertiary-fixed":"#00201d","on-error-container":"#93000a","primary":"#0054cd","on-secondary-container":"#fffbff","secondary-fixed-dim":"#d2bbff","on-tertiary-container":"#f4fffc","border-rule":"#dbe4ee","error":"#ba1a1a","surface-bright":"#f8f9ff","on-error":"#ffffff","tertiary-fixed":"#89f5e7","tertiary-container":"#008378","text-muted":"#5b6b7e","outline-variant":"#c2c6d8","inverse-surface":"#233142","surface":"#ffffff","on-primary":"#ffffff","status-error":"#dc2626","on-primary-container":"#fefcff","status-warn":"#e0852b","surface-container":"#e5efff","error-container":"#ffdad6","bg-page":"#eef3f8","primary-fixed":"#dae2ff","on-tertiary":"#ffffff","on-secondary":"#ffffff","tertiary-fixed-dim":"#6bd8cb","surface-variant":"#d5e4fa","on-primary-fixed":"#001847","surface-container-lowest":"#ffffff","on-background":"#0d1c2c","secondary":"#712ae2","on-primary-fixed-variant":"#0040a1","on-tertiary-fixed-variant":"#005049","tertiary":"#00685f","secondary-container":"#8a4cfc","surface-tint":"#0056d2","inverse-on-surface":"#e9f1ff","on-surface":"#0d1c2c","surface-container-high":"#dbe9ff","on-secondary-fixed-variant":"#5a00c6","background":"#f8f9ff","surface-container-highest":"#d5e4fa","primary-fixed-dim":"#b2c5ff","outline":"#727787","secondary-fixed":"#eaddff","accent-subtle":"rgba(29,111,255,0.08)","primary-container":"#136bfb","on-surface-variant":"#424655","surface-dim":"#ccdbf1","on-secondary-fixed":"#25005a"},"borderRadius":{"DEFAULT":"0.25rem","lg":"0.5rem","xl":"0.75rem","full":"9999px"},"spacing":{"gutter":"16px","sm":"12px","base":"4px","xl":"40px","lg":"24px","container-max":"1200px","md":"16px","xs":"8px"},"fontFamily":{"data-mono":["jetbrainsMono"],"headline-md-mobile":["hankenGrotesk"],"body-md":["hankenGrotesk"],"label-bold":["hankenGrotesk"],"display-lg":["hankenGrotesk"],"caption-sm":["hankenGrotesk"],"headline-md":["hankenGrotesk"],"title-sm":["hankenGrotesk"]},"fontSize":{"data-mono":["12px",{"lineHeight":"16px","fontWeight":"500"}],"headline-md-mobile":["20px",{"lineHeight":"26px","fontWeight":"700"}],"body-md":["15px",{"lineHeight":"24px","fontWeight":"400"}],"label-bold":["13px",{"lineHeight":"16px","fontWeight":"700"}],"display-lg":["32px",{"lineHeight":"40px","fontWeight":"700"}],"caption-sm":["12px",{"lineHeight":"14px","fontWeight":"700"}],"headline-md":["23px",{"lineHeight":"28px","fontWeight":"700"}],"title-sm":["17px",{"lineHeight":"24px","fontWeight":"700"}]}}}}</script></head><body class="bg-bg-page font-body-md text-body-md text-on-surface antialiased"><aside class="fixed left-0 top-0 h-full w-64 bg-inverse-surface z-50 flex flex-col justify-between shadow-[0_1px_8px_rgba(0,0,0,0.04)]"><div class="flex flex-col"><div class="h-16 px-md flex items-center gap-xs bg-inverse-surface"><img alt="Xintong Power logo, industrial technology style, electric bolt and smart connectivity icon, professional, modern, high precision, deep blue palette.. Design context: - Primary color: #1d6fff
- Font: hankenGrotesk
- Mode: light
- Roundness: rounded-md
. The logo should be visually consistent with these brand tokens." class="h-8 w-auto object-contain" src="https://lh3.googleusercontent.com/aida/AEtjO1VpTaoSkZCyImEBOewVJbw8cN4OK_4XN0nYWf-d6prkZBdtwy8_i4RFQPli9kdGdwctP8bUQgQk3yLHoUJzYawS5_rm4a_iDAdFxgEcJm0oLqXiMHJ4KIqg8lTf2EdzS9w08_C-fdfalXZmfY_o9mNwGjr7KB5oPG_MHTU9vVSLrJTz4e0Lv7VNEf16_F81a5fH6QJk6fVBQhWTCesvcyO9LLqZ3nOoMdpFh4_7nHayUU88Ni14Re8Q80c"><div class="flex flex-col ml-xs"><span class="font-label-bold text-label-bold text-inverse-on-surface tracking-wide">鑫潼智电</span><span class="font-caption-sm text-caption-sm text-inverse-primary tracking-wider">XT-POWER OS</span></div></div><nav class="flex flex-col gap-base px-sm pt-md" data-active-classes="bg-primary text-on-primary font-label-bold rounded-lg shadow-sm"><a class="flex items-center gap-sm px-sm py-xs rounded-lg text-inverse-on-surface/80 hover:bg-inverse-surface/80 hover:text-inverse-on-surface transition-colors" data-path="workbench" href="#"><span class="material-symbols-outlined text-[20px]">space_dashboard</span><span class="font-body-md text-body-md">运营工作台</span></a><a aria-current="page" class="flex items-center gap-sm px-sm py-xs transition-colors bg-primary text-on-primary font-label-bold rounded-lg shadow-sm" data-path="customer-management" href="#"><span class="material-symbols-outlined text-[20px]">groups</span><span class="font-body-md text-body-md">客户管理</span></a><a class="flex items-center gap-sm px-sm py-xs rounded-lg text-inverse-on-surface/80 hover:bg-inverse-surface/80 hover:text-inverse-on-surface transition-colors" data-path="project-management" href="#"><span class="material-symbols-outlined text-[20px]">lan</span><span class="font-body-md text-body-md">项目管理</span></a><a class="flex items-center gap-sm px-sm py-xs rounded-lg text-inverse-on-surface/80 hover:bg-inverse-surface/80 hover:text-inverse-on-surface transition-colors" data-path="dispatch-orders" href="#"><span class="material-symbols-outlined text-[20px]">assignment</span><span class="font-body-md text-body-md">运营派单</span></a><a class="flex items-center gap-sm px-sm py-xs rounded-lg text-inverse-on-surface/80 hover:bg-inverse-surface/80 hover:text-inverse-on-surface transition-colors" data-path="device-monitoring" href="#"><span class="material-symbols-outlined text-[20px]">bolt</span><span class="font-body-md text-body-md">设备监控</span></a><a class="flex items-center gap-sm px-sm py-xs rounded-lg text-inverse-on-surface/80 hover:bg-inverse-surface/80 hover:text-inverse-on-surface transition-colors" data-path="statistics-reports" href="#"><span class="material-symbols-outlined text-[20px]">analytics</span><span class="font-body-md text-body-md">统计报表</span></a><a class="flex items-center gap-sm px-sm py-xs rounded-lg text-inverse-on-surface/80 hover:bg-inverse-surface/80 hover:text-inverse-on-surface transition-colors" data-path="system-settings" href="#"><span class="material-symbols-outlined text-[20px]">settings</span><span class="font-body-md text-body-md">系统管理</span></a></nav></div><div class="p-sm m-sm rounded-lg bg-surface-container-low/10 flex flex-col gap-xs"><div class="flex items-center justify-between"><span class="font-caption-sm text-caption-sm text-inverse-on-surface/70">边缘网关通信</span><span class="flex h-2 w-2 relative"><span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-tertiary-fixed opacity-75"></span><span class="relative inline-flex rounded-full h-2 w-2 bg-tertiary-fixed"></span></span></div><div class="flex items-baseline justify-between"><span class="font-data-mono text-data-mono text-tertiary-fixed">在线 99.98%</span><span class="font-caption-sm text-caption-sm text-inverse-on-surface/50">ICC链路</span></div></div></aside><div class="pl-64"><header class="fixed top-0 left-64 right-0 h-16 bg-surface/95 backdrop-blur-xl shadow-[0_1px_8px_rgba(0,0,0,0.04)] z-40 flex items-center justify-between px-lg"><div class="flex items-center gap-md"><div class="flex items-center gap-xs px-sm py-base rounded-full bg-surface-container-low"><span class="w-2 h-2 rounded-full bg-tertiary"></span><span class="font-caption-sm text-caption-sm text-tertiary">ICC 实时数据同步正常</span></div><div class="relative flex items-center"><span class="material-symbols-outlined absolute left-sm text-text-muted text-[18px]">search</span><input class="w-80 h-9 pl-xl pr-sm rounded-lg bg-surface-container-low text-on-surface placeholder:text-text-muted placeholder:text-[13px] text-body-md outline-none focus:bg-surface focus:shadow-sm transition-all" placeholder="按项目号 / 客户 / 变电站 / 工单号检索..." type="text"></div></div><div class="flex items-center gap-md"><button class="relative flex items-center justify-center w-9 h-9 rounded-lg hover:bg-surface-container-low hover:text-on-surface text-on-surface-variant transition-colors" type="button"><span class="material-symbols-outlined text-[20px]">notificatLine truncated
<!-- Top Command & KPI Header Section -->
<div class="px-lg pt-lg pb-lg flex flex-col gap-lg bg-surface/60 backdrop-blur-md">
<!-- Breadcrumb & Main Actions -->
<div class="flex flex-wrap items-center justify-between gap-md">
<div class="flex flex-col">
<div class="flex items-center gap-xs font-caption-sm text-caption-sm text-text-muted mb-base">
<span class="">运营管控体系</span>
<span class="material-symbols-outlined text-[14px]">chevron_right</span>
<span class="">L3 业务服务层</span>
<span class="material-symbols-outlined text-[14px]">chevron_right</span>
<span class="text-primary font-label-bold">客户档案与空间拓扑</span>
</div>
<div class="flex items-center gap-sm">
<h1 class="font-headline-md text-headline-md text-on-surface tracking-tight">客户档案管理</h1>
<span class="px-2.5 py-1 rounded-full bg-primary/10 text-primary font-data-mono text-data-mono font-medium">Customer Directory &amp; Tenant Topology</span>
</div>
<p class="font-body-md text-body-md text-text-muted mt-1.5 max-w-3xl leading-relaxed">
集中统管全省电力托管单位、用电建筑空间三级拓扑、大华 ICC 边缘物联网设备映射及终端微信小程序访问权限。
</p>
</div>
<!-- Action Buttons -->
<div class="flex items-center gap-sm">
<button class="h-10 px-md rounded-full bg-surface-container text-on-surface hover:bg-surface-variant font-label-bold text-label-bold flex items-center gap-xs shadow-sm transition-all duration-150" type="button">
<span class="material-symbols-outlined text-[18px]">file_download</span>
<span class="">导出客户档案</span>
</button>
<button class="h-10 px-md rounded-full bg-surface-container text-primary hover:bg-surface-container-high font-label-bold text-label-bold flex items-center gap-xs shadow-sm transition-all duration-150" type="button">
<span class="material-symbols-outlined text-[18px] animate-spin" id="syncIcon">sync</span>
<span class="">从大华 ICC 同步</span>
</button>
<button class="h-10 px-lg rounded-full bg-primary hover:bg-primary-container text-on-primary font-label-bold text-label-bold flex items-center gap-xs shadow-md hover:shadow-lg transition-all duration-200" type="button">
<span class="material-symbols-outlined text-[18px]">add</span>
<span class="">+ 新增托管客户</span>
</button>
</div>
</div>
<!-- 4 Industrial Metric KPI Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mt-1"><div class="p-4 rounded-xl bg-surface shadow-sm border border-border-rule/40 flex items-center justify-between"><div class="flex flex-col gap-1"><span class="font-caption-sm text-caption-sm text-text-muted">托管客户总数</span><div class="flex items-baseline gap-2"><span class="font-headline-md text-headline-md font-bold text-on-surface tracking-tight">128</span><span class="font-caption-sm text-caption-sm text-text-muted">家</span></div><div class="flex items-center gap-1 font-caption-sm text-[12px] text-tertiary font-medium"><span class="material-symbols-outlined text-[14px]">trending_up</span><span class="">本月新增 +4 · 覆盖全省 8 地市</span></div></div><div class="w-10 h-10 rounded-xl bg-primary/10 text-primary flex items-center justify-center shrink-0"><span class="material-symbols-outlined text-[22px]">domain</span></div></div><div class="p-4 rounded-xl bg-surface shadow-sm border border-border-rule/40 flex items-center justify-between"><div class="flex flex-col gap-1"><span class="font-caption-sm text-caption-sm text-text-muted">接入智能建筑数</span><div class="flex items-baseline gap-2"><span class="font-headline-md text-headline-md font-bold text-on-surface tracking-tight">342</span><span class="font-caption-sm text-caption-sm text-text-muted">栋</span></div><div class="flex items-center gap-1 font-caption-sm text-[12px] text-text-muted"><span class="material-symbols-outlined text-[14px] text-primary">verified</span><span class="">高校·三甲·园区 100% 空间建模</span></div></div><div class="w-10 h-10 rounded-xl bg-secondary/10 text-secondary flex items-center justify-center shrink-0"><span class="material-symbols-outlined text-[22px]">apartment</span></div></div><div class="p-4 rounded-xl bg-surface shadow-sm border border-border-rule/40 flex items-center justify-between"><div class="flex flex-col gap-1"><span class="font-caption-sm text-caption-sm text-text-muted">监控配电回路</span><div class="flex items-baseline gap-2"><span class="font-headline-md text-headline-md font-bold text-on-surface tracking-tight">3,676</span><span class="font-caption-sm text-caption-sm text-text-muted">路</span></div><div class="flex items-center gap-1 font-caption-sm text-[12px] text-tertiary font-medium"><span class="w-2 h-2 rounded-full bg-tertiary inline-block mr-0.5"></span><span class="">在线监测率 99.1% · 34 闭环</span></div></div><div class="w-10 h-10 rounded-xl bg-tertiary/10 text-tertiary flex items-center justify-center shrink-0"><span class="material-symbols-outlined text-[22px]">bolt</span></div></div><div class="p-4 rounded-xl bg-surface shadow-sm border border-border-rule/40 flex items-center justify-between"><div class="flex flex-col gap-1"><span class="font-caption-sm text-caption-sm text-text-muted">小程序授权客户</span><div class="flex items-baseline gap-2"><span class="font-headline-md text-headline-md font-bold text-on-surface tracking-tight">216</span><span class="font-caption-sm text-caption-sm text-text-muted">人</span></div><div class="flex items-center gap-1 font-caption-sm text-[12px] text-text-muted"><span class="material-symbols-outlined text-[14px] text-secondary">smartphone</span><span class="">7日活跃 189 人 · JWT认证正常</span></div></div><div class="w-10 h-10 rounded-xl bg-surface-container text-on-surface-variant flex items-center justify-center shrink-0"><span class="material-symbols-outlined text-[22px]">group</span></div></div></div></div>
<!-- Main Work Area (Filter + Split View) -->
<div class="p-lg flex flex-col gap-lg">
<!-- Filter Card -->
<div class="rounded-xl bg-surface p-5 shadow-sm flex flex-col gap-4">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
<!-- Search Keyword -->
<div class="flex flex-col gap-1.5">
<label class="font-caption-sm text-caption-sm text-text-muted">关键词检索</label>
<div class="relative flex items-center">
<span class="material-symbols-outlined absolute left-sm text-text-muted text-[18px]">search</span>
<input class="w-full h-10 pl-xl pr-sm rounded-lg bg-surface-container-low text-on-surface placeholder:text-text-muted text-body-md outline-none focus:ring-1 focus:ring-primary focus:bg-surface transition-all" placeholder="客户名称 / 统一代码 / 负责人" type="text" value="武汉软件工程">
</div>
</div>
<!-- Province / Region Cascader -->
<div class="flex flex-col gap-1.5">
<label class="font-caption-sm text-caption-sm text-text-muted">所属行政区划</label>
<div class="relative flex items-center">
<span class="material-symbols-outlined absolute left-sm text-text-muted text-[18px]">location_on</span>
<select class="w-full h-10 pl-xl pr-md rounded-lg bg-surface-container-low text-on-surface text-body-md outline-none appearance-none focus:ring-1 focus:ring-primary">
<option>全省范围 (湖北省)</option>
<option selected="">武汉市 · 江夏区</option>
<option>武汉市 · 洪山区</option>
<option>武汉市 · 武昌区</option>
<option>宜昌市 · 西陵区</option>
<option>襄阳市 · 襄城区</option>
</select>
<span class="material-symbols-outlined absolute right-sm text-text-muted pointer-events-none text-[18px]">expand_more</span>
</div>
</div>
<!-- Industry Type -->
<div class="flex flex-col gap-1.5">
<label class="font-caption-sm text-caption-sm text-text-muted">行业属性</label>
<div class="relative flex items-center">
<span class="material-symbols-outlined absolute left-sm text-text-muted text-[18px]">category</span>
<select class="w-full h-10 pl-xl pr-md rounded-lg bg-surface-container-low text-on-surface text-body-md outline-none appearance-none focus:ring-1 focus:ring-primary">
<option>全部行业大类</option>
<option selected="">教育科研 (高校 / 职业院校)</option>
<option>医疗卫生 (三甲 / 专科)</option>
<option>高新产业园 (光谷)</option>
<option>商业综合体</option>
<option>党政机关 / 市政公用</option>
</select>
<span class="material-symbols-outlined absolute right-sm text-text-muted pointer-events-none text-[18px]">expand_more</span>
</div>
</div>
<!-- Contract Status -->
<div class="flex flex-col gap-1.5">
<label class="font-caption-sm text-caption-sm text-text-muted">托管服务状态</label>
<div class="relative flex items-center">
<span class="material-symbols-outlined absolute left-sm text-text-muted text-[18px]">verified</span>
<select class="w-full h-10 pl-xl pr-md rounded-lg bg-surface-container-low text-on-surface text-body-md outline-none appearance-none focus:ring-1 focus:ring-primary">
<option>全部状态</option>
<option selected="">正常托管中</option>
<option>方案施工 / 网关调试</option>
<option>竣工待验收</option>
<option>合同即将到期 (30天内)</option>
</select>
<span class="material-symbols-outlined absolute right-sm text-text-muted pointer-events-none text-[18px]">expand_more</span>
</div>
</div>
<!-- Sales Rep & Operations -->
<div class="flex flex-col gap-1.5">
<label class="font-caption-sm text-caption-sm text-text-muted">责任业务经理</label>
<div class="relative flex items-center">
<span class="material-symbols-outlined absolute left-sm text-text-muted text-[18px]">person_pin</span>
<select class="w-full h-10 pl-xl pr-md rounded-lg bg-surface-container-low text-on-surface text-body-md outline-none appearance-none focus:ring-1 focus:ring-primary">
<option>全部工程师/业务经理</option>
<option selected="">陈建军 (业务一部)</option>
<option>李明 (医疗保障组)</option>
<option>王工 (园区电务)</option>
</select>
<span class="material-symbols-outlined absolute right-sm text-text-muted pointer-events-none text-[18px]">expand_more</span>
</div>
</div>
</div>
<!-- Action Bottom in Filter -->
<div class="flex items-center justify-between pt-2 border-t border-border-rule/40 flex-wrap gap-2">
<div class="flex items-center gap-2 flex-wrap">
<span class="font-caption-sm text-caption-sm text-text-muted mr-1">快速标签过滤:</span>
<span class="px-2.5 py-1 rounded-full bg-primary text-on-primary font-caption-sm text-[12px] cursor-pointer whitespace-nowrap shadow-xs">全部重点单位 (48)</span>
<span class="px-2.5 py-1 rounded-full bg-surface-container text-on-surface-variant hover:bg-surface-variant font-caption-sm text-[12px] cursor-pointer whitespace-nowrap transition-colors">含高压一次配电</span>
<span class="px-2.5 py-1 rounded-full bg-surface-container text-on-surface-variant hover:bg-surface-variant font-caption-sm text-[12px] cursor-pointer whitespace-nowrap transition-colors">大华边缘网关异常 (0)</span>
</div>
<div class="flex items-center gap-sm ml-auto">
<button class="px-4 py-1.5 rounded-lg bg-surface hover:bg-surface-container-low text-text-muted font-label-bold text-label-bold transition-colors" type="button">重置</button>
<button class="px-5 py-1.5 rounded-lg bg-primary text-on-primary hover:bg-primary-container font-label-bold text-label-bold flex items-center gap-xs shadow-sm transition-colors" type="button">
<span class="material-symbols-outlined text-[16px]">tune</span>
<span class="">查询检索</span>
</button>
</div>
</div>
</div>
<!-- Split Master-Detail Layout (Optimized ratio: 69% / 31% on 2xl, 8 cols / 4 cols on xl) -->
<div class="w-full flex flex-col gap-sm"><div class="bg-surface rounded-xl shadow-sm overflow-hidden flex flex-col"><div class="p-4 bg-surface flex items-center justify-between border-b border-border-rule/50"><div class="flex items-center gap-3"><span class="font-title-sm text-title-sm text-on-surface">托管客户清单</span><span class="px-2 py-0.5 rounded bg-surface-container text-on-surface-variant font-data-mono text-[12px]">共 128 家 / 当前显示 4 家</span></div><div class="flex items-center gap-1.5 text-text-muted"><button class="p-1.5 rounded-lg hover:bg-surface-container-low text-on-surface-variant transition-colors" title="刷新列表"><span class="material-symbols-outlined text-[18px]">refresh</span></button><button class="p-1.5 rounded-lg hover:bg-surface-container-low text-on-surface-variant transition-colors" title="自定义表头列"><span class="material-symbols-outlined text-[18px]">view_column</span></button></div></div><div class="overflow-x-auto"><table class="w-full text-left border-collapse min-w-[760px]"><thead><tr class="bg-surface-container-low/70 text-text-muted font-caption-sm text-caption-sm border-b border-border-rule/40"><th class="py-3.5 px-5 font-semibold w-[26%]">客户主体与性质</th><th class="py-3.5 px-4 font-semibold w-[25%]">建筑/配电拓扑</th><th class="py-3.5 px-4 font-semibold w-[18%]">托管周期 &amp; 费用</th><th class="py-3.5 px-4 font-semibold w-[16%]">ICC设备状态</th><th class="py-3.5 px-5 font-semibold w-[15%] text-right pr-5">空间拓扑与操作</th></tr></thead><tbody class="divide-y divide-border-rule/30 text-body-md text-on-surface"><tr class="bg-surface-container/30 hover:bg-surface-container/60 transition-colors cursor-pointer"><td class="py-4 px-5 align-top"><div class="flex items-start gap-3"><div class="w-10 h-10 rounded-xl bg-primary/10 text-primary flex-shrink-0 flex items-center justify-center"><span class="material-symbols-outlined text-[22px]">school</span></div><div class="flex flex-col gap-1 min-w-0"><div class="flex items-center gap-2 flex-wrap"><span class="font-label-bold text-[14px] text-on-surface hover:text-primary whitespace-nowrap">武汉软件工程职业学院</span><span class="px-2 py-0.5 rounded bg-primary text-on-primary font-caption-sm text-[11px] whitespace-nowrap">A级重要</span></div><span class="font-data-mono text-[12px] text-text-muted">PRJ-HB-WH-00192 · 高教职业本科</span><span class="font-caption-sm text-[12px] text-text-muted flex items-center gap-1"><span class="material-symbols-outlined text-[14px] text-primary/70 shrink-0">place</span><span class="truncate">江夏区藏龙岛科技园光谷大道117号</span></span></div></div></td><td class="py-4 px-4 align-top"><div class="flex flex-col gap-1.5"><div class="flex items-center gap-2"><span class="font-label-bold text-[13px] text-on-surface">5 栋建筑空间</span><span class="px-1.5 py-0.5 bg-surface-container-high rounded text-text-muted font-data-mono text-[11px]">36回路</span></div><span class="font-caption-sm text-[12px] text-text-muted">宿舍楼、实训中心A/B、配电总房</span><div class="flex items-center gap-2 mt-0.5 font-data-mono text-[11px] text-text-muted"><span class="px-1.5 py-0.5 bg-surface-container-low rounded">48台断路器</span><span class="px-1.5 py-0.5 bg-surface-container-low rounded">4台网关</span></div></div></td><td class="py-4 px-4 align-top"><div class="flex flex-col gap-1"><span class="font-data-mono text-label-bold text-on-surface text-[13px]">2024.03 - 2027.03</span><span class="font-caption-sm text-[12px] text-text-muted">3年全托管 · ¥18.6万/年</span><span class="font-caption-sm text-[12px] text-on-surface-variant flex items-center gap-1"><span class="material-symbols-outlined text-[13px] text-text-muted">badge</span><span class="">经理: 陈建军</span></span></div></td><td class="py-4 px-4 align-top"><div class="flex flex-col gap-1"><div class="flex items-center gap-1.5"><span class="w-2 h-2 rounded-full bg-tertiary"></span><span class="font-label-bold text-[12px] text-tertiary whitespace-nowrap">已联通 4/4 台</span></div><span class="font-data-mono text-[11px] text-text-muted">ICC: 192.168.10.88</span><span class="px-2 py-0.5 rounded bg-tertiary/10 text-tertiary font-caption-sm text-[11px] inline-flex items-center w-fit gap-1 mt-1"><span class="w-1.5 h-1.5 rounded-full bg-tertiary"></span><span class="whitespace-nowrap">正常托管中</span></span></div></td><td class="py-4 px-5 align-top text-right"><div class="flex flex-col items-end gap-1.5"><div class="flex items-center gap-1.5"><button class="px-3 py-1.5 rounded-lg bg-primary text-on-primary font-label-bold text-[12px] hover:bg-primary-container shadow-xs whitespace-nowrap transition-colors flex items-center gap-1"><span class="material-symbols-outlined text-[14px]">account_tree</span><span class="">空间拓扑</span></button><button class="px-2.5 py-1.5 rounded-lg bg-surface-container hover:bg-surface-variant teLine truncated
</div>
</div>
<script>
function switchTab(tabIndex) {
// Buttons styling
const tabs = [1, 2, 3];
tabs.forEach(i => {
const btn = document.getElementById('tabBtn' + i);
const content = document.getElementById('tabContent' + i);
if (i === tabIndex) {
btn.classList.add('text-primary', 'border-primary');
btn.classList.remove('text-text-muted', 'border-transparent');
content.classList.remove('hidden');
} else {
btn.classList.remove('text-primary', 'border-primary');
btn.classList.add('text-text-muted', 'border-transparent');
content.classList.add('hidden');
}
});
}
function toggleDrawer() {
const drawer = document.getElementById('detailDrawer');
drawer.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function selectRow(index) {
// Visual feedback micro-interaction
const syncIcon = document.getElementById('syncIcon');
syncIcon.classList.add('animate-spin');
setTimeout(() => {
syncIcon.classList.remove('animate-spin');
}, 400);
}
</script></main></div>
</body></html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

@@ -0,0 +1,200 @@
---
name: Industrial Precision
colors:
surface: '#ffffff'
surface-dim: '#ccdbf1'
surface-bright: '#f8f9ff'
surface-container-lowest: '#ffffff'
surface-container-low: '#eef4ff'
surface-container: '#e5efff'
surface-container-high: '#dbe9ff'
surface-container-highest: '#d5e4fa'
on-surface: '#0d1c2c'
on-surface-variant: '#424655'
inverse-surface: '#233142'
inverse-on-surface: '#e9f1ff'
outline: '#727787'
outline-variant: '#c2c6d8'
surface-tint: '#0056d2'
primary: '#0054cd'
on-primary: '#ffffff'
primary-container: '#136bfb'
on-primary-container: '#fefcff'
inverse-primary: '#b2c5ff'
secondary: '#712ae2'
on-secondary: '#ffffff'
secondary-container: '#8a4cfc'
on-secondary-container: '#fffbff'
tertiary: '#00685f'
on-tertiary: '#ffffff'
tertiary-container: '#008378'
on-tertiary-container: '#f4fffc'
error: '#ba1a1a'
on-error: '#ffffff'
error-container: '#ffdad6'
on-error-container: '#93000a'
primary-fixed: '#dae2ff'
primary-fixed-dim: '#b2c5ff'
on-primary-fixed: '#001847'
on-primary-fixed-variant: '#0040a1'
secondary-fixed: '#eaddff'
secondary-fixed-dim: '#d2bbff'
on-secondary-fixed: '#25005a'
on-secondary-fixed-variant: '#5a00c6'
tertiary-fixed: '#89f5e7'
tertiary-fixed-dim: '#6bd8cb'
on-tertiary-fixed: '#00201d'
on-tertiary-fixed-variant: '#005049'
background: '#f8f9ff'
on-background: '#0d1c2c'
surface-variant: '#d5e4fa'
bg-page: '#eef3f8'
text-muted: '#5b6b7e'
border-rule: '#dbe4ee'
status-warn: '#e0852b'
status-error: '#dc2626'
accent-subtle: rgba(29,111,255,0.08)
typography:
display-lg:
fontFamily: hankenGrotesk
fontSize: 32px
fontWeight: '700'
lineHeight: 40px
headline-md:
fontFamily: hankenGrotesk
fontSize: 23px
fontWeight: '700'
lineHeight: 28px
title-sm:
fontFamily: hankenGrotesk
fontSize: 17px
fontWeight: '700'
lineHeight: 24px
body-md:
fontFamily: hankenGrotesk
fontSize: 15px
fontWeight: '400'
lineHeight: 24px
label-bold:
fontFamily: hankenGrotesk
fontSize: 13px
fontWeight: '700'
lineHeight: 16px
data-mono:
fontFamily: jetbrainsMono
fontSize: 12px
fontWeight: '500'
lineHeight: 16px
caption-sm:
fontFamily: hankenGrotesk
fontSize: 12px
fontWeight: '700'
lineHeight: 14px
headline-md-mobile:
fontFamily: hankenGrotesk
fontSize: 20px
fontWeight: '700'
lineHeight: 26px
rounded:
sm: 0.25rem
DEFAULT: 0.5rem
md: 0.75rem
lg: 1rem
xl: 1.5rem
full: 9999px
spacing:
base: 4px
xs: 8px
sm: 12px
md: 16px
lg: 24px
xl: 40px
container-max: 1200px
gutter: 16px
---
## Brand & Style
The design system is engineered for the **Xintong Smart Power Operation Management APP**, targeting power engineers and grid operators. The brand personality is **Professional, Efficient, and Industrial Tech**. It emphasizes reliability through a structured, high-density interface that transforms complex electrical data into actionable insights.
The design style is a blend of **Corporate Modern** and **Information-Dense Minimalism**. It utilizes a "Layered Surface" approach where data is organized into logical card blocks, using subtle elevations and clear semantic color-coding to denote system status. The aesthetic is clean and functional, avoiding decorative elements in favor of technical precision and rapid scanability.
- **Professionalism:** High-contrast typography and a restrained color palette.
- **Efficiency:** Condensed layouts that prioritize data visualization and task status.
- **Reliability:** Solid borders, consistent spacing, and clear state indicators.
## Colors
The color architecture is strictly functional, mapping specifically to the operation layers of the power management system.
- **Primary (Deep Blue):** Reserved for core branding, navigation, and "Client" layer interactions.
- **Secondary (Purple):** Dedicated to "Gateway" operations and task-specific management.
- **Tertiary (Teal):** Used for "Service" layer success states and optimized performance indicators.
- **Neutral (Ink):** Used for primary text to ensure maximum legibility against light surfaces.
**Semantic Usage:**
- Use **status-warn (Amber)** for non-critical alerts or data synchronization states.
- Use **status-error (Red)** for critical power failures or permission denials.
- The **bg-page** provides a soft, low-strain canvas for long-duration monitoring, while **surface** (White) distinguishes interactive cards and data containers.
## Typography
This design system uses **Hanken Grotesk** as the primary typeface for its sharp, contemporary feel that bridges the gap between industrial and tech. **JetBrains Mono** is employed for technical data, device IDs, and numerical values to emphasize the system's engineering DNA.
- **Hierarchy:** Use `display-lg` for dashboard summaries. `title-sm` is the default for card headers.
- **Data Visualization:** All tabular data and technical strings must use `data-mono` for character alignment and clarity.
- **Mobile Adaptation:** For mobile screens, headlines should scale down to `headline-md-mobile` to maintain visual balance while preserving authority.
## Layout & Spacing
The layout utilizes a **Flexible Grid System** with a focus on logical grouping through card-based containment.
- **Grid:** A 12-column grid is used for desktop views, transitioning to a single-column stacked layout for mobile.
- **Rhythm:** An 8px-based spacing scale ensures vertical consistency. Use `lg (24px)` for outer card padding and `md (16px)` for internal element spacing.
- **Mobile Margins:** Standard mobile side margins are set to `md (16px)` to maximize screen real estate for dense data tables.
- **Sectioning:** Distinct operational modules should be separated by `xl (40px)` of vertical space to prevent cognitive overload.
## Elevation & Depth
This design system conveys hierarchy through **Tonal Layering** and **Subtle Ambient Shadows**. We avoid heavy skeuomorphism in favor of a "Flat-Plus" approach.
- **Base Layer:** The page background (`bg-page`) sits at the lowest elevation.
- **Mid Layer (Cards):** UI cards use a white surface with a subtle shadow (`0 4px 14px rgba(0,0,0,.07)`) to appear slightly lifted.
- **Interactive Layer:** On hover or active state, cards may transition to a higher elevation (`0 8px 28px rgba(0,0,0,.1)`) with a slight upward translation of -2px.
- **Overlays:** Modals and dropdowns use the highest z-index with a distinct backdrop blur to maintain focus on the task at hand.
- **Gradients:** Use soft radial gradients in the background (Blue/Purple) to create environmental depth without interfering with content legibility.
## Shapes
The shape language is **Rounded**, balancing industrial rigidity with modern software approachability.
- **Cards & Sections:** Use the standard `rounded-lg (1rem)` for primary containers.
- **Nodes & Blocks:** Architecture and data nodes use `rounded (0.5rem)` to fit more compactly within layouts.
- **Interactive Elements:** Buttons and tags use a "Pill-shape" (`rounded-xl` or 999px) to distinguish them from structural containers.
- **Borders:** Use `1px solid` with the `border-rule` color for most containers. Active states or primary focus areas may increase to `1.5px`.
## Components
**Buttons:**
- **Primary:** Solid `primary_color` with white text. Pill-shaped.
- **Secondary:** Transparent with `primary_color` border or subtle purple tint for operation tasks.
- **Danger:** Solid `status-error` for irreversible actions.
**Cards:**
- White background, `1px` border-rule, and `rounded-lg` corners.
- Headers should have a `18px` internal padding and a bottom border to separate from the content body.
**Input Fields:**
- Soft white background with `1px` border.
- Focused states should use a `primary_color` border with a subtle outer glow.
**Chips & Tags:**
- Used for status (e.g., "Active", "Offline").
- Backgrounds should use `8% opacity` versions of the semantic colors (Blue, Green, Red) with bolded text in the 100% opacity color.
**Data Visualization:**
- Charts should use the primary blue and secondary purple as the main data series colors.
- Use `data-mono` for all axis labels and tooltips to ensure technical accuracy.
**Status Indicators:**
- Small `7px` circular dots used within lists or architecture nodes to show real-time connectivity status.
@@ -0,0 +1,788 @@
<!DOCTYPE html>
<html lang="zh-CN"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>新增客户档案 - 鑫潼智电 XT-POWER OS</title>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=hankenGrotesk:wght@100..900&amp;family=JetBrains+Mono:wght@400..700&amp;display=swap" rel="stylesheet"/>
<style>
@layer base {
html, body { margin: 0; padding: 0; height: 100%; }
body { overscroll-behavior: none; }
}
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.12); border-radius: 4px; }
::-webkit-scrollbar-track { background: transparent; }
</style>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
colors: {
"surface": "#ffffff",
"primary": "#0054cd",
"primary-container": "#136bfb",
"primary-fixed": "#dae2ff",
"primary-fixed-dim": "#b2c5ff",
"on-primary": "#ffffff",
"on-primary-container": "#fefcff",
"bg-page": "#eef3f8",
"background": "#f8f9ff",
"border-rule": "#e2e8f0",
"outline": "#727787",
"outline-variant": "#c2c6d8",
"text-muted": "#5b6b7e",
"on-surface": "#0d1c2c",
"on-surface-variant": "#424655",
"surface-container": "#e5efff",
"surface-container-low": "#eef4ff",
"surface-container-lowest": "#ffffff",
"surface-container-high": "#dbe9ff",
"surface-container-highest": "#d5e4fa",
"surface-variant": "#d5e4fa",
"inverse-surface": "#0f172a",
"inverse-on-surface": "#e2e8f0",
"tertiary": "#00685f",
"tertiary-container": "#008378",
"tertiary-fixed": "#89f5e7",
"secondary": "#712ae2",
"secondary-container": "#8a4cfc",
"status-warn": "#e0852b",
"status-error": "#dc2626",
"error": "#ba1a1a"
},
borderRadius: {
DEFAULT: "0.25rem",
lg: "0.5rem",
xl: "0.75rem",
full: "9999px"
},
fontFamily: {
"body-md": ["hankenGrotesk", "sans-serif"],
"headline-md": ["hankenGrotesk", "sans-serif"],
"label-bold": ["hankenGrotesk", "sans-serif"],
"title-sm": ["hankenGrotesk", "sans-serif"],
"display-lg": ["hankenGrotesk", "sans-serif"],
"caption-sm": ["hankenGrotesk", "sans-serif"],
"data-mono": ["JetBrains Mono", "monospace"]
}
}
}
};
</script>
</head>
<body class="bg-bg-page font-body-md text-on-surface antialiased flex">
<!-- Left Sidebar (240px wide, deep industrial dark navy) -->
<aside class="fixed left-0 top-0 bottom-0 w-60 bg-inverse-surface text-inverse-on-surface z-50 flex flex-col justify-between select-none shadow-[0_1px_8px_rgba(0,0,0,0.08)] border-r border-slate-800">
<div class="flex flex-col flex-1 min-h-0">
<!-- Logo Branding Header -->
<div class="h-16 px-4 flex items-center gap-3 bg-inverse-surface border-b border-slate-800">
<div class="w-9 h-9 rounded-lg bg-primary flex items-center justify-center text-white shadow-md shadow-primary/30">
<span class="material-symbols-outlined text-[22px]">bolt</span>
</div>
<div class="flex flex-col">
<div class="flex items-center gap-1.5">
<span class="font-headline-md text-base font-bold text-white tracking-wide">XT-POWER</span>
<span class="font-caption-sm text-[10px] bg-primary px-1.5 py-0.5 rounded text-white font-bold leading-none">OS</span>
</div>
<span class="font-caption-sm text-[11px] text-slate-400 tracking-wider mt-0.5">鑫潼智电 · 智能配用电中枢</span>
</div>
</div>
<!-- Navigation Tree -->
<div class="flex-1 overflow-y-auto px-2 py-3">
<div class="px-3 py-1 mb-1">
<span class="font-caption-sm text-[11px] text-slate-400 font-bold uppercase tracking-wider">综合运营管控</span>
</div>
<nav class="flex flex-col gap-1">
<!-- 运营工作台 -->
<a class="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-400 hover:bg-slate-800/60 hover:text-white transition-colors group" href="#">
<span class="material-symbols-outlined text-[19px] group-hover:text-white">dashboard</span>
<span class="font-label-bold text-xs">运营工作台</span>
</a>
<!-- 客户管理 (Active Accordion) -->
<div class="flex flex-col">
<a class="flex items-center justify-between px-3 py-2 rounded-lg bg-primary text-white font-label-bold text-xs shadow-sm transition-colors" href="#">
<div class="flex items-center gap-3">
<span class="material-symbols-outlined text-[19px]">corporate_fare</span>
<span>客户管理</span>
</div>
<span class="material-symbols-outlined text-[16px]">expand_more</span>
</a>
<!-- Submenu Items -->
<div class="flex flex-col ml-4 pl-3 mt-1 mb-1 gap-1 border-l border-slate-700">
<a class="px-2.5 py-1.5 rounded-md text-slate-300 hover:text-white font-label-bold text-[12px] transition-colors flex items-center gap-2" href="#">
<span class="w-1.5 h-1.5 rounded-full bg-slate-500"></span>
<span>客户档案列表</span>
</a>
<a class="px-2.5 py-1.5 rounded-md bg-white/10 text-white font-label-bold text-[12px] transition-colors flex items-center gap-2 shadow-xs" href="#">
<span class="w-1.5 h-1.5 rounded-full bg-tertiary-fixed"></span>
<span>新增客户档案</span>
</a>
<a class="px-2.5 py-1.5 rounded-md text-slate-400 hover:text-white font-label-bold text-[12px] transition-colors flex items-center gap-2" href="#">
<span class="w-1.5 h-1.5 rounded-full bg-slate-600"></span>
<span>空间拓扑与用电架构</span>
</a>
<a class="px-2.5 py-1.5 rounded-md text-slate-400 hover:text-white font-label-bold text-[12px] transition-colors flex items-center gap-2" href="#">
<span class="w-1.5 h-1.5 rounded-full bg-slate-600"></span>
<span>微信小程序授权</span>
</a>
</div>
</div>
<!-- 项目管理 -->
<a class="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-400 hover:bg-slate-800/60 hover:text-white transition-colors group" href="#">
<span class="material-symbols-outlined text-[19px] group-hover:text-white">lan</span>
<span class="font-label-bold text-xs">项目管理</span>
</a>
<!-- 运营派单 -->
<a class="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-400 hover:bg-slate-800/60 hover:text-white transition-colors group" href="#">
<span class="material-symbols-outlined text-[19px] group-hover:text-white">assignment_turned_in</span>
<span class="font-label-bold text-xs">运营派单</span>
</a>
<!-- 设备监控 -->
<a class="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-400 hover:bg-slate-800/60 hover:text-white transition-colors group" href="#">
<span class="material-symbols-outlined text-[19px] group-hover:text-white">sensors</span>
<span class="font-label-bold text-xs">设备监控</span>
</a>
<!-- 统计报表 -->
<a class="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-400 hover:bg-slate-800/60 hover:text-white transition-colors group" href="#">
<span class="material-symbols-outlined text-[19px] group-hover:text-white">query_stats</span>
<span class="font-label-bold text-xs">统计报表</span>
</a>
<!-- 系统管理 -->
<a class="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-400 hover:bg-slate-800/60 hover:text-white transition-colors group" href="#">
<span class="material-symbols-outlined text-[19px] group-hover:text-white">settings</span>
<span class="font-label-bold text-xs">系统管理</span>
</a>
</nav>
</div>
</div>
<!-- Sidebar Bottom Telemetry Micro-Card -->
<div class="p-3 m-2.5 rounded-lg bg-slate-800/80 border border-slate-700/60 flex flex-col gap-1.5">
<div class="flex items-center justify-between">
<div class="flex items-center gap-1.5">
<span class="relative flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-tertiary-fixed opacity-75"></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-emerald-400"></span>
</span>
<span class="font-caption-sm text-[11px] text-slate-200 font-medium">边缘网关通信链路</span>
</div>
<span class="font-data-mono text-[11px] text-tertiary-fixed font-semibold">在线 24/24</span>
</div>
<div class="flex items-center justify-between font-data-mono text-[11px] text-slate-400">
<span>时延: 14ms</span>
<span>丢包: 0.00%</span>
</div>
</div>
</aside>
<!-- Right Application Canvas -->
<div class="pl-60 flex flex-col flex-1 min-h-screen">
<!-- Top Professional Header Bar -->
<header class="fixed top-0 left-60 right-0 h-16 bg-surface/95 backdrop-blur-md z-40 border-b border-border-rule px-6 flex items-center justify-between gap-4 shadow-[0_1px_4px_rgba(0,0,0,0.03)]">
<!-- Search & Status Section -->
<div class="flex items-center gap-4 flex-1 max-w-2xl">
<div class="flex items-center gap-1.5 px-3 py-1 rounded-full bg-emerald-50 border border-emerald-200/80 shrink-0">
<span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
<span class="font-caption-sm text-[12px] text-emerald-700 font-bold">ICC 实时数据同步正常</span>
<span class="font-data-mono text-[11px] text-slate-400 ml-1">120FPS</span>
</div>
<div class="h-4 w-px bg-slate-200"></div>
<div class="relative flex-1">
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 text-[18px]">search</span>
<input class="w-full pl-9 pr-16 py-1.5 rounded-lg bg-slate-50 border border-slate-200 text-xs text-on-surface placeholder:text-slate-400 focus:outline-none focus:bg-white focus:border-primary transition-all shadow-xs" placeholder="按项目号 / 变电站 / 工单编号检索..." type="text"/>
<span class="absolute right-2.5 top-1/2 -translate-y-1/2 px-1.5 py-0.5 rounded bg-slate-200/70 text-[10px] font-data-mono text-slate-500">Ctrl + K</span>
</div>
</div>
<!-- User Profile & Auxiliary Tools -->
<div class="flex items-center gap-3">
<button class="relative p-2 rounded-lg text-slate-600 hover:bg-slate-100 transition-colors" title="通知提醒" type="button">
<span class="material-symbols-outlined text-[20px]">notifications</span>
<span class="absolute top-1 right-1 flex h-4 w-4 items-center justify-center rounded-full bg-status-error text-[10px] font-bold text-white">3</span>
</button>
<button class="p-2 rounded-lg text-slate-600 hover:bg-slate-100 transition-colors" title="应用矩阵" type="button">
<span class="material-symbols-outlined text-[20px]">apps</span>
</button>
<div class="h-5 w-px bg-slate-200"></div>
<div class="flex items-center gap-2 cursor-pointer p-1.5 rounded-lg hover:bg-slate-100 transition-colors">
<div class="w-8 h-8 rounded-full bg-primary flex items-center justify-center text-white shadow-xs">
<span class="material-symbols-outlined text-[18px]">person</span>
</div>
<div class="flex flex-col text-left">
<span class="font-label-bold text-xs text-on-surface leading-tight">李工</span>
<span class="font-caption-sm text-[11px] text-slate-500 leading-tight">主控工程师 · 调度中心</span>
</div>
<span class="material-symbols-outlined text-slate-400 text-[16px]">keyboard_arrow_down</span>
</div>
</div>
</header>
<!-- Main Workspace Container -->
<main class="w-full pt-20 px-6 pb-28 flex-1 bg-bg-page">
<div class="w-full max-w-[1400px] mx-auto flex flex-col gap-5">
<!-- Breadcrumbs & Master Header Section -->
<div class="flex flex-col gap-2">
<!-- Breadcrumb -->
<div class="flex items-center gap-1.5 text-xs text-slate-500">
<span class="hover:text-primary cursor-pointer transition-colors">运营管控体系</span>
<span class="material-symbols-outlined text-[13px]">chevron_right</span>
<span class="hover:text-primary cursor-pointer transition-colors">客户管理</span>
<span class="material-symbols-outlined text-[13px]">chevron_right</span>
<span class="hover:text-primary cursor-pointer transition-colors">客户档案列表</span>
<span class="material-symbols-outlined text-[13px]">chevron_right</span>
<span class="text-primary font-bold">新增客户档案</span>
</div>
<!-- Master Title & Quick Action Toolbar -->
<div class="flex flex-col lg:flex-row lg:items-center justify-between gap-4 bg-surface p-5 rounded-xl border border-border-rule shadow-sm">
<div class="flex flex-col gap-1">
<div class="flex items-center gap-2.5 flex-wrap">
<div class="w-2.5 h-6 bg-primary rounded-full"></div>
<h1 class="font-headline-md text-xl font-bold text-on-surface tracking-tight">新增客户档案</h1>
<span class="font-data-mono text-xs px-2 py-0.5 rounded bg-surface-container-high text-primary font-bold">CREATE CUSTOMER PROFILE</span>
<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full bg-emerald-50 text-emerald-700 border border-emerald-200 text-xs font-semibold">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
架构解耦机制已激活
</span>
</div>
<p class="font-body-md text-xs text-slate-500 pl-3.5">
纯粹客户基础信息建档 · 聚焦主体资质与基础用电参数,后续拓扑与设备配置已完全解耦。
</p>
</div>
<!-- Top Actions -->
<div class="flex items-center gap-2.5 flex-wrap">
<button class="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-lg bg-surface-container-low hover:bg-surface-container-high text-primary font-label-bold text-xs border border-primary/20 transition-all shadow-xs group" type="button">
<span class="material-symbols-outlined text-[17px] group-hover:scale-105 transition-transform">document_scanner</span>
<span>导入营业执照智能识别</span>
</button>
<button class="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-lg bg-slate-100 hover:bg-slate-200 text-slate-700 font-label-bold text-xs border border-slate-200 transition-all shadow-xs" type="button">
<span class="material-symbols-outlined text-[17px]">sync_alt</span>
<span>从意向客户导入</span>
</button>
<button class="px-3.5 py-1.5 rounded-lg bg-white border border-slate-200 text-slate-700 hover:bg-slate-50 font-label-bold text-xs transition-colors shadow-xs" type="button">
保存草稿
</button>
<button class="inline-flex items-center gap-1.5 px-4 py-1.5 rounded-lg bg-primary hover:bg-primary-container text-white font-label-bold text-xs transition-all shadow-sm shadow-primary/25" type="button">
<span>完成建档并进入空间拓扑</span>
<span class="material-symbols-outlined text-[16px]">arrow_forward</span>
</button>
</div>
</div>
</div>
<!-- Two-column Layout: 8:4 Grid -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-5 items-start">
<!-- Left Column (8 cols): Decoupled Core Form Cards -->
<div class="lg:col-span-8 flex flex-col gap-5">
<!-- SECTION 01: 客户主体与工商资质 -->
<section class="bg-surface rounded-xl border border-border-rule shadow-sm p-6 flex flex-col gap-4 transition-all hover:border-slate-300">
<div class="flex items-center justify-between pb-3 border-b border-border-rule">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center text-primary font-data-mono font-bold text-xs">
01
</div>
<div class="flex flex-col">
<div class="flex items-center gap-2">
<span class="font-data-mono text-xs font-bold text-slate-400 uppercase tracking-wide">SECTION 01</span>
<h2 class="font-title-sm text-base font-bold text-on-surface">客户主体与工商资质</h2>
</div>
<span class="font-caption-sm text-[11px] text-slate-500">国家发改委及国家电网要求之真实用电主体工商底册</span>
</div>
</div>
<span class="px-2.5 py-1 rounded bg-emerald-50 text-emerald-700 border border-emerald-200 text-xs font-semibold flex items-center gap-1">
<span class="material-symbols-outlined text-[14px]">verified</span>
全国工商业信息库直联已就绪
</span>
</div>
<!-- Form Inputs -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 pt-1">
<!-- 机构全称 -->
<div class="md:col-span-2 flex flex-col gap-1.5">
<label class="font-label-bold text-xs text-on-surface flex items-center justify-between">
<span>机构全称 (工商登记法定全称) <span class="text-status-error">*</span></span>
<span class="text-emerald-700 text-[11px] font-semibold flex items-center gap-0.5">
<span class="material-symbols-outlined text-[14px]">check_circle</span>
工商联网核验通过
</span>
</label>
<div class="relative flex items-center">
<input class="w-full pl-3 pr-20 py-2 rounded-lg bg-slate-50/70 border border-slate-200 text-xs text-on-surface focus:outline-none focus:bg-white focus:border-primary transition-all font-medium" type="text" value="武汉华星光电半导体显示技术有限公司"/>
<span class="absolute right-2 px-2 py-0.5 rounded bg-blue-100 text-primary font-data-mono text-[11px] font-semibold">已验资</span>
</div>
</div>
<!-- 统一社会信用代码 -->
<div class="flex flex-col gap-1.5">
<label class="font-label-bold text-xs text-on-surface flex items-center justify-between">
<span>统一社会信用代码 (18位) <span class="text-status-error">*</span></span>
<button class="text-primary text-[11px] font-semibold flex items-center gap-0.5 hover:underline" type="button">
<span class="material-symbols-outlined text-[13px]">center_focus_strong</span>
OCR识别
</button>
</label>
<input class="w-full px-3 py-2 rounded-lg bg-slate-50/70 border border-slate-200 font-data-mono text-xs text-on-surface uppercase focus:outline-none focus:bg-white focus:border-primary transition-all" maxlength="18" type="text" value="91420100MA4KMYXU72"/>
</div>
<!-- 所属行业门类 -->
<div class="flex flex-col gap-1.5">
<label class="font-label-bold text-xs text-on-surface">
所属行业门类 <span class="text-status-error">*</span>
</label>
<div class="relative flex items-center">
<select class="w-full px-3 py-2 rounded-lg bg-slate-50/70 border border-slate-200 text-xs text-on-surface appearance-none focus:outline-none focus:bg-white focus:border-primary transition-all">
<option selected="">先进制造 · 半导体与精密光电</option>
<option>生物医药 · 基因工程产业基地</option>
<option>高校科研 · 重点实验中心</option>
<option>医疗卫生 · 三级甲等综合医院</option>
<option>商务楼宇 · 超甲级智能总部大厦</option>
<option>市政交通 · 特级轨道交通枢纽</option>
</select>
<span class="material-symbols-outlined absolute right-2.5 text-slate-400 pointer-events-none text-[18px]">unfold_more</span>
</div>
</div>
<!-- 法定代表人姓名 -->
<div class="flex flex-col gap-1.5">
<label class="font-label-bold text-xs text-on-surface">
法定代表人姓名 <span class="text-status-error">*</span>
</label>
<input class="w-full px-3 py-2 rounded-lg bg-slate-50/70 border border-slate-200 text-xs text-on-surface focus:outline-none focus:bg-white focus:border-primary transition-all" placeholder="输入法定代表人姓名" type="text" value="赵宏伟"/>
</div>
<!-- 法定代表人联系电话 -->
<div class="flex flex-col gap-1.5">
<label class="font-label-bold text-xs text-on-surface">
法定代表人联系电话
</label>
<input class="w-full px-3 py-2 rounded-lg bg-slate-50/70 border border-slate-200 font-data-mono text-xs text-on-surface focus:outline-none focus:bg-white focus:border-primary transition-all" placeholder="座机或移动手机" type="tel" value="027-8790-8800"/>
</div>
<!-- 所属行政区划 (三级联动) -->
<div class="md:col-span-2 flex flex-col gap-1.5">
<label class="font-label-bold text-xs text-on-surface">
所属行政区划 (三级联动) <span class="text-status-error">*</span>
</label>
<div class="grid grid-cols-3 gap-3">
<div class="relative">
<select class="w-full px-3 py-2 rounded-lg bg-slate-50/70 border border-slate-200 text-xs text-on-surface appearance-none focus:outline-none focus:bg-white focus:border-primary">
<option selected="">湖北省</option>
</select>
<span class="material-symbols-outlined absolute right-2.5 top-2.5 text-slate-400 pointer-events-none text-[16px]">expand_more</span>
</div>
<div class="relative">
<select class="w-full px-3 py-2 rounded-lg bg-slate-50/70 border border-slate-200 text-xs text-on-surface appearance-none focus:outline-none focus:bg-white focus:border-primary">
<option selected="">武汉市</option>
</select>
<span class="material-symbols-outlined absolute right-2.5 top-2.5 text-slate-400 pointer-events-none text-[16px]">expand_more</span>
</div>
<div class="relative">
<select class="w-full px-3 py-2 rounded-lg bg-slate-50/70 border border-slate-200 text-xs text-on-surface appearance-none focus:outline-none focus:bg-white focus:border-primary">
<option selected="">洪山区 (东湖新技术开发区)</option>
<option>江夏区</option>
<option>武昌区</option>
</select>
<span class="material-symbols-outlined absolute right-2.5 top-2.5 text-slate-400 pointer-events-none text-[16px]">expand_more</span>
</div>
</div>
</div>
<!-- 实际用电厂区/建筑地址 -->
<div class="md:col-span-2 flex flex-col gap-1.5">
<label class="font-label-bold text-xs text-on-surface flex items-center justify-between">
<span>实际用电厂区/建筑地址 <span class="text-status-error">*</span></span>
<span class="text-slate-500 text-[11px] flex items-center gap-1">
<span class="material-symbols-outlined text-[14px] text-primary">pin_drop</span>
经纬度坐标点选已锁定
</span>
</label>
<div class="relative flex items-center">
<input class="w-full pl-3 pr-24 py-2 rounded-lg bg-slate-50/70 border border-slate-200 text-xs text-on-surface focus:outline-none focus:bg-white focus:border-primary transition-all font-medium" type="text" value="光谷智能制造产业园高新大道999号 华星光电武汉光谷显示基地 T3/T4厂区"/>
<button class="absolute right-1.5 px-2.5 py-1 rounded bg-blue-50 hover:bg-blue-100 text-primary font-label-bold text-[11px] flex items-center gap-1 border border-primary/20 transition-colors" type="button">
<span class="material-symbols-outlined text-[15px]">map</span>
<span>地图定标</span>
</button>
</div>
</div>
</div>
</section>
<!-- SECTION 02: 基础用电申报参数 -->
<section class="bg-surface rounded-xl border border-border-rule shadow-sm p-6 flex flex-col gap-4 transition-all hover:border-slate-300">
<div class="flex items-center justify-between pb-3 border-b border-border-rule">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-lg bg-amber-500/10 flex items-center justify-center text-amber-600 font-data-mono font-bold text-xs">
02
</div>
<div class="flex flex-col">
<div class="flex items-center gap-2">
<span class="font-data-mono text-xs font-bold text-slate-400 uppercase tracking-wide">SECTION 02</span>
<h2 class="font-title-sm text-base font-bold text-on-surface">基础用电申报参数</h2>
</div>
<span class="font-caption-sm text-[11px] text-slate-500">电网报装装见容量、受电电压与应急安全责任底册</span>
</div>
</div>
<span class="px-2.5 py-1 rounded bg-slate-100 text-slate-600 text-[11px] font-medium">
用于负荷容量规划与越限告警基线
</span>
</div>
<!-- Form Inputs -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 pt-1">
<!-- 供电局电网用电户号 -->
<div class="flex flex-col gap-1.5">
<label class="font-label-bold text-xs text-on-surface flex items-center justify-between">
<span>供电局电网用电户号 <span class="text-status-error">*</span></span>
<span class="text-emerald-700 text-[11px] font-semibold flex items-center gap-0.5">
<span class="material-symbols-outlined text-[13px]">verified_user</span>
户名校验一致
</span>
</label>
<input class="w-full px-3 py-2 rounded-lg bg-slate-50/70 border border-slate-200 font-data-mono text-xs text-on-surface tracking-wider focus:outline-none focus:bg-white focus:border-primary transition-all" placeholder="例如: 0982736192" type="text" value="0982736192"/>
</div>
<!-- 进线受电电压等级 -->
<div class="flex flex-col gap-1.5">
<label class="font-label-bold text-xs text-on-surface">
进线受电电压等级 <span class="text-status-error">*</span>
</label>
<div class="relative flex items-center">
<select class="w-full px-3 py-2 rounded-lg bg-slate-50/70 border border-slate-200 text-xs text-on-surface appearance-none focus:outline-none focus:bg-white focus:border-primary transition-all">
<option selected="">10kV 高压专线供电</option>
<option>35kV 变电站高压受电</option>
<option>110kV 专用变电站</option>
<option>380V 低压综合动力</option>
</select>
<span class="material-symbols-outlined absolute right-2.5 text-slate-400 pointer-events-none text-[18px]">unfold_more</span>
</div>
</div>
<!-- 报装装见总容量 -->
<div class="flex flex-col gap-1.5">
<label class="font-label-bold text-xs text-on-surface">
报装装见总容量 <span class="text-status-error">*</span>
</label>
<div class="relative flex items-center">
<input class="w-full pl-3 pr-12 py-2 rounded-lg bg-slate-50/70 border border-slate-200 font-data-mono text-xs text-on-surface focus:outline-none focus:bg-white focus:border-primary transition-all" type="number" value="16000"/>
<span class="absolute right-3 text-slate-400 font-data-mono text-xs pointer-events-none">kVA</span>
</div>
</div>
<!-- 变配电室规划总数 -->
<div class="flex flex-col gap-1.5">
<label class="font-label-bold text-xs text-on-surface">
变配电室规划总数 <span class="text-status-error">*</span>
</label>
<div class="relative flex items-center">
<input class="w-full pl-3 pr-12 py-2 rounded-lg bg-slate-50/70 border border-slate-200 font-data-mono text-xs text-on-surface focus:outline-none focus:bg-white focus:border-primary transition-all" type="number" value="3"/>
<span class="absolute right-3 text-slate-400 text-xs pointer-events-none">处所</span>
</div>
</div>
<!-- 规划用电建筑栋数 -->
<div class="flex flex-col gap-1.5">
<label class="font-label-bold text-xs text-on-surface">
规划用电建筑栋数 <span class="text-status-error">*</span>
</label>
<div class="relative flex items-center">
<input class="w-full pl-3 pr-12 py-2 rounded-lg bg-slate-50/70 border border-slate-200 font-data-mono text-xs text-on-surface focus:outline-none focus:bg-white focus:border-primary transition-all" type="number" value="5"/>
<span class="absolute right-3 text-slate-400 text-xs pointer-events-none">栋</span>
</div>
</div>
<!-- 计划监测回路预估数 -->
<div class="flex flex-col gap-1.5">
<label class="font-label-bold text-xs text-on-surface">
计划监测回路预估数 <span class="text-status-error">*</span>
</label>
<div class="relative flex items-center">
<input class="w-full pl-3 pr-12 py-2 rounded-lg bg-slate-50/70 border border-slate-200 font-data-mono text-xs text-on-surface focus:outline-none focus:bg-white focus:border-primary transition-all" type="number" value="84"/>
<span class="absolute right-3 text-slate-400 text-xs pointer-events-none">回路</span>
</div>
</div>
<!-- 客户方电气第一责任人 -->
<div class="flex flex-col gap-1.5">
<label class="font-label-bold text-xs text-on-surface">
客户方电气第一责任人 <span class="text-status-error">*</span>
</label>
<input class="w-full px-3 py-2 rounded-lg bg-slate-50/70 border border-slate-200 text-xs text-on-surface focus:outline-none focus:bg-white focus:border-primary transition-all" placeholder="例如: 陈明德 (动力部总监)" type="text" value="陈明德 (动力部总监)"/>
</div>
<!-- 24小时应急值班手机号 -->
<div class="md:col-span-2 flex flex-col gap-1.5">
<label class="font-label-bold text-xs text-on-surface flex items-center justify-between">
<span>24小时应急值班手机号 <span class="text-status-error">*</span></span>
<span class="text-slate-400 text-[11px]">用于接收调度令与严重故障短信推送</span>
</label>
<div class="relative flex items-center">
<span class="material-symbols-outlined absolute left-3 text-slate-400 text-[18px]">phone_in_talk</span>
<input class="w-full pl-9 pr-3 py-2 rounded-lg bg-slate-50/70 border border-slate-200 font-data-mono text-xs text-on-surface focus:outline-none focus:bg-white focus:border-primary transition-all" type="tel" value="139-7128-9921"/>
</div>
</div>
</div>
</section>
<!-- SECTION 03: 商务签约与托管服务级别 -->
<section class="bg-surface rounded-xl border border-border-rule shadow-sm p-6 flex flex-col gap-4 transition-all hover:border-slate-300">
<div class="flex items-center justify-between pb-3 border-b border-border-rule">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-lg bg-purple-500/10 flex items-center justify-center text-purple-600 font-data-mono font-bold text-xs">
03
</div>
<div class="flex flex-col">
<div class="flex items-center gap-2">
<span class="font-data-mono text-xs font-bold text-slate-400 uppercase tracking-wide">SECTION 03</span>
<h2 class="font-title-sm text-base font-bold text-on-surface">商务签约与托管服务级别</h2>
</div>
<span class="font-caption-sm text-[11px] text-slate-500">界定响应时限、巡检频次与责任客户经理权责</span>
</div>
</div>
<span class="px-2.5 py-1 rounded bg-purple-50 text-purple-700 border border-purple-200 text-xs font-semibold">
年度托管协议 (SLA)
</span>
</div>
<!-- SLA Tier Selection Cards -->
<div class="flex flex-col gap-2 pt-1">
<label class="font-label-bold text-xs text-on-surface">
鑫潼智电智能运维保障等级 (SLA 服务策略) <span class="text-status-error">*</span>
</label>
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-3">
<!-- A+ 特级保电 (Active) -->
<div class="relative p-3.5 rounded-xl border-2 border-primary bg-blue-50/50 flex flex-col gap-1.5 cursor-pointer transition-all shadow-xs">
<div class="flex items-center justify-between">
<span class="px-2 py-0.5 rounded bg-primary text-white font-data-mono text-[11px] font-bold">A+ 级</span>
<span class="material-symbols-outlined text-primary text-[18px]">radio_button_checked</span>
</div>
<span class="font-label-bold text-xs font-bold text-on-surface">特级保电托管</span>
<p class="font-caption-sm text-[11px] text-slate-500 leading-relaxed">
15分钟应急到场 · 7×24小时专家派单巡检 · 关键负荷双电源容灾
</p>
</div>
<!-- A 级重大保电 -->
<div class="relative p-3.5 rounded-xl border border-slate-200 hover:border-slate-300 bg-white flex flex-col gap-1.5 cursor-pointer transition-all">
<div class="flex items-center justify-between">
<span class="px-2 py-0.5 rounded bg-slate-100 text-slate-600 font-data-mono text-[11px] font-bold">A 级</span>
<span class="material-symbols-outlined text-slate-400 text-[18px]">radio_button_unchecked</span>
</div>
<span class="font-label-bold text-xs font-bold text-on-surface">重大用电保障</span>
<p class="font-caption-sm text-[11px] text-slate-500 leading-relaxed">
30分钟响应 · 每周常规物理巡检 · 负荷越限动态调优
</p>
</div>
<!-- B 级标准托管 -->
<div class="relative p-3.5 rounded-xl border border-slate-200 hover:border-slate-300 bg-white flex flex-col gap-1.5 cursor-pointer transition-all">
<div class="flex items-center justify-between">
<span class="px-2 py-0.5 rounded bg-slate-100 text-slate-600 font-data-mono text-[11px] font-bold">B 级</span>
<span class="material-symbols-outlined text-slate-400 text-[18px]">radio_button_unchecked</span>
</div>
<span class="font-label-bold text-xs font-bold text-on-surface">标准集中托管</span>
<p class="font-caption-sm text-[11px] text-slate-500 leading-relaxed">
2小时响应 · 每月预防性预防性维护 · 报表定期抄送
</p>
</div>
<!-- C 级基础监测 -->
<div class="relative p-3.5 rounded-xl border border-slate-200 hover:border-slate-300 bg-white flex flex-col gap-1.5 cursor-pointer transition-all">
<div class="flex items-center justify-between">
<span class="px-2 py-0.5 rounded bg-slate-100 text-slate-600 font-data-mono text-[11px] font-bold">C 级</span>
<span class="material-symbols-outlined text-slate-400 text-[18px]">radio_button_unchecked</span>
</div>
<span class="font-label-bold text-xs font-bold text-on-surface">基础数据监测</span>
<p class="font-caption-sm text-[11px] text-slate-500 leading-relaxed">
仅云端数据上报与告警 · 不含线下人工巡维派遣
</p>
</div>
</div>
</div>
<!-- Contract Dates & Responsible Manager -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 pt-1">
<div class="flex flex-col gap-1.5">
<label class="font-label-bold text-xs text-on-surface">
托管服务生效起始日 <span class="text-status-error">*</span>
</label>
<input class="w-full px-3 py-2 rounded-lg bg-slate-50/70 border border-slate-200 font-data-mono text-xs text-on-surface focus:outline-none focus:bg-white focus:border-primary transition-all" type="date" value="2025-01-01"/>
</div>
<div class="flex flex-col gap-1.5">
<label class="font-label-bold text-xs text-on-surface">
托管服务协议终止日 <span class="text-status-error">*</span>
</label>
<input class="w-full px-3 py-2 rounded-lg bg-slate-50/70 border border-slate-200 font-data-mono text-xs text-on-surface focus:outline-none focus:bg-white focus:border-primary transition-all" type="date" value="2027-12-31"/>
</div>
<div class="flex flex-col gap-1.5">
<label class="font-label-bold text-xs text-on-surface">
鑫潼责任客户经理 <span class="text-status-error">*</span>
</label>
<div class="relative flex items-center">
<select class="w-full px-3 py-2 rounded-lg bg-slate-50/70 border border-slate-200 text-xs text-on-surface appearance-none focus:outline-none focus:bg-white focus:border-primary transition-all">
<option selected="">张峻 (华中区域大客户一部 · 资深总监)</option>
<option>孙楚 (工业园区服务专班 · 客户经理)</option>
<option>郭少峰 (智能配电交付部 · 主管)</option>
</select>
<span class="material-symbols-outlined absolute right-2.5 text-slate-400 pointer-events-none text-[18px]">unfold_more</span>
</div>
</div>
</div>
</section>
</div>
<!-- Right Column (4 cols): Guiding Rail & Completeness Sidebar -->
<div class="lg:col-span-4 flex flex-col gap-5 sticky top-20">
<!-- Card 1: Decoupling Architecture Guide -->
<div class="bg-surface rounded-xl border border-border-rule shadow-sm p-5 flex flex-col gap-3.5">
<div class="flex items-center gap-2.5">
<div class="w-8 h-8 rounded-lg bg-blue-50 text-primary flex items-center justify-center">
<span class="material-symbols-outlined text-[20px]">account_tree</span>
</div>
<div class="flex flex-col">
<h3 class="font-label-bold text-xs font-bold text-on-surface">系统模块解耦指引</h3>
<span class="font-caption-sm text-[11px] text-slate-400">Decoupled Lifecycle Architecture</span>
</div>
</div>
<div class="p-3 rounded-lg bg-slate-50 border border-slate-200/80 text-xs text-slate-600 leading-relaxed flex items-start gap-2">
<span class="material-symbols-outlined text-primary text-[17px] shrink-0 mt-0.5">info</span>
<span>
本界面<strong>仅负责用电主体工商与基础电气申报资质</strong>。保存后将自动生成全局唯一 <code class="font-data-mono text-primary font-bold bg-white px-1 py-0.5 rounded border border-slate-200">customer_id</code>,后续环节已完全独立解耦:
</span>
</div>
<!-- Sequential decoupled steps -->
<div class="flex flex-col gap-3 pt-1">
<!-- Step 1 -->
<div class="flex gap-2.5 items-start p-2.5 rounded-lg bg-slate-50/60 border border-slate-200/60">
<div class="w-6 h-6 rounded-full bg-primary text-white font-data-mono text-[11px] flex items-center justify-center font-bold shrink-0 shadow-xs mt-0.5">
01
</div>
<div class="flex flex-col gap-0.5">
<span class="font-label-bold text-xs text-on-surface">空间拓扑与用电架构搭建</span>
<p class="font-caption-sm text-[11px] text-slate-500 leading-normal">
建档完成后即刻引导进入。按厂区/建筑/10kV配电室搭建一次电气单线图与母线拓扑。
</p>
<div class="flex items-center gap-1 text-[11px] text-primary font-medium mt-0.5">
<span class="w-1.5 h-1.5 rounded-full bg-primary"></span>
<span>建档后首要下一步</span>
</div>
</div>
</div>
<!-- Step 2 -->
<div class="flex gap-2.5 items-start p-2.5 rounded-lg bg-slate-50/60 border border-slate-200/60">
<div class="w-6 h-6 rounded-full bg-slate-200 text-slate-600 font-data-mono text-[11px] flex items-center justify-center font-bold shrink-0 mt-0.5">
02
</div>
<div class="flex flex-col gap-0.5">
<span class="font-label-bold text-xs text-on-surface">大华 ICC 物联网智能接入</span>
<p class="font-caption-sm text-[11px] text-slate-500 leading-normal">
在独立的【设备监控】模块指派边缘网关硬件,配置 ICC 专网隧道与通信协议。
</p>
<span class="text-[11px] text-slate-400">已完全从当前页面剥离</span>
</div>
</div>
<!-- Step 3 -->
<div class="flex gap-2.5 items-start p-2.5 rounded-lg bg-slate-50/60 border border-slate-200/60">
<div class="w-6 h-6 rounded-full bg-slate-200 text-slate-600 font-data-mono text-[11px] flex items-center justify-center font-bold shrink-0 mt-0.5">
03
</div>
<div class="flex flex-col gap-0.5">
<span class="font-label-bold text-xs text-on-surface">微信小程序权限与人员授权</span>
<p class="font-caption-sm text-[11px] text-slate-500 leading-normal">
由【系统管理 &gt; 权限分配】独立派发,指派企业法人、运维电工及主管查看权限。
</p>
<span class="text-[11px] text-slate-400">已完全从当前页面剥离</span>
</div>
</div>
</div>
</div>
<!-- Card 2: Filing Completeness & Real-time Verification -->
<div class="bg-surface rounded-xl border border-border-rule shadow-sm p-5 flex flex-col gap-3.5">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded-lg bg-emerald-50 text-emerald-600 flex items-center justify-center">
<span class="material-symbols-outlined text-[20px]">fact_check</span>
</div>
<span class="font-title-sm text-xs font-bold text-on-surface">建档规范与完整度</span>
</div>
<span class="font-data-mono text-base text-primary font-bold">85%</span>
</div>
<!-- Progress Bar -->
<div class="flex flex-col gap-1">
<div class="w-full bg-slate-100 h-2 rounded-full overflow-hidden">
<div class="bg-primary h-full rounded-full transition-all duration-500" style="width: 85%;"></div>
</div>
<div class="flex justify-between font-caption-sm text-[11px] text-slate-400 pt-0.5">
<span>基础准入阈值: 70%</span>
<span class="text-emerald-700 font-bold">已达成建档标准</span>
</div>
</div>
<!-- Verification Checklist -->
<div class="flex flex-col gap-2 pt-1">
<div class="flex items-center justify-between p-2 rounded-lg bg-slate-50 border border-slate-100">
<div class="flex items-center gap-2">
<span class="material-symbols-outlined text-emerald-600 text-[17px]">check_circle</span>
<span class="text-xs text-slate-700">统一社会信用代码合规</span>
</div>
<span class="font-caption-sm text-[11px] text-emerald-700 font-bold">已核验</span>
</div>
<div class="flex items-center justify-between p-2 rounded-lg bg-slate-50 border border-slate-100">
<div class="flex items-center gap-2">
<span class="material-symbols-outlined text-emerald-600 text-[17px]">check_circle</span>
<span class="text-xs text-slate-700">电网户号与电压等级匹配</span>
</div>
<span class="font-caption-sm text-[11px] text-emerald-700 font-bold">已核验</span>
</div>
<div class="flex items-center justify-between p-2 rounded-lg bg-slate-50 border border-slate-100">
<div class="flex items-center gap-2">
<span class="material-symbols-outlined text-emerald-600 text-[17px]">check_circle</span>
<span class="text-xs text-slate-700">变电室规划总数已录入</span>
</div>
<span class="font-data-mono text-[11px] text-slate-600">3 处所</span>
</div>
<div class="flex items-center justify-between p-2 rounded-lg bg-amber-50/50 border border-amber-100">
<div class="flex items-center gap-2">
<span class="material-symbols-outlined text-amber-500 text-[17px]">pending</span>
<span class="text-xs text-slate-600">托管协议盖章扫描件</span>
</div>
<span class="font-caption-sm text-[11px] text-amber-600 font-medium">建档后补录</span>
</div>
</div>
</div>
<!-- Card 3: Quick Input Auxiliary Tools -->
<div class="bg-surface rounded-xl border border-border-rule shadow-sm p-5 flex flex-col gap-3">
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded-lg bg-blue-50 text-primary flex items-center justify-center">
<span class="material-symbols-outlined text-[20px]">electric_bolt</span>
</div>
<span class="font-title-sm text-xs font-bold text-on-surface">快捷输入辅助工具</span>
</div>
<div class="flex flex-col gap-2">
<button class="w-full flex items-center justify-between p-3 rounded-lg border border-slate-200 hover:border-primary hover:bg-blue-50/30 transition-all text-left group" type="button">
<div class="flex items-center gap-2.5">
<div class="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center text-primary group-hover:scale-105 transition-transform">
<span class="material-symbols-outlined text-[19px]">contact_page</span>
</div>
<div class="flex flex-col">
<span class="font-label-bold text-xs text-on-surface font-bold">导入企业营业执照副本</span>
<span class="font-caption-sm text-[11px] text-slate-400">支持 PDF / JPG / PNG 格式解析</span>
</div>
</div>
<span class="material-symbols-outlined text-slate-400 group-hover:text-primary transition-colors text-[17px]">chevron_right</span>
</button>
<button class="w-full flex items-center justify-between p-3 rounded-lg border border-slate-200 hover:border-purple-400 hover:bg-purple-50/30 transition-all text-left group" type="button">
<div class="flex items-center gap-2.5">
<div class="w-8 h-8 rounded-lg bg-purple-50 flex items-center justify-center text-secondary group-hover:scale-105 transition-transform">
<span class="material-symbols-outlined text-[19px]">hub</span>
</div>
<div class="flex flex-col">
<span class="font-label-bold text-xs text-on-surface font-bold">同步意向客户 (CRM商机)</span>
<span class="font-caption-sm text-[11px] text-slate-400">转正前期签约备忘录主体</span>
</div>
</div>
<span class="material-symbols-outlined text-slate-400 group-hover:text-secondary transition-colors text-[17px]">chevron_right</span>
</button>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Sticky Bottom Fixed Action Bar -->
<div class="fixed bottom-0 left-60 right-0 bg-surface/95 backdrop-blur-md border-t border-border-rule py-3 px-6 flex flex-col sm:flex-row items-center justify-between gap-3 z-40 shadow-[0_-4px_16px_rgba(0,0,0,0.05)]">
<!-- Left Safety Statement -->
<div class="flex items-center gap-2 text-slate-500 font-caption-sm text-xs">
<span class="material-symbols-outlined text-emerald-600 text-[18px]">verified_user</span>
<span>系统安全声明:基础客户档案录入后将自动生成独立全局 <code class="font-data-mono text-on-surface font-bold">customer_id</code> 并实施行级多租户数据隔离。</span>
</div>
<!-- Right Action Buttons -->
<div class="flex items-center gap-2.5 w-full sm:w-auto justify-end">
<button class="px-4 py-2 rounded-lg text-slate-600 hover:text-on-surface hover:bg-slate-100 font-label-bold text-xs transition-colors" type="button">
返回客户列表
</button>
<button class="px-4 py-2 rounded-lg border border-slate-200 bg-white hover:bg-slate-50 text-slate-700 font-label-bold text-xs transition-colors shadow-xs" type="button">
保存为草稿
</button>
<button class="flex items-center gap-1.5 px-5 py-2 rounded-lg bg-primary hover:bg-primary-container text-white font-label-bold text-xs transition-all shadow-sm shadow-primary/25" type="button">
<span>完成建档并进入空间拓扑</span>
<span class="material-symbols-outlined text-[16px]">arrow_forward</span>
</button>
</div>
</div>
</div>
</body></html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

@@ -0,0 +1,200 @@
---
name: Industrial Precision
colors:
surface: '#ffffff'
surface-dim: '#ccdbf1'
surface-bright: '#f8f9ff'
surface-container-lowest: '#ffffff'
surface-container-low: '#eef4ff'
surface-container: '#e5efff'
surface-container-high: '#dbe9ff'
surface-container-highest: '#d5e4fa'
on-surface: '#0d1c2c'
on-surface-variant: '#424655'
inverse-surface: '#233142'
inverse-on-surface: '#e9f1ff'
outline: '#727787'
outline-variant: '#c2c6d8'
surface-tint: '#0056d2'
primary: '#0054cd'
on-primary: '#ffffff'
primary-container: '#136bfb'
on-primary-container: '#fefcff'
inverse-primary: '#b2c5ff'
secondary: '#712ae2'
on-secondary: '#ffffff'
secondary-container: '#8a4cfc'
on-secondary-container: '#fffbff'
tertiary: '#00685f'
on-tertiary: '#ffffff'
tertiary-container: '#008378'
on-tertiary-container: '#f4fffc'
error: '#ba1a1a'
on-error: '#ffffff'
error-container: '#ffdad6'
on-error-container: '#93000a'
primary-fixed: '#dae2ff'
primary-fixed-dim: '#b2c5ff'
on-primary-fixed: '#001847'
on-primary-fixed-variant: '#0040a1'
secondary-fixed: '#eaddff'
secondary-fixed-dim: '#d2bbff'
on-secondary-fixed: '#25005a'
on-secondary-fixed-variant: '#5a00c6'
tertiary-fixed: '#89f5e7'
tertiary-fixed-dim: '#6bd8cb'
on-tertiary-fixed: '#00201d'
on-tertiary-fixed-variant: '#005049'
background: '#f8f9ff'
on-background: '#0d1c2c'
surface-variant: '#d5e4fa'
bg-page: '#eef3f8'
text-muted: '#5b6b7e'
border-rule: '#dbe4ee'
status-warn: '#e0852b'
status-error: '#dc2626'
accent-subtle: rgba(29,111,255,0.08)
typography:
display-lg:
fontFamily: hankenGrotesk
fontSize: 32px
fontWeight: '700'
lineHeight: 40px
headline-md:
fontFamily: hankenGrotesk
fontSize: 23px
fontWeight: '700'
lineHeight: 28px
title-sm:
fontFamily: hankenGrotesk
fontSize: 17px
fontWeight: '700'
lineHeight: 24px
body-md:
fontFamily: hankenGrotesk
fontSize: 15px
fontWeight: '400'
lineHeight: 24px
label-bold:
fontFamily: hankenGrotesk
fontSize: 13px
fontWeight: '700'
lineHeight: 16px
data-mono:
fontFamily: jetbrainsMono
fontSize: 12px
fontWeight: '500'
lineHeight: 16px
caption-sm:
fontFamily: hankenGrotesk
fontSize: 12px
fontWeight: '700'
lineHeight: 14px
headline-md-mobile:
fontFamily: hankenGrotesk
fontSize: 20px
fontWeight: '700'
lineHeight: 26px
rounded:
sm: 0.25rem
DEFAULT: 0.5rem
md: 0.75rem
lg: 1rem
xl: 1.5rem
full: 9999px
spacing:
base: 4px
xs: 8px
sm: 12px
md: 16px
lg: 24px
xl: 40px
container-max: 1200px
gutter: 16px
---
## Brand & Style
The design system is engineered for the **Xintong Smart Power Operation Management APP**, targeting power engineers and grid operators. The brand personality is **Professional, Efficient, and Industrial Tech**. It emphasizes reliability through a structured, high-density interface that transforms complex electrical data into actionable insights.
The design style is a blend of **Corporate Modern** and **Information-Dense Minimalism**. It utilizes a "Layered Surface" approach where data is organized into logical card blocks, using subtle elevations and clear semantic color-coding to denote system status. The aesthetic is clean and functional, avoiding decorative elements in favor of technical precision and rapid scanability.
- **Professionalism:** High-contrast typography and a restrained color palette.
- **Efficiency:** Condensed layouts that prioritize data visualization and task status.
- **Reliability:** Solid borders, consistent spacing, and clear state indicators.
## Colors
The color architecture is strictly functional, mapping specifically to the operation layers of the power management system.
- **Primary (Deep Blue):** Reserved for core branding, navigation, and "Client" layer interactions.
- **Secondary (Purple):** Dedicated to "Gateway" operations and task-specific management.
- **Tertiary (Teal):** Used for "Service" layer success states and optimized performance indicators.
- **Neutral (Ink):** Used for primary text to ensure maximum legibility against light surfaces.
**Semantic Usage:**
- Use **status-warn (Amber)** for non-critical alerts or data synchronization states.
- Use **status-error (Red)** for critical power failures or permission denials.
- The **bg-page** provides a soft, low-strain canvas for long-duration monitoring, while **surface** (White) distinguishes interactive cards and data containers.
## Typography
This design system uses **Hanken Grotesk** as the primary typeface for its sharp, contemporary feel that bridges the gap between industrial and tech. **JetBrains Mono** is employed for technical data, device IDs, and numerical values to emphasize the system's engineering DNA.
- **Hierarchy:** Use `display-lg` for dashboard summaries. `title-sm` is the default for card headers.
- **Data Visualization:** All tabular data and technical strings must use `data-mono` for character alignment and clarity.
- **Mobile Adaptation:** For mobile screens, headlines should scale down to `headline-md-mobile` to maintain visual balance while preserving authority.
## Layout & Spacing
The layout utilizes a **Flexible Grid System** with a focus on logical grouping through card-based containment.
- **Grid:** A 12-column grid is used for desktop views, transitioning to a single-column stacked layout for mobile.
- **Rhythm:** An 8px-based spacing scale ensures vertical consistency. Use `lg (24px)` for outer card padding and `md (16px)` for internal element spacing.
- **Mobile Margins:** Standard mobile side margins are set to `md (16px)` to maximize screen real estate for dense data tables.
- **Sectioning:** Distinct operational modules should be separated by `xl (40px)` of vertical space to prevent cognitive overload.
## Elevation & Depth
This design system conveys hierarchy through **Tonal Layering** and **Subtle Ambient Shadows**. We avoid heavy skeuomorphism in favor of a "Flat-Plus" approach.
- **Base Layer:** The page background (`bg-page`) sits at the lowest elevation.
- **Mid Layer (Cards):** UI cards use a white surface with a subtle shadow (`0 4px 14px rgba(0,0,0,.07)`) to appear slightly lifted.
- **Interactive Layer:** On hover or active state, cards may transition to a higher elevation (`0 8px 28px rgba(0,0,0,.1)`) with a slight upward translation of -2px.
- **Overlays:** Modals and dropdowns use the highest z-index with a distinct backdrop blur to maintain focus on the task at hand.
- **Gradients:** Use soft radial gradients in the background (Blue/Purple) to create environmental depth without interfering with content legibility.
## Shapes
The shape language is **Rounded**, balancing industrial rigidity with modern software approachability.
- **Cards & Sections:** Use the standard `rounded-lg (1rem)` for primary containers.
- **Nodes & Blocks:** Architecture and data nodes use `rounded (0.5rem)` to fit more compactly within layouts.
- **Interactive Elements:** Buttons and tags use a "Pill-shape" (`rounded-xl` or 999px) to distinguish them from structural containers.
- **Borders:** Use `1px solid` with the `border-rule` color for most containers. Active states or primary focus areas may increase to `1.5px`.
## Components
**Buttons:**
- **Primary:** Solid `primary_color` with white text. Pill-shaped.
- **Secondary:** Transparent with `primary_color` border or subtle purple tint for operation tasks.
- **Danger:** Solid `status-error` for irreversible actions.
**Cards:**
- White background, `1px` border-rule, and `rounded-lg` corners.
- Headers should have a `18px` internal padding and a bottom border to separate from the content body.
**Input Fields:**
- Soft white background with `1px` border.
- Focused states should use a `primary_color` border with a subtle outer glow.
**Chips & Tags:**
- Used for status (e.g., "Active", "Offline").
- Backgrounds should use `8% opacity` versions of the semantic colors (Blue, Green, Red) with bolded text in the 100% opacity color.
**Data Visualization:**
- Charts should use the primary blue and secondary purple as the main data series colors.
- Use `data-mono` for all axis labels and tooltips to ensure technical accuracy.
**Status Indicators:**
- Small `7px` circular dots used within lists or architecture nodes to show real-time connectivity status.
@@ -0,0 +1,553 @@
<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="utf-8"/><meta content="width=device-width, initial-scale=1.0" name="viewport"/><link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=hankenGrotesk:wght@100..900&display=swap" rel="stylesheet"/><style>@layer base { html, body { margin: 0; padding: 0; } body { overscroll-behavior: none; } main > :first-child { margin-top: 0 !important; } main > :last-child { margin-bottom: 0 !important; } } ::-webkit-scrollbar { display: none; }</style><script src="https://cdn.tailwindcss.com"></script><script id="tailwind-config">tailwind.config = { darkMode: "class", theme: { extend: { "colors": { "bg-page": "#eef3f8", "text-muted": "#5b6b7e", "surface-bright": "#f8f9ff", "primary-fixed-dim": "#b2c5ff", "accent-subtle": "rgba(29,111,255,0.08)", "on-surface-variant": "#424655", "on-surface": "#0d1c2c", "outline": "#727787", "outline-variant": "#c2c6d8", "secondary-fixed": "#eaddff", "status-error": "#dc2626", "on-tertiary-container": "#f4fffc", "inverse-primary": "#b2c5ff", "surface-container-low": "#eef4ff", "on-secondary-container": "#fffbff", "error": "#ba1a1a", "surface-container-high": "#dbe9ff", "on-primary-container": "#fefcff", "on-secondary-fixed": "#25005a", "on-secondary": "#ffffff", "surface-tint": "#0056d2", "inverse-surface": "#233142", "on-tertiary": "#ffffff", "on-secondary-fixed-variant": "#5a00c6", "secondary": "#712ae2", "on-tertiary-fixed-variant": "#005049", "primary-fixed": "#dae2ff", "inverse-on-surface": "#e9f1ff", "on-primary": "#ffffff", "status-warn": "#e0852b", "on-primary-fixed": "#001847", "on-background": "#0d1c2c", "secondary-fixed-dim": "#d2bbff", "surface-dim": "#ccdbf1", "surface-variant": "#d5e4fa", "surface-container": "#e5efff", "on-tertiary-fixed": "#00201d", "tertiary": "#00685f", "primary": "#0054cd", "on-primary-fixed-variant": "#0040a1", "primary-container": "#136bfb", "secondary-container": "#8a4cfc", "background": "#f8f9ff", "surface-container-highest": "#d5e4fa", "tertiary-fixed-dim": "#6bd8cb", "surface": "#ffffff", "on-error": "#ffffff", "surface-container-lowest": "#ffffff", "border-rule": "#dbe4ee", "error-container": "#ffdad6", "tertiary-fixed": "#89f5e7", "on-error-container": "#93000a", "tertiary-container": "#008378" }, "borderRadius": { "DEFAULT": "0.25rem", "lg": "0.5rem", "xl": "0.75rem", "full": "9999px" }, "spacing": { "xs": "8px", "md": "16px", "container-max": "1200px", "sm": "12px", "lg": "24px", "base": "4px", "gutter": "16px", "xl": "40px" }, "fontFamily": { "body-md": ["hankenGrotesk"], "headline-md-mobile": ["hankenGrotesk"], "label-bold": ["hankenGrotesk"], "title-sm": ["hankenGrotesk"], "display-lg": ["hankenGrotesk"], "headline-md": ["hankenGrotesk"], "caption-sm": ["hankenGrotesk"], "data-mono": ["jetbrainsMono"] }, "fontSize": { "body-md": ["15px", { "lineHeight": "24px", "fontWeight": "400" }], "headline-md-mobile": ["20px", { "lineHeight": "26px", "fontWeight": "700" }], "label-bold": ["13px", { "lineHeight": "16px", "fontWeight": "700" }], "title-sm": ["17px", { "lineHeight": "24px", "fontWeight": "700" }], "display-lg": ["32px", { "lineHeight": "40px", "fontWeight": "700" }], "headline-md": ["23px", { "lineHeight": "28px", "fontWeight": "700" }], "caption-sm": ["12px", { "lineHeight": "14px", "fontWeight": "700" }], "data-mono": ["12px", { "lineHeight": "16px", "fontWeight": "500" }] } } } }</script></head><body class="bg-bg-page font-body-md text-body-md text-on-surface"><aside class="fixed left-0 top-0 h-full w-64 bg-inverse-surface z-50 flex flex-col justify-between select-none shadow-[0_1px_8px_rgba(0,0,0,0.04)]"><div class="flex flex-col"><div class="h-16 px-md flex items-center gap-sm bg-inverse-surface"><img alt="Xintong Power logo, industrial technology style, electric bolt and smart connectivity icon, professional, modern, high precision, deep blue palette.. Design context: - Primary color: #1d6fff
- Font: hankenGrotesk
- Mode: light
- Roundness: rounded-md
. The logo should be visually consistent with these brand tokens." class="h-8 w-auto object-contain" src="https://lh3.googleusercontent.com/aida/AEtjO1VpTaoSkZCyImEBOewVJbw8cN4OK_4XN0nYWf-d6prkZBdtwy8_i4RFQPli9kdGdwctP8bUQgQk3yLHoUJzYawS5_rm4a_iDAdFxgEcJm0oLqXiMHJ4KIqg8lTf2EdzS9w08_C-fdfalXZmfY_o9mNwGjr7KB5oPG_MHTU9vVSLrJTz4e0Lv7VNEf16_F81a5fH6QJk6fVBQhWTCesvcyO9LLqZ3nOoMdpFh4_7nHayUU88Ni14Re8Q80c"/><div class="flex flex-col"><span class="font-title-sm text-title-sm text-inverse-on-surface tracking-tight leading-tight">鑫潼智电</span><span class="font-data-mono text-data-mono text-outline tracking-wider">XT-POWER OS</span></div></div><div class="px-md py-xs"><div class="font-caption-sm text-caption-sm text-outline uppercase tracking-wider px-xs py-base">综合运营管控</div></div><nav class="flex flex-col gap-base px-md" data-active-classes="bg-primary text-on-primary font-label-bold rounded-lg shadow-sm"><a aria-current="page" class="flex items-center gap-sm px-md py-xs transition-all group bg-primary text-on-primary font-label-bold rounded-lg shadow-sm" data-path="dashboard" href="#"><span class="material-symbols-outlined text-[20px] transition-transform group-hover:scale-105">monitoring</span><span class="font-body-md text-body-md">运营工作台</span></a><a class="flex items-center gap-sm px-md py-xs rounded-lg text-inverse-on-surface hover:bg-surface-variant hover:text-on-surface transition-all group" data-path="client-management" href="#"><span class="material-symbols-outlined text-[20px] transition-transform group-hover:scale-105">corporate_fare</span><span class="font-body-md text-body-md">客户管理</span></a><a class="flex items-center gap-sm px-md py-xs rounded-lg text-inverse-on-surface hover:bg-surface-variant hover:text-on-surface transition-all group" data-path="project-management" href="#"><span class="material-symbols-outlined text-[20px] transition-transform group-hover:scale-105">lan</span><span class="font-body-md text-body-md">项目管理</span></a><a class="flex items-center gap-sm px-md py-xs rounded-lg text-inverse-on-surface hover:bg-surface-variant hover:text-on-surface transition-all group" data-path="dispatch-orders" href="#"><span class="material-symbols-outlined text-[20px] transition-transform group-hover:scale-105">assignment_turned_in</span><span class="font-body-md text-body-md">运营派单</span></a><a class="flex items-center gap-sm px-md py-xs rounded-lg text-inverse-on-surface hover:bg-surface-variant hover:text-on-surface transition-all group" data-path="device-telemetry" href="#"><span class="material-symbols-outlined text-[20px] transition-transform group-hover:scale-105">precision_manufacturing</span><span class="font-body-md text-body-md">设备监控</span></a><a class="flex items-center gap-sm px-md py-xs rounded-lg text-inverse-on-surface hover:bg-surface-variant hover:text-on-surface transition-all group" data-path="analytics-reports" href="#"><span class="material-symbols-outlined text-[20px] transition-transform group-hover:scale-105">query_stats</span><span class="font-body-md text-body-md">统计报表</span></a><a class="flex items-center gap-sm px-md py-xs rounded-lg text-inverse-on-surface hover:bg-surface-variant hover:text-on-surface transition-all group" data-path="system-settings" href="#"><span class="material-symbols-outlined text-[20px] transition-transform group-hover:scale-105">settings_suggest</span><span class="font-body-md text-body-md">系统管理</span></a></nav></div><div class="p-md m-md rounded-lg bg-surface-variant flex flex-col gap-xs"><div class="flex items-center justify-between"><span class="font-caption-sm text-caption-sm text-on-surface font-bold">边缘网关通信</span><span class="flex h-2 w-2 relative"><span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-tertiary opacity-75"></span><span class="relative inline-flex rounded-full h-2 w-2 bg-tertiary"></span></span></div><div class="flex items-center justify-between font-data-mono text-data-mono text-text-muted"><span>ICC Node #04</span><span>99.98%</span></div></div></aside><div class="pl-64 flex flex-col min-h-screen"><header class="fixed top-0 left-64 right-0 h-16 bg-surface/90 backdrop-blur-xl shadow-[0_1px_8px_rgba(0,0,0,0.04)] z-40 flex items-center justify-between px-lg"><div class="flex items-center gap-md"><div class="flex items-center gap-xs px-sm py-xs rounded-full bg-surface-container"><span class="h-2 w-2 rounded-full bg-tertiary"></span><span class="font-caption-sm text-caption-sm text-on-surface">ICC 实时数据同步正常</span></div><div class="h-4 w-px bg-outline-variant"></div><div class="relative flex items-center"><span class="material-symbols-outlined absolute left-sm text-outline text-[18px]">search</span><input class="w-72 pl-9 pr-md py-xs rounded-lg bg-surface-container-low font-body-md text-body-md text-on-surface placeholder:text-outline focus:outline-none focus:ring-1 focus:ring-primary" placeholder="按项目Line truncated
<!-- Top Navigation & Header Breadcrumb -->
<div class="flex flex-col gap-sm mb-lg">
<div class="flex items-center gap-xs font-caption-sm text-caption-sm text-text-muted">
<span class="hover:text-primary cursor-pointer transition-colors">运营管控体系</span>
<span class="material-symbols-outlined text-[14px]">chevron_right</span>
<span class="hover:text-primary cursor-pointer transition-colors">客户管理</span>
<span class="material-symbols-outlined text-[14px]">chevron_right</span>
<span class="hover:text-primary cursor-pointer transition-colors">客户档案列表</span>
<span class="material-symbols-outlined text-[14px]">chevron_right</span>
<span class="text-primary font-bold">新增客户档案</span>
</div>
<div class="flex flex-col lg:flex-row lg:items-center justify-between gap-md">
<div class="flex flex-col gap-base">
<div class="flex items-center gap-sm flex-wrap">
<h1 class="font-headline-md text-headline-md text-on-surface tracking-tight">新增客户档案</h1>
<span class="font-data-mono text-data-mono px-xs py-0.5 rounded bg-surface-container-high text-primary font-bold">CREATE CUSTOMER PROFILE</span>
<span class="flex items-center gap-1 px-sm py-0.5 rounded-full bg-surface-container font-caption-sm text-caption-sm text-tertiary">
<span class="w-1.5 h-1.5 rounded-full bg-tertiary"></span>
架构解耦机制已激活
</span>
</div>
<p class="font-body-md text-body-md text-text-muted">
纯粹客户基础信息建档 · 聚焦主体资质与基础用电参数,后续拓扑与设备配置已完全解耦。
</p>
</div>
<div class="flex items-center gap-sm flex-wrap">
<button class="flex items-center gap-xs px-md py-xs rounded-full bg-surface-container-low hover:bg-surface-container text-primary font-label-bold text-label-bold transition-all shadow-sm group" type="button">
<span class="material-symbols-outlined text-[18px] transition-transform group-hover:scale-110">document_scanner</span>
<span>导入营业执照智能识别</span>
</button>
<button class="flex items-center gap-xs px-md py-xs rounded-full bg-surface-container-low hover:bg-surface-container text-on-surface-variant font-label-bold text-label-bold transition-all shadow-sm" type="button">
<span class="material-symbols-outlined text-[18px]">sync_alt</span>
<span>从意向客户导入</span>
</button>
<button class="px-md py-xs rounded-full bg-surface text-on-surface hover:bg-surface-container font-label-bold text-label-bold transition-colors shadow-sm" type="button">
保存草稿
</button>
<button class="flex items-center gap-xs px-lg py-xs rounded-full bg-primary hover:bg-primary-container text-on-primary font-label-bold text-label-bold transition-all shadow-md shadow-primary/20" type="button">
<span>完成建档并进入空间拓扑</span>
<span class="material-symbols-outlined text-[18px]">arrow_forward</span>
</button>
</div>
</div>
</div>
<!-- Main Grid: 8:4 Two-column layout -->
<div class="grid grid-cols-12 gap-lg pb-xl">
<!-- Left Column: Master Form (8 cols) -->
<div class="col-span-12 lg:col-span-8 flex flex-col gap-lg">
<!-- Module 01: Customer Entity & Legal Qualification -->
<section class="bg-surface rounded-lg shadow-sm p-lg flex flex-col gap-md transition-shadow hover:shadow-md">
<div class="flex items-center justify-between pb-sm border-b-0">
<div class="flex items-center gap-sm">
<div class="w-8 h-8 rounded-lg bg-surface-container-low flex items-center justify-center text-primary">
<span class="material-symbols-outlined text-[20px]">badge</span>
</div>
<div class="flex flex-col">
<div class="flex items-center gap-xs">
<span class="font-data-mono text-data-mono text-text-muted">SECTION 01</span>
<span class="font-title-sm text-title-sm text-on-surface">客户主体与工商资质</span>
</div>
<span class="font-caption-sm text-caption-sm text-text-muted">国家发改委及国家电网要求之真实用电主体工商底册</span>
</div>
</div>
<span class="px-sm py-base rounded bg-surface-container text-primary font-caption-sm text-caption-sm font-bold flex items-center gap-1">
<span class="material-symbols-outlined text-[14px]">verified</span>
全国工商业信息库直联已就绪
</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-md pt-xs">
<!-- Institution Full Name -->
<div class="md:col-span-2 flex flex-col gap-xs">
<label class="font-label-bold text-label-bold text-on-surface flex items-center justify-between">
<span>机构全称 (工商登记法定全称) <span class="text-status-error">*</span></span>
<span class="text-tertiary font-caption-sm text-caption-sm flex items-center gap-0.5">
<span class="material-symbols-outlined text-[14px]">check_circle</span>
工商联网核验通过
</span>
</label>
<div class="relative flex items-center">
<input class="w-full pl-md pr-28 py-xs rounded bg-surface-container-lowest font-body-md text-body-md text-on-surface shadow-sm focus:outline-none focus:bg-surface focus:shadow-md transition-all" type="text" value="武汉华星光电半导体显示技术有限公司"/>
<div class="absolute right-xs flex items-center gap-1">
<span class="px-xs py-0.5 rounded bg-surface-container-high font-data-mono text-data-mono text-primary">已验资</span>
</div>
</div>
</div>
<!-- USCC -->
<div class="flex flex-col gap-xs">
<label class="font-label-bold text-label-bold text-on-surface flex items-center justify-between">
<span>统一社会信用代码 (18位) <span class="text-status-error">*</span></span>
<button class="text-primary font-caption-sm text-caption-sm flex items-center gap-0.5 hover:underline" type="button">
<span class="material-symbols-outlined text-[14px]">center_focus_strong</span>
OCR识别
</button>
</label>
<input class="w-full px-md py-xs rounded bg-surface-container-lowest font-data-mono text-data-mono text-on-surface tracking-wider shadow-sm focus:outline-none focus:bg-surface focus:shadow-md transition-all" type="text" value="91420100MA4KMYXU72"/>
</div>
<!-- Industry Category -->
<div class="flex flex-col gap-xs">
<label class="font-label-bold text-label-bold text-on-surface">
所属行业门类 <span class="text-status-error">*</span>
</label>
<div class="relative flex items-center">
<select class="w-full px-md py-xs rounded bg-surface-container-lowest font-body-md text-body-md text-on-surface appearance-none shadow-sm focus:outline-none focus:bg-surface focus:shadow-md transition-all">
<option selected="">先进制造 · 半导体与精密光电</option>
<option>生物医药 · 基因工程产业基地</option>
<option>高校科研 · 重点实验中心</option>
<option>医疗卫生 · 三级甲等综合医院</option>
<option>商务楼宇 · 超甲级智能总部大厦</option>
</select>
<span class="material-symbols-outlined absolute right-sm text-outline pointer-events-none text-[18px]">unfold_more</span>
</div>
</div>
<!-- Legal Person -->
<div class="flex flex-col gap-xs">
<label class="font-label-bold text-label-bold text-on-surface">
法定代表人姓名 <span class="text-status-error">*</span>
</label>
<input class="w-full px-md py-xs rounded bg-surface-container-lowest font-body-md text-body-md text-on-surface shadow-sm focus:outline-none focus:bg-surface focus:shadow-md transition-all" type="text" value="赵宏伟"/>
</div>
<!-- Legal Person Phone -->
<div class="flex flex-col gap-xs">
<label class="font-label-bold text-label-bold text-on-surface">
法定代表人联系电话
</label>
<input class="w-full px-md py-xs rounded bg-surface-container-lowest font-data-mono text-data-mono text-on-surface shadow-sm focus:outline-none focus:bg-surface focus:shadow-md transition-all" type="tel" value="027-8790-8800"/>
</div>
<!-- Administrative Division (3-level) -->
<div class="md:col-span-2 flex flex-col gap-xs">
<label class="font-label-bold text-label-bold text-on-surface">
所属行政区划 (三级联动) <span class="text-status-error">*</span>
</label>
<div class="grid grid-cols-3 gap-sm">
<div class="relative">
<select class="w-full px-md py-xs rounded bg-surface-container-lowest font-body-md text-body-md text-on-surface appearance-none shadow-sm focus:outline-none">
<option selected="">湖北省</option>
</select>
<span class="material-symbols-outlined absolute right-xs top-2.5 text-outline pointer-events-none text-[16px]">expand_more</span>
</div>
<div class="relative">
<select class="w-full px-md py-xs rounded bg-surface-container-lowest font-body-md text-body-md text-on-surface appearance-none shadow-sm focus:outline-none">
<option selected="">武汉市</option>
</select>
<span class="material-symbols-outlined absolute right-xs top-2.5 text-outline pointer-events-none text-[16px]">expand_more</span>
</div>
<div class="relative">
<select class="w-full px-md py-xs rounded bg-surface-container-lowest font-body-md text-body-md text-on-surface appearance-none shadow-sm focus:outline-none">
<option selected="">洪山区 (东湖新技术开发区)</option>
</select>
<span class="material-symbols-outlined absolute right-xs top-2.5 text-outline pointer-events-none text-[16px]">expand_more</span>
</div>
</div>
</div>
<!-- Physical Power Site Address with Map trigger -->
<div class="md:col-span-2 flex flex-col gap-xs">
<label class="font-label-bold text-label-bold text-on-surface flex items-center justify-between">
<span>实际用电厂区/建筑地址 <span class="text-status-error">*</span></span>
<span class="text-text-muted font-caption-sm text-caption-sm flex items-center gap-1">
<span class="material-symbols-outlined text-[14px] text-primary">pin_drop</span>
经纬度坐标点选已锁定
</span>
</label>
<div class="relative flex items-center">
<input class="w-full pl-md pr-28 py-xs rounded bg-surface-container-lowest font-body-md text-body-md text-on-surface shadow-sm focus:outline-none focus:bg-surface focus:shadow-md transition-all" type="text" value="光谷智能制造产业园高新大道999号 华星光电武汉光谷显示基地 T3/T4厂区"/>
<button class="absolute right-xs px-sm py-1 rounded bg-surface-container-high hover:bg-surface-variant font-label-bold text-label-bold text-primary flex items-center gap-1 transition-colors" type="button">
<span class="material-symbols-outlined text-[16px]">map</span>
<span>地图定标</span>
</button>
</div>
</div>
</div>
</section>
<!-- Module 02: Grid Power Filing Parameters -->
<section class="bg-surface rounded-lg shadow-sm p-lg flex flex-col gap-md transition-shadow hover:shadow-md">
<div class="flex items-center justify-between pb-sm">
<div class="flex items-center gap-sm">
<div class="w-8 h-8 rounded-lg bg-surface-container-low flex items-center justify-center text-primary">
<span class="material-symbols-outlined text-[20px]">bolt</span>
</div>
<div class="flex flex-col">
<div class="flex items-center gap-xs">
<span class="font-data-mono text-data-mono text-text-muted">SECTION 02</span>
<span class="font-title-sm text-title-sm text-on-surface">基础用电申报参数</span>
</div>
<span class="font-caption-sm text-caption-sm text-text-muted">电网报装装见容量、受电电压与应急安全责任底册</span>
</div>
</div>
<span class="px-sm py-base rounded bg-surface-container-low text-on-surface-variant font-caption-sm text-caption-sm">
用于负荷容量规划与越限告警基线
</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-md pt-xs">
<!-- Power Consumer Account Number -->
<div class="flex flex-col gap-xs">
<label class="font-label-bold text-label-bold text-on-surface flex items-center justify-between">
<span>供电局电网用电户号 <span class="text-status-error">*</span></span>
<span class="text-tertiary font-caption-sm text-caption-sm flex items-center gap-0.5">
<span class="material-symbols-outlined text-[13px]">verified_user</span>
户名校验一致
</span>
</label>
<input class="w-full px-md py-xs rounded bg-surface-container-lowest font-data-mono text-data-mono text-on-surface tracking-wider shadow-sm focus:outline-none focus:bg-surface focus:shadow-md transition-all" type="text" value="0982736192"/>
</div>
<!-- Incomer Voltage Level -->
<div class="flex flex-col gap-xs">
<label class="font-label-bold text-label-bold text-on-surface">
进线受电电压等级 <span class="text-status-error">*</span>
</label>
<div class="relative flex items-center">
<select class="w-full px-md py-xs rounded bg-surface-container-lowest font-body-md text-body-md text-on-surface appearance-none shadow-sm focus:outline-none focus:bg-surface focus:shadow-md transition-all">
<option selected="">10kV 高压专线供电</option>
<option>35kV 变电站高压受电</option>
<option>110kV 专用变电站</option>
<option>380V 低压综合动力</option>
</select>
<span class="material-symbols-outlined absolute right-sm text-outline pointer-events-none text-[18px]">unfold_more</span>
</div>
</div>
<!-- Total Installed Capacity -->
<div class="flex flex-col gap-xs">
<label class="font-label-bold text-label-bold text-on-surface">
报装装见总容量 <span class="text-status-error">*</span>
</label>
<div class="relative flex items-center">
<input class="w-full pl-md pr-14 py-xs rounded bg-surface-container-lowest font-data-mono text-data-mono text-on-surface shadow-sm focus:outline-none focus:bg-surface focus:shadow-md transition-all" type="number" value="16000"/>
<span class="absolute right-sm text-outline font-data-mono text-data-mono pointer-events-none">kVA</span>
</div>
</div>
<!-- Distribution Rooms Planned -->
<div class="flex flex-col gap-xs">
<label class="font-label-bold text-label-bold text-on-surface">
变配电室规划总数
</label>
<div class="relative flex items-center">
<input class="w-full pl-md pr-12 py-xs rounded bg-surface-container-lowest font-data-mono text-data-mono text-on-surface shadow-sm focus:outline-none" type="number" value="3"/>
<span class="absolute right-sm text-outline font-caption-sm text-caption-sm pointer-events-none">处所</span>
</div>
</div>
<!-- Planned Buildings -->
<div class="flex flex-col gap-xs">
<label class="font-label-bold text-label-bold text-on-surface">
规划用电建筑栋数
</label>
<div class="relative flex items-center">
<input class="w-full pl-md pr-12 py-xs rounded bg-surface-container-lowest font-data-mono text-data-mono text-on-surface shadow-sm focus:outline-none" type="number" value="5"/>
<span class="absolute right-sm text-outline font-caption-sm text-caption-sm pointer-events-none">栋</span>
</div>
</div>
<!-- Planned Monitored Circuits -->
<div class="flex flex-col gap-xs">
<label class="font-label-bold text-label-bold text-on-surface">
计划监测回路预估数
</label>
<div class="relative flex items-center">
<input class="w-full pl-md pr-12 py-xs rounded bg-surface-container-lowest font-data-mono text-data-mono text-on-surface shadow-sm focus:outline-none" type="number" value="84"/>
<span class="absolute right-sm text-outline font-caption-sm text-caption-sm pointer-events-none">回路</span>
</div>
</div>
<!-- Customer Electrical Safety Officer -->
<div class="flex flex-col gap-xs">
<label class="font-label-bold text-label-bold text-on-surface">
客户方电气第一责任人 <span class="text-status-error">*</span>
</label>
<input class="w-full px-md py-xs rounded bg-surface-container-lowest font-body-md text-body-md text-on-surface shadow-sm focus:outline-none" type="text" value="陈明德 (动力部总监)"/>
</div>
<!-- 24h Emergency Phone -->
<div class="md:col-span-2 flex flex-col gap-xs">
<label class="font-label-bold text-label-bold text-on-surface flex items-center justify-between">
<span>24小时应急值班手机号 <span class="text-status-error">*</span></span>
<span class="text-text-muted font-caption-sm text-caption-sm">用于接收调度令与严重故障短信推送</span>
</label>
<div class="relative flex items-center">
<span class="material-symbols-outlined absolute left-sm text-outline text-[18px]">phone_in_talk</span>
<input class="w-full pl-10 pr-md py-xs rounded bg-surface-container-lowest font-data-mono text-data-mono text-on-surface shadow-sm focus:outline-none focus:bg-surface focus:shadow-md transition-all" type="tel" value="139-7128-9921"/>
</div>
</div>
</div>
</section>
<!-- Module 03: Business Contract & SLA Service Tier -->
<section class="bg-surface rounded-lg shadow-sm p-lg flex flex-col gap-md transition-shadow hover:shadow-md">
<div class="flex items-center justify-between pb-sm">
<div class="flex items-center gap-sm">
<div class="w-8 h-8 rounded-lg bg-surface-container-low flex items-center justify-center text-primary">
<span class="material-symbols-outlined text-[20px]">handshake</span>
</div>
<div class="flex flex-col">
<div class="flex items-center gap-xs">
<span class="font-data-mono text-data-mono text-text-muted">SECTION 03</span>
<span class="font-title-sm text-title-sm text-on-surface">商务签约与托管服务级别</span>
</div>
<span class="font-caption-sm text-caption-sm text-text-muted">界定响应时限、巡检频次与责任客户经理权责</span>
</div>
</div>
</div>
<div class="flex flex-col gap-md pt-xs">
<!-- SLA Tier Selection Cards -->
<div class="flex flex-col gap-xs">
<label class="font-label-bold text-label-bold text-on-surface">
鑫潼智电智能运维保障等级 (SLA) <span class="text-status-error">*</span>
</label>
<div class="grid grid-cols-1 md:grid-cols-4 gap-sm">
<!-- Tier 1: A+ -->
<div class="relative p-md rounded-lg bg-surface-container-low cursor-pointer hover:bg-surface-container transition-all flex flex-col gap-xs group">
<div class="flex items-center justify-between">
<span class="font-label-bold text-label-bold text-primary">A+ 特级保电托管</span>
<span class="material-symbols-outlined text-[18px] text-primary" style="font-variation-settings: 'FILL' 1;">radio_button_checked</span>
</div>
<p class="font-caption-sm text-caption-sm text-text-muted leading-relaxed">
15分钟应急到场 · 7×24小时专家派单巡检 · 关键负荷双电源容灾
</p>
</div>
<!-- Tier 2: A -->
<div class="p-md rounded-lg bg-surface-container-lowest cursor-pointer hover:bg-surface-container-low transition-all flex flex-col gap-xs">
<div class="flex items-center justify-between">
<span class="font-label-bold text-label-bold text-on-surface">A 级重大用电保障</span>
<span class="material-symbols-outlined text-[18px] text-outline">radio_button_unchecked</span>
</div>
<p class="font-caption-sm text-caption-sm text-text-muted leading-relaxed">
30分钟响应 · 每周常规物理巡检 · 负荷越限动态调优
</p>
</div>
<!-- Tier 3: B -->
<div class="p-md rounded-lg bg-surface-container-lowest cursor-pointer hover:bg-surface-container-low transition-all flex flex-col gap-xs">
<div class="flex items-center justify-between">
<span class="font-label-bold text-label-bold text-on-surface">B 级标准集中托管</span>
<span class="material-symbols-outlined text-[18px] text-outline">radio_button_unchecked</span>
</div>
<p class="font-caption-sm text-caption-sm text-text-muted leading-relaxed">
2小时响应 · 每月预防性预防性维护 · 报表定期抄送
</p>
</div>
<!-- Tier 4: C -->
<div class="p-md rounded-lg bg-surface-container-lowest cursor-pointer hover:bg-surface-container-low transition-all flex flex-col gap-xs">
<div class="flex items-center justify-between">
<span class="font-label-bold text-label-bold text-on-surface">C 级基础数据监测</span>
<span class="material-symbols-outlined text-[18px] text-outline">radio_button_unchecked</span>
</div>
<p class="font-caption-sm text-caption-sm text-text-muted leading-relaxed">
仅云端数据上报与告警 · 不含线下人工巡维派遣
</p>
</div>
</div>
</div>
<!-- Contract Dates & Responsible Manager -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-md">
<div class="flex flex-col gap-xs">
<label class="font-label-bold text-label-bold text-on-surface">
托管服务生效起始日 <span class="text-status-error">*</span>
</label>
<input class="w-full px-md py-xs rounded bg-surface-container-lowest font-data-mono text-data-mono text-on-surface shadow-sm focus:outline-none" type="date" value="2025-01-01"/>
</div>
<div class="flex flex-col gap-xs">
<label class="font-label-bold text-label-bold text-on-surface">
托管服务协议终止日 <span class="text-status-error">*</span>
</label>
<input class="w-full px-md py-xs rounded bg-surface-container-lowest font-data-mono text-data-mono text-on-surface shadow-sm focus:outline-none" type="date" value="2027-12-31"/>
</div>
<div class="flex flex-col gap-xs">
<label class="font-label-bold text-label-bold text-on-surface">
鑫潼责任客户经理 <span class="text-status-error">*</span>
</label>
<div class="relative flex items-center">
<select class="w-full px-md py-xs rounded bg-surface-container-lowest font-body-md text-body-md text-on-surface appearance-none shadow-sm focus:outline-none">
<option selected="">张峻 (华中区域大客户一部 · 资深总监)</option>
<option>孙楚 (工业园区服务专班 · 客户经理)</option>
<option>郭少峰 (智能配电交付部 · 主管)</option>
</select>
<span class="material-symbols-outlined absolute right-sm text-outline pointer-events-none text-[18px]">unfold_more</span>
</div>
</div>
</div>
</div>
</section>
</div>
<!-- Right Column: Decoupling Guide & Compliance Sidebar (4 cols) -->
<div class="col-span-12 lg:col-span-4 flex flex-col gap-lg">
<!-- Card 1: Decoupling Architecture Guide -->
<aside class="bg-surface rounded-lg shadow-sm p-lg flex flex-col gap-md">
<div class="flex items-center gap-sm">
<div class="w-8 h-8 rounded-lg bg-surface-container flex items-center justify-center text-primary">
<span class="material-symbols-outlined text-[20px]">account_tree</span>
</div>
<div>
<h2 class="font-title-sm text-title-sm text-on-surface">系统模块解耦指引</h2>
<p class="font-caption-sm text-caption-sm text-text-muted">Decoupled Lifecycle Architecture</p>
</div>
</div>
<div class="p-sm rounded-lg bg-surface-container-low text-on-surface-variant font-caption-sm text-caption-sm leading-relaxed flex items-start gap-xs">
<span class="material-symbols-outlined text-primary text-[18px] shrink-0 mt-0.5">info</span>
<span>
本界面<strong>仅负责用电主体工商与基础电气资质建档</strong>。保存后将自动生成全局唯一 <code class="font-data-mono text-primary font-bold">customer_id</code>,后续各环节已完全解耦,由专属工作流承接:
</span>
</div>
<!-- Sequential decoupled steps -->
<div class="flex flex-col gap-md">
<!-- Node 1 -->
<div class="flex gap-sm items-start relative">
<div class="w-7 h-7 rounded-full bg-primary text-on-primary font-data-mono text-data-mono flex items-center justify-center font-bold shrink-0 shadow-sm">
01
</div>
<div class="flex flex-col gap-base">
<span class="font-label-bold text-label-bold text-on-surface">空间拓扑与用电架构搭建</span>
<p class="font-caption-sm text-caption-sm text-text-muted leading-normal">
建档完成后即刻引导进入。按厂区、建筑、10kV 配电室层级绘制一次电气单线图与母线拓扑。
</p>
<div class="flex items-center gap-1 font-caption-sm text-caption-sm text-primary">
<span class="w-1.5 h-1.5 rounded-full bg-primary"></span>
<span>建档后首要下一步</span>
</div>
</div>
</div>
<!-- Node 2 -->
<div class="flex gap-sm items-start relative">
<div class="w-7 h-7 rounded-full bg-surface-container-high text-on-surface-variant font-data-mono text-data-mono flex items-center justify-center font-bold shrink-0">
02
</div>
<div class="flex flex-col gap-base">
<span class="font-label-bold text-label-bold text-on-surface">大华 ICC 物联网智能接入</span>
<p class="font-caption-sm text-caption-sm text-text-muted leading-normal">
在独立的<strong>【设备监控】</strong>模块指派网关硬件。配置 ICC 专网隧道、Modbus/IEC 104 采集频率及国密传输通道。
</p>
<span class="font-caption-sm text-caption-sm text-outline">已完全从当前页面剥离</span>
</div>
</div>
<!-- Node 3 -->
<div class="flex gap-sm items-start relative">
<div class="w-7 h-7 rounded-full bg-surface-container-high text-on-surface-variant font-data-mono text-data-mono flex items-center justify-center font-bold shrink-0">
03
</div>
<div class="flex flex-col gap-base">
<span class="font-label-bold text-label-bold text-on-surface">微信小程序权限与人员授权</span>
<p class="font-caption-sm text-caption-sm text-text-muted leading-normal">
由<strong>【系统管理 > 权限分配】</strong>独立派发。按岗位角色(企业法定代表人、值班电工、运维班长)绑码下发微信消息推送。
</p>
<span class="font-caption-sm text-caption-sm text-outline">已完全从当前页面剥离</span>
</div>
</div>
</div>
</aside>
<!-- Card 2: Filing Completeness & Real-time Verification -->
<aside class="bg-surface rounded-lg shadow-sm p-lg flex flex-col gap-md">
<div class="flex items-center justify-between">
<div class="flex items-center gap-sm">
<div class="w-8 h-8 rounded-lg bg-surface-container flex items-center justify-center text-primary">
<span class="material-symbols-outlined text-[20px]">fact_check</span>
</div>
<span class="font-title-sm text-title-sm text-on-surface">建档规范与完整度</span>
</div>
<span class="font-data-mono text-title-sm text-primary font-bold">85%</span>
</div>
<!-- Visual Ring/Bar Gauge -->
<div class="flex flex-col gap-xs">
<div class="w-full bg-surface-container h-2 rounded-full overflow-hidden">
<div class="bg-primary h-full rounded-full transition-all duration-500" style="width: 85%;"></div>
</div>
<div class="flex justify-between font-caption-sm text-caption-sm text-text-muted">
<span>基础准入阈值: 70%</span>
<span class="text-tertiary font-bold">已达成建档标准</span>
</div>
</div>
<!-- Verification Checklist -->
<div class="flex flex-col gap-sm pt-xs">
<div class="flex items-center justify-between p-xs rounded bg-surface-container-lowest">
<div class="flex items-center gap-xs">
<span class="material-symbols-outlined text-tertiary text-[18px]">check_circle</span>
<span class="font-body-md text-body-md text-on-surface">统一社会信用代码合规</span>
</div>
<span class="font-caption-sm text-caption-sm text-tertiary font-bold">已核验</span>
</div>
<div class="flex items-center justify-between p-xs rounded bg-surface-container-lowest">
<div class="flex items-center gap-xs">
<span class="material-symbols-outlined text-tertiary text-[18px]">check_circle</span>
<span class="font-body-md text-body-md text-on-surface">电网户号与电压等级匹配</span>
</div>
<span class="font-caption-sm text-caption-sm text-tertiary font-bold">已核验</span>
</div>
<div class="flex items-center justify-between p-xs rounded bg-surface-container-lowest">
<div class="flex items-center gap-xs">
<span class="material-symbols-outlined text-tertiary text-[18px]">check_circle</span>
<span class="font-body-md text-body-md text-on-surface">变电室规划总数已录入</span>
</div>
<span class="font-data-mono text-data-mono text-on-surface">3 处所</span>
</div>
<div class="flex items-center justify-between p-xs rounded bg-surface-container-low">
<div class="flex items-center gap-xs">
<span class="material-symbols-outlined text-status-warn text-[18px]">pending</span>
<span class="font-body-md text-body-md text-text-muted">托管协议盖章扫描件</span>
</div>
<span class="font-caption-sm text-caption-sm text-text-muted">建档后补录</span>
</div>
</div>
</aside>
<!-- Card 3: Quick Helper & Auxiliary Import Tools -->
<aside class="bg-surface rounded-lg shadow-sm p-lg flex flex-col gap-md">
<div class="flex items-center gap-sm">
<div class="w-8 h-8 rounded-lg bg-surface-container flex items-center justify-center text-primary">
<span class="material-symbols-outlined text-[20px]">electric_bolt</span>
</div>
<span class="font-title-sm text-title-sm text-on-surface">快捷输入辅助工具</span>
</div>
<div class="flex flex-col gap-sm">
<button class="w-full flex items-center justify-between p-sm rounded-lg bg-surface-container-lowest hover:bg-surface-container-low transition-colors text-left group" type="button">
<div class="flex items-center gap-sm">
<span class="material-symbols-outlined text-primary text-[22px]">contact_page</span>
<div class="flex flex-col">
<span class="font-label-bold text-label-bold text-on-surface">导入企业营业执照副本</span>
<span class="font-caption-sm text-caption-sm text-text-muted">支持 PDF/JPG/PNG 格式快速解析</span>
</div>
</div>
<span class="material-symbols-outlined text-outline group-hover:text-primary transition-colors text-[18px]">chevron_right</span>
</button>
<button class="w-full flex items-center justify-between p-sm rounded-lg bg-surface-container-lowest hover:bg-surface-container-low transition-colors text-left group" type="button">
<div class="flex items-center gap-sm">
<span class="material-symbols-outlined text-secondary text-[22px]">hub</span>
<div class="flex flex-col">
<span class="font-label-bold text-label-bold text-on-surface">同步意向客户 (CRM商机)</span>
<span class="font-caption-sm text-caption-sm text-text-muted">转正前期签约备忘录主体</span>
</div>
</div>
<span class="material-symbols-outlined text-outline group-hover:text-secondary transition-colors text-[18px]">chevron_right</span>
</button>
</div>
</aside>
</div>
</div>
<!-- Sticky Bottom Action Bar -->
<div class="sticky bottom-0 -mx-lg px-lg py-md bg-surface/95 backdrop-blur-md shadow-[0_-4px_16px_rgba(0,0,0,0.06)] z-30 flex flex-col sm:flex-row items-center justify-between gap-md">
<div class="flex items-center gap-xs text-text-muted font-caption-sm text-caption-sm">
<span class="material-symbols-outlined text-tertiary text-[18px]">shield</span>
<span>系统安全声明:基础客户档案录入后将自动生成独立全局 <code class="font-data-mono text-on-surface font-bold">customer_id</code> 并实施行级多租户数据隔离。</span>
</div>
<div class="flex items-center gap-sm w-full sm:w-auto justify-end">
<button class="px-lg py-xs rounded-full bg-surface-container-low hover:bg-surface-container text-on-surface font-label-bold text-label-bold transition-colors" type="button">
返回客户列表
</button>
<button class="px-lg py-xs rounded-full bg-surface-container hover:bg-surface-container-high text-on-surface font-label-bold text-label-bold transition-colors" type="button">
保存为草稿
</button>
<button class="flex items-center gap-xs px-xl py-xs rounded-full bg-primary hover:bg-primary-container text-on-primary font-label-bold text-label-bold transition-all shadow-md shadow-primary/20" type="button">
<span>完成建档并进入空间拓扑</span>
<span class="material-symbols-outlined text-[18px]">arrow_forward</span>
</button>
</div>
</div>
</div></main></div></body></html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 546 KiB

@@ -0,0 +1,200 @@
---
name: Industrial Precision
colors:
surface: '#ffffff'
surface-dim: '#ccdbf1'
surface-bright: '#f8f9ff'
surface-container-lowest: '#ffffff'
surface-container-low: '#eef4ff'
surface-container: '#e5efff'
surface-container-high: '#dbe9ff'
surface-container-highest: '#d5e4fa'
on-surface: '#0d1c2c'
on-surface-variant: '#424655'
inverse-surface: '#233142'
inverse-on-surface: '#e9f1ff'
outline: '#727787'
outline-variant: '#c2c6d8'
surface-tint: '#0056d2'
primary: '#0054cd'
on-primary: '#ffffff'
primary-container: '#136bfb'
on-primary-container: '#fefcff'
inverse-primary: '#b2c5ff'
secondary: '#712ae2'
on-secondary: '#ffffff'
secondary-container: '#8a4cfc'
on-secondary-container: '#fffbff'
tertiary: '#00685f'
on-tertiary: '#ffffff'
tertiary-container: '#008378'
on-tertiary-container: '#f4fffc'
error: '#ba1a1a'
on-error: '#ffffff'
error-container: '#ffdad6'
on-error-container: '#93000a'
primary-fixed: '#dae2ff'
primary-fixed-dim: '#b2c5ff'
on-primary-fixed: '#001847'
on-primary-fixed-variant: '#0040a1'
secondary-fixed: '#eaddff'
secondary-fixed-dim: '#d2bbff'
on-secondary-fixed: '#25005a'
on-secondary-fixed-variant: '#5a00c6'
tertiary-fixed: '#89f5e7'
tertiary-fixed-dim: '#6bd8cb'
on-tertiary-fixed: '#00201d'
on-tertiary-fixed-variant: '#005049'
background: '#f8f9ff'
on-background: '#0d1c2c'
surface-variant: '#d5e4fa'
bg-page: '#eef3f8'
text-muted: '#5b6b7e'
border-rule: '#dbe4ee'
status-warn: '#e0852b'
status-error: '#dc2626'
accent-subtle: rgba(29,111,255,0.08)
typography:
display-lg:
fontFamily: hankenGrotesk
fontSize: 32px
fontWeight: '700'
lineHeight: 40px
headline-md:
fontFamily: hankenGrotesk
fontSize: 23px
fontWeight: '700'
lineHeight: 28px
title-sm:
fontFamily: hankenGrotesk
fontSize: 17px
fontWeight: '700'
lineHeight: 24px
body-md:
fontFamily: hankenGrotesk
fontSize: 15px
fontWeight: '400'
lineHeight: 24px
label-bold:
fontFamily: hankenGrotesk
fontSize: 13px
fontWeight: '700'
lineHeight: 16px
data-mono:
fontFamily: jetbrainsMono
fontSize: 12px
fontWeight: '500'
lineHeight: 16px
caption-sm:
fontFamily: hankenGrotesk
fontSize: 12px
fontWeight: '700'
lineHeight: 14px
headline-md-mobile:
fontFamily: hankenGrotesk
fontSize: 20px
fontWeight: '700'
lineHeight: 26px
rounded:
sm: 0.25rem
DEFAULT: 0.5rem
md: 0.75rem
lg: 1rem
xl: 1.5rem
full: 9999px
spacing:
base: 4px
xs: 8px
sm: 12px
md: 16px
lg: 24px
xl: 40px
container-max: 1200px
gutter: 16px
---
## Brand & Style
The design system is engineered for the **Xintong Smart Power Operation Management APP**, targeting power engineers and grid operators. The brand personality is **Professional, Efficient, and Industrial Tech**. It emphasizes reliability through a structured, high-density interface that transforms complex electrical data into actionable insights.
The design style is a blend of **Corporate Modern** and **Information-Dense Minimalism**. It utilizes a "Layered Surface" approach where data is organized into logical card blocks, using subtle elevations and clear semantic color-coding to denote system status. The aesthetic is clean and functional, avoiding decorative elements in favor of technical precision and rapid scanability.
- **Professionalism:** High-contrast typography and a restrained color palette.
- **Efficiency:** Condensed layouts that prioritize data visualization and task status.
- **Reliability:** Solid borders, consistent spacing, and clear state indicators.
## Colors
The color architecture is strictly functional, mapping specifically to the operation layers of the power management system.
- **Primary (Deep Blue):** Reserved for core branding, navigation, and "Client" layer interactions.
- **Secondary (Purple):** Dedicated to "Gateway" operations and task-specific management.
- **Tertiary (Teal):** Used for "Service" layer success states and optimized performance indicators.
- **Neutral (Ink):** Used for primary text to ensure maximum legibility against light surfaces.
**Semantic Usage:**
- Use **status-warn (Amber)** for non-critical alerts or data synchronization states.
- Use **status-error (Red)** for critical power failures or permission denials.
- The **bg-page** provides a soft, low-strain canvas for long-duration monitoring, while **surface** (White) distinguishes interactive cards and data containers.
## Typography
This design system uses **Hanken Grotesk** as the primary typeface for its sharp, contemporary feel that bridges the gap between industrial and tech. **JetBrains Mono** is employed for technical data, device IDs, and numerical values to emphasize the system's engineering DNA.
- **Hierarchy:** Use `display-lg` for dashboard summaries. `title-sm` is the default for card headers.
- **Data Visualization:** All tabular data and technical strings must use `data-mono` for character alignment and clarity.
- **Mobile Adaptation:** For mobile screens, headlines should scale down to `headline-md-mobile` to maintain visual balance while preserving authority.
## Layout & Spacing
The layout utilizes a **Flexible Grid System** with a focus on logical grouping through card-based containment.
- **Grid:** A 12-column grid is used for desktop views, transitioning to a single-column stacked layout for mobile.
- **Rhythm:** An 8px-based spacing scale ensures vertical consistency. Use `lg (24px)` for outer card padding and `md (16px)` for internal element spacing.
- **Mobile Margins:** Standard mobile side margins are set to `md (16px)` to maximize screen real estate for dense data tables.
- **Sectioning:** Distinct operational modules should be separated by `xl (40px)` of vertical space to prevent cognitive overload.
## Elevation & Depth
This design system conveys hierarchy through **Tonal Layering** and **Subtle Ambient Shadows**. We avoid heavy skeuomorphism in favor of a "Flat-Plus" approach.
- **Base Layer:** The page background (`bg-page`) sits at the lowest elevation.
- **Mid Layer (Cards):** UI cards use a white surface with a subtle shadow (`0 4px 14px rgba(0,0,0,.07)`) to appear slightly lifted.
- **Interactive Layer:** On hover or active state, cards may transition to a higher elevation (`0 8px 28px rgba(0,0,0,.1)`) with a slight upward translation of -2px.
- **Overlays:** Modals and dropdowns use the highest z-index with a distinct backdrop blur to maintain focus on the task at hand.
- **Gradients:** Use soft radial gradients in the background (Blue/Purple) to create environmental depth without interfering with content legibility.
## Shapes
The shape language is **Rounded**, balancing industrial rigidity with modern software approachability.
- **Cards & Sections:** Use the standard `rounded-lg (1rem)` for primary containers.
- **Nodes & Blocks:** Architecture and data nodes use `rounded (0.5rem)` to fit more compactly within layouts.
- **Interactive Elements:** Buttons and tags use a "Pill-shape" (`rounded-xl` or 999px) to distinguish them from structural containers.
- **Borders:** Use `1px solid` with the `border-rule` color for most containers. Active states or primary focus areas may increase to `1.5px`.
## Components
**Buttons:**
- **Primary:** Solid `primary_color` with white text. Pill-shaped.
- **Secondary:** Transparent with `primary_color` border or subtle purple tint for operation tasks.
- **Danger:** Solid `status-error` for irreversible actions.
**Cards:**
- White background, `1px` border-rule, and `rounded-lg` corners.
- Headers should have a `18px` internal padding and a bottom border to separate from the content body.
**Input Fields:**
- Soft white background with `1px` border.
- Focused states should use a `primary_color` border with a subtle outer glow.
**Chips & Tags:**
- Used for status (e.g., "Active", "Offline").
- Backgrounds should use `8% opacity` versions of the semantic colors (Blue, Green, Red) with bolded text in the 100% opacity color.
**Data Visualization:**
- Charts should use the primary blue and secondary purple as the main data series colors.
- Use `data-mono` for all axis labels and tooltips to ensure technical accuracy.
**Status Indicators:**
- Small `7px` circular dots used within lists or architecture nodes to show real-time connectivity status.
@@ -0,0 +1,898 @@
<!DOCTYPE html>
<html lang="zh-CN"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>空间拓扑与用电架构 - XT-POWER OS</title>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=hankenGrotesk:wght@300;400;500;600;700&amp;family=JetBrains+Mono:wght@400;500;600;700&amp;display=swap" rel="stylesheet"/>
<style>
@layer base {
html, body { margin: 0; padding: 0; }
body { overscroll-behavior: none; }
}
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 9999px; }
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
.canvas-grid-bg {
background-color: #f8fafc;
background-image: radial-gradient(#cbd5e1 1px, transparent 1px);
background-size: 20px 20px;
}
</style>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
colors: {
"border-rule": "#dbe4ee",
"surface-container-lowest": "#ffffff",
"outline-variant": "#c2c6d8",
"surface-dim": "#ccdbf1",
"on-primary-fixed-variant": "#0040a1",
"on-tertiary-fixed-variant": "#005049",
"text-muted": "#5b6b7e",
"accent-subtle": "rgba(29,111,255,0.08)",
"outline": "#727787",
"secondary-fixed-dim": "#d2bbff",
"surface-container-low": "#eef4ff",
"primary-fixed-dim": "#b2c5ff",
"on-surface": "#0d1c2c",
"secondary-fixed": "#eaddff",
"on-tertiary-fixed": "#00201d",
"on-tertiary": "#ffffff",
"error-container": "#ffdad6",
"tertiary": "#00685f",
"on-secondary": "#ffffff",
"on-secondary-fixed": "#25005a",
"on-primary-fixed": "#001847",
"tertiary-fixed-dim": "#6bd8cb",
"surface-container": "#e5efff",
"on-error-container": "#93000a",
"surface-container-highest": "#d5e4fa",
"on-surface-variant": "#424655",
"inverse-primary": "#b2c5ff",
"inverse-surface": "#1e293b",
"tertiary-fixed": "#89f5e7",
"bg-page": "#f1f5f9",
"on-tertiary-container": "#f4fffc",
"surface-tint": "#0056d2",
"surface-container-high": "#dbe9ff",
"surface-variant": "#d5e4fa",
"primary-container": "#136bfb",
"on-secondary-container": "#fffbff",
"on-error": "#ffffff",
"primary-fixed": "#dae2ff",
"primary": "#0054cd",
"status-warn": "#e0852b",
"tertiary-container": "#008378",
"secondary": "#6366f1",
"on-primary": "#ffffff",
"on-background": "#0d1c2c",
"surface-bright": "#f8f9ff",
"inverse-on-surface": "#e9f1ff",
"on-secondary-fixed-variant": "#5a00c6",
"background": "#f1f5f9",
"secondary-container": "#8a4cfc",
"error": "#ba1a1a",
"on-primary-container": "#fefcff",
"surface": "#ffffff",
"status-error": "#dc2626"
},
fontFamily: {
"body-md": ["hankenGrotesk", "sans-serif"],
"data-mono": ["JetBrains Mono", "monospace"]
}
}
}
}
</script>
</head>
<body class="bg-bg-page font-body-md text-[14px] text-on-surface antialiased min-h-screen flex">
<!-- ==================== 1. 左侧固定全高全局导航栏 ==================== -->
<aside class="fixed left-0 top-0 h-full w-64 bg-inverse-surface text-inverse-on-surface z-50 flex flex-col justify-between shadow-xl">
<div class="flex flex-col flex-1 min-h-0">
<!-- 平台 Brand Logo -->
<div class="h-16 px-4 flex items-center gap-3 border-b border-slate-700/60 bg-slate-900/40">
<div class="w-9 h-9 rounded-lg bg-primary flex items-center justify-center text-white shadow-md shadow-blue-500/20">
<span class="material-symbols-outlined text-[22px]">bolt</span>
</div>
<div class="flex flex-col">
<div class="flex items-center gap-1.5">
<span class="font-bold text-[17px] text-white tracking-wide">XT-POWER</span>
<span class="text-[10px] font-semibold bg-primary text-white px-1.5 py-0.5 rounded leading-none">OS</span>
</div>
<span class="text-[11px] text-slate-400 tracking-tight">鑫潼智电 · 智能配用电中枢</span>
</div>
</div>
<!-- 导航功能菜单 -->
<div class="flex-1 overflow-y-auto px-3 py-4 space-y-1">
<a class="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-300 hover:bg-slate-800 hover:text-white transition-colors" href="#">
<span class="material-symbols-outlined text-[20px]">dashboard</span>
<span class="font-medium text-[13.5px]">运营工作台</span>
</a>
<!-- 激活的客户管理父节点 -->
<div class="space-y-1">
<div class="flex items-center justify-between px-3 py-2 rounded-lg bg-slate-800/80 text-white font-semibold">
<div class="flex items-center gap-3">
<span class="material-symbols-outlined text-[20px] text-blue-400">apartment</span>
<span class="text-[13.5px]">客户管理</span>
</div>
<span class="material-symbols-outlined text-[18px] text-slate-400">expand_more</span>
</div>
<!-- 二级菜单 -->
<div class="pl-4 pr-1 py-1 space-y-1 border-l-2 border-slate-700 ml-4">
<a class="flex items-center gap-2 px-3 py-1.5 rounded text-[13px] text-slate-400 hover:text-white hover:bg-slate-800 transition-colors" href="#">
<span class="w-1.5 h-1.5 rounded-full bg-slate-500"></span>
客户档案列表
</a>
<a class="flex items-center gap-2 px-3 py-1.5 rounded-lg text-[13px] font-semibold bg-primary text-white shadow-sm" href="#">
<span class="w-1.5 h-1.5 rounded-full bg-cyan-300"></span>
空间拓扑与用电架构
</a>
<a class="flex items-center gap-2 px-3 py-1.5 rounded text-[13px] text-slate-400 hover:text-white hover:bg-slate-800 transition-colors" href="#">
<span class="w-1.5 h-1.5 rounded-full bg-slate-500"></span>
微信小程序授权
</a>
</div>
</div>
<a class="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-300 hover:bg-slate-800 hover:text-white transition-colors" href="#">
<span class="material-symbols-outlined text-[20px]">assignment</span>
<span class="font-medium text-[13.5px]">项目管理</span>
</a>
<a class="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-300 hover:bg-slate-800 hover:text-white transition-colors" href="#">
<span class="material-symbols-outlined text-[20px]">send_time_extension</span>
<span class="font-medium text-[13.5px]">运营派单</span>
</a>
<a class="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-300 hover:bg-slate-800 hover:text-white transition-colors" href="#">
<span class="material-symbols-outlined text-[20px]">sensors</span>
<span class="font-medium text-[13.5px]">设备监控</span>
</a>
<a class="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-300 hover:bg-slate-800 hover:text-white transition-colors" href="#">
<span class="material-symbols-outlined text-[20px]">query_stats</span>
<span class="font-medium text-[13.5px]">统计报表</span>
</a>
<a class="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-300 hover:bg-slate-800 hover:text-white transition-colors" href="#">
<span class="material-symbols-outlined text-[20px]">settings</span>
<span class="font-medium text-[13.5px]">系统管理</span>
</a>
</div>
</div>
<!-- 侧栏底端通信链路卡 -->
<div class="p-3 m-3 rounded-lg bg-slate-800/80 border border-slate-700">
<div class="flex items-center justify-between mb-1.5">
<div class="flex items-center gap-1.5">
<span class="relative flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
</span>
<span class="text-[11px] font-medium text-slate-200">边缘网关链路</span>
</div>
<span class="font-data-mono text-[11px] text-emerald-400 font-semibold">在线 24/24</span>
</div>
<div class="flex items-center justify-between font-data-mono text-[11px] text-slate-400">
<span>时延: 14ms</span>
<span>丢包: 0.00%</span>
</div>
</div>
</aside>
<!-- ==================== 右侧主视窗区域 (pl-64) ==================== -->
<div class="pl-64 flex-1 flex flex-col min-h-screen bg-bg-page">
<!-- 顶部固定系统 Header -->
<header class="fixed top-0 left-64 right-0 h-16 bg-white/95 backdrop-blur-md z-40 border-b border-border-rule px-6 flex items-center justify-between">
<div class="flex items-center gap-5 flex-1 max-w-2xl">
<!-- 实时同步状态 Badge -->
<div class="flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-emerald-50 border border-emerald-200 text-emerald-700 shrink-0">
<span class="material-symbols-outlined text-emerald-600 text-[16px]">sync</span>
<span class="text-[12px] font-semibold">ICC 实时数据正常同步</span>
<span class="font-data-mono text-[11px] text-emerald-600/80 font-medium ml-1">120FPS</span>
</div>
<!-- 全局搜索 -->
<div class="relative flex-1">
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 text-[18px]">search</span>
<input class="w-full pl-9 pr-4 py-1.5 rounded-lg bg-slate-50 border border-slate-200 text-[13px] text-on-surface placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all" placeholder="按变电所编码、变压器回路或客户名称全局检索 (Ctrl + K)..." type="text"/>
</div>
</div>
<!-- 右侧工程师账户操作 -->
<div class="flex items-center gap-3">
<button class="relative p-2 rounded-lg hover:bg-slate-100 text-slate-600 transition-colors" title="消息通知">
<span class="material-symbols-outlined text-[20px]">notifications</span>
<span class="absolute top-1.5 right-1.5 w-2 h-2 rounded-full bg-status-error"></span>
</button>
<button class="p-2 rounded-lg hover:bg-slate-100 text-slate-600 transition-colors" title="工程工具">
<span class="material-symbols-outlined text-[20px]">apps</span>
</button>
<div class="h-4 w-[1px] bg-slate-200 mx-1"></div>
<div class="flex items-center gap-2.5 pl-1 cursor-pointer">
<div class="w-8 h-8 rounded-full bg-primary flex items-center justify-center shadow-sm text-white font-semibold text-[13px]">
李
</div>
<div class="flex flex-col text-left">
<span class="font-semibold text-[13px] text-slate-800 leading-tight">李工</span>
<span class="font-data-mono text-[11px] text-slate-400 leading-tight">主控工程师 · 调度中心</span>
</div>
<span class="material-symbols-outlined text-slate-400 text-[18px]">expand_more</span>
</div>
</div>
</header>
<!-- 主工作区内容 (pt-16) -->
<main class="pt-16 p-5 flex-1 flex flex-col gap-4">
<!-- ================= 顶栏:面包屑导航 + 客户信息与项目指标主栏 ================= -->
<div class="flex flex-col gap-2.5">
<!-- 路径导航与总线协议状态 -->
<div class="flex items-center justify-between">
<div class="flex items-center gap-1.5 font-data-mono text-[12px] text-slate-500">
<span>运营管控体系</span>
<span class="material-symbols-outlined text-[14px]">chevron_right</span>
<span>客户管理</span>
<span class="material-symbols-outlined text-[14px]">chevron_right</span>
<span class="text-primary font-semibold">空间拓扑与用电架构</span>
</div>
<div class="flex items-center gap-2 px-3 py-1 rounded-full bg-slate-100 border border-slate-200 text-slate-600 text-[11.5px] font-data-mono">
<span class="relative flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
</span>
<span>ICC 网关总线协议 (L3 API): 同步畅通 (102 pkt/s)</span>
</div>
</div>
<!-- 客户综合管理卡片 (水平一览化排布) -->
<div class="bg-white rounded-xl border border-border-rule shadow-sm px-5 py-3.5 flex flex-wrap items-center justify-between gap-4">
<!-- 客户切换与重点标签 -->
<div class="flex items-center gap-4">
<div class="flex items-center gap-3 bg-slate-50 border border-slate-200 px-3 py-2 rounded-lg hover:border-primary/40 cursor-pointer transition-colors group">
<div class="w-9 h-9 rounded bg-blue-50 text-primary flex items-center justify-center">
<span class="material-symbols-outlined text-[22px]">school</span>
</div>
<div class="flex flex-col">
<div class="flex items-center gap-2">
<span class="font-bold text-[15px] text-slate-900 group-hover:text-primary transition-colors">武汉软件工程职业学院</span>
<span class="material-symbols-outlined text-slate-400 text-[16px]">unfold_more</span>
</div>
<span class="font-data-mono text-[11.5px] text-slate-500">PRJ-HB-WH-00192 · 高校科研园区</span>
</div>
</div>
<!-- 业务标签 -->
<div class="flex items-center gap-1.5">
<span class="px-2 py-0.5 rounded bg-blue-50 text-blue-700 text-[11px] font-semibold border border-blue-200/60">A级重要负荷</span>
<span class="px-2 py-0.5 rounded bg-teal-50 text-teal-700 text-[11px] font-semibold border border-teal-200/60">双回路主供</span>
<span class="px-2 py-0.5 rounded bg-indigo-50 text-indigo-700 text-[11px] font-semibold border border-indigo-200/60">ICC物联映射中</span>
</div>
</div>
<!-- 关键工程指标 (5项指标) -->
<div class="flex items-center gap-6 bg-slate-50 border border-slate-100 rounded-lg px-4 py-2">
<div class="flex flex-col">
<span class="text-[11px] text-slate-500 font-medium">已接入建筑</span>
<div class="flex items-baseline gap-1">
<span class="font-bold text-[18px] text-slate-800 leading-tight">5</span>
<span class="text-[11px] text-slate-500">栋</span>
</div>
</div>
<div class="h-6 w-[1px] bg-slate-200"></div>
<div class="flex flex-col">
<span class="text-[11px] text-slate-500 font-medium">变电所/配电室</span>
<div class="flex items-baseline gap-1">
<span class="font-bold text-[18px] text-slate-800 leading-tight">4</span>
<span class="text-[11px] text-slate-500">处</span>
</div>
</div>
<div class="h-6 w-[1px] bg-slate-200"></div>
<div class="flex flex-col">
<span class="text-[11px] text-slate-500 font-medium">监测用电回路</span>
<div class="flex items-baseline gap-1">
<span class="font-bold text-[18px] text-primary leading-tight font-data-mono">36</span>
<span class="text-[11px] text-slate-500">路</span>
</div>
</div>
<div class="h-6 w-[1px] bg-slate-200"></div>
<div class="flex flex-col">
<span class="text-[11px] text-slate-500 font-medium">ICC边缘网关</span>
<div class="flex items-baseline gap-1">
<span class="font-bold text-[18px] text-emerald-600 leading-tight font-data-mono">4</span>
<span class="text-[11px] text-emerald-600 font-medium">台在线</span>
</div>
</div>
<div class="h-6 w-[1px] bg-slate-200"></div>
<div class="flex flex-col">
<span class="text-[11px] text-slate-500 font-medium">空间建模覆盖率</span>
<div class="flex items-center gap-1">
<span class="font-bold text-[18px] text-slate-800 leading-tight font-data-mono">100%</span>
<span class="material-symbols-outlined text-emerald-500 text-[18px]">verified</span>
</div>
</div>
</div>
<!-- 右侧操作按钮组 -->
<div class="flex items-center gap-2">
<button class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-slate-100 hover:bg-slate-200 text-slate-700 text-[12.5px] font-semibold transition-colors border border-slate-200">
<span class="material-symbols-outlined text-[17px] text-primary">cloud_sync</span>
<span>从ICC同步拓扑</span>
</button>
<button class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-slate-100 hover:bg-slate-200 text-slate-700 text-[12.5px] font-semibold transition-colors border border-slate-200">
<span class="material-symbols-outlined text-[17px]">file_upload</span>
<span>CAD/BIM导入</span>
</button>
<button class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-slate-100 hover:bg-slate-200 text-slate-700 text-[12.5px] font-semibold transition-colors border border-slate-200">
<span class="material-symbols-outlined text-[17px]">download</span>
<span>导出高清拓扑图</span>
</button>
<button class="flex items-center gap-1 px-3.5 py-1.5 rounded-lg bg-primary hover:bg-blue-700 text-white text-[12.5px] font-semibold shadow-sm transition-all hover:shadow">
<span class="material-symbols-outlined text-[17px]">add</span>
<span>新增空间节点</span>
</button>
</div>
</div>
</div>
<!-- ================= 核心工作区:三栏严谨布局 ================= -->
<!-- 左:280px 空间树 | 中:自适应宽度配电单线拓扑流 | 右:320px 属性检查与映射 -->
<div class="grid grid-cols-12 gap-4 flex-1 items-start">
<!-- ======================= LEFT COLUMN: 空间与建筑导航树 (280px左右 -> 3 cols) ======================= -->
<div class="col-span-12 lg:col-span-3 xl:col-span-3 bg-white rounded-xl border border-border-rule shadow-sm flex flex-col h-[780px] overflow-hidden">
<!-- 树顶控制栏 -->
<div class="p-3.5 border-b border-border-rule bg-slate-50/70 flex flex-col gap-2.5">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="material-symbols-outlined text-primary text-[20px]">account_tree</span>
<span class="font-bold text-[14px] text-slate-800">空间用电多维架构树</span>
</div>
<div class="flex items-center gap-1">
<button class="p-1 rounded hover:bg-slate-200 text-slate-500 hover:text-slate-800 transition-colors" title="折叠/展开全部">
<span class="material-symbols-outlined text-[17px]">unfold_less</span>
</button>
<button class="p-1 rounded hover:bg-slate-200 text-slate-500 hover:text-slate-800 transition-colors" title="快速定位">
<span class="material-symbols-outlined text-[17px]">my_location</span>
</button>
</div>
</div>
<!-- 搜索框 -->
<div class="relative">
<span class="material-symbols-outlined absolute left-2.5 top-1/2 -translate-y-1/2 text-slate-400 text-[16px]">search</span>
<input class="w-full pl-8 pr-3 py-1.5 rounded-lg bg-white border border-slate-200 text-[12.5px] placeholder:text-slate-400 focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary" placeholder="快速过滤建筑/配电柜/回路..." type="text"/>
</div>
</div>
<!-- 树形层级滚动容器 -->
<div class="flex-1 overflow-y-auto p-2.5 space-y-1 text-[13px]">
<!-- 根节点:武汉软件工程职业学院 (总校区) -->
<div class="flex items-center justify-between p-2 rounded-lg bg-blue-50/70 border border-blue-100 text-slate-900 font-semibold cursor-pointer">
<div class="flex items-center gap-1.5">
<span class="material-symbols-outlined text-primary text-[18px]">hub</span>
<span>武汉软件工程职业学院</span>
</div>
<span class="font-data-mono text-[10px] text-blue-600 bg-white px-1.5 py-0.5 rounded border border-blue-200">根节点</span>
</div>
<!-- Level 1 缩进列表 -->
<div class="pl-2.5 space-y-1 mt-1 border-l-2 border-slate-100 ml-3">
<!-- 1. 变电所节点 -->
<div class="flex items-center justify-between p-1.5 rounded-lg hover:bg-slate-100 text-slate-700 cursor-pointer group">
<div class="flex items-center gap-1.5">
<span class="material-symbols-outlined text-slate-400 text-[16px]">chevron_right</span>
<span class="material-symbols-outlined text-amber-600 text-[16px]">electrical_services</span>
<span class="text-[12.5px]">10kV 开闭所 / 变配电总房</span>
</div>
<span class="font-data-mono text-[11px] text-slate-400">主变 2台</span>
</div>
<!-- 2. 建筑节点:学生宿舍 12 号楼 (当前选中高亮展开) -->
<div class="rounded-lg border border-blue-200/80 bg-blue-50/30 overflow-hidden">
<div class="flex items-center justify-between p-1.5 bg-blue-100/50 text-primary font-bold cursor-pointer">
<div class="flex items-center gap-1.5">
<span class="material-symbols-outlined text-primary text-[16px]">arrow_drop_down</span>
<span class="material-symbols-outlined text-primary text-[16px]">domain</span>
<span class="text-[12.5px]">学生宿舍 12 号楼</span>
</div>
<div class="flex items-center gap-1">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
<span class="font-data-mono text-[10px] bg-primary text-white px-1.5 py-0.2 rounded font-medium">36回路</span>
</div>
</div>
<!-- 12号楼子节点 -->
<div class="pl-3 pr-1 py-1 space-y-0.5 text-[12px]">
<!-- 1F -->
<div class="flex items-center justify-between p-1 rounded hover:bg-slate-100 text-slate-600 cursor-pointer">
<div class="flex items-center gap-1.5">
<span class="material-symbols-outlined text-slate-400 text-[14px]">chevron_right</span>
<span class="material-symbols-outlined text-slate-400 text-[15px]">inventory_2</span>
<span>1F 楼层配电总箱 (DB-1E)</span>
</div>
<span class="font-data-mono text-[10.5px] text-slate-400">6 回路</span>
</div>
<!-- 2F (展开选中) -->
<div class="space-y-0.5">
<div class="flex items-center justify-between p-1 rounded bg-blue-100/70 text-slate-900 font-semibold cursor-pointer">
<div class="flex items-center gap-1.5">
<span class="material-symbols-outlined text-slate-600 text-[14px]">arrow_drop_down</span>
<span class="material-symbols-outlined text-indigo-600 text-[15px]">view_in_ar</span>
<span>2F 东分配电箱 (DB-2E)</span>
</div>
<span class="font-data-mono text-[10px] text-emerald-700 bg-emerald-100/80 px-1 py-0.2 rounded font-medium">已绑定</span>
</div>
<!-- 2F 下属各回路 -->
<div class="pl-4 space-y-0.5 border-l border-blue-200 ml-2">
<!-- 聚焦回路 -->
<div class="flex items-center justify-between p-1.5 rounded bg-primary text-white font-semibold shadow-sm cursor-pointer">
<div class="flex items-center gap-1">
<span class="material-symbols-outlined text-amber-300 text-[14px]">bolt</span>
<span class="text-[11.5px] truncate">201-208 宿舍照明与空调</span>
</div>
<span class="font-data-mono text-[10px] bg-white/20 px-1 rounded">焦点</span>
</div>
<!-- 其它回路 -->
<div class="flex items-center justify-between p-1 rounded hover:bg-slate-100 text-slate-600 cursor-pointer">
<div class="flex items-center gap-1">
<span class="material-symbols-outlined text-slate-400 text-[14px]">bolt</span>
<span class="text-[11.5px] truncate">2F 走廊应急指示与消防</span>
</div>
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
</div>
</div>
</div>
<!-- 3F - 6F -->
<div class="flex items-center justify-between p-1 rounded hover:bg-slate-100 text-slate-600 cursor-pointer">
<div class="flex items-center gap-1.5">
<span class="material-symbols-outlined text-slate-400 text-[14px]">chevron_right</span>
<span class="material-symbols-outlined text-slate-400 text-[15px]">folder</span>
<span>3F-6F 配电箱与各回路</span>
</div>
<span class="font-data-mono text-[10.5px] text-slate-400">24 回路</span>
</div>
</div>
</div>
<!-- 3. 数控智造实训中心 B座 -->
<div class="flex items-center justify-between p-1.5 rounded-lg hover:bg-slate-100 text-slate-700 cursor-pointer group">
<div class="flex items-center gap-1.5">
<span class="material-symbols-outlined text-slate-400 text-[16px]">chevron_right</span>
<span class="material-symbols-outlined text-amber-600 text-[16px]">precision_manufacturing</span>
<span class="text-[12.5px]">数控智造实训中心 B座</span>
</div>
<span class="font-data-mono text-[10px] text-amber-700 bg-amber-50 border border-amber-200 px-1.5 py-0.2 rounded font-medium">动力重载 ⚠️</span>
</div>
<!-- 4. 笃学图书馆 -->
<div class="flex items-center justify-between p-1.5 rounded-lg hover:bg-slate-100 text-slate-700 cursor-pointer group">
<div class="flex items-center gap-1.5">
<span class="material-symbols-outlined text-slate-400 text-[16px]">chevron_right</span>
<span class="material-symbols-outlined text-blue-500 text-[16px]">local_library</span>
<span class="text-[12.5px]">笃学图书馆 (主机房)</span>
</div>
<span class="text-[10.5px] text-slate-400">恒温恒湿</span>
</div>
<!-- 5. 师生第一餐饮中心 -->
<div class="flex items-center justify-between p-1.5 rounded-lg hover:bg-slate-100 text-slate-700 cursor-pointer group">
<div class="flex items-center gap-1.5">
<span class="material-symbols-outlined text-slate-400 text-[16px]">chevron_right</span>
<span class="material-symbols-outlined text-emerald-600 text-[16px]">restaurant</span>
<span class="text-[12.5px]">师生第一餐饮中心</span>
</div>
<span class="font-data-mono text-[10px] text-emerald-700 bg-emerald-50 border border-emerald-200 px-1.5 py-0.2 rounded">厨房排烟</span>
</div>
</div>
</div>
<!-- 树底状态小卡片 -->
<div class="p-3 border-t border-border-rule bg-slate-50 flex items-center justify-between">
<span class="text-[11.5px] text-slate-500">拓扑完整度校验:</span>
<div class="flex items-center gap-1 text-emerald-600 font-semibold text-[11.5px] font-data-mono">
<span class="material-symbols-outlined text-[15px]">check_circle</span>
<span>100% 完整无环路</span>
</div>
</div>
</div>
<!-- ======================= CENTER COLUMN: 配电拓扑全景流图 (6 cols) ======================= -->
<div class="col-span-12 lg:col-span-6 xl:col-span-6 bg-white rounded-xl border border-border-rule shadow-sm flex flex-col h-[780px] overflow-hidden">
<!-- 画布顶部工具栏 -->
<div class="px-4 py-2.5 border-b border-border-rule bg-slate-50/90 flex items-center justify-between">
<!-- 视图切换与当前焦点 -->
<div class="flex items-center gap-3">
<div class="flex items-center bg-slate-200/80 p-0.5 rounded-lg">
<button class="px-3 py-1 rounded-md bg-white text-primary text-[12px] font-semibold shadow-xs">
电气单线拓扑流
</button>
<button class="px-3 py-1 rounded-md text-slate-600 hover:text-slate-900 text-[12px] font-medium transition-colors">
空间三维映射
</button>
</div>
<div class="h-4 w-[1px] bg-slate-300"></div>
<div class="flex items-center gap-1 text-[12px]">
<span class="text-slate-500">焦点:</span>
<span class="font-semibold text-slate-800 bg-blue-50 text-primary px-2 py-0.5 rounded border border-blue-100">学生宿舍 12# · 2F配电系统</span>
</div>
</div>
<!-- 画布控制器 -->
<div class="flex items-center gap-1">
<button class="p-1 rounded hover:bg-slate-200 text-slate-600" title="缩小画布">
<span class="material-symbols-outlined text-[18px]">zoom_out</span>
</button>
<span class="font-data-mono text-[11.5px] font-semibold text-slate-700 px-1">100%</span>
<button class="p-1 rounded hover:bg-slate-200 text-slate-600" title="放大画布">
<span class="material-symbols-outlined text-[18px]">zoom_in</span>
</button>
<button class="p-1 rounded hover:bg-slate-200 text-slate-600" title="适应窗口">
<span class="material-symbols-outlined text-[18px]">fit_screen</span>
</button>
<div class="h-4 w-[1px] bg-slate-300 mx-1"></div>
<!-- 告警高亮标签 -->
<div class="flex items-center gap-1 px-2.5 py-1 rounded-md bg-amber-50 border border-amber-200 text-amber-700 text-[11px] font-semibold cursor-pointer hover:bg-amber-100 transition-colors">
<span class="material-symbols-outlined text-[15px] text-amber-600">warning</span>
<span>高亮预警 (1)</span>
</div>
</div>
</div>
<!-- 画布内容区 (工业微网格工程底图 + 自上而下层次严谨拓扑) -->
<div class="flex-1 canvas-grid-bg relative overflow-y-auto p-4 flex flex-col items-center select-none">
<!-- SVG 层:贯穿上下层次的母线与分支流线 -->
<svg class="absolute inset-0 w-full h-full pointer-events-none z-0" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- 箭头标记 -->
<marker id="bus-arrow" markerheight="6" markerwidth="6" orient="auto" refx="3" refy="3">
<path d="M0,0 L6,3 L0,6 Z" fill="#0054cd"></path>
</marker>
<marker id="bus-arrow-warn" markerheight="6" markerwidth="6" orient="auto" refx="3" refy="3">
<path d="M0,0 L6,3 L0,6 Z" fill="#e0852b"></path>
</marker>
</defs>
<!-- Level 1 -> Level 2 进线引下母线 -->
<line stroke="#0054cd" stroke-dasharray="4 2" stroke-width="2.5" x1="50%" x2="50%" y1="58" y2="88"></line>
<!-- Level 2 -> Level 3 变压器引出到 ICC 边缘网关 -->
<line stroke="#0054cd" stroke-width="2.5" x1="50%" x2="50%" y1="150" y2="182"></line>
<!-- Level 3 -> 低压总分配横向母线 (Busbar) -->
<line stroke="#0054cd" stroke-width="2.5" x1="50%" x2="50%" y1="262" y2="295"></line>
<line stroke="#0054cd" stroke-width="3" x1="18%" x2="82%" y1="295" y2="295"></line>
<!-- 低压母线分别引下到末端 3 组出线卡片 -->
<!-- 出线 1: 学生宿舍 (当前聚焦) -->
<line marker-end="url(#bus-arrow)" stroke="#0054cd" stroke-width="2.5" x1="18%" x2="18%" y1="295" y2="330"></line>
<!-- 出线 2: 实训中心 (动力重载预警) -->
<line marker-end="url(#bus-arrow-warn)" stroke="#e0852b" stroke-dasharray="3 2" stroke-width="2.5" x1="50%" x2="50%" y1="295" y2="330"></line>
<!-- 出线 3: 餐饮中心 -->
<line marker-end="url(#bus-arrow)" stroke="#0054cd" stroke-width="2.5" x1="82%" x2="82%" y1="295" y2="330"></line>
</svg>
<!-- ================= LEVEL 1: 10kV 高压进线段 ================= -->
<div class="relative z-10 w-full max-w-md bg-white border border-slate-200 rounded-lg shadow-sm px-4 py-2 flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="w-2.5 h-2.5 rounded-full bg-emerald-500 shadow-sm"></span>
<span class="font-bold text-[12.5px] text-slate-800">10kV 高压进线柜 (开闭所 AH1/AH2)</span>
</div>
<div class="flex items-center gap-2 font-data-mono text-[11px] text-slate-500">
<span class="bg-slate-100 px-1.5 py-0.5 rounded">10.45 kV</span>
<span class="bg-slate-100 px-1.5 py-0.5 rounded">50.02 Hz</span>
<span class="text-emerald-600 font-semibold">正常</span>
</div>
</div>
<!-- 垂直间隔 -->
<div class="h-7"></div>
<!-- ================= LEVEL 2: 变压段 ================= -->
<div class="relative z-10 w-full max-w-lg bg-white border-2 border-primary/40 rounded-xl shadow-md p-3 flex items-center justify-between bg-gradient-to-r from-blue-50/50 via-white to-blue-50/30">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-primary text-white flex items-center justify-center shadow-sm">
<span class="material-symbols-outlined text-[24px]">transform</span>
</div>
<div class="flex flex-col">
<div class="flex items-center gap-2">
<span class="font-bold text-[13.5px] text-slate-900">#1 变压器</span>
<span class="text-[11px] font-data-mono bg-blue-100 text-primary px-1.5 py-0.2 rounded font-medium">SCB13-1250kVA</span>
</div>
<span class="text-[11.5px] text-slate-500 font-data-mono">10kV 降 0.4kV 低压主母排供电</span>
</div>
</div>
<div class="flex items-center gap-3 font-data-mono">
<div class="flex flex-col items-end">
<span class="text-[10px] text-slate-400">负荷率</span>
<span class="text-[14px] font-bold text-primary">54.3%</span>
</div>
<div class="h-6 w-[1px] bg-slate-200"></div>
<div class="flex flex-col items-end">
<span class="text-[10px] text-slate-400">变压器温升</span>
<span class="text-[13px] font-semibold text-slate-700">42.0 ℃</span>
</div>
</div>
</div>
<!-- 垂直间隔 -->
<div class="h-8"></div>
<!-- ================= LEVEL 3: 大华 ICC 边缘网关层 ================= -->
<div class="relative z-10 w-full max-w-xl bg-white border border-indigo-200 rounded-xl shadow-sm p-3 flex items-center justify-between bg-gradient-to-r from-indigo-50/40 via-white to-indigo-50/20">
<div class="flex items-center gap-3">
<div class="w-9 h-9 rounded-lg bg-indigo-600 text-white flex items-center justify-center shadow-sm">
<span class="material-symbols-outlined text-[20px]">router</span>
</div>
<div class="flex flex-col">
<div class="flex items-center gap-2">
<span class="font-bold text-[13px] text-slate-900">大华 ICC 物联网边缘网关 #4</span>
<span class="font-data-mono text-[10px] bg-indigo-100 text-indigo-700 px-1.5 py-0.2 rounded font-medium">ICC-GW-V3.2</span>
</div>
<span class="font-data-mono text-[11px] text-slate-500">IP: 192.168.10.88 · RS485 总线 · 挂载36只智能微断</span>
</div>
</div>
<div class="flex items-center gap-3 font-data-mono text-[11px]">
<div class="flex flex-col items-end">
<span class="text-[10px] text-slate-400">通信在线率</span>
<span class="font-bold text-emerald-600">99.98%</span>
</div>
<div class="flex flex-col items-end">
<span class="text-[10px] text-slate-400">轮询周期</span>
<span class="font-semibold text-slate-700">1000ms</span>
</div>
</div>
</div>
<!-- 垂直间隔 -->
<div class="h-14"></div>
<!-- ================= LEVEL 4: 末端配电箱与监测回路卡片 (并列网格展现,绝不错位) ================= -->
<div class="relative z-10 w-full grid grid-cols-3 gap-3.5 pt-1">
<!-- 卡片 1: 学生宿舍 12# 2F东配电箱回路 (当前选中/焦点) -->
<div class="bg-white rounded-xl p-3 border-2 border-primary shadow-md flex flex-col justify-between relative cursor-pointer ring-4 ring-primary/10">
<!-- 顶部焦点标记与编码 -->
<div class="flex items-start justify-between pb-1 border-b border-slate-100">
<div class="flex items-center gap-1.5">
<span class="w-2 h-2 rounded-full bg-emerald-500"></span>
<span class="font-bold text-[12.5px] text-slate-900">宿舍照明空调回路</span>
</div>
<span class="font-data-mono text-[10px] bg-primary text-white px-1.5 py-0.5 rounded font-semibold">#CIR-0219</span>
</div>
<span class="text-[11px] text-slate-500 my-1 font-medium truncate">学生宿舍12号楼 · 2F东分配电箱</span>
<!-- 实时电气指标网格 (2x2) -->
<div class="grid grid-cols-2 gap-1.5 p-2 bg-slate-50 rounded-lg font-data-mono text-[11px] my-1.5 border border-slate-100">
<div class="flex justify-between">
<span class="text-slate-400">电压(U):</span>
<span class="font-bold text-slate-800">223.5 V</span>
</div>
<div class="flex justify-between">
<span class="text-slate-400">电流(I):</span>
<span class="font-bold text-slate-800">18.2 A</span>
</div>
<div class="flex justify-between">
<span class="text-slate-400">有功(P):</span>
<span class="font-bold text-primary">3.82 kW</span>
</div>
<div class="flex justify-between">
<span class="text-slate-400">功率因数:</span>
<span class="font-bold text-slate-800">0.94</span>
</div>
</div>
<!-- 运行状态与微断型号 -->
<div class="flex items-center justify-between text-[11px] font-data-mono pt-1 text-slate-600">
<span class="flex items-center gap-0.5">
<span class="material-symbols-outlined text-[13px] text-slate-400">thermostat</span>
<span>38.2℃</span>
</span>
<span class="text-emerald-700 font-semibold bg-emerald-50 px-1 rounded">正常 (62%载)</span>
</div>
<div class="mt-1 text-[10px] font-data-mono text-slate-400 border-t border-slate-100 pt-1 truncate">
ICC映射: DH-MCB-202409-9821
</div>
</div>
<!-- 卡片 2: 实训中心 B座 · 1F MCC-1 数控机床动力回路 (重载预警) -->
<div class="bg-white rounded-xl p-3 border border-amber-300 shadow-sm flex flex-col justify-between relative cursor-pointer hover:border-amber-400 transition-all">
<div class="flex items-start justify-between pb-1 border-b border-slate-100">
<div class="flex items-center gap-1.5">
<span class="relative flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-amber-500"></span>
</span>
<span class="font-bold text-[12.5px] text-slate-900">数控机床组动力专线</span>
</div>
<span class="font-data-mono text-[10px] bg-amber-100 text-amber-800 px-1.5 py-0.5 rounded font-semibold">#CIR-0881</span>
</div>
<span class="text-[11px] text-slate-500 my-1 font-medium truncate">实训中心 B座 · 1F MCC-1</span>
<div class="grid grid-cols-2 gap-1.5 p-2 bg-amber-50/50 rounded-lg font-data-mono text-[11px] my-1.5 border border-amber-100">
<div class="flex justify-between">
<span class="text-slate-400">电压(U):</span>
<span class="font-bold text-slate-800">382.1 V</span>
</div>
<div class="flex justify-between">
<span class="text-slate-400">电流(I):</span>
<span class="font-bold text-amber-700">58.4 A</span>
</div>
<div class="flex justify-between">
<span class="text-slate-400">瞬时(P):</span>
<span class="font-bold text-amber-700">38.4 kW</span>
</div>
<div class="flex justify-between">
<span class="text-slate-400">三相平衡:</span>
<span class="font-bold text-slate-800">98.2%</span>
</div>
</div>
<div class="flex items-center justify-between text-[11px] font-data-mono pt-1 text-slate-600">
<span class="flex items-center gap-0.5">
<span class="material-symbols-outlined text-[13px] text-slate-400">thermostat</span>
<span>54.8℃</span>
</span>
<span class="text-amber-700 font-semibold bg-amber-100 px-1 rounded">⚠️ 负荷率 76%</span>
</div>
<div class="mt-1 text-[10px] font-data-mono text-slate-400 border-t border-slate-100 pt-1 truncate">
ICC映射: DH-MCB-202409-1142
</div>
</div>
<!-- 卡片 3: 第一餐饮中心 · 动力配电箱厨房排烟风机 -->
<div class="bg-white rounded-xl p-3 border border-slate-200 shadow-sm flex flex-col justify-between relative cursor-pointer hover:border-slate-300 transition-all">
<div class="flex items-start justify-between pb-1 border-b border-slate-100">
<div class="flex items-center gap-1.5">
<span class="w-2 h-2 rounded-full bg-emerald-500"></span>
<span class="font-bold text-[12.5px] text-slate-900">厨房高压排烟风机</span>
</div>
<span class="font-data-mono text-[10px] bg-slate-100 text-slate-700 px-1.5 py-0.5 rounded font-semibold">#CIR-0402</span>
</div>
<span class="text-[11px] text-slate-500 my-1 font-medium truncate">第一餐饮中心 · 动力配电箱</span>
<div class="grid grid-cols-2 gap-1.5 p-2 bg-slate-50 rounded-lg font-data-mono text-[11px] my-1.5 border border-slate-100">
<div class="flex justify-between">
<span class="text-slate-400">电压(U):</span>
<span class="font-bold text-slate-800">380.4 V</span>
</div>
<div class="flex justify-between">
<span class="text-slate-400">电流(I):</span>
<span class="font-bold text-slate-800">14.1 A</span>
</div>
<div class="flex justify-between">
<span class="text-slate-400">瞬时(P):</span>
<span class="font-bold text-emerald-700">7.2 kW</span>
</div>
<div class="flex justify-between">
<span class="text-slate-400">燃气闭锁:</span>
<span class="font-bold text-emerald-700">闭合正常</span>
</div>
</div>
<div class="flex items-center justify-between text-[11px] font-data-mono pt-1 text-slate-600">
<span class="flex items-center gap-0.5">
<span class="material-symbols-outlined text-[13px] text-slate-400">thermostat</span>
<span>32.1℃</span>
</span>
<span class="text-emerald-700 font-semibold bg-emerald-50 px-1 rounded">负荷 38%</span>
</div>
<div class="mt-1 text-[10px] font-data-mono text-slate-400 border-t border-slate-100 pt-1 truncate">
ICC映射: DH-MCB-202409-5509
</div>
</div>
</div>
</div>
</div>
<!-- ======================= RIGHT COLUMN: 选中测点属性与映射配置 (320px固定 -> 3 cols) ======================= -->
<div class="col-span-12 lg:col-span-3 xl:col-span-3 bg-white rounded-xl border border-border-rule shadow-sm flex flex-col h-[780px] overflow-hidden">
<!-- 检查器头部 -->
<div class="p-3.5 border-b border-border-rule bg-slate-50/80 flex flex-col gap-1">
<div class="flex items-center justify-between">
<span class="text-[11px] font-bold text-slate-500 uppercase tracking-wider">节点属性与物联映射</span>
<span class="text-[11px] font-semibold bg-emerald-100 text-emerald-800 px-2 py-0.5 rounded-full flex items-center gap-1">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
已完成大华ICC映射
</span>
</div>
<h3 class="font-bold text-[14.5px] text-slate-900 mt-1 leading-tight">2F东分配电箱-照明回路</h3>
<span class="font-data-mono text-[11px] text-slate-500">#CIR-HB-WH00192-DB2E-01</span>
</div>
<!-- 属性滚动区域 -->
<div class="flex-1 overflow-y-auto p-3.5 space-y-3.5 text-[12.5px]">
<!-- 模块 1: 空间物理归属 -->
<div class="space-y-2">
<div class="flex items-center gap-1.5 text-primary font-bold text-[12px]">
<span class="material-symbols-outlined text-[16px]">apartment</span>
<span>空间物理归属信息</span>
</div>
<div class="bg-slate-50 border border-slate-200/70 rounded-lg p-2.5 space-y-1.5 text-[12px]">
<div class="flex justify-between">
<span class="text-slate-500">所属建筑:</span>
<span class="font-semibold text-slate-800">学生宿舍 12 号楼</span>
</div>
<div class="flex justify-between">
<span class="text-slate-500">所在楼层:</span>
<span class="font-semibold text-slate-800 font-data-mono">2 层 (Floor 02)</span>
</div>
<div class="flex justify-between">
<span class="text-slate-500">物理箱体:</span>
<span class="text-slate-800">东侧强电井 DB-2E 箱</span>
</div>
<div class="flex justify-between">
<span class="text-slate-500">覆盖房间:</span>
<span class="text-slate-800 font-semibold font-data-mono">201 ~ 208 宿舍房间</span>
</div>
</div>
</div>
<!-- 模块 2: 用电责任与能耗归属 -->
<div class="space-y-2">
<div class="flex items-center gap-1.5 text-indigo-600 font-bold text-[12px]">
<span class="material-symbols-outlined text-[16px]">corporate_fare</span>
<span>用电责任与能耗归属</span>
</div>
<div class="bg-slate-50 border border-slate-200/70 rounded-lg p-2.5 space-y-2">
<div class="space-y-1">
<label class="text-[11px] text-slate-500 font-medium">责任部门</label>
<select class="w-full px-2.5 py-1.5 bg-white border border-slate-200 rounded text-[12px] text-slate-800 font-medium focus:ring-1 focus:ring-primary focus:outline-none">
<option selected="">后勤物业管理处 - 宿管科</option>
<option>教务处 - 实验实训管理中心</option>
<option>保卫处 - 消防安全微型站</option>
<option>信息化办公室 - 网络数据中心</option>
</select>
</div>
<div class="grid grid-cols-2 gap-2">
<div class="space-y-1">
<label class="text-[11px] text-slate-500">责任人</label>
<input class="w-full px-2 py-1 bg-white border border-slate-200 rounded text-[12px] text-slate-800" type="text" value="王主管 (宿管)"/>
</div>
<div class="space-y-1">
<label class="text-[11px] text-slate-500">联系电话</label>
<input class="w-full px-2 py-1 bg-white border border-slate-200 rounded text-[11.5px] font-data-mono text-slate-800" type="text" value="138****5621"/>
</div>
</div>
<div class="flex items-center justify-between pt-1 border-t border-slate-200/60 text-[11.5px]">
<span class="text-slate-500">考核方式:</span>
<span class="text-primary font-semibold bg-blue-50 px-1.5 py-0.2 rounded border border-blue-100">定额包干 / 梯度考核</span>
</div>
</div>
</div>
<!-- 模块 3: 大华 ICC 边缘物联网映射 -->
<div class="space-y-2">
<div class="flex items-center gap-1.5 text-emerald-700 font-bold text-[12px]">
<span class="material-symbols-outlined text-[16px]">sensors</span>
<span>大华 ICC 边缘物联网映射</span>
</div>
<div class="bg-slate-50 border border-slate-200/70 rounded-lg p-2.5 space-y-1.5 font-data-mono text-[11.5px]">
<div class="flex justify-between items-center">
<span class="text-slate-500">绑定ICC设备码:</span>
<span class="font-bold text-slate-800">DH-MCB-202409-9821</span>
</div>
<div class="flex justify-between items-center">
<span class="text-slate-500">上联网关:</span>
<span class="text-indigo-600 font-semibold">ICC-GW-04 (192.168.10.88)</span>
</div>
<div class="flex justify-between items-center">
<span class="text-slate-500">采集轮询频率:</span>
<span class="text-slate-800 font-medium">5分钟主动拉取 (L3)</span>
</div>
<div class="flex justify-between items-center">
<span class="text-slate-500">额定电气规格:</span>
<span class="text-slate-800">63A / 220V 单相</span>
</div>
</div>
</div>
<!-- 模块 4: 告警联动阈值设置 -->
<div class="space-y-2">
<div class="flex items-center gap-1.5 text-amber-700 font-bold text-[12px]">
<span class="material-symbols-outlined text-[16px]">shield</span>
<span>告警联动阈值设置</span>
</div>
<div class="grid grid-cols-3 gap-1.5 text-center font-data-mono">
<div class="bg-slate-50 border border-slate-200 rounded p-1.5 flex flex-col">
<span class="text-[10px] text-slate-500">过载保护</span>
<span class="font-bold text-[12.5px] text-amber-700">&gt; 50.0 A</span>
<span class="text-[9px] text-slate-400">(80%额定)</span>
</div>
<div class="bg-slate-50 border border-slate-200 rounded p-1.5 flex flex-col">
<span class="text-[10px] text-slate-500">温升预警</span>
<span class="font-bold text-[12.5px] text-rose-600">&gt; 60.0 ℃</span>
<span class="text-[9px] text-slate-400">探头温度</span>
</div>
<div class="bg-slate-50 border border-slate-200 rounded p-1.5 flex flex-col">
<span class="text-[10px] text-slate-500">漏电动作</span>
<span class="font-bold text-[12.5px] text-primary">&gt; 30 mA</span>
<span class="text-[9px] text-slate-400">瞬时速断</span>
</div>
</div>
</div>
</div>
<!-- 右侧底部固定操作栏 -->
<div class="p-3 border-t border-border-rule bg-slate-50/90 flex flex-col gap-2 mt-auto">
<button class="w-full py-2 rounded-lg bg-primary hover:bg-blue-700 text-white font-semibold text-[13px] shadow-sm transition-all text-center">
保存节点与 ICC 映射配置
</button>
<button class="w-full py-1.5 rounded-lg bg-white hover:bg-slate-100 border border-slate-200 text-slate-700 font-semibold text-[12px] transition-colors text-center">
下发 ICC 采集同步
</button>
</div>
</div>
</div>
<!-- ================= 底部微状态栏 (严谨工业级指标) ================= -->
<footer class="bg-white rounded-lg border border-border-rule px-4 py-2 flex flex-wrap items-center justify-between text-[11.5px] font-data-mono text-slate-500 shadow-xs">
<div class="flex items-center gap-5">
<div class="flex items-center gap-1.5">
<span class="w-2 h-2 rounded-full bg-emerald-500"></span>
<span>拓扑校验状态: <span class="text-emerald-700 font-semibold">100% 完整无环路</span></span>
</div>
<span>•</span>
<span>当前坐标引擎: <span class="text-slate-700">Graph-Core v2.4</span></span>
<span>•</span>
<span>NTP同步偏差: <span class="text-slate-700 font-semibold">&lt; 2μs (NTP-CHENGDU-PTP)</span></span>
</div>
<div class="flex items-center gap-4">
<span class="text-emerald-700 font-medium">边缘通信: 在线 24/24 链路正常</span>
<span class="text-slate-400">|</span>
<span class="text-slate-700 font-semibold">鑫潼智电 · XT-POWER OS</span>
</div>
</footer>
</main>
</div>
</body></html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 645 KiB

+3 -1
View File
@@ -806,9 +806,11 @@ erDiagram
<div class="table-wrap"><table><thead><tr><th>字段</th><th>类型</th><th>键</th><th>说明</th></tr></thead><tbody>
<tr><td><code>id</code></td><td>BIGSERIAL</td><td><span class="pill p-pk">PK</span></td><td>自增主键</td></tr>
<tr><td><code>worker_name</code></td><td>VARCHAR(64)</td><td></td><td>电工姓名</td></tr>
<tr><td><code>phone</code></td><td>VARCHAR(20)</td><td><span class="pill p-pi">IDX</span></td><td>手机号</td></tr>
<tr><td><code>phone</code></td><td>VARCHAR(20)</td><td><span class="pill p-uk">UNIQUE</span></td><td>手机号(登录帐号)</td></tr>
<tr><td><code>password_hash</code></td><td>VARCHAR(255)</td><td></td><td>密码哈希(BCrypt)</td></tr>
<tr><td><code>id_card</code></td><td>VARCHAR(18)</td><td></td><td>身份证号</td></tr>
<tr><td><code>status</code></td><td>SMALLINT</td><td></td><td>状态:1在岗 0离职</td></tr>
<tr><td><code>last_login_at</code></td><td>TIMESTAMPTZ</td><td></td><td>最后登录时间</td></tr>
<tr><td><code>created_at</code></td><td>TIMESTAMPTZ</td><td></td><td>创建时间</td></tr>
<tr><td><code>updated_at</code></td><td>TIMESTAMPTZ</td><td></td><td>更新时间</td></tr>
</tbody></table></div>