refactor(客户管理): 完成客户管理模块UI与架构全面调整
1. 调整菜单与路由:重命名客户信息/客户账号为客户档案管理/客户小程序授权管理,移除客户建筑/房间路由,新增空间拓扑路由 2. 后端新增客户统计接口`/customers/stats`,前端同步API与类型定义 3. 重构客户档案与小程序授权页面,仅保留数据库支持字段,移除无支撑功能 4. 新增空间拓扑页面,合并建筑与房间管理为树形拓扑视图 5. 更新数据库种子数据与权限配置,清理废弃菜单与绑定关系 6. 补充UI设计文档与规范说明
This commit is contained in:
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
|
||||
@@ -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),
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
@@ -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 & 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
@@ -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>
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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 }
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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&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":{"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&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?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 & 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%]">托管周期 & 费用</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&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=hankenGrotesk:wght@100..900&family=JetBrains+Mono:wght@400..700&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">
|
||||
由【系统管理 > 权限分配】独立派发,指派企业法人、运维电工及主管查看权限。
|
||||
</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&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=hankenGrotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&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">> 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">> 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">> 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">< 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
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user