feat: 完成客户拜访模块全功能开发

本次提交实现了完整的客户拜访业务系统:
1.  新增对象存储上传服务,支持S3兼容协议的RustFS存储
2.  完善客户拜访实体与数据库表结构,补充全业务字段
3.  开发拜访台账与记录的前后端接口,包含增删改查、批量创建、状态流转等能力
4.  新增拜访表单页面与路由,支持父级菜单补全面包屑
5.  新增勘察列表页预填客户ID跳转逻辑
6.  补充UI设计文档与页面截图
7.  调整日志中间件跳过上传路由,优化RBAC权限配置
This commit is contained in:
andyesfly committed 2026-09-14 11:42:10 +08:00
1 parent 68af363ca9
commit 1c83e09fa1
33 files changed
+6874 -541

No files matched your search

+185 -15
View File
@@ -1,5 +1,67 @@
import request from '@/utils/request';
// 拜访类型:1首访摸排 2方案沟通 3商务二访 4维保回访 5应急巡检
export const VISIT_TYPE_OPTIONS = [
{ value: 1, label: '首访摸排' },
{ value: 2, label: '方案沟通' },
{ value: 3, label: '商务二访' },
{ value: 4, label: '维保回访' },
{ value: 5, label: '应急巡检' }
];
// 拜访方式:1现场拜访 2商务拜访 3电话沟通 4线上会议
export const VISIT_MODE_OPTIONS = [
{ value: 1, label: '现场拜访' },
{ value: 2, label: '商务拜访' },
{ value: 3, label: '电话沟通' },
{ value: 4, label: '线上会议' }
];
// 优先级:1特急P1 2加急P2 3常规P3
export const VISIT_PRIORITY_OPTIONS = [
{ value: 1, label: '特急 · P1高优' },
{ value: 2, label: '加急 · P2高优' },
{ value: 3, label: '常规 · P3回访' }
];
// 意向评级:1=A高意向 2=B中意向 3=C低意向
export const VISIT_INTENT_OPTIONS = [
{ value: 1, label: 'A级 · 高意向' },
{ value: 2, label: 'B级 · 中意向' },
{ value: 3, label: 'C级 · 低意向' }
];
// 状态:0待执行 1已完成待复核 2取证待技术复核 3已立项转勘察 4已归档 5已取消
export const VISIT_STATUS_OPTIONS = [
{ value: 0, label: '待执行' },
{ value: 1, label: '已完成待复核' },
{ value: 2, label: '取证待技术复核' },
{ value: 3, label: '已立项转勘察' },
{ value: 4, label: '已归档' },
{ value: 5, label: '已取消' }
];
export const VISIT_STATUS = {
PENDING: 0,
DONE: 1,
EVIDENCE: 2,
SURVEY: 3,
ARCHIVED: 4,
CANCELED: 5
};
// 关联客户(列表 include 的精简字段)
export interface CustomerVisitCustomer {
id: number;
customerNo?: string;
customerName?: string;
customerLevel?: string;
contactPerson?: string;
contactPhone?: string;
address?: string;
salesmanId?: number;
}
export interface CustomerVisitInfo {
id: number;
visitNo: string;
@@ -10,7 +72,40 @@ export interface CustomerVisitInfo {
visitTime?: string;
salesmanId?: number;
willPlan?: number;
createdAt: string;
visitType?: number;
visitMode?: number;
priority?: number;
subject?: string;
planTime?: string;
planEndTime?: string;
checkinTime?: string;
checkinStatus?: number;
checkinLng?: number;
checkinLat?: number;
spaceDesc?: string;
intentLevel?: number;
intentDesc?: string;
demands?: string;
companion?: string;
receptionPerson?: string;
receptionTitle?: string;
receptionPhone?: string;
resultRating?: string;
status: number;
nextVisitTime?: string;
nextAction?: string;
// 现场照片对象 key 列表(持久化字段)
photos?: string[];
// 现场照片预签名访问 URL 列表(由后端动态生成)
photoUrls?: string[];
surveyOrderId?: number;
projectNo?: string;
created_at?: string;
updated_at?: string;
// 关联客户与业务员信息(列表/详情附带)
customer?: CustomerVisitCustomer;
salesmanName?: string;
salesmanPhone?: string;
}
export interface CustomerVisitQuery {
@@ -19,6 +114,11 @@ export interface CustomerVisitQuery {
keyword?: string;
customerId?: number;
salesmanId?: number;
visitType?: number;
status?: number;
priority?: number;
planStartDate?: string;
planEndDate?: string;
}
export interface CustomerVisitListResult {
@@ -28,39 +128,109 @@ export interface CustomerVisitListResult {
pageSize: number;
}
export interface CustomerVisitCreateParams {
customerId: number;
visitPerson: string;
visitPhone?: string;
visitContent?: string;
visitTime?: string;
salesmanId?: number;
willPlan?: number;
export interface CustomerVisitStats {
total: number;
pending: number;
todayPlan: number;
urgentPending: number;
conversionRate: number;
pendingReview: number;
completed: number;
converted: number;
intentTotal: number;
}
export interface CustomerVisitUpdateParams {
customerId?: number;
export interface CustomerVisitCreateParams {
customerId: number;
visitPerson?: string;
visitPhone?: string;
visitContent?: string;
visitTime?: string;
salesmanId?: number;
willPlan?: number;
visitType?: number;
visitMode?: number;
priority?: number;
subject?: string;
planTime?: string;
planEndTime?: string;
spaceDesc?: string;
intentLevel?: number;
intentDesc?: string;
demands?: string;
companion?: string;
receptionPerson?: string;
receptionTitle?: string;
receptionPhone?: string;
resultRating?: string;
status?: number;
nextVisitTime?: string;
nextAction?: string;
photos?: string[];
}
export type CustomerVisitUpdateParams = Partial<CustomerVisitCreateParams> & {
checkinStatus?: number;
checkinTime?: string;
checkinLng?: number;
checkinLat?: number;
};
export interface CustomerVisitBatchCreateParams {
customerIds: number[];
planTime: string;
planEndTime?: string;
salesmanId?: number;
visitType?: number;
visitMode?: number;
priority?: number;
subject?: string;
}
export interface CustomerVisitBatchCreateResult {
total: number;
ids: number[];
}
export interface CustomerVisitStatusParams {
status: number;
checkinTime?: string;
resultRating?: string;
surveyOrderId?: number;
projectNo?: string;
}
export interface CustomerVisitReassignParams {
salesmanId: number;
}
export const customerVisitApi = {
getList: (params: CustomerVisitQuery) =>
request.get<CustomerVisitListResult>('/customer-visits', { params }),
// KPI 统计卡
getStats: () => request.get<CustomerVisitStats>('/customer-visits/stats'),
getDetail: (id: number) =>
request.get<CustomerVisitInfo>(`/customer-visits/${id}`),
create: (data: CustomerVisitCreateParams) =>
request.post('/customer-visits', data),
request.post<CustomerVisitInfo>('/customer-visits', data),
// 批量安排拜访:多选客户生成统一的待执行计划
batchCreate: (data: CustomerVisitBatchCreateParams) =>
request.post<CustomerVisitBatchCreateResult>('/customer-visits/batch', data),
update: (id: number, data: CustomerVisitUpdateParams) =>
request.put(`/customer-visits/${id}`, data),
request.put<CustomerVisitInfo>(`/customer-visits/${id}`, data),
delete: (id: number) =>
request.delete(`/customer-visits/${id}`),
// 状态流转:执行跟进 / 审核现场取证 / 归档 / 取消
updateStatus: (id: number, data: CustomerVisitStatusParams) =>
request.put<CustomerVisitInfo>(`/customer-visits/${id}/status`, data),
// 改派:更换跟进业务员
reassign: (id: number, data: CustomerVisitReassignParams) =>
request.put<CustomerVisitInfo>(`/customer-visits/${id}/reassign`, data),
delete: (id: number) => request.delete(`/customer-visits/${id}`)
};
+23
View File
@@ -0,0 +1,23 @@
import request from '@/utils/request';
export interface UploadImageResult {
// 对象存储 key(用于随表单持久化)
key: string;
// 预签名访问 URL(用于即时预览)
url: string;
name: string;
size: number;
}
// 上传现场照片到对象存储(RustFS),返回对象 key 与预签名 URL
export const uploadApi = {
uploadImage: (file: File) => {
const formData = new FormData();
formData.append('files', file);
return request.post<UploadImageResult>('/upload/image', formData, {
// 显式声明 multipart,避免 axios 将 FormData 序列化为 JSON
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 60000
});
}
};
+13 -2
View File
@@ -228,10 +228,21 @@ interface BreadcrumbItem {
const breadcrumbList = computed<BreadcrumbItem[]>(() => {
const matched = route.matched.filter(item => item.meta && item.meta.title)
return matched.map(item => ({
const items: BreadcrumbItem[] = []
for (const item of matched) {
// 详情/表单类页面路由与之平铺,需在 meta 中声明上级菜单以补齐缺失层级
if (item.meta?.parentTitle) {
items.push({
title: item.meta.parentTitle as string,
path: item.meta.parentPath as string | undefined,
})
}
items.push({
title: item.meta?.title as string,
path: item.path,
}))
})
}
return items
})
// 监听路由变化,更新菜单选中状态
+24
View File
@@ -55,6 +55,30 @@ const routes: RouteRecordRaw[] = [
component: () => import("@/views/customer/visit/index.vue"),
meta: { requiresAuth: true, title: "客户拜访", permission: "customer:visit" }
},
{
path: "customer/visit/create",
name: "CustomerVisitCreate",
component: () => import("@/views/customer/visit/form.vue"),
meta: {
requiresAuth: true,
title: "新增客户拜访记录",
permission: "customer:visit",
parentTitle: "客户拜访",
parentPath: "/customer/visit"
}
},
{
path: "customer/visit/edit/:id",
name: "CustomerVisitEdit",
component: () => import("@/views/customer/visit/form.vue"),
meta: {
requiresAuth: true,
title: "编辑客户拜访记录",
permission: "customer:visit",
parentTitle: "客户拜访",
parentPath: "/customer/visit"
}
},
{
path: "customer/topology",
name: "CustomerTopology",
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+10 -2
View File
@@ -125,6 +125,7 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { message } from 'ant-design-vue';
import { SearchOutlined, ReloadOutlined, PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
import dayjs from 'dayjs';
@@ -133,6 +134,7 @@ import { customerApi } from '@/api/customer';
import { useTableScroll } from '@/composables/useTableScroll';
const { wrapperRef, tableHeight } = useTableScroll();
const route = useRoute();
const loading = ref(false);
const dataSource = ref<SurveyOrderInfo[]>([]);
@@ -177,9 +179,15 @@ const pagination = computed(() => ({
showTotal: (t: number) => `共 ${t} 条数据`,
}));
onMounted(() => {
onMounted(async () => {
fetchList();
fetchCustomerOptions();
await fetchCustomerOptions();
// 客户拜访台账「一键发起勘察派单」跳转预填:/project/survey?customerId=xx
const preCustomerId = Number(route.query.customerId || 0);
if (preCustomerId) {
handleAdd();
formData.customerId = preCustomerId;
}
});
async function fetchList() {
+58 -2
View File
@@ -267,7 +267,33 @@ CREATE TABLE customer_visit (
visit_time DATE,
salesman_id BIGINT,
will_plan SMALLINT DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
visit_type SMALLINT DEFAULT 1,
visit_mode SMALLINT DEFAULT 1,
priority SMALLINT DEFAULT 3,
subject VARCHAR(128),
plan_time TIMESTAMPTZ,
plan_end_time TIMESTAMPTZ,
checkin_time TIMESTAMPTZ,
checkin_status SMALLINT DEFAULT 0,
checkin_lng DECIMAL(10,6),
checkin_lat DECIMAL(10,6),
space_desc VARCHAR(128),
intent_level SMALLINT,
intent_desc VARCHAR(64),
demands VARCHAR(512),
companion VARCHAR(128),
reception_person VARCHAR(64),
reception_title VARCHAR(64),
reception_phone VARCHAR(20),
result_rating VARCHAR(64),
status SMALLINT DEFAULT 0,
next_visit_time DATE,
next_action VARCHAR(128),
photos VARCHAR(1024),
survey_order_id BIGINT,
project_no VARCHAR(64),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE customer_visit IS '客户拜访信息表';
COMMENT ON COLUMN customer_visit.id IS '自增主键';
@@ -275,13 +301,43 @@ COMMENT ON COLUMN customer_visit.visit_no IS '拜访单号';
COMMENT ON COLUMN customer_visit.customer_id IS '客户 ID(→ customer)';
COMMENT ON COLUMN customer_visit.visit_person IS '拜访人';
COMMENT ON COLUMN customer_visit.visit_phone IS '拜访人电话';
COMMENT ON COLUMN customer_visit.visit_content IS '拜访内容';
COMMENT ON COLUMN customer_visit.visit_content IS '拜访内容/沟通纪要';
COMMENT ON COLUMN customer_visit.visit_time IS '拜访时间';
COMMENT ON COLUMN customer_visit.salesman_id IS '业务员';
COMMENT ON COLUMN customer_visit.will_plan IS '是否做方案:0不同意 1同意';
COMMENT ON COLUMN customer_visit.visit_type IS '拜访类型:1首访摸排 2方案沟通 3商务二访 4维保回访 5应急巡检';
COMMENT ON COLUMN customer_visit.visit_mode IS '拜访方式:1现场拜访 2商务拜访 3电话沟通 4线上会议';
COMMENT ON COLUMN customer_visit.priority IS '优先级:1特急P1 2加急P2 3常规P3';
COMMENT ON COLUMN customer_visit.subject IS '拜访主题/目的';
COMMENT ON COLUMN customer_visit.plan_time IS '计划拜访开始时间';
COMMENT ON COLUMN customer_visit.plan_end_time IS '计划拜访结束时间';
COMMENT ON COLUMN customer_visit.checkin_time IS '到场签到时间';
COMMENT ON COLUMN customer_visit.checkin_status IS '签到状态:0未签到 1已签到';
COMMENT ON COLUMN customer_visit.checkin_lng IS '签到经度';
COMMENT ON COLUMN customer_visit.checkin_lat IS '签到纬度';
COMMENT ON COLUMN customer_visit.space_desc IS '客户空间性质/场所描述';
COMMENT ON COLUMN customer_visit.intent_level IS '意向评级:1=A高意向 2=B中意向 3=C低意向';
COMMENT ON COLUMN customer_visit.intent_desc IS '现场意向说明';
COMMENT ON COLUMN customer_visit.demands IS '诉求痛点(逗号分隔)';
COMMENT ON COLUMN customer_visit.companion IS '陪同人员';
COMMENT ON COLUMN customer_visit.reception_person IS '客户接待人';
COMMENT ON COLUMN customer_visit.reception_title IS '接待人职务';
COMMENT ON COLUMN customer_visit.reception_phone IS '接待人联系方式';
COMMENT ON COLUMN customer_visit.result_rating IS '现场成果评级/结论';
COMMENT ON COLUMN customer_visit.status IS '状态:0待执行 1已完成待复核 2取证待技术复核 3已立项转勘察 4已归档 5已取消';
COMMENT ON COLUMN customer_visit.next_visit_time IS '预计下次回访日期';
COMMENT ON COLUMN customer_visit.next_action IS '下一步行动目标';
COMMENT ON COLUMN customer_visit.photos IS '现场照片对象key(JSON数组字符串)';
COMMENT ON COLUMN customer_visit.survey_order_id IS '关联勘察单ID';
COMMENT ON COLUMN customer_visit.project_no IS '关联工程编号';
COMMENT ON COLUMN customer_visit.created_at IS '创建时间';
COMMENT ON COLUMN customer_visit.updated_at IS '更新时间';
CREATE INDEX idx_visit_customer ON customer_visit (customer_id);
CREATE INDEX idx_visit_salesman ON customer_visit (salesman_id);
CREATE INDEX idx_visit_status ON customer_visit (status);
CREATE INDEX idx_visit_plan_time ON customer_visit (plan_time);
CREATE TRIGGER trg_customer_visit_updated_at BEFORE UPDATE ON customer_visit
FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at();
-- 11. 客户智电改造建筑信息表
DROP TABLE IF EXISTS customer_building CASCADE;
@@ -0,0 +1,88 @@
-- 客户拜访功能扩展:补齐台账/记录页所需的业务字段、更新时间与查询索引
-- 背景:db_init.sql 中 customer_visit 原为 9 字段的极简结构,
-- 现按「客户拜访台帐」「客户拜访记录」两份 UI 稿完整落地。
-- 幂等:全部使用 IF NOT EXISTS,可重复执行。
BEGIN;
-- 1) 拜访属性
ALTER TABLE customer_visit ADD COLUMN IF NOT EXISTS visit_type SMALLINT DEFAULT 1; -- 拜访类型:1首访摸排 2方案沟通 3商务二访 4维保回访 5应急巡检
ALTER TABLE customer_visit ADD COLUMN IF NOT EXISTS visit_mode SMALLINT DEFAULT 1; -- 拜访方式:1现场拜访 2商务拜访 3电话沟通 4线上会议
ALTER TABLE customer_visit ADD COLUMN IF NOT EXISTS priority SMALLINT DEFAULT 3; -- 优先级:1特急P1 2加急P2 3常规P3
ALTER TABLE customer_visit ADD COLUMN IF NOT EXISTS subject VARCHAR(128); -- 拜访主题/目的
-- 2) 计划与签到
ALTER TABLE customer_visit ADD COLUMN IF NOT EXISTS plan_time TIMESTAMPTZ; -- 计划拜访开始时间
ALTER TABLE customer_visit ADD COLUMN IF NOT EXISTS plan_end_time TIMESTAMPTZ; -- 计划拜访结束时间
ALTER TABLE customer_visit ADD COLUMN IF NOT EXISTS checkin_time TIMESTAMPTZ; -- 到场签到时间
ALTER TABLE customer_visit ADD COLUMN IF NOT EXISTS checkin_status SMALLINT DEFAULT 0; -- 签到状态:0未签到 1已签到
ALTER TABLE customer_visit ADD COLUMN IF NOT EXISTS checkin_lng DECIMAL(10,6); -- 签到经度
ALTER TABLE customer_visit ADD COLUMN IF NOT EXISTS checkin_lat DECIMAL(10,6); -- 签到纬度
-- 3) 现场信息与意向
ALTER TABLE customer_visit ADD COLUMN IF NOT EXISTS space_desc VARCHAR(128); -- 客户空间性质/场所描述
ALTER TABLE customer_visit ADD COLUMN IF NOT EXISTS intent_level SMALLINT; -- 意向评级:1=A高意向 2=B中意向 3=C低意向
ALTER TABLE customer_visit ADD COLUMN IF NOT EXISTS intent_desc VARCHAR(64); -- 现场意向说明
ALTER TABLE customer_visit ADD COLUMN IF NOT EXISTS demands VARCHAR(512); -- 诉求痛点(逗号分隔)
ALTER TABLE customer_visit ADD COLUMN IF NOT EXISTS companion VARCHAR(128); -- 陪同人员
ALTER TABLE customer_visit ADD COLUMN IF NOT EXISTS reception_person VARCHAR(64); -- 客户接待人
ALTER TABLE customer_visit ADD COLUMN IF NOT EXISTS reception_title VARCHAR(64); -- 接待人职务
ALTER TABLE customer_visit ADD COLUMN IF NOT EXISTS reception_phone VARCHAR(20); -- 接待人联系方式
ALTER TABLE customer_visit ADD COLUMN IF NOT EXISTS result_rating VARCHAR(64); -- 现场成果评级/结论
-- 4) 状态与后续跟进
ALTER TABLE customer_visit ADD COLUMN IF NOT EXISTS status SMALLINT DEFAULT 0; -- 状态:0待执行 1已完成待复核 2取证待技术复核 3已立项转勘察 4已归档 5已取消
ALTER TABLE customer_visit ADD COLUMN IF NOT EXISTS next_visit_time DATE; -- 预计下次回访日期
ALTER TABLE customer_visit ADD COLUMN IF NOT EXISTS next_action VARCHAR(128); -- 下一步行动目标
-- 5) 现场照片(对象存储 key 的 JSON 数组字符串)
ALTER TABLE customer_visit ADD COLUMN IF NOT EXISTS photos VARCHAR(1024); -- 现场照片对象key(JSON数组字符串)
-- 6) 工程关联
ALTER TABLE customer_visit ADD COLUMN IF NOT EXISTS survey_order_id BIGINT; -- 关联勘察单ID
ALTER TABLE customer_visit ADD COLUMN IF NOT EXISTS project_no VARCHAR(64); -- 关联工程编号
-- 7) 更新时间(Sequelize 实体已开启 updated_at,缺列会导致写入失败)
ALTER TABLE customer_visit ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
-- 8) 字段注释
COMMENT ON COLUMN customer_visit.visit_type IS '拜访类型:1首访摸排 2方案沟通 3商务二访 4维保回访 5应急巡检';
COMMENT ON COLUMN customer_visit.visit_mode IS '拜访方式:1现场拜访 2商务拜访 3电话沟通 4线上会议';
COMMENT ON COLUMN customer_visit.priority IS '优先级:1特急P1 2加急P2 3常规P3';
COMMENT ON COLUMN customer_visit.subject IS '拜访主题/目的';
COMMENT ON COLUMN customer_visit.plan_time IS '计划拜访开始时间';
COMMENT ON COLUMN customer_visit.plan_end_time IS '计划拜访结束时间';
COMMENT ON COLUMN customer_visit.checkin_time IS '到场签到时间';
COMMENT ON COLUMN customer_visit.checkin_status IS '签到状态:0未签到 1已签到';
COMMENT ON COLUMN customer_visit.checkin_lng IS '签到经度';
COMMENT ON COLUMN customer_visit.checkin_lat IS '签到纬度';
COMMENT ON COLUMN customer_visit.space_desc IS '客户空间性质/场所描述';
COMMENT ON COLUMN customer_visit.intent_level IS '意向评级:1=A高意向 2=B中意向 3=C低意向';
COMMENT ON COLUMN customer_visit.intent_desc IS '现场意向说明';
COMMENT ON COLUMN customer_visit.demands IS '诉求痛点(逗号分隔)';
COMMENT ON COLUMN customer_visit.companion IS '陪同人员';
COMMENT ON COLUMN customer_visit.reception_person IS '客户接待人';
COMMENT ON COLUMN customer_visit.reception_title IS '接待人职务';
COMMENT ON COLUMN customer_visit.reception_phone IS '接待人联系方式';
COMMENT ON COLUMN customer_visit.result_rating IS '现场成果评级/结论';
COMMENT ON COLUMN customer_visit.status IS '状态:0待执行 1已完成待复核 2取证待技术复核 3已立项转勘察 4已归档 5已取消';
COMMENT ON COLUMN customer_visit.next_visit_time IS '预计下次回访日期';
COMMENT ON COLUMN customer_visit.next_action IS '下一步行动目标';
COMMENT ON COLUMN customer_visit.photos IS '现场照片对象key(JSON数组字符串)';
COMMENT ON COLUMN customer_visit.survey_order_id IS '关联勘察单ID';
COMMENT ON COLUMN customer_visit.project_no IS '关联工程编号';
COMMENT ON COLUMN customer_visit.updated_at IS '更新时间';
-- 9) 查询索引(台账按状态、计划时间过滤与排序)
CREATE INDEX IF NOT EXISTS idx_visit_status ON customer_visit (status);
CREATE INDEX IF NOT EXISTS idx_visit_plan_time ON customer_visit (plan_time);
-- 10) 更新时间触发器(与 customer 等表保持一致的库级约定)
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trg_customer_visit_updated_at') THEN
CREATE TRIGGER trg_customer_visit_updated_at BEFORE UPDATE ON customer_visit
FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at();
END IF;
END $$;
COMMIT;
+3
View File
@@ -12,11 +12,14 @@
"build": "midway-bin build -c"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.1131.0",
"@aws-sdk/s3-request-presigner": "^3.1131.0",
"@midwayjs/cache-manager": "^3.19.0",
"@midwayjs/core": "^3.19.0",
"@midwayjs/koa": "^3.19.0",
"@midwayjs/logger": "^4.0.1",
"@midwayjs/sequelize": "^3.19.0",
"@midwayjs/upload": "^3.20.0",
"@midwayjs/validate": "^3.19.0",
"@types/validator": "^13.15.10",
"bcryptjs": "^2.4.3",
+30
View File
@@ -1,6 +1,7 @@
import { MidwayConfig } from "@midwayjs/core";
import { existsSync } from "fs";
import { join } from "path";
import { tmpdir } from "os";
import { config as loadEnv } from "dotenv";
// 加载项目根目录的环境变量文件:
@@ -65,6 +66,35 @@ export default {
refreshExpiresIn: "7d"
},
// multipart/form-data 解析配置(现场照片上传)
upload: {
mode: "file",
fileSize: "10mb",
whitelist: [".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp"],
tmpdir: join(tmpdir(), "smart-grid-upload"),
cleanTimeout: 5 * 60 * 1000
},
// RustFS 对象存储(S3 兼容协议、path-style 访问)
rustfs: {
endpoint: process.env.RUSTFS_ENDPOINT || "127.0.0.1",
port: Number(process.env.RUSTFS_PORT) || 9000,
useSSL: process.env.RUSTFS_USE_SSL === "true",
accessKey: process.env.RUSTFS_ACCESS_KEY || "",
secretKey: process.env.RUSTFS_SECRET_KEY || "",
region: process.env.RUSTFS_REGION || "us-east-1",
// 当前生效的桶(生产/测试通过 .env 与 .env.local 切换)
bucket: process.env.RUSTFS_BUCKET || process.env.RUSTFS_BUCKET_PROD || "",
bucketProd: process.env.RUSTFS_BUCKET_PROD || "",
bucketTest: process.env.RUSTFS_BUCKET_TEST || "",
// RustFS 仅支持 path-style 访问
forcePathStyle: true,
// 预签名 URL 有效期(秒)
presignExpires: 3600,
// 上传对象 key 前缀
keyPrefix: "customer-visit"
},
cors: {
origin: "*",
allowMethods: "GET,HEAD,PUT,POST,DELETE,PATCH,OPTIONS",
+2 -1
View File
@@ -2,13 +2,14 @@ import { Configuration, App, ILifeCycle } from "@midwayjs/core";
import * as Koa from "@midwayjs/koa";
import * as Sequelize from "@midwayjs/sequelize";
import * as CacheManager from "@midwayjs/cache-manager";
import * as Upload from "@midwayjs/upload";
import { join } from "path";
import { AuthMiddleware } from "./middleware/auth.middleware";
import { RbacMiddleware } from "./middleware/rbac.middleware";
import { LogMiddleware } from "./middleware/log.middleware";
@Configuration({
imports: [Koa, Sequelize, CacheManager],
imports: [Koa, Sequelize, CacheManager, Upload],
importConfigs: [join(__dirname, "./config")]
})
export class ContainerConfigLifeCycle implements ILifeCycle {
@@ -8,8 +8,26 @@ export class CustomerVisitController {
@Inject() customerVisitService!: CustomerVisitService;
@Get("/") async list(@Query() query: any) { return this.customerVisitService.list(query, this.ctx); }
// KPI 统计卡;必须声明在 /:id 之前,否则会被动态路由吞掉
@Get("/stats") async stats() { return this.customerVisitService.stats(this.ctx); }
// 批量安排拜访(多选客户生成待执行计划)
@Post("/batch") async batch(@Body() body: any) { return this.customerVisitService.batchCreate(body, this.ctx); }
@Get("/:id") async detail(@Param("id") id: string) { return this.customerVisitService.detail(Number(id), this.ctx); }
@Post("/") async create(@Body() body: any) { return this.customerVisitService.create(body, this.ctx); }
@Put("/:id") async update(@Param("id") id: string, @Body() body: any) { return this.customerVisitService.update(Number(id), body, this.ctx); }
// 状态流转:执行跟进 / 审核现场取证 / 归档 / 取消
@Put("/:id/status") async changeStatus(@Param("id") id: string, @Body() body: any) {
return this.customerVisitService.changeStatus(Number(id), body, this.ctx);
}
// 改派:更换跟进业务员
@Put("/:id/reassign") async reassign(@Param("id") id: string, @Body() body: any) {
return this.customerVisitService.reassign(Number(id), body, this.ctx);
}
@Del("/:id") async delete(@Param("id") id: string) { return this.customerVisitService.delete(Number(id), this.ctx); }
}
@@ -0,0 +1,28 @@
import { Controller, Post, Inject, Files } from "@midwayjs/core";
import { Context } from "@midwayjs/koa";
import { UploadFileInfo } from "@midwayjs/upload";
import { UploadService } from "../service/upload.service";
@Controller("/api/upload")
export class UploadController {
@Inject()
ctx!: Context;
@Inject()
uploadService!: UploadService;
// 上传现场照片,返回对象 key(用于持久化)与预签名 URL(用于即时预览)
@Post("/image")
async uploadImage(@Files() files: UploadFileInfo<string>[]) {
const file = files && files[0];
if (!file) {
return { code: 400, message: "请选择要上传的图片", data: null };
}
try {
const data = await this.uploadService.uploadFile(file);
return { code: 200, message: "上传成功", data };
} catch (err: any) {
return { code: 500, message: `图片上传失败:${err?.message || "对象存储不可用"}`, data: null };
}
}
}
+68 -10
View File
@@ -1,4 +1,16 @@
import { IsNotEmpty, IsOptional, IsString, IsInt, IsNumber, Matches, Min, Max, MaxLength } from "class-validator";
import {
IsNotEmpty,
IsOptional,
IsString,
IsInt,
IsArray,
IsDateString,
ArrayNotEmpty,
Matches,
Min,
Max,
MaxLength
} from "class-validator";
export class CustomerVisitQueryDTO {
@IsOptional() @IsInt() @Min(1) page?: number = 1;
@@ -6,24 +18,70 @@ export class CustomerVisitQueryDTO {
@IsOptional() @IsString() @MaxLength(64) keyword?: string;
@IsOptional() @IsInt() customerId?: number;
@IsOptional() @IsInt() salesmanId?: number;
@IsOptional() @IsInt() @Min(1) @Max(5) visitType?: number;
@IsOptional() @IsInt() @Min(0) @Max(5) status?: number;
@IsOptional() @IsInt() @Min(1) @Max(3) priority?: number;
// 计划拜访日期窗口(YYYY-MM-DD)
@IsOptional() @IsString() @MaxLength(10) planStartDate?: string;
@IsOptional() @IsString() @MaxLength(10) planEndDate?: string;
}
export class CustomerVisitCreateDTO {
@IsNotEmpty({ message: '所属客户不能为空' }) @IsInt() customerId!: number;
@IsNotEmpty({ message: '拜访人不能为空' }) @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) visitPerson!: string;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) visitPerson?: string;
@IsOptional() @Matches(/^1[3-9]\d{9}$/, { message: '手机号格式不正确' }) visitPhone?: string;
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) visitContent?: string;
@IsOptional() @IsString() @MaxLength(10) visitTime?: string;
@IsOptional() @IsInt() salesmanId?: number;
@IsOptional() @IsInt() @Min(0) @Max(1) willPlan?: number;
@IsOptional() @IsInt() @Min(1) @Max(5) visitType?: number;
@IsOptional() @IsInt() @Min(1) @Max(4) visitMode?: number;
@IsOptional() @IsInt() @Min(1) @Max(3) priority?: number;
@IsOptional() @IsString() @MaxLength(128) @Matches(/^[^<>]*$/) subject?: string;
@IsOptional() @IsDateString() planTime?: string;
@IsOptional() @IsDateString() planEndTime?: string;
@IsOptional() @IsString() @MaxLength(128) @Matches(/^[^<>]*$/) spaceDesc?: string;
@IsOptional() @IsInt() @Min(1) @Max(3) intentLevel?: number;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) intentDesc?: string;
@IsOptional() @IsString() @MaxLength(512) @Matches(/^[^<>]*$/) demands?: string;
@IsOptional() @IsString() @MaxLength(128) @Matches(/^[^<>]*$/) companion?: string;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) receptionPerson?: string;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) receptionTitle?: string;
@IsOptional() @Matches(/^1[3-9]\d{9}$/, { message: '接待人联系方式格式不正确' }) receptionPhone?: string;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) resultRating?: string;
@IsOptional() @IsInt() @Min(0) @Max(5) status?: number;
@IsOptional() @IsString() @MaxLength(10) nextVisitTime?: string;
@IsOptional() @IsString() @MaxLength(128) @Matches(/^[^<>]*$/) nextAction?: string;
// 现场照片对象 key 列表(对象存储),最多 2 张
@IsOptional() @IsArray() @MaxLength(2) photos?: string[];
}
export class CustomerVisitUpdateDTO {
@IsOptional() @IsInt() customerId?: number;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) visitPerson?: string;
@IsOptional() @Matches(/^1[3-9]\d{9}$/) visitPhone?: string;
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) visitContent?: string;
@IsOptional() @IsString() @MaxLength(10) visitTime?: string;
@IsOptional() @IsInt() salesmanId?: number;
@IsOptional() @IsInt() @Min(0) @Max(1) willPlan?: number;
export class CustomerVisitUpdateDTO extends CustomerVisitCreateDTO {
@IsOptional() @IsInt() declare customerId: number;
@IsOptional() @IsInt() @Min(0) @Max(5) declare status?: number;
@IsOptional() @IsInt() @Min(0) @Max(1) declare checkinStatus?: number;
@IsOptional() @IsDateString() declare checkinTime?: string;
@IsOptional() declare checkinLng?: number;
@IsOptional() declare checkinLat?: number;
}
// 批量安排拜访:多选客户 + 统一计划信息,批量生成待执行(status=0)记录
export class CustomerVisitBatchCreateDTO {
@IsArray() @ArrayNotEmpty({ message: '请至少选择一个客户' }) @IsInt({ each: true }) customerIds!: number[];
@IsNotEmpty({ message: '计划拜访时间不能为空' }) @IsDateString() planTime!: string;
@IsOptional() @IsDateString() planEndTime?: string;
@IsOptional() @IsInt() salesmanId?: number;
@IsOptional() @IsInt() @Min(1) @Max(5) visitType?: number;
@IsOptional() @IsInt() @Min(1) @Max(4) visitMode?: number;
@IsOptional() @IsInt() @Min(1) @Max(3) priority?: number;
@IsOptional() @IsString() @MaxLength(128) @Matches(/^[^<>]*$/) subject?: string;
}
// 状态流转(执行跟进 / 审核现场取证 / 取消 / 归档)
export class CustomerVisitStatusDTO {
@IsNotEmpty({ message: '目标状态不能为空' }) @IsInt() @Min(0) @Max(5) status!: number;
@IsOptional() @IsDateString() checkinTime?: string;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) resultRating?: string;
@IsOptional() @IsInt() surveyOrderId?: number;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) projectNo?: string;
}
+82 -4
View File
@@ -1,4 +1,4 @@
import { Column, Model, Table, DataType, CreatedAt, ForeignKey, BelongsTo } from "sequelize-typescript";
import { Column, Model, Table, DataType, CreatedAt, UpdatedAt, ForeignKey, BelongsTo } from "sequelize-typescript";
import Customer from "./customer.entity";
@Table({
@@ -6,7 +6,7 @@ import Customer from "./customer.entity";
comment: "客户拜访信息表",
timestamps: true,
createdAt: "created_at",
updatedAt: false,
updatedAt: "updated_at",
indexes: [
{ name: "idx_visit_customer", fields: ["customer_id"] },
{ name: "idx_visit_salesman", fields: ["salesman_id"] }
@@ -29,10 +29,10 @@ export default class CustomerVisit extends Model {
@Column({ type: DataType.STRING(20), field: "visit_phone", comment: "拜访人电话" })
visitPhone!: string;
@Column({ type: DataType.TEXT, field: "visit_content", comment: "拜访内容" })
@Column({ type: DataType.TEXT, field: "visit_content", comment: "拜访内容/沟通纪要" })
visitContent!: string;
@Column({ type: DataType.DATEONLY, field: "visit_time", comment: "拜访时间" })
@Column({ type: DataType.DATEONLY, field: "visit_time", comment: "拜访时间(日期)" })
visitTime!: string;
@Column({ type: DataType.BIGINT, field: "salesman_id", comment: "业务员(关联sys_user.id)" })
@@ -41,9 +41,87 @@ export default class CustomerVisit extends Model {
@Column({ type: DataType.SMALLINT, defaultValue: 0, field: "will_plan", comment: "是否做方案:0不同意 1同意" })
willPlan!: number;
@Column({ type: DataType.SMALLINT, defaultValue: 1, field: "visit_type", comment: "拜访类型:1首访摸排 2方案沟通 3商务二访 4维保回访 5应急巡检" })
visitType!: number;
@Column({ type: DataType.SMALLINT, defaultValue: 1, field: "visit_mode", comment: "拜访方式:1现场拜访 2商务拜访 3电话沟通 4线上会议" })
visitMode!: number;
@Column({ type: DataType.SMALLINT, defaultValue: 3, field: "priority", comment: "优先级:1特急P1 2加急P2 3常规P3" })
priority!: number;
@Column({ type: DataType.STRING(128), field: "subject", comment: "拜访主题/目的" })
subject!: string;
@Column({ type: DataType.DATE, field: "plan_time", comment: "计划拜访开始时间" })
planTime!: Date;
@Column({ type: DataType.DATE, field: "plan_end_time", comment: "计划拜访结束时间" })
planEndTime!: Date;
@Column({ type: DataType.DATE, field: "checkin_time", comment: "到场签到时间" })
checkinTime!: Date;
@Column({ type: DataType.SMALLINT, defaultValue: 0, field: "checkin_status", comment: "签到状态:0未签到 1已签到" })
checkinStatus!: number;
@Column({ type: DataType.DECIMAL(10, 6), field: "checkin_lng", comment: "签到经度" })
checkinLng!: number;
@Column({ type: DataType.DECIMAL(10, 6), field: "checkin_lat", comment: "签到纬度" })
checkinLat!: number;
@Column({ type: DataType.STRING(128), field: "space_desc", comment: "客户空间性质/场所描述" })
spaceDesc!: string;
@Column({ type: DataType.SMALLINT, field: "intent_level", comment: "意向评级:1=A高意向 2=B中意向 3=C低意向" })
intentLevel!: number;
@Column({ type: DataType.STRING(64), field: "intent_desc", comment: "现场意向说明" })
intentDesc!: string;
@Column({ type: DataType.STRING(512), field: "demands", comment: "诉求痛点(逗号分隔)" })
demands!: string;
@Column({ type: DataType.STRING(128), field: "companion", comment: "陪同人员" })
companion!: string;
@Column({ type: DataType.STRING(64), field: "reception_person", comment: "客户接待人" })
receptionPerson!: string;
@Column({ type: DataType.STRING(64), field: "reception_title", comment: "接待人职务" })
receptionTitle!: string;
@Column({ type: DataType.STRING(20), field: "reception_phone", comment: "接待人联系方式" })
receptionPhone!: string;
@Column({ type: DataType.STRING(64), field: "result_rating", comment: "现场成果评级/结论" })
resultRating!: string;
@Column({ type: DataType.SMALLINT, defaultValue: 0, field: "status", comment: "状态:0待执行 1已完成待复核 2取证待技术复核 3已立项转勘察 4已归档 5已取消" })
status!: number;
@Column({ type: DataType.DATEONLY, field: "next_visit_time", comment: "预计下次回访日期" })
nextVisitTime!: string;
@Column({ type: DataType.STRING(128), field: "next_action", comment: "下一步行动目标" })
nextAction!: string;
@Column({ type: DataType.STRING(1024), field: "photos", comment: "现场照片对象key(JSON数组字符串)" })
photos!: string;
@Column({ type: DataType.BIGINT, field: "survey_order_id", comment: "关联勘察单ID" })
surveyOrderId!: number;
@Column({ type: DataType.STRING(64), field: "project_no", comment: "关联工程编号" })
projectNo!: string;
@BelongsTo(() => Customer)
customer!: Customer;
@CreatedAt
created_at!: Date;
@UpdatedAt
updated_at!: Date;
}
+2 -2
View File
@@ -6,8 +6,8 @@ import SysOperationLog from '../entity/sysOperationLog.entity';
@Provide()
export class LogMiddleware implements IMiddleware<Context, NextFunction> {
// 中间件匹配范围:全局生效,跳过日志查询、健康检查与登录(登录由内部记录)
ignore = ['/api/logs/*', '/api/health', '/api/auth/login'];
// 中间件匹配范围:全局生效,跳过日志查询、健康检查、登录(登录由内部记录)与文件上传(multipart 无记录价值)
ignore = ['/api/logs/*', '/api/health', '/api/auth/login', '/api/upload/*'];
@InjectRepository(SysOperationLog)
logModel!: typeof SysOperationLog;
+1
View File
@@ -17,6 +17,7 @@ const PERMISSION_MAP: Record<string, string> = {
'/api/customer-departments': 'customer:department',
'/api/customer-accounts': 'customer:account',
'/api/customer-visits': 'customer:visit',
'/api/upload': 'customer:visit',
'/api/customer-buildings': 'customer:building',
'/api/customer-rooms': 'customer:room',
'/api/survey-orders': 'project:survey',
+281 -25
View File
@@ -1,12 +1,19 @@
import { Provide } from "@midwayjs/core";
import { Provide, Inject } from "@midwayjs/core";
import { InjectRepository } from "@midwayjs/sequelize";
import { Repository } from "sequelize-typescript";
import { Op, UniqueConstraintError } from "sequelize";
import { Op, UniqueConstraintError, literal } from "sequelize";
import { Context } from "@midwayjs/koa";
import CustomerVisit from "../entity/customer_visit.entity";
import Customer from "../entity/customer.entity";
import SysUser from "../entity/sysUser.entity";
import SysUserRole from "../entity/sysUserRole.entity";
import SysRole from "../entity/sysRole.entity";
import { UploadService } from "./upload.service";
// 客户拜访状态:0待执行 1已完成待复核 2取证待技术复核 3已立项转勘察 4已归档 5已取消
const STATUS = { PENDING: 0, DONE: 1, EVIDENCE: 2, SURVEY: 3, ARCHIVED: 4, CANCELED: 5 };
// 有转化价值的意向评级:1=A高意向 2=B中意向
const INTENT_LEVELS = [1, 2];
@Provide()
export class CustomerVisitService {
@@ -16,12 +23,18 @@ export class CustomerVisitService {
@InjectRepository(Customer)
customerModel!: Repository<Customer>;
@InjectRepository(SysUser)
userModel!: Repository<SysUser>;
@InjectRepository(SysUserRole)
userRoleModel!: Repository<SysUserRole>;
@InjectRepository(SysRole)
roleModel!: Repository<SysRole>;
@Inject()
uploadService!: UploadService;
private async isAdmin(userId: number): Promise<boolean> {
const userRoles = await this.userRoleModel.findAll({ where: { userId }, include: [this.roleModel] });
return userRoles.some((ur) => (ur as any).role?.roleCode === "ADMIN");
@@ -33,6 +46,13 @@ export class CustomerVisitService {
return !!customer && customer.salesmanId === userId;
}
// 业务负责人姓名(用于填充必填的 visit_person 字段)
private async getUserRealName(userId: number): Promise<string> {
if (!userId) return "未知";
const user = await this.userModel.findByPk(userId, { attributes: ["id", "realName", "username"] });
return user?.realName || (user as any)?.username || "未知";
}
private async generateVisitNo(retry: number = 0): Promise<string> {
const now = new Date();
const offsetMs = now.getTime() + 8 * 60 * 60 * 1000;
@@ -51,61 +71,239 @@ export class CustomerVisitService {
return result;
}
async list(query: any, ctx: Context): Promise<any> {
const page = Number(query.page) || 1;
const pageSize = Number(query.pageSize) || 10;
const userId = ctx.state.user.userId;
// 把 query 参数转换为 Sequelize where 条件
private buildWhere(query: any): any {
const where: any = {};
if (query.keyword) {
where[Op.or] = [
{ visitNo: { [Op.like]: `%${query.keyword}%` } },
{ subject: { [Op.like]: `%${query.keyword}%` } },
{ visitPerson: { [Op.like]: `%${query.keyword}%` } },
{ visitPhone: { [Op.like]: `%${query.keyword}%` } }
];
}
if (query.customerId !== undefined) where.customerId = query.customerId;
if (query.salesmanId) where.salesmanId = query.salesmanId;
if (query.customerId !== undefined && query.customerId !== "") where.customerId = Number(query.customerId);
if (query.salesmanId) where.salesmanId = Number(query.salesmanId);
if (query.visitType) where.visitType = Number(query.visitType);
if (query.status !== undefined && query.status !== "") where.status = Number(query.status);
if (query.priority) where.priority = Number(query.priority);
if (query.planStartDate || query.planEndDate) {
const range: any = {};
if (query.planStartDate) range[Op.gte] = new Date(`${query.planStartDate}T00:00:00+08:00`);
if (query.planEndDate) range[Op.lte] = new Date(`${query.planEndDate}T23:59:59+08:00`);
where.planTime = range;
}
return where;
}
// 行级隔离:非管理员仅能查看自己名下客户的拜访记录;返回 null 表示无可见数据
private async scopeWhere(baseWhere: any, ctx: Context): Promise<any | null> {
const userId = ctx.state.user.userId;
const where: any = { ...baseWhere };
if (await this.isAdmin(userId)) return where;
if (!(await this.isAdmin(userId))) {
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
const ids = myCustomers.map((c) => c.id);
if (ids.length === 0) return { code: 200, message: "获取客户拜访列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
if (ids.length === 0) return null;
if (where.customerId !== undefined) {
if (!ids.includes(where.customerId)) return { code: 200, message: "获取客户拜访列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
if (!ids.includes(Number(where.customerId))) return null;
} else {
where.customerId = { [Op.in]: ids };
}
return where;
}
const { count, rows } = await this.visitModel.findAndCountAll({ where, limit: pageSize, offset: (page - 1) * pageSize, order: [["created_at", "DESC"]] });
return { code: 200, message: "获取客户拜访列表成功", data: { list: rows.map((r) => r.toJSON()), total: count, page: Number(page), pageSize: Number(pageSize) } };
private parsePhotos(raw: any): string[] {
if (!raw) return [];
if (Array.isArray(raw)) return raw;
try {
const arr = JSON.parse(raw);
return Array.isArray(arr) ? arr : [];
} catch {
return [];
}
}
// 输出前把 photos 从 JSON 字符串转为 key 数组,并附带预签名访问 URL
private async toVO(record: CustomerVisit, urlMap?: Record<string, string>): Promise<any> {
const json: any = record.toJSON();
const keys = this.parsePhotos(json.photos);
json.photos = keys;
if (keys.length === 0) {
json.photoUrls = [];
return json;
}
const map = urlMap || (await this.uploadService.getPresignedUrlMap(keys));
json.photoUrls = keys.map((k) => map[k] || "");
return json;
}
// 补充客户与业务员信息,便于台账列表直接渲染
private async decorate(rows: any[]): Promise<any[]> {
if (rows.length === 0) return rows;
const allKeys = rows.flatMap((r) => this.parsePhotos(r.rawPhotos));
const urlMap = allKeys.length ? await this.uploadService.getPresignedUrlMap(allKeys) : {};
const salesmanIds = [...new Set(rows.map((r) => r.salesmanId).filter(Boolean))];
const users = salesmanIds.length
? await this.userModel.findAll({ where: { id: { [Op.in]: salesmanIds } }, attributes: ["id", "realName", "phone"] })
: [];
const userMap = new Map(users.map((u: any) => [u.id, u]));
for (const row of rows) {
const keys = this.parsePhotos(row.rawPhotos);
row.photos = keys;
row.photoUrls = keys.map((k) => urlMap[k] || "");
delete row.rawPhotos;
const user: any = userMap.get(row.salesmanId);
row.salesmanName = user?.realName || "";
row.salesmanPhone = user?.phone || "";
}
return rows;
}
async list(query: any, ctx: Context): Promise<any> {
const page = Number(query.page) || 1;
const pageSize = Number(query.pageSize) || 10;
const where = await this.scopeWhere(this.buildWhere(query), ctx);
if (!where) {
return { code: 200, message: "获取客户拜访列表成功", data: { list: [], total: 0, page, pageSize } };
}
const { count, rows } = await this.visitModel.findAndCountAll({
where,
include: [{ model: Customer, attributes: ["id", "customerNo", "customerName", "customerLevel", "contactPerson", "contactPhone", "address"] }],
limit: pageSize,
offset: (page - 1) * pageSize,
order: [["plan_time", "DESC"], ["id", "DESC"]],
distinct: true
});
const list = rows.map((r: any) => {
const json = r.toJSON();
json.rawPhotos = json.photos;
json.photos = [];
return json;
});
return { code: 200, message: "获取客户拜访列表成功", data: { list: await this.decorate(list), total: count, page, pageSize } };
}
// KPI 统计卡(仅按当前用户可见范围统计,不受列表筛选条件影响)
async stats(ctx: Context): Promise<any> {
const where = await this.scopeWhere({}, ctx);
const empty = {
total: 0,
pending: 0,
todayPlan: 0,
urgentPending: 0,
conversionRate: 0,
pendingReview: 0,
completed: 0,
converted: 0,
intentTotal: 0
};
if (!where) return { code: 200, message: "获取拜访统计成功", data: empty };
const countBy = (extra: any) => this.visitModel.count({ where: { ...where, ...extra } });
const [total, pending, todayPlan, urgentPending, pendingReview, completed, converted, intentTotal] = await Promise.all([
countBy({}),
countBy({ status: STATUS.PENDING }),
countBy({
status: STATUS.PENDING,
[Op.and]: [literal("(plan_time AT TIME ZONE 'Asia/Shanghai')::date = (NOW() AT TIME ZONE 'Asia/Shanghai')::date")]
}),
countBy({ status: STATUS.PENDING, priority: 1 }),
countBy({ status: STATUS.EVIDENCE }),
countBy({ status: { [Op.in]: [STATUS.DONE, STATUS.ARCHIVED] } }),
countBy({ status: STATUS.SURVEY }),
countBy({ intentLevel: { [Op.in]: INTENT_LEVELS } })
]);
const conversionRate = intentTotal > 0 ? Number(((converted / intentTotal) * 100).toFixed(1)) : 0;
return {
code: 200,
message: "获取拜访统计成功",
data: { total, pending, todayPlan, urgentPending, conversionRate, pendingReview, completed, converted, intentTotal }
};
}
async detail(id: number, ctx: Context): Promise<any> {
const record = await this.visitModel.findByPk(id);
const record: any = await this.visitModel.findByPk(id, {
include: [{ model: Customer, attributes: ["id", "customerNo", "customerName", "customerLevel", "contactPerson", "contactPhone", "address", "salesmanId"] }]
});
if (!record) return { code: 404, message: "客户拜访不存在", data: null };
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限访问该拜访记录", data: null };
return { code: 200, message: "获取客户拜访详情成功", data: record.toJSON() };
const vo = await this.toVO(record);
vo.salesmanName = await this.getUserRealName(record.salesmanId);
return { code: 200, message: "获取客户拜访详情成功", data: vo };
}
async create(data: any, ctx: Context): Promise<any> {
const userId = ctx.state.user.userId;
const isAdmin = await this.isAdmin(userId);
if (!(await this.isCustomerOwner(userId, data.customerId))) return { code: 403, message: "无权限为该客户创建拜访记录", data: null };
if (!(await this.isAdmin(userId))) data.salesmanId = userId;
if (!isAdmin) data.salesmanId = userId;
const photos = this.parsePhotos(data.photos);
if (photos.length > 2) return { code: 400, message: "现场照片最多上传 2 张", data: null };
const payload = this.normalizePayload(data, photos);
if (!payload.visitPerson) payload.visitPerson = await this.getUserRealName(payload.salesmanId || userId);
try {
data.visitNo = await this.generateVisitNo();
const created = await this.visitModel.create(data);
return { code: 200, message: "创建客户拜访成功", data: created.toJSON() };
payload.visitNo = await this.generateVisitNo();
const created = await this.visitModel.create(payload);
return { code: 200, message: "创建客户拜访成功", data: await this.toVO(created) };
} catch (err: any) {
if (err instanceof UniqueConstraintError) {
data.visitNo = await this.generateVisitNo();
const created = await this.visitModel.create(data);
return { code: 200, message: "创建客户拜访成功", data: created.toJSON() };
payload.visitNo = await this.generateVisitNo();
const created = await this.visitModel.create(payload);
return { code: 200, message: "创建客户拜访成功", data: await this.toVO(created) };
}
throw err;
}
}
// 批量安排拜访:多选客户生成统一的待执行计划
async batchCreate(data: any, ctx: Context): Promise<any> {
const userId = ctx.state.user.userId;
const isAdmin = await this.isAdmin(userId);
const customerIds = [...new Set((data.customerIds || []).map(Number).filter(Boolean))];
if (customerIds.length === 0) return { code: 400, message: "请至少选择一个客户", data: null };
if (!data.planTime) return { code: 400, message: "请选择计划拜访时间", data: null };
const salesmanId = isAdmin && data.salesmanId ? Number(data.salesmanId) : userId;
if (!isAdmin) {
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId, id: { [Op.in]: customerIds } }, attributes: ["id"] });
if (myCustomers.length !== customerIds.length) return { code: 403, message: "存在无权限安排的客户", data: null };
} else {
const exists = await this.customerModel.count({ where: { id: { [Op.in]: customerIds } } });
if (exists !== customerIds.length) return { code: 400, message: "存在无效的客户", data: null };
}
const visitPerson = await this.getUserRealName(salesmanId);
const ids: number[] = [];
for (const customerId of customerIds) {
const payload: any = {
visitNo: await this.generateVisitNo(),
customerId,
visitPerson,
salesmanId,
status: STATUS.PENDING,
visitType: data.visitType || 1,
visitMode: data.visitMode || 1,
priority: data.priority || 3,
subject: data.subject || "批量安排的拜访计划",
planTime: data.planTime,
planEndTime: data.planEndTime || null
};
const created = await this.visitModel.create(payload);
ids.push(created.id);
}
return { code: 200, message: `成功创建 ${ids.length} 条拜访计划`, data: { total: ids.length, ids } };
}
async update(id: number, data: any, ctx: Context): Promise<any> {
const record = await this.visitModel.findByPk(id);
if (!record) return { code: 404, message: "客户拜访不存在", data: null };
@@ -116,8 +314,56 @@ export class CustomerVisitService {
delete data.customerId;
}
delete data.visitNo;
await record.update(data);
return { code: 200, message: "更新客户拜访成功", data: record.toJSON() };
const photos = data.photos === undefined ? undefined : this.parsePhotos(data.photos);
if (photos && photos.length > 2) return { code: 400, message: "现场照片最多上传 2 张", data: null };
const payload = this.normalizePayload(data, photos);
await record.update(payload);
const fresh = await this.visitModel.findByPk(id);
return { code: 200, message: "更新客户拜访成功", data: await this.toVO(fresh!) };
}
// 状态流转:执行跟进(1) / 审核现场取证(2、3) / 归档(4) / 取消(5)
async changeStatus(id: number, data: any, ctx: Context): Promise<any> {
const record = await this.visitModel.findByPk(id);
if (!record) return { code: 404, message: "客户拜访不存在", data: null };
const isAdmin = await this.isAdmin(ctx.state.user.userId);
if (!isAdmin && !(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) {
return { code: 403, message: "无权限变更该拜访记录状态", data: null };
}
const target = Number(data.status);
if (!Object.values(STATUS).includes(target)) return { code: 400, message: "状态值不合法", data: null };
const patch: any = { status: target };
// 标记完成/取证时同步到场签到信息
if (target === STATUS.DONE || target === STATUS.EVIDENCE) {
patch.checkinStatus = 1;
patch.checkinTime = data.checkinTime || record.checkinTime || new Date();
}
if (data.resultRating) patch.resultRating = data.resultRating;
if (data.surveyOrderId) patch.surveyOrderId = Number(data.surveyOrderId);
if (data.projectNo) patch.projectNo = data.projectNo;
await record.update(patch);
return { code: 200, message: "更新拜访状态成功", data: await this.toVO(record) };
}
// 改派:更换跟进业务员
async reassign(id: number, data: any, ctx: Context): Promise<any> {
const record = await this.visitModel.findByPk(id);
if (!record) return { code: 404, message: "客户拜访不存在", data: null };
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) {
return { code: 403, message: "无权限改派该拜访记录", data: null };
}
const salesmanId = Number(data.salesmanId);
if (!salesmanId) return { code: 400, message: "请选择改派业务员", data: null };
const user = await this.userModel.findByPk(salesmanId, { attributes: ["id"] });
if (!user) return { code: 400, message: "改派业务员不存在", data: null };
await record.update({ salesmanId, visitPerson: await this.getUserRealName(salesmanId) });
return { code: 200, message: "改派成功", data: await this.toVO(record) };
}
async delete(id: number, ctx: Context): Promise<any> {
@@ -127,4 +373,14 @@ export class CustomerVisitService {
await record.destroy();
return { code: 200, message: "删除客户拜访成功", data: null };
}
// 统一清洗入参:空串转 null、photos 数组转 JSON 字符串
private normalizePayload(data: any, photos?: string[]): any {
const payload: any = { ...data };
if (photos !== undefined) payload.photos = photos.length ? JSON.stringify(photos) : null;
for (const key of Object.keys(payload)) {
if (payload[key] === "") payload[key] = null;
}
return payload;
}
}
+122
View File
@@ -0,0 +1,122 @@
import { Provide, Config } from "@midwayjs/core";
import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { createReadStream, statSync } from "fs";
import { randomUUID } from "crypto";
import { extname } from "path";
import { UploadFileInfo } from "@midwayjs/upload";
// RustFS 配置结构(见 config.default.ts 的 rustfs 段)
interface RustFsConfig {
endpoint: string;
port: number;
useSSL: boolean;
accessKey: string;
secretKey: string;
region: string;
bucket: string;
bucketProd: string;
bucketTest: string;
forcePathStyle: boolean;
presignExpires: number;
keyPrefix: string;
}
export interface UploadResult {
key: string;
url: string;
name: string;
size: number;
}
@Provide()
export class UploadService {
@Config("rustfs")
rustfsConfig!: RustFsConfig;
private client: S3Client | null = null;
private getClient(): S3Client {
if (!this.client) {
const protocol = this.rustfsConfig.useSSL ? "https" : "http";
this.client = new S3Client({
endpoint: `${protocol}://${this.rustfsConfig.endpoint}:${this.rustfsConfig.port}`,
region: this.rustfsConfig.region,
credentials: {
accessKeyId: this.rustfsConfig.accessKey,
secretAccessKey: this.rustfsConfig.secretKey
},
// RustFS 仅支持 path-style 访问
forcePathStyle: this.rustfsConfig.forcePathStyle
});
}
return this.client;
}
private get bucket(): string {
return this.rustfsConfig.bucket;
}
/**
* 上传单个文件到 RustFS,返回对象 key 与预签名访问 URL
*/
async uploadFile(file: UploadFileInfo<string>): Promise<UploadResult> {
if (!file || !file.data) {
throw new Error("上传文件为空");
}
const ext = extname(file.filename || "").toLowerCase();
const now = new Date();
const yyyymm = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}`;
const key = `${this.rustfsConfig.keyPrefix}/${yyyymm}/${randomUUID()}${ext}`;
await this.getClient().send(
new PutObjectCommand({
Bucket: this.bucket,
Key: key,
Body: createReadStream(file.data),
ContentType: file.mimeType || "application/octet-stream"
})
);
let size = 0;
try {
size = statSync(file.data).size;
} catch {
size = 0;
}
return {
key,
url: await this.getPresignedUrl(key),
name: file.filename,
size
};
}
/**
* 按对象 key 生成预签名访问 URL(默认 1 小时有效)
*/
async getPresignedUrl(key: string): Promise<string> {
if (!key) return "";
return getSignedUrl(
this.getClient(),
new GetObjectCommand({ Bucket: this.bucket, Key: key }),
{ expiresIn: this.rustfsConfig.presignExpires }
);
}
/**
* 批量把对象 key 转成预签名 URL,返回 key → url 映射(key 非法时返回空串)
*/
async getPresignedUrlMap(keys: string[]): Promise<Record<string, string>> {
const map: Record<string, string> = {};
for (const key of keys) {
try {
map[key] = await this.getPresignedUrl(key);
} catch {
map[key] = "";
}
}
return map;
}
}
+539 -162
View File
File diff suppressed because it is too large. Load diff
@@ -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,687 @@
<!DOCTYPE html>
<html lang="zh-CN"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>客户拜访管理台账 - 鑫潼智电 XT-POWER OS</title>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=hankenGrotesk:wght@100..900&amp;family=JetBrains+Mono:wght@400..700&amp;display=swap" rel="stylesheet"/>
<style>
@layer base {
html, body { margin: 0; padding: 0; }
body { overscroll-behavior: none; }
}
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 9999px; }
::-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: {
"primary": "#0054cd",
"primary-container": "#136bfb",
"on-primary": "#ffffff",
"surface": "#ffffff",
"bg-page": "#f4f7fb",
"on-surface": "#0d1c2c",
"on-surface-variant": "#424655",
"text-muted": "#5b6b7e",
"border-rule": "#e2e8f0",
"inverse-surface": "#1e293b",
"surface-container-low": "#f8fafc",
"surface-container": "#eef2f7",
"surface-container-high": "#e2e8f0",
"accent-subtle": "rgba(0,84,205,0.08)",
"tertiary": "#00685f",
"status-warn": "#d97706",
"error": "#dc2626"
},
fontFamily: {
"sans": ["hankenGrotesk", "sans-serif"],
"mono": ["JetBrains Mono", "monospace"]
}
}
}
};
</script>
</head>
<body class="bg-bg-page font-sans text-on-surface antialiased text-[13.5px]">
<!-- Global Industrial Sidebar -->
<aside class="fixed left-0 top-0 h-full w-64 bg-inverse-surface z-50 flex flex-col justify-between shadow-xl">
<div class="flex flex-col h-full">
<!-- Logo -->
<div class="h-16 flex items-center px-5 gap-3 border-b border-white/10">
<div class="w-8 h-8 rounded-lg bg-primary flex items-center justify-center text-white font-bold shadow-md shadow-blue-500/30">
<span class="material-symbols-outlined text-[20px]">bolt</span>
</div>
<div class="flex flex-col">
<span class="text-white font-bold text-[15px] tracking-wide">鑫潼智电</span>
<span class="font-mono text-[11px] text-slate-400 tracking-wider">XT-POWER OS v3.8</span>
</div>
</div>
<!-- Navigation Links -->
<nav class="flex-1 px-3 py-4 space-y-1 overflow-y-auto">
<a class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-300 hover:bg-white/10 hover:text-white transition-all text-[13.5px]" href="#">
<span class="material-symbols-outlined text-[20px]">dashboard</span>
<span>运营工作台</span>
</a>
<!-- Menu Group: Customer Management (Expanded & Active) -->
<div class="pt-1">
<div class="flex items-center justify-between px-3 py-2 text-white font-medium text-[13.5px]">
<div class="flex items-center gap-3">
<span class="material-symbols-outlined text-[20px] text-blue-400">corporate_fare</span>
<span>客户管理</span>
</div>
<span class="material-symbols-outlined text-[16px] text-slate-400">expand_less</span>
</div>
<div class="pl-7 pr-2 space-y-1 mt-1 border-l border-white/10 ml-5">
<a class="flex items-center py-2 px-3 rounded-lg text-slate-400 hover:bg-white/10 hover:text-white text-[13px] transition-all" href="#">客户档案列表</a>
<a class="flex items-center py-2 px-3 rounded-lg text-slate-400 hover:bg-white/10 hover:text-white text-[13px] transition-all" href="#">空间拓扑与用电架构</a>
<a class="flex items-center py-2 px-3 rounded-lg bg-primary text-white font-semibold text-[13px] shadow-sm shadow-blue-900/40" href="#">客户拜访管理</a>
<a class="flex items-center py-2 px-3 rounded-lg text-slate-400 hover:bg-white/10 hover:text-white text-[13px] transition-all" href="#">客户小程序账号</a>
</div>
</div>
<!-- Menu Group: Project Management (Collapsed) -->
<div class="pt-1">
<div class="flex items-center justify-between px-3 py-2 text-slate-300 hover:bg-white/10 hover:text-white rounded-lg transition-all cursor-pointer text-[13.5px]">
<div class="flex items-center gap-3">
<span class="material-symbols-outlined text-[20px]">schema</span>
<span>项目管理</span>
</div>
<span class="material-symbols-outlined text-[16px] text-slate-400">expand_more</span>
</div>
</div>
<a class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-300 hover:bg-white/10 hover:text-white transition-all text-[13.5px]" href="#">
<span class="material-symbols-outlined text-[20px]">send_time_extension</span>
<span>运营派单</span>
</a>
<a class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-300 hover:bg-white/10 hover:text-white transition-all text-[13.5px]" href="#">
<span class="material-symbols-outlined text-[20px]">sensors</span>
<span>设备监控</span>
</a>
<a class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-300 hover:bg-white/10 hover:text-white transition-all text-[13.5px]" href="#">
<span class="material-symbols-outlined text-[20px]">query_stats</span>
<span>统计报表</span>
</a>
<a class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-300 hover:bg-white/10 hover:text-white transition-all text-[13.5px]" href="#">
<span class="material-symbols-outlined text-[20px]">settings</span>
<span>系统管理</span>
</a>
</nav>
<!-- Bottom Status -->
<div class="p-4 border-t border-white/10 bg-slate-900/50">
<div class="flex items-center justify-between text-[12px] text-slate-400">
<span>华东区 · 枢纽调度</span>
<span class="flex items-center gap-1.5 text-emerald-400 font-mono text-[11px]">
<span class="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span> 系统稳定
</span>
</div>
</div>
</div>
</aside>
<!-- Main Shell Viewport -->
<div class="pl-64 flex flex-col min-h-screen">
<!-- Top Navigation Bar -->
<header class="fixed top-0 left-64 right-0 h-16 z-40 bg-surface/90 backdrop-blur-md border-b border-border-rule px-8 flex items-center justify-between shadow-sm">
<div class="flex items-center gap-5">
<div class="flex items-center gap-2 text-slate-500 text-[13px]">
<span class="hover:text-slate-700 cursor-pointer">客户管理</span>
<span class="text-slate-300">/</span>
<span class="text-on-surface font-semibold">客户拜访管理台账</span>
</div>
<div class="relative w-80">
<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 bg-slate-100/90 text-on-surface rounded-lg text-[13px] border border-transparent focus:border-primary focus:bg-white focus:outline-none transition-all" placeholder="检索客户名称、拜访流水号、业务员..." type="text"/>
</div>
</div>
<div class="flex items-center gap-4">
<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 text-[12px] font-medium">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
<span>ICC 联通正常</span>
</div>
<div class="flex items-center gap-2 pr-3 border-r border-border-rule">
<button class="relative w-8 h-8 flex items-center justify-center 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 bg-red-500 rounded-full"></span>
</button>
<button class="w-8 h-8 flex items-center justify-center rounded-lg hover:bg-slate-100 text-slate-600 transition-colors" title="使用帮助">
<span class="material-symbols-outlined text-[20px]">help</span>
</button>
</div>
<div class="flex items-center gap-3 pl-1">
<div class="text-right">
<div class="font-semibold text-slate-800 text-[13px] flex items-center justify-end gap-1.5">
<span>张工</span>
<span class="text-[10.5px] px-1.5 py-0.2 rounded bg-blue-50 text-primary border border-blue-100 font-normal">调度席位</span>
</div>
<div class="text-[11px] text-slate-500 font-mono">XT-ENG-0842</div>
</div>
<div class="w-9 h-9 rounded-full bg-gradient-to-tr from-primary to-blue-500 text-white flex items-center justify-center font-bold text-[14px] shadow-sm">
张
</div>
</div>
</div>
</header>
<!-- Main Workspace Ledger -->
<main class="w-full pt-20 flex-1 p-8 space-y-5">
<!-- Page Header: Title + Ledger Actions -->
<div class="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
<div>
<div class="flex items-center gap-2.5">
<h1 class="text-[22px] font-bold text-slate-900 tracking-tight">客户拜访管理台账</h1>
<span class="text-[12px] font-mono font-medium px-2 py-0.5 rounded-md bg-slate-100 text-slate-600 border border-slate-200">CUSTOMER VISIT LEDGER</span>
</div>
<p class="text-[13px] text-slate-500 mt-1">
统筹全周期客户现场拜访任务、意向转化节点跟进、现场需求取证及拜访转立项派单全流程。
</p>
</div>
<div class="flex items-center gap-2.5">
<button class="px-3.5 py-2 rounded-lg bg-white border border-slate-200 hover:bg-slate-50 hover:border-slate-300 text-slate-700 font-medium text-[13px] shadow-sm transition-all flex items-center gap-1.5">
<span class="material-symbols-outlined text-[17px] text-slate-500">file_download</span>
<span>导出台账数据</span>
</button>
<button class="px-3.5 py-2 rounded-lg bg-white border border-slate-200 hover:bg-slate-50 hover:border-slate-300 text-slate-700 font-medium text-[13px] shadow-sm transition-all flex items-center gap-1.5">
<span class="material-symbols-outlined text-[17px] text-slate-500">calendar_month</span>
<span>批量安排拜访</span>
</button>
<button class="px-4 py-2 rounded-lg bg-primary hover:bg-blue-700 text-white font-semibold text-[13px] shadow-sm shadow-blue-500/25 transition-all flex items-center gap-1.5">
<span class="material-symbols-outlined text-[18px]">add</span>
<span>+ 新增拜访计划</span>
</button>
</div>
</div>
<!-- Ledger Top Status Metric Cards (AntD style 5-column metric cards) -->
<div class="grid grid-cols-2 md:grid-cols-5 gap-3">
<!-- Card 1: All Visits -->
<div class="bg-surface rounded-xl p-3.5 border-2 border-primary/20 bg-gradient-to-b from-blue-50/50 to-surface shadow-sm cursor-pointer hover:border-primary transition-all">
<div class="flex items-center justify-between">
<span class="text-[12.5px] font-semibold text-slate-700">全部拜访</span>
<span class="material-symbols-outlined text-[18px] text-primary">view_kanban</span>
</div>
<div class="mt-2 flex items-baseline gap-2">
<span class="text-[22px] font-bold text-slate-900 font-mono">128</span>
<span class="text-[11px] text-slate-400">总台账量</span>
</div>
<div class="mt-1 text-[11.5px] text-slate-500 flex items-center gap-1">
<span>当月环比</span>
<span class="text-emerald-600 font-medium font-mono">+15.8%</span>
</div>
</div>
<!-- Card 2: Pending / Today Visits -->
<div class="bg-surface rounded-xl p-3.5 border border-border-rule shadow-sm hover:border-amber-400 cursor-pointer transition-all">
<div class="flex items-center justify-between">
<div class="flex items-center gap-1.5">
<span class="w-2 h-2 rounded-full bg-amber-500"></span>
<span class="text-[12.5px] font-semibold text-slate-700">待执行 / 今日待访</span>
</div>
<span class="px-1.5 py-0.5 rounded text-[10.5px] bg-amber-50 text-amber-700 font-medium border border-amber-200">预警</span>
</div>
<div class="mt-2 flex items-baseline gap-2">
<span class="text-[22px] font-bold text-amber-600 font-mono">8</span>
<span class="text-[11px] text-slate-400">场任务排期</span>
</div>
<div class="mt-1 text-[11.5px] text-amber-600 font-medium flex items-center gap-1">
<span class="material-symbols-outlined text-[13px]">alarm</span>
<span>今日待拜访 3 场 · 特急响应 1 场</span>
</div>
</div>
<!-- Card 3: Conversion Rate -->
<div class="bg-surface rounded-xl p-3.5 border border-border-rule shadow-sm hover:border-emerald-400 cursor-pointer transition-all">
<div class="flex items-center justify-between">
<div class="flex items-center gap-1.5">
<span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
<span class="text-[12.5px] font-semibold text-slate-700">意向勘察转化率</span>
</div>
<span class="material-symbols-outlined text-[18px] text-emerald-600">transform</span>
</div>
<div class="mt-2 flex items-baseline gap-2">
<span class="text-[22px] font-bold text-emerald-600 font-mono">68.4%</span>
<span class="text-[11px] text-emerald-700 font-mono font-medium">+5.2%</span>
</div>
<div class="mt-1 text-[11.5px] text-slate-500 flex items-center gap-1">
<span>已转立项</span>
<span class="text-emerald-700 font-medium font-mono">36 家</span>
</div>
</div>
<!-- Card 4: Under Review -->
<div class="bg-surface rounded-xl p-3.5 border border-border-rule shadow-sm hover:border-purple-400 cursor-pointer transition-all">
<div class="flex items-center justify-between">
<div class="flex items-center gap-1.5">
<span class="w-2 h-2 rounded-full bg-purple-500"></span>
<span class="text-[12.5px] font-semibold text-slate-700">取证待技术复核</span>
</div>
<span class="material-symbols-outlined text-[18px] text-purple-600">fact_check</span>
</div>
<div class="mt-2 flex items-baseline gap-2">
<span class="text-[22px] font-bold text-purple-600 font-mono">7</span>
<span class="text-[11px] text-slate-400">场待核验</span>
</div>
<div class="mt-1 text-[11.5px] text-slate-500 flex items-center gap-1">
<span>平均复核时效</span>
<span class="text-purple-600 font-medium font-mono">1.8 小时</span>
</div>
</div>
<!-- Card 5: Completed / Archived -->
<div class="bg-surface rounded-xl p-3.5 border border-border-rule shadow-sm hover:border-blue-400 cursor-pointer transition-all">
<div class="flex items-center justify-between">
<div class="flex items-center gap-1.5">
<span class="w-2 h-2 rounded-full bg-blue-500"></span>
<span class="text-[12.5px] font-semibold text-slate-700">拜访完成 / 已归档</span>
</div>
<span class="material-symbols-outlined text-[18px] text-emerald-600">check_circle</span>
</div>
<div class="mt-2 flex items-baseline gap-2">
<span class="text-[22px] font-bold text-slate-800 font-mono">113</span>
<span class="text-[11px] text-slate-400">场已归档</span>
</div>
<div class="mt-1 text-[11.5px] text-slate-500 flex items-center gap-1">
<span>满意度</span>
<span class="text-amber-600 font-mono font-medium">4.95分</span>
<span class="text-slate-300">·</span>
<span>方案率</span>
<span class="text-emerald-600 font-mono font-medium">100%</span>
</div>
</div>
</div>
<!-- Filter Bar: Search, Filters & Action buttons -->
<div class="bg-surface rounded-xl p-4 border border-border-rule shadow-sm space-y-3">
<div class="grid grid-cols-1 md:grid-cols-12 gap-3 items-center">
<!-- Search Query -->
<div class="md:col-span-4">
<label class="block text-[11.5px] font-medium text-slate-500 mb-1">模糊搜索</label>
<div class="relative">
<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-3 py-1.5 text-[13px] bg-slate-50 border border-slate-200 rounded-lg text-slate-800 placeholder-slate-400 focus:bg-white focus:border-primary focus:outline-none transition-colors" placeholder="输入客户名称、拜访流水号、跟进业务员..." type="text"/>
</div>
</div>
<!-- Visit Type Filter -->
<div class="md:col-span-2">
<label class="block text-[11.5px] font-medium text-slate-500 mb-1">拜访类型</label>
<select class="w-full py-1.5 px-3 text-[13px] bg-slate-50 border border-slate-200 rounded-lg text-slate-800 focus:bg-white focus:border-primary focus:outline-none">
<option value="all">全部类型 (128)</option>
<option value="1">首访摸排</option>
<option value="2">方案沟通</option>
<option value="3">商务二访</option>
<option value="4">维保回访</option>
<option value="5">应急巡检</option>
</select>
</div>
<!-- Sales Representative Filter -->
<div class="md:col-span-2">
<label class="block text-[11.5px] font-medium text-slate-500 mb-1">跟进业务员</label>
<select class="w-full py-1.5 px-3 text-[13px] bg-slate-50 border border-slate-200 rounded-lg text-slate-800 focus:bg-white focus:border-primary focus:outline-none">
<option value="all">全部业务员 (全员)</option>
<option value="1">李志敏 (高级工勘顾问)</option>
<option value="2">王建国 (资深电力架构师)</option>
<option value="3">周慧 (大客户总监)</option>
</select>
</div>
<!-- Date Picker -->
<div class="md:col-span-2">
<label class="block text-[11.5px] font-medium text-slate-500 mb-1">计划拜访日期窗口</label>
<div class="relative">
<span class="material-symbols-outlined absolute left-2.5 top-1/2 -translate-y-1/2 text-slate-400 text-[16px]">calendar_today</span>
<input class="w-full pl-8 pr-2.5 py-1.5 text-[12.5px] bg-slate-50 border border-slate-200 rounded-lg text-slate-800 focus:bg-white focus:border-primary focus:outline-none font-mono" placeholder="2026-03-01 ~ 03-31" type="text" value="2026-03-01 ~ 03-31"/>
</div>
</div>
<!-- Buttons -->
<div class="md:col-span-2 flex items-center gap-2 pt-5">
<button class="flex-1 py-1.5 bg-primary hover:bg-blue-700 text-white font-medium rounded-lg text-[13px] shadow-sm transition-colors flex items-center justify-center gap-1">
<span class="material-symbols-outlined text-[16px]">search</span>
<span>查询</span>
</button>
<button class="px-3 py-1.5 bg-white border border-slate-200 hover:bg-slate-50 text-slate-600 rounded-lg text-[13px] transition-colors">
重置
</button>
<button class="text-primary hover:text-blue-700 text-[12px] flex items-center gap-0.5 whitespace-nowrap pl-1" title="展开高级筛选">
<span>高级</span>
<span class="material-symbols-outlined text-[15px]">tune</span>
</button>
</div>
</div>
</div>
<!-- Main Data Table Container (Ant Design Pro Table Style) -->
<div class="bg-surface rounded-xl border border-border-rule shadow-sm overflow-hidden">
<!-- Table Subheader -->
<div class="px-5 py-3.5 border-b border-border-rule bg-slate-50/60 flex items-center justify-between">
<div class="flex items-center gap-3 text-[13px]">
<span class="font-semibold text-slate-800">拜访数据台账</span>
<span class="text-slate-400">|</span>
<span class="text-slate-500 text-[12px]">显示第 <span class="font-mono text-slate-700 font-semibold">1</span> 到 <span class="font-mono text-slate-700 font-semibold">5</span> 笔,共 <span class="font-mono text-primary font-bold">128</span> 笔拜访记录</span>
</div>
<div class="flex items-center gap-2 text-[12px] text-slate-500">
<span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-red-500"></span>特急 2</span>
<span class="flex items-center gap-1 ml-2"><span class="w-2 h-2 rounded-full bg-amber-500"></span>加急 6</span>
<span class="flex items-center gap-1 ml-2"><span class="w-2 h-2 rounded-full bg-slate-400"></span>常规 120</span>
<button class="ml-3 text-slate-400 hover:text-slate-700 p-1" title="刷新表格">
<span class="material-symbols-outlined text-[18px]">refresh</span>
</button>
</div>
</div>
<!-- Actual Table -->
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse text-[13px]">
<thead>
<tr class="bg-slate-50/90 text-slate-600 text-[12px] border-b border-slate-200 uppercase tracking-wider font-semibold">
<th class="py-3 px-4 w-10 text-center">
<input class="rounded border-slate-300 text-primary focus:ring-0 w-4 h-4 cursor-pointer" type="checkbox"/>
</th>
<th class="py-3 px-3 min-w-[150px]">拜访流水 / 优先级</th>
<th class="py-3 px-3 min-w-[210px]">关联客户与空间性质</th>
<th class="py-3 px-3 min-w-[160px]">拜访类型与目的</th>
<th class="py-3 px-3 min-w-[140px]">跟进业务员</th>
<th class="py-3 px-3 min-w-[180px]">计划与到场时间 / GPS签到</th>
<th class="py-3 px-3 min-w-[150px]">现场意向与成果评级</th>
<th class="py-3 px-3 min-w-[120px] text-center">当前状态</th>
<th class="py-3 px-4 min-w-[180px] text-center sticky right-0 bg-slate-50">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 text-slate-700">
<!-- Row 1: VST-2026-0312 特急/高优 -->
<tr class="hover:bg-blue-50/40 transition-colors bg-amber-50/20">
<td class="py-3.5 px-4 text-center">
<input class="rounded border-slate-300 text-primary focus:ring-0 w-4 h-4 cursor-pointer" type="checkbox"/>
</td>
<td class="py-3.5 px-3">
<div class="flex items-center gap-1.5">
<span class="font-mono font-bold text-primary hover:underline cursor-pointer">VST-2026-0312</span>
</div>
<div class="mt-1">
<span class="text-[10.5px] px-1.5 py-0.5 rounded bg-red-100 text-red-700 font-semibold border border-red-200">特急 · P1高优</span>
</div>
</td>
<td class="py-3.5 px-3">
<div class="font-semibold text-slate-900 leading-snug line-clamp-1">华中师范大学南湖校区</div>
<div class="text-[11.5px] text-slate-500 mt-0.5 flex items-center gap-1">
<span class="material-symbols-outlined text-[14px] text-slate-400">domain</span>
<span>后勤基建处 · 10kV南湖开闭所</span>
</div>
</td>
<td class="py-3.5 px-3">
<div class="font-medium text-slate-800">配电物联网与微断改造</div>
<div class="text-[11.5px] text-slate-500">首访摸排 · 技改升级</div>
</td>
<td class="py-3.5 px-3">
<div class="flex items-center gap-2">
<div class="w-6 h-6 rounded-full bg-primary/10 text-primary flex items-center justify-center font-bold text-[11px]">李</div>
<div>
<div class="font-medium text-slate-800">李志敏</div>
<div class="font-mono text-[11.5px] text-slate-400">139****8821</div>
</div>
</div>
</td>
<td class="py-3.5 px-3">
<div class="font-mono text-[12.5px] text-slate-800">2026-03-10 14:15</div>
<div class="text-[11px] text-emerald-600 flex items-center gap-0.5 mt-0.5">
<span class="material-symbols-outlined text-[13px]">verified</span>
<span>已签到 (ICC 防伪偏差&lt;15m)</span>
</div>
</td>
<td class="py-3.5 px-3">
<div class="inline-flex items-center gap-1 px-2 py-0.5 rounded bg-emerald-50 text-emerald-700 border border-emerald-200 font-semibold text-[11.5px]">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
<span>A级 · 极高意向 (预算充足)</span>
</div>
<div class="text-[11px] text-slate-400 mt-0.5">需求: 40回路智能微断边缘计算</div>
</td>
<td class="py-3.5 px-3 text-center">
<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[12px] font-medium bg-emerald-50 text-emerald-700 border border-emerald-200">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>已立项转勘察
</span>
</td>
<td class="py-3.5 px-4 text-center sticky right-0 bg-white/95">
<div class="flex items-center justify-center gap-2">
<button class="px-2 py-1 rounded bg-primary text-white font-semibold text-[12px] hover:bg-blue-700 shadow-sm transition-colors">一键发起勘察派单</button>
<span class="text-slate-200">|</span>
<button class="text-slate-600 hover:text-slate-900 text-[12.5px]">查看详情</button>
<span class="text-slate-200">|</span>
<button class="text-slate-400 hover:text-slate-700 text-[12.5px]">编辑</button>
</div>
</td>
</tr>
<!-- Row 2: VST-2026-0315 加急 -->
<tr class="hover:bg-blue-50/40 transition-colors">
<td class="py-3.5 px-4 text-center">
<input class="rounded border-slate-300 text-primary focus:ring-0 w-4 h-4 cursor-pointer" type="checkbox"/>
</td>
<td class="py-3.5 px-3">
<div class="flex items-center gap-1.5">
<span class="font-mono font-bold text-primary hover:underline cursor-pointer">VST-2026-0315</span>
</div>
<div class="mt-1">
<span class="text-[10.5px] px-1.5 py-0.5 rounded bg-amber-100 text-amber-800 font-semibold border border-amber-200">加急 · P2高优</span>
</div>
</td>
<td class="py-3.5 px-3">
<div class="font-semibold text-slate-900 leading-snug line-clamp-1">武汉软件工程职业学院</div>
<div class="text-[11.5px] text-slate-500 mt-0.5 flex items-center gap-1">
<span class="material-symbols-outlined text-[14px] text-slate-400">domain</span>
<span>实训设备保障处 · 智能制造产教园</span>
</div>
</td>
<td class="py-3.5 px-3">
<div class="font-medium text-slate-800">高能耗负荷测绘拓扑方案</div>
<div class="text-[11.5px] text-slate-500">方案沟通 · 商务二访</div>
</td>
<td class="py-3.5 px-3">
<div class="flex items-center gap-2">
<div class="w-6 h-6 rounded-full bg-secondary/20 text-purple-700 flex items-center justify-center font-bold text-[11px]">王</div>
<div>
<div class="font-medium text-slate-800">王建国</div>
<div class="font-mono text-[11.5px] text-slate-400">135****4829</div>
</div>
</div>
</td>
<td class="py-3.5 px-3">
<div class="font-mono text-[12.5px] text-amber-600 font-semibold">今日 15:30 待访</div>
<div class="text-[11px] text-slate-400 flex items-center gap-0.5 mt-0.5">
<span class="material-symbols-outlined text-[13px]">alarm</span>
<span>距离拜访还剩 1.5 小时</span>
</div>
</td>
<td class="py-3.5 px-3">
<div class="inline-flex items-center gap-1 px-2 py-0.5 rounded bg-amber-50 text-amber-800 border border-amber-200 font-medium text-[11.5px]">
<span class="w-1.5 h-1.5 rounded-full bg-amber-500"></span>
<span>B+级 · 方案研讨阶段</span>
</div>
<div class="text-[11px] text-slate-400 mt-0.5">待递交拓扑图纸草案</div>
</td>
<td class="py-3.5 px-3 text-center">
<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[12px] font-medium bg-amber-50 text-amber-700 border border-amber-200">
<span class="w-1.5 h-1.5 rounded-full bg-amber-500"></span>待执行 · 今日待办
</span>
</td>
<td class="py-3.5 px-4 text-center sticky right-0 bg-white/95">
<div class="flex items-center justify-center gap-2">
<button class="text-primary hover:text-blue-700 font-semibold text-[12.5px]">执行跟进</button>
<span class="text-slate-200">|</span>
<button class="text-slate-600 hover:text-slate-900 text-[12.5px]">改派</button>
<span class="text-slate-200">|</span>
<button class="text-slate-400 hover:text-red-600 text-[12.5px]">取消</button>
</div>
</td>
</tr>
<!-- Row 3: VST-2026-0309 常规 -->
<tr class="hover:bg-blue-50/40 transition-colors">
<td class="py-3.5 px-4 text-center">
<input class="rounded border-slate-300 text-primary focus:ring-0 w-4 h-4 cursor-pointer" type="checkbox"/>
</td>
<td class="py-3.5 px-3">
<div class="flex items-center gap-1.5">
<span class="font-mono font-bold text-primary hover:underline cursor-pointer">VST-2026-0309</span>
</div>
<div class="mt-1">
<span class="text-[10.5px] px-1.5 py-0.5 rounded bg-slate-100 text-slate-600 font-medium border border-slate-200">常规 · P3回访</span>
</div>
</td>
<td class="py-3.5 px-3">
<div class="font-semibold text-slate-900 leading-snug line-clamp-1">汉口新华医院</div>
<div class="text-[11.5px] text-slate-500 mt-0.5 flex items-center gap-1">
<span class="material-symbols-outlined text-[14px] text-slate-400">domain</span>
<span>总务科 · 急救中心低压配电室</span>
</div>
</td>
<td class="py-3.5 px-3">
<div class="font-medium text-slate-800">春季应急电巡检与改造沟通</div>
<div class="text-[11.5px] text-slate-500">维保回访 · 备自投测试</div>
</td>
<td class="py-3.5 px-3">
<div class="flex items-center gap-2">
<div class="w-6 h-6 rounded-full bg-emerald-100 text-emerald-800 flex items-center justify-center font-bold text-[11px]">李</div>
<div>
<div class="font-medium text-slate-800">李志敏</div>
<div class="font-mono text-[11.5px] text-slate-400">139****8821</div>
</div>
</div>
</td>
<td class="py-3.5 px-3">
<div class="font-mono text-[12.5px] text-slate-800">2026-03-08 10:00</div>
<div class="text-[11px] text-slate-400 flex items-center gap-0.5 mt-0.5">
<span class="material-symbols-outlined text-[13px] text-emerald-600">check_circle</span>
<span>已完工 (历时 2.5 小时)</span>
</div>
</td>
<td class="py-3.5 px-3">
<div class="inline-flex items-center gap-1 px-2 py-0.5 rounded bg-purple-50 text-purple-700 border border-purple-200 font-medium text-[11.5px]">
<span class="w-1.5 h-1.5 rounded-full bg-purple-500"></span>
<span>取证待审核 (含3张防伪照片)</span>
</div>
<div class="text-[11px] text-slate-500 mt-0.5">意向 B级 · 方案比选中</div>
</td>
<td class="py-3.5 px-3 text-center">
<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[12px] font-medium bg-purple-50 text-purple-700 border border-purple-200">
<span class="w-1.5 h-1.5 rounded-full bg-purple-500"></span>成果待技术复核
</span>
</td>
<td class="py-3.5 px-4 text-center sticky right-0 bg-white/95">
<div class="flex items-center justify-center gap-2">
<button class="text-purple-600 hover:text-purple-800 font-bold text-[12.5px]">审核现场取证</button>
<span class="text-slate-200">|</span>
<button class="text-slate-600 hover:text-slate-900 text-[12.5px]">导出简报</button>
</div>
</td>
</tr>
<!-- Row 4: VST-2026-0304 常规 -->
<tr class="hover:bg-blue-50/40 transition-colors">
<td class="py-3.5 px-4 text-center">
<input class="rounded border-slate-300 text-primary focus:ring-0 w-4 h-4 cursor-pointer" type="checkbox"/>
</td>
<td class="py-3.5 px-3">
<div class="flex items-center gap-1.5">
<span class="font-mono font-bold text-primary hover:underline cursor-pointer">VST-2026-0304</span>
</div>
<div class="mt-1">
<span class="text-[10.5px] px-1.5 py-0.5 rounded bg-slate-100 text-slate-600 font-medium border border-slate-200">常规 · 方案研讨</span>
</div>
</td>
<td class="py-3.5 px-3">
<div class="font-semibold text-slate-900 leading-snug line-clamp-1">东湖高新智造产业园</div>
<div class="text-[11.5px] text-slate-500 mt-0.5 flex items-center gap-1">
<span class="material-symbols-outlined text-[14px] text-slate-400">domain</span>
<span>工程运维中心 · 2号地块配电站</span>
</div>
</td>
<td class="py-3.5 px-3">
<div class="font-medium text-slate-800">智慧微电网与储能并网可行性</div>
<div class="text-[11.5px] text-slate-500">方案沟通 · 商务研讨</div>
</td>
<td class="py-3.5 px-3">
<div class="flex items-center gap-2">
<div class="w-6 h-6 rounded-full bg-blue-100 text-primary flex items-center justify-center font-bold text-[11px]">周</div>
<div>
<div class="font-medium text-slate-800">周慧</div>
<div class="font-mono text-[11.5px] text-slate-400">137****6632</div>
</div>
</div>
</td>
<td class="py-3.5 px-3">
<div class="font-mono text-[12.5px] text-slate-800">2026-03-05 16:40</div>
<div class="text-[11px] text-slate-400 flex items-center gap-0.5 mt-0.5">
<span class="material-symbols-outlined text-[13px] text-emerald-600">done_all</span>
<span>客户已确认纪要</span>
</div>
</td>
<td class="py-3.5 px-3">
<div class="inline-flex items-center gap-1 px-2 py-0.5 rounded bg-blue-50 text-primary border border-blue-200 font-semibold text-[11.5px]">
<span class="w-1.5 h-1.5 rounded-full bg-primary"></span>
<span>A级 · 已立项勘察 (PRJ-2026-041)</span>
</div>
<div class="text-[11px] text-slate-400 mt-0.5">方案已同步至项目库</div>
</td>
<td class="py-3.5 px-3 text-center">
<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[12px] font-medium bg-emerald-50 text-emerald-700 border border-emerald-200">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>已立项转勘察
</span>
</td>
<td class="py-3.5 px-4 text-center sticky right-0 bg-white/95">
<div class="flex items-center justify-center gap-2">
<button class="text-primary hover:text-blue-700 font-medium text-[12.5px]">查看关联工程</button>
<span class="text-slate-200">|</span>
<button class="text-slate-600 hover:text-slate-900 text-[12.5px]">报告归档</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Table Footer & Ant Design Pagination -->
<div class="px-5 py-4 border-t border-border-rule bg-surface flex flex-col md:flex-row items-center justify-between gap-4">
<!-- Batch Actions -->
<div class="flex items-center gap-3">
<span class="text-[12.5px] text-slate-500">
已选中 <strong class="text-primary font-mono font-bold">0</strong> 项
</span>
<button class="px-3 py-1.5 rounded-lg border border-slate-200 hover:bg-slate-50 text-slate-600 text-[12px] font-medium transition-colors disabled:opacity-50" disabled="">
批量转勘察派单
</button>
<button class="px-3 py-1.5 rounded-lg border border-slate-200 hover:bg-slate-50 text-slate-600 text-[12px] font-medium transition-colors disabled:opacity-50" disabled="">
批量导出拜访记录
</button>
</div>
<!-- Pagination Controls -->
<div class="flex items-center gap-2 text-[13px]">
<span class="text-slate-500 text-[12.5px]">共 128 条</span>
<!-- Page Size Selector -->
<select class="py-1 px-2.5 border border-slate-200 rounded-lg text-[12px] text-slate-700 bg-slate-50 focus:outline-none focus:border-primary">
<option value="10">10 条/页</option>
<option value="20">20 条/页</option>
<option value="50">50 条/页</option>
</select>
<!-- Page Buttons -->
<div class="flex items-center gap-1 ml-2">
<button class="w-8 h-8 rounded-lg border border-slate-200 flex items-center justify-center text-slate-400 hover:bg-slate-50 disabled:opacity-40" disabled="">
<span class="material-symbols-outlined text-[16px]">chevron_left</span>
</button>
<button class="w-8 h-8 rounded-lg bg-primary text-white font-bold flex items-center justify-center text-[12.5px] shadow-sm">
1
</button>
<button class="w-8 h-8 rounded-lg border border-slate-200 text-slate-600 hover:bg-slate-50 flex items-center justify-center text-[12.5px]">
2
</button>
<button class="w-8 h-8 rounded-lg border border-slate-200 text-slate-600 hover:bg-slate-50 flex items-center justify-center text-[12.5px]">
3
</button>
<span class="text-slate-400 px-1 font-mono text-[12px]">...</span>
<button class="w-8 h-8 rounded-lg border border-slate-200 text-slate-600 hover:bg-slate-50 flex items-center justify-center text-[12.5px]">
13
</button>
<button class="w-8 h-8 rounded-lg border border-slate-200 flex items-center justify-center text-slate-600 hover:bg-slate-50">
<span class="material-symbols-outlined text-[16px]">chevron_right</span>
</button>
</div>
<!-- Quick Jumper -->
<div class="flex items-center gap-1.5 ml-2 text-slate-500 text-[12.5px]">
<span>跳至</span>
<input class="w-12 py-1 px-1 text-center font-mono border border-slate-200 rounded-lg text-[12px] focus:outline-none focus:border-primary" type="text" value="1"/>
<span>页</span>
</div>
</div>
</div>
</div>
</main>
</div>
</body></html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 467 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,19 @@
<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="utf-8"/><meta content="width=device-width, initial-scale=1.0" name="viewport"/><link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=hankenGrotesk:wght@100..900&amp;display=swap" rel="stylesheet"/><style>@layer base{html,body{margin:0;padding:0;}body{overscroll-behavior:none;}main>:first-child{margin-top:0!important;}main>:last-child{margin-bottom:0!important;}}::-webkit-scrollbar{display:none;}</style><script src="https://cdn.tailwindcss.com"></script><script id="tailwind-config">tailwind.config={"darkMode":"class","theme":{"extend":{"colors":{"secondary-fixed-dim":"#d2bbff","secondary":"#712ae2","secondary-fixed":"#eaddff","on-primary":"#ffffff","surface-container-lowest":"#ffffff","error":"#ba1a1a","on-error":"#ffffff","on-surface":"#0d1c2c","on-primary-fixed-variant":"#0040a1","outline-variant":"#c2c6d8","tertiary-fixed-dim":"#6bd8cb","on-secondary-fixed-variant":"#5a00c6","tertiary-container":"#008378","primary-container":"#136bfb","secondary-container":"#8a4cfc","tertiary":"#00685f","on-tertiary-fixed-variant":"#005049","on-secondary-fixed":"#25005a","surface-container-highest":"#d5e4fa","surface-container-low":"#eef4ff","background":"#f8f9ff","on-error-container":"#93000a","inverse-primary":"#b2c5ff","surface-dim":"#ccdbf1","on-primary-fixed":"#001847","surface-container-high":"#dbe9ff","outline":"#727787","status-warn":"#e0852b","on-secondary":"#ffffff","surface-tint":"#0056d2","bg-page":"#eef3f8","surface-variant":"#d5e4fa","primary":"#0054cd","on-tertiary-container":"#f4fffc","inverse-surface":"#233142","text-muted":"#5b6b7e","on-tertiary":"#ffffff","accent-subtle":"rgba(29,111,255,0.08)","primary-fixed":"#dae2ff","tertiary-fixed":"#89f5e7","surface-bright":"#f8f9ff","on-primary-container":"#fefcff","on-background":"#0d1c2c","primary-fixed-dim":"#b2c5ff","border-rule":"#dbe4ee","status-error":"#dc2626","inverse-on-surface":"#e9f1ff","error-container":"#ffdad6","on-tertiary-fixed":"#00201d","on-secondary-container":"#fffbff","on-surface-variant":"#424655","surface":"#ffffff","surface-container":"#e5efff"},"borderRadius":{"DEFAULT":"0.25rem","lg":"0.5rem","xl":"0.75rem","full":"9999px"},"spacing":{"md":"16px","sm":"12px","xl":"40px","container-max":"1200px","base":"4px","xs":"8px","gutter":"16px","lg":"24px"},"fontFamily":{"body-md":["hankenGrotesk"],"headline-md-mobile":["hankenGrotesk"],"caption-sm":["hankenGrotesk"],"label-bold":["hankenGrotesk"],"headline-md":["hankenGrotesk"],"title-sm":["hankenGrotesk"],"data-mono":["jetbrainsMono"],"display-lg":["hankenGrotesk"]},"fontSize":{"body-md":["15px",{"lineHeight":"24px","fontWeight":"400"}],"headline-md-mobile":["20px",{"lineHeight":"26px","fontWeight":"700"}],"caption-sm":["12px",{"lineHeight":"14px","fontWeight":"700"}],"label-bold":["13px",{"lineHeight":"16px","fontWeight":"700"}],"headline-md":["23px",{"lineHeight":"28px","fontWeight":"700"}],"title-sm":["17px",{"lineHeight":"24px","fontWeight":"700"}],"data-mono":["12px",{"lineHeight":"16px","fontWeight":"500"}],"display-lg":["32px",{"lineHeight":"40px","fontWeight":"700"}]}}}}</script></head><body class="bg-background font-body-md text-body-md text-on-surface antialiased"><aside class="fixed left-0 top-0 h-full w-72 bg-inverse-surface text-inverse-on-surface z-50 flex flex-col justify-between shadow-[0_1px_8px_rgba(0,0,0,0.04)]"><div class="flex flex-col flex-1 min-h-0"><div class="h-16 px-md flex items-center gap-sm bg-inverse-surface"><div class="w-10 h-10 rounded-lg bg-primary flex items-center justify-center text-on-primary shadow-sm"><span class="material-symbols-outlined text-[24px]">bolt</span></div><div class="flex flex-col"><div class="flex items-center gap-xs"><span class="font-headline-md text-headline-md text-white tracking-wide">XT-POWER</span><span class="font-caption-sm text-caption-sm bg-primary px-xs py-0.5 rounded text-white">OS</span></div><span class="font-caption-sm text-caption-sm text-outline-variant tracking-wider">鑫潼智电 · 智能配用电中枢</span></div></div><div class="flex-1 overflow-y-auto px-xs py-md"><nav class="flex flex-col gap-1" data-active-classes="bg-primary text-white font-label-bold rounded-lg shadow-sm"><a class="flex items-center gap-sm px-md py-sm rounded-lg text-outline-variant hover:bg-surface-container-highest/10 hover:text-white transition-colors" data-path="workbench" href="#"><span class="material-symbols-outlined text-[20px]">dashboard</span><span class="font-label-bold text-label-bold">运营工作台</span></a><div class="flex flex-col"><a class="flex items-center justify-between px-md py-sm rounded-lg bg-primary text-white font-label-bold shadow-sm transition-colors" data-path="customer-management" href="#"><div class="flex items-center gap-sm"><span class="material-symbols-outlined text-[20px]">apartment</span><span class="font-label-bold text-label-bold">客户管理</span></div><span class="material-symbols-outlined text-[16px]">expand_more</span></a><div class="flex flex-col ml-lg pl-md mt-1 mb-1 gap-1 border-l border-outline/30"><a aria-current="page" class="px-sm py-1.5 rounded-lg text-white font-label-bLine truncated
<!-- 面包屑导航与页头标题区 -->
<div class="flex flex-col gap-xs">
<nav class="flex items-center gap-xs text-text-muted font-caption-sm text-caption-sm">
<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-on-surface font-bold">新增客户拜访记录</span>
</nav>
<div class="flex flex-wrap items-center justify-between gap-md mt-1"><div class="flex items-center gap-sm"><div class="w-10 h-10 rounded-lg bg-surface-container-high flex items-center justify-center text-primary shadow-sm"><span class="material-symbols-outlined text-[24px]">handshake</span></div><div><div class="flex items-center gap-xs"><h1 class="font-headline-md text-headline-md text-on-surface">新增拜访跟进记录</h1><span class="px-xs py-0.5 rounded text-primary font-caption-sm text-caption-sm bg-primary/10 font-bold">售前CRM跟进</span></div><p class="font-caption-sm text-caption-sm text-text-muted mt-0.5">聚焦售前需求沟通、合作意向摸排与跟进计划,轻量高效记录</p></div></div><div class="flex items-center gap-xs"><button class="px-md py-1.5 rounded-lg bg-surface text-text-muted hover:text-on-surface border border-border-rule font-caption-sm text-caption-sm transition-colors flex items-center gap-1 shadow-sm" type="button"><span class="material-symbols-outlined text-[16px]">history</span><span>历史跟进台账</span></button></div></div>
</div>
<!-- 双栏主次架构 (8 : 4) -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-lg items-start"><div class="lg:col-span-8 flex flex-col gap-lg min-w-0"><section class="bg-surface rounded-xl p-lg shadow-sm flex flex-col gap-md"><div class="flex items-center justify-between pb-sm bg-surface-container-low/40 -mx-lg -mt-lg px-lg pt-md rounded-t-xl"><div class="flex items-center gap-xs"><div class="w-2 h-4 rounded-full bg-primary"></div><h2 class="font-title-sm text-title-sm text-on-surface">模块一:拜访基本信息</h2><span class="font-data-mono text-data-mono text-text-muted">(Visit Details)</span></div><span class="text-status-warn font-caption-sm text-caption-sm flex items-center gap-1"><span class="material-symbols-outlined text-[14px]">info</span>* 为必填项</span></div><div class="grid grid-cols-1 md:grid-cols-2 gap-md pt-xs"><div class="flex flex-col gap-1.5 md:col-span-2"><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="font-caption-sm text-caption-sm text-text-muted font-normal">支持客户全称或拼音首字母智能检索</span></label><div class="w-full flex items-center justify-between px-md py-2.5 rounded-lg bg-background text-on-surface cursor-pointer shadow-inner"><div class="flex items-center gap-sm"><span class="material-symbols-outlined text-primary text-[20px]">business</span><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.5 bg-primary/10 text-primary font-data-mono text-data-mono rounded">CUST-2026-0012</span></div></div><div class="flex items-center gap-xs"><span class="px-xs py-0.5 rounded font-caption-sm text-caption-sm bg-primary/10 text-primary font-bold">重点推进客户</span><span class="material-symbols-outlined text-text-muted text-[18px]">arrow_drop_down</span></div></div></div><div class="flex flex-col gap-1.5 md:col-span-2"><label class="font-label-bold text-label-bold text-on-surface">拜访方式 <span class="text-status-error">*</span></label><div class="grid grid-cols-2 sm:grid-cols-4 gap-xs"><button class="flex items-center justify-center gap-1 py-2 px-sm rounded-lg bg-primary text-on-primary font-label-bold text-label-bold shadow-sm" type="button"><span class="material-symbols-outlined text-[16px]">pin_drop</span><span>现场拜访</span></button><button class="flex items-center justify-center gap-1 py-2 px-sm rounded-lg bg-surface-container-low text-on-surface font-label-bold text-label-bold hover:bg-surface-container transition-colors" type="button"><span class="material-symbols-outlined text-[16px]">handshake</span><span>商务拜访</span></button><button class="flex items-center justify-center gap-1 py-2 px-sm rounded-lg bg-surface-container-low text-on-surface font-label-bold text-label-bold hover:bg-surface-container transition-colors" type="button"><span class="material-symbols-outlined text-[16px]">phone_in_talk</span><span>电话沟通</span></button><button class="flex items-center justify-center gap-1 py-2 px-sm rounded-lg bg-surface-container-low text-on-surface font-label-bold text-label-bold hover:bg-surface-container transition-colors" type="button"><span class="material-symbols-outlined text-[16px]">videocam</span><span>线上会议</span></button></div></div><div class="flex flex-col gap-1.5"><label class="font-label-bold text-label-bold text-on-surface">拜访时间 <span class="text-status-error">*</span></label><div class="flex items-center justify-between px-md py-2 rounded-lg bg-background text-on-surface shadow-inner"><div class="flex items-center gap-xs"><span class="material-symbols-outlined text-text-muted text-[18px]">calendar_today</span><span class="font-data-mono text-data-mono">2026-03-28 14:30 ~ 16:00</span></div><span class="font-caption-sm text-caption-sm text-primary font-bold bg-primary/10 px-xs py-0.5 rounded">1.5h</span></div></div><div class="flex flex-col gap-1.5"><label class="font-label-bold text-label-bold text-on-surface">业务负责人及陪同人员 <span class="text-status-error">*</span></label><div class="flex items-center gap-xs"><div class="flex-1 flex items-center gap-xs px-sm py-2 rounded-lg bg-background shadow-inner text-on-surface"><span class="w-5 h-5 rounded-full bg-primary text-white flex items-center justify-center text-[10px] font-bold">李</span><span class="font-body-md text-body-md">李志敏</span><span class="font-caption-sm text-caption-sm text-text-muted">(负责人)</span></div><div class="flex-1 flex items-center gap-xs px-sm py-2 rounded-lg bg-background shadow-inner text-on-surface"><span class="w-5 h-5 rounded-full bg-surface-container-highest text-on-surface flex items-center justify-center text-[10px] font-bold">王</span><input class="w-full bg-transparent focus:outline-none text-body-md font-body-md text-on-surface" type="text" value="王助理 (商务陪同)"/></div></divLine truncated
</div></main></div></body></html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 507 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,744 @@
<!DOCTYPE html>
<html lang="zh-CN"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>现场勘察派单台账 - 鑫潼智电 XT-POWER OS</title>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=hankenGrotesk:wght@100..900&amp;family=JetBrains+Mono:wght@400..700&amp;display=swap" rel="stylesheet"/>
<style>
@layer base {
html, body { margin: 0; padding: 0; }
body { overscroll-behavior: none; }
}
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 9999px; }
::-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: {
"primary": "#0054cd",
"primary-container": "#136bfb",
"on-primary": "#ffffff",
"surface": "#ffffff",
"bg-page": "#f4f7fb",
"on-surface": "#0d1c2c",
"on-surface-variant": "#424655",
"text-muted": "#5b6b7e",
"border-rule": "#e2e8f0",
"inverse-surface": "#1e293b",
"surface-container-low": "#f8fafc",
"surface-container": "#eef2f7",
"surface-container-high": "#e2e8f0",
"accent-subtle": "rgba(0,84,205,0.08)",
"tertiary": "#00685f",
"status-warn": "#d97706",
"error": "#dc2626"
},
fontFamily: {
"sans": ["hankenGrotesk", "sans-serif"],
"mono": ["JetBrains Mono", "monospace"]
}
}
}
};
</script>
</head>
<body class="bg-bg-page font-sans text-on-surface antialiased text-[13.5px]">
<!-- Global Industrial Sidebar -->
<aside class="fixed left-0 top-0 h-full w-64 bg-inverse-surface z-50 flex flex-col justify-between shadow-xl">
<div class="flex flex-col h-full">
<!-- Logo -->
<div class="h-16 flex items-center px-5 gap-3 border-b border-white/10">
<div class="w-8 h-8 rounded-lg bg-primary flex items-center justify-center text-white font-bold shadow-md shadow-blue-500/30">
<span class="material-symbols-outlined text-[20px]">bolt</span>
</div>
<div class="flex flex-col">
<span class="text-white font-bold text-[15px] tracking-wide">鑫潼智电</span>
<span class="font-mono text-[11px] text-slate-400 tracking-wider">XT-POWER OS v3.8</span>
</div>
</div>
<!-- Navigation Links -->
<nav class="flex-1 px-3 py-4 space-y-1 overflow-y-auto">
<a class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-300 hover:bg-white/10 hover:text-white transition-all text-[13.5px]" href="#">
<span class="material-symbols-outlined text-[20px]">dashboard</span>
<span>运营工作台</span>
</a>
<a class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-300 hover:bg-white/10 hover:text-white transition-all text-[13.5px]" href="#">
<span class="material-symbols-outlined text-[20px]">corporate_fare</span>
<span>客户管理</span>
</a>
<!-- Menu Group: Project Management (Expanded & Active) -->
<div class="pt-2">
<div class="flex items-center justify-between px-3 py-2 text-white font-medium text-[13.5px]">
<div class="flex items-center gap-3">
<span class="material-symbols-outlined text-[20px] text-blue-400">schema</span>
<span>项目管理</span>
</div>
<span class="material-symbols-outlined text-[16px] text-slate-400">expand_less</span>
</div>
<div class="pl-7 pr-2 space-y-1 mt-1 border-l border-white/10 ml-5">
<a class="flex items-center py-2 px-3 rounded-lg text-slate-400 hover:bg-white/10 hover:text-white text-[13px] transition-all" href="#">全周期工程看板</a>
<a class="flex items-center py-2 px-3 rounded-lg bg-primary text-white font-semibold text-[13px] shadow-sm shadow-blue-900/40" href="#">现场勘察派单</a>
<a class="flex items-center py-2 px-3 rounded-lg text-slate-400 hover:bg-white/10 hover:text-white text-[13px] transition-all" href="#">项目合同管理</a>
<a class="flex items-center py-2 px-3 rounded-lg text-slate-400 hover:bg-white/10 hover:text-white text-[13px] transition-all" href="#">工程安装派单</a>
<a class="flex items-center py-2 px-3 rounded-lg text-slate-400 hover:bg-white/10 hover:text-white text-[13px] transition-all" href="#">工程竣工验收</a>
</div>
</div>
<a class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-300 hover:bg-white/10 hover:text-white transition-all text-[13.5px]" href="#">
<span class="material-symbols-outlined text-[20px]">send_time_extension</span>
<span>运营派单</span>
</a>
<a class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-300 hover:bg-white/10 hover:text-white transition-all text-[13.5px]" href="#">
<span class="material-symbols-outlined text-[20px]">sensors</span>
<span>设备监控</span>
</a>
<a class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-300 hover:bg-white/10 hover:text-white transition-all text-[13.5px]" href="#">
<span class="material-symbols-outlined text-[20px]">query_stats</span>
<span>统计报表</span>
</a>
<a class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-300 hover:bg-white/10 hover:text-white transition-all text-[13.5px]" href="#">
<span class="material-symbols-outlined text-[20px]">settings</span>
<span>系统管理</span>
</a>
</nav>
<!-- Bottom Status -->
<div class="p-4 border-t border-white/10 bg-slate-900/50">
<div class="flex items-center justify-between text-[12px] text-slate-400">
<span>华东区 · 枢纽调度</span>
<span class="flex items-center gap-1.5 text-emerald-400 font-mono text-[11px]">
<span class="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span> 系统稳定
</span>
</div>
</div>
</div>
</aside>
<!-- Main Shell Viewport -->
<div class="pl-64 flex flex-col min-h-screen">
<!-- Top Navigation Bar -->
<header class="fixed top-0 left-64 right-0 h-16 z-40 bg-surface/90 backdrop-blur-md border-b border-border-rule px-8 flex items-center justify-between shadow-sm">
<div class="flex items-center gap-5">
<div class="flex items-center gap-2 text-slate-500 text-[13px]">
<span class="hover:text-slate-700 cursor-pointer">项目管理</span>
<span class="text-slate-300">/</span>
<span class="text-on-surface font-semibold">现场勘察派单台账</span>
</div>
<div class="relative w-80">
<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 bg-slate-100/90 text-on-surface rounded-lg text-[13px] border border-transparent focus:border-primary focus:bg-white focus:outline-none transition-all" placeholder="检索工单编号、客户或项目..." type="text"/>
</div>
</div>
<div class="flex items-center gap-4">
<div class="flex items-center gap-2 pr-3 border-r border-border-rule">
<button class="relative w-8 h-8 flex items-center justify-center 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 bg-red-500 rounded-full"></span>
</button>
<button class="w-8 h-8 flex items-center justify-center rounded-lg hover:bg-slate-100 text-slate-600 transition-colors" title="使用帮助">
<span class="material-symbols-outlined text-[20px]">help</span>
</button>
</div>
<div class="flex items-center gap-3 pl-1">
<div class="text-right">
<div class="font-semibold text-slate-800 text-[13px] flex items-center justify-end gap-1.5">
<span>李工</span>
<span class="text-[10.5px] px-1.5 py-0.2 rounded bg-blue-50 text-primary border border-blue-100 font-normal">主控工程师</span>
</div>
<div class="text-[11px] text-slate-500 font-mono">调度中心 · 席位04</div>
</div>
<div class="w-9 h-9 rounded-full bg-gradient-to-tr from-primary to-blue-500 text-white flex items-center justify-center font-bold text-[14px] shadow-sm">
李
</div>
</div>
</div>
</header>
<!-- Main Workspace Ledger -->
<main class="w-full pt-20 flex-1 p-8 space-y-5">
<!-- Page Header: Title + Ledger Actions -->
<div class="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
<div>
<div class="flex items-center gap-2.5">
<h1 class="text-[22px] font-bold text-slate-900 tracking-tight">现场勘察派单台账</h1>
<span class="text-[12px] font-mono font-medium px-2 py-0.5 rounded-md bg-slate-100 text-slate-600 border border-slate-200">SURVEY DISPATCH LEDGER</span>
</div>
<p class="text-[13px] text-slate-500 mt-1">
统一调度与全周期管理电力工程勘察测绘工单,监控现场执行时效、电工指派状态及测绘成果归档。
</p>
</div>
<div class="flex items-center gap-2.5">
<button class="px-3.5 py-2 rounded-lg bg-white border border-slate-200 hover:bg-slate-50 hover:border-slate-300 text-slate-700 font-medium text-[13px] shadow-sm transition-all flex items-center gap-1.5">
<span class="material-symbols-outlined text-[17px] text-slate-500">file_download</span>
<span>导出台账数据</span>
</button>
<button class="px-3.5 py-2 rounded-lg bg-white border border-slate-200 hover:bg-slate-50 hover:border-slate-300 text-slate-700 font-medium text-[13px] shadow-sm transition-all flex items-center gap-1.5">
<span class="material-symbols-outlined text-[17px] text-slate-500">group_add</span>
<span>批量派工</span>
</button>
<button class="px-4 py-2 rounded-lg bg-primary hover:bg-blue-700 text-white font-semibold text-[13px] shadow-sm shadow-blue-500/25 transition-all flex items-center gap-1.5">
<span class="material-symbols-outlined text-[18px]">add</span>
<span>发起勘察派单</span>
</button>
</div>
</div>
<!-- Ledger Top Status Metric Tabs (AntD style filter tabs) -->
<div class="grid grid-cols-2 md:grid-cols-5 gap-3">
<!-- Card 1: All -->
<div class="bg-surface rounded-xl p-3.5 border-2 border-primary/20 bg-gradient-to-b from-blue-50/50 to-surface shadow-sm cursor-pointer hover:border-primary transition-all">
<div class="flex items-center justify-between">
<span class="text-[12.5px] font-semibold text-slate-700">全部工单</span>
<span class="material-symbols-outlined text-[18px] text-primary">view_kanban</span>
</div>
<div class="mt-2 flex items-baseline gap-2">
<span class="text-[22px] font-bold text-slate-900 font-mono">170</span>
<span class="text-[11px] text-slate-400">总台账量</span>
</div>
<div class="mt-1 text-[11.5px] text-slate-500 flex items-center gap-1">
<span>当月环比</span>
<span class="text-emerald-600 font-medium font-mono">+12.4%</span>
</div>
</div>
<!-- Card 2: Pending Dispatch -->
<div class="bg-surface rounded-xl p-3.5 border border-border-rule shadow-sm hover:border-amber-400 cursor-pointer transition-all">
<div class="flex items-center justify-between">
<div class="flex items-center gap-1.5">
<span class="w-2 h-2 rounded-full bg-amber-500"></span>
<span class="text-[12.5px] font-semibold text-slate-700">待指派 / 待派单</span>
</div>
<span class="px-1.5 py-0.5 rounded text-[10.5px] bg-amber-50 text-amber-700 font-medium border border-amber-200">预警</span>
</div>
<div class="mt-2 flex items-baseline gap-2">
<span class="text-[22px] font-bold text-amber-600 font-mono">4</span>
<span class="text-[11px] text-slate-400">单急需调度</span>
</div>
<div class="mt-1 text-[11.5px] text-amber-600 font-medium flex items-center gap-1">
<span class="material-symbols-outlined text-[13px]">alarm</span>
<span>1 单即将超出到场时效</span>
</div>
</div>
<!-- Card 3: In Progress -->
<div class="bg-surface rounded-xl p-3.5 border border-border-rule shadow-sm hover:border-blue-400 cursor-pointer transition-all">
<div class="flex items-center justify-between">
<div class="flex items-center gap-1.5">
<span class="w-2 h-2 rounded-full bg-blue-500 animate-pulse"></span>
<span class="text-[12.5px] font-semibold text-slate-700">现场勘察中</span>
</div>
<span class="material-symbols-outlined text-[18px] text-blue-500">architecture</span>
</div>
<div class="mt-2 flex items-baseline gap-2">
<span class="text-[22px] font-bold text-primary font-mono">12</span>
<span class="text-[11px] text-slate-400">单在场测绘</span>
</div>
<div class="mt-1 text-[11.5px] text-slate-500 flex items-center gap-1">
<span>平均履约到场率</span>
<span class="text-blue-600 font-medium font-mono">98.6%</span>
</div>
</div>
<!-- Card 4: Under Technical Review -->
<div class="bg-surface rounded-xl p-3.5 border border-border-rule shadow-sm hover:border-purple-400 cursor-pointer transition-all">
<div class="flex items-center justify-between">
<div class="flex items-center gap-1.5">
<span class="w-2 h-2 rounded-full bg-purple-500"></span>
<span class="text-[12.5px] font-semibold text-slate-700">成果待技术复核</span>
</div>
<span class="material-symbols-outlined text-[18px] text-purple-600">fact_check</span>
</div>
<div class="mt-2 flex items-baseline gap-2">
<span class="text-[22px] font-bold text-purple-600 font-mono">6</span>
<span class="text-[11px] text-slate-400">单已回传照片</span>
</div>
<div class="mt-1 text-[11.5px] text-slate-500 flex items-center gap-1">
<span>总工复审平均</span>
<span class="text-purple-600 font-medium font-mono">2.1 小时</span>
</div>
</div>
<!-- Card 5: Archived/Completed -->
<div class="bg-surface rounded-xl p-3.5 border border-border-rule shadow-sm hover:border-emerald-400 cursor-pointer transition-all">
<div class="flex items-center justify-between">
<div class="flex items-center gap-1.5">
<span class="w-2 h-2 rounded-full bg-emerald-500"></span>
<span class="text-[12.5px] font-semibold text-slate-700">勘察完成 / 已归档</span>
</div>
<span class="material-symbols-outlined text-[18px] text-emerald-600">check_circle</span>
</div>
<div class="mt-2 flex items-baseline gap-2">
<span class="text-[22px] font-bold text-emerald-600 font-mono">148</span>
<span class="text-[11px] text-slate-400">单本月归档</span>
</div>
<div class="mt-1 text-[11.5px] text-slate-500 flex items-center gap-1">
<span>方案出具率</span>
<span class="text-emerald-600 font-medium font-mono">100%</span>
</div>
</div>
</div>
<!-- Filter Bar: Search, Filters & Action buttons -->
<div class="bg-surface rounded-xl p-4 border border-border-rule shadow-sm space-y-3">
<div class="grid grid-cols-1 md:grid-cols-12 gap-3 items-center">
<!-- Search Query -->
<div class="md:col-span-4">
<label class="block text-[11.5px] font-medium text-slate-500 mb-1">模糊搜索</label>
<div class="relative">
<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-3 py-1.5 text-[13px] bg-slate-50 border border-slate-200 rounded-lg text-slate-800 placeholder-slate-400 focus:bg-white focus:border-primary focus:outline-none transition-colors" placeholder="工单编号、客户名称、物理建筑、业务员" type="text"/>
</div>
</div>
<!-- Status Filter -->
<div class="md:col-span-2">
<label class="block text-[11.5px] font-medium text-slate-500 mb-1">工单流程状态</label>
<select class="w-full py-1.5 px-3 text-[13px] bg-slate-50 border border-slate-200 rounded-lg text-slate-800 focus:bg-white focus:border-primary focus:outline-none">
<option value="all">全部状态 (170)</option>
<option value="pending">待指派 (4)</option>
<option value="progress">勘察中 (12)</option>
<option value="review">待技术复核 (6)</option>
<option value="completed">已归档 (148)</option>
</select>
</div>
<!-- Electrician Filter -->
<div class="md:col-span-2">
<label class="block text-[11.5px] font-medium text-slate-500 mb-1">指派电工</label>
<select class="w-full py-1.5 px-3 text-[13px] bg-slate-50 border border-slate-200 rounded-lg text-slate-800 focus:bg-white focus:border-primary focus:outline-none">
<option value="all">全部电工 (38人)</option>
<option value="unassigned">待指派电工</option>
<option value="1">赵志强 (特种高压/4.98分)</option>
<option value="2">孙明远 (继电保护/4.92分)</option>
<option value="3">王建国 (特种低压/4.95分)</option>
<option value="4">周正海 (电气试验/4.88分)</option>
</select>
</div>
<!-- Date Picker -->
<div class="md:col-span-2">
<label class="block text-[11.5px] font-medium text-slate-500 mb-1">计划到场窗口</label>
<div class="relative">
<span class="material-symbols-outlined absolute left-2.5 top-1/2 -translate-y-1/2 text-slate-400 text-[16px]">calendar_today</span>
<input class="w-full pl-8 pr-2.5 py-1.5 text-[12.5px] bg-slate-50 border border-slate-200 rounded-lg text-slate-800 focus:bg-white focus:border-primary focus:outline-none font-mono" placeholder="2026-03-01 ~ 03-31" type="text" value="2026-03-01 ~ 03-15"/>
</div>
</div>
<!-- Buttons -->
<div class="md:col-span-2 flex items-center gap-2 pt-5">
<button class="flex-1 py-1.5 bg-primary hover:bg-blue-700 text-white font-medium rounded-lg text-[13px] shadow-sm transition-colors flex items-center justify-center gap-1">
<span class="material-symbols-outlined text-[16px]">search</span>
<span>查询</span>
</button>
<button class="px-3 py-1.5 bg-white border border-slate-200 hover:bg-slate-50 text-slate-600 rounded-lg text-[13px] transition-colors">
重置
</button>
<button class="text-primary hover:text-blue-700 text-[12px] flex items-center gap-0.5 whitespace-nowrap pl-1" title="展开高级筛选">
<span>高级</span>
<span class="material-symbols-outlined text-[15px]">tune</span>
</button>
</div>
</div>
</div>
<!-- Main Data Table Container (Ant Design Pro Table Style) -->
<div class="bg-surface rounded-xl border border-border-rule shadow-sm overflow-hidden">
<!-- Table Subheader -->
<div class="px-5 py-3.5 border-b border-border-rule bg-slate-50/60 flex items-center justify-between">
<div class="flex items-center gap-3 text-[13px]">
<span class="font-semibold text-slate-800">勘察派工数据台账</span>
<span class="text-slate-400">|</span>
<span class="text-slate-500 text-[12px]">显示第 <span class="font-mono text-slate-700 font-semibold">1</span> 到 <span class="font-mono text-slate-700 font-semibold">5</span> 笔,共 <span class="font-mono text-primary font-bold">170</span> 笔工单记录</span>
</div>
<div class="flex items-center gap-2 text-[12px] text-slate-500">
<span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-red-500"></span>特急 2</span>
<span class="flex items-center gap-1 ml-2"><span class="w-2 h-2 rounded-full bg-amber-500"></span>加急 5</span>
<span class="flex items-center gap-1 ml-2"><span class="w-2 h-2 rounded-full bg-slate-400"></span>常规 163</span>
<button class="ml-3 text-slate-400 hover:text-slate-700 p-1" title="刷新表格">
<span class="material-symbols-outlined text-[18px]">refresh</span>
</button>
</div>
</div>
<!-- Actual Table -->
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse text-[13px]">
<thead>
<tr class="bg-slate-50/90 text-slate-600 text-[12px] border-b border-slate-200 uppercase tracking-wider font-semibold">
<th class="py-3 px-4 w-10 text-center">
<input class="rounded border-slate-300 text-primary focus:ring-0 w-4 h-4 cursor-pointer" type="checkbox"/>
</th>
<th class="py-3 px-3 min-w-[150px]">工单编号 / 优先级</th>
<th class="py-3 px-3 min-w-[210px]">关联客户与项目</th>
<th class="py-3 px-3 min-w-[180px]">现场空间位置</th>
<th class="py-3 px-3 min-w-[150px]">测绘回路 / 规格</th>
<th class="py-3 px-3 min-w-[130px]">责任业务员</th>
<th class="py-3 px-3 min-w-[170px]">调度指派电工</th>
<th class="py-3 px-3 min-w-[150px]">期望时效 / SLA</th>
<th class="py-3 px-3 min-w-[100px]">预估劳务费</th>
<th class="py-3 px-3 min-w-[110px] text-center">工单状态</th>
<th class="py-3 px-4 min-w-[160px] text-center sticky right-0 bg-slate-50">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 text-slate-700">
<!-- Row 1: 待指派 (WH-SURV-2026-0302) 特急 -->
<tr class="hover:bg-blue-50/40 transition-colors bg-amber-50/20">
<td class="py-3.5 px-4 text-center">
<input class="rounded border-slate-300 text-primary focus:ring-0 w-4 h-4 cursor-pointer" type="checkbox"/>
</td>
<td class="py-3.5 px-3">
<div class="flex items-center gap-1.5">
<span class="font-mono font-bold text-primary hover:underline cursor-pointer">WH-SURV-2026-0302</span>
</div>
<div class="mt-1">
<span class="text-[10.5px] px-1.5 py-0.5 rounded bg-red-100 text-red-700 font-semibold border border-red-200">特急派发</span>
</div>
</td>
<td class="py-3.5 px-3">
<div class="font-semibold text-slate-900 leading-snug line-clamp-1">武汉软件工程职业学院 · 实训楼智能配电改造</div>
<div class="text-[11.5px] text-slate-400 mt-0.5">签约客户:武汉软件工程职业学院后勤处</div>
</td>
<td class="py-3.5 px-3">
<div class="flex items-start gap-1 text-[12.5px] text-slate-700">
<span class="material-symbols-outlined text-[15px] text-slate-400 shrink-0 mt-0.5">pin_drop</span>
<span>实训综合楼 1F低压室 及 2~3F强电竖井</span>
</div>
</td>
<td class="py-3.5 px-3">
<div class="font-mono font-semibold text-slate-800">40 回路</div>
<div class="text-[11.5px] text-slate-500">380V / 400A MCCB</div>
</td>
<td class="py-3.5 px-3">
<div class="font-medium text-slate-800">程经理</div>
<div class="font-mono text-[11.5px] text-slate-400">138****9201</div>
</td>
<td class="py-3.5 px-3">
<div class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-amber-50 border border-amber-200 text-amber-800">
<span class="material-symbols-outlined text-[15px] text-amber-600">person_search</span>
<span class="font-medium text-[12px]">待指派电工</span>
<span class="text-[11px] text-primary underline cursor-pointer ml-0.5">指派</span>
</div>
</td>
<td class="py-3.5 px-3">
<div class="text-amber-700 font-medium text-[12.5px] flex items-center gap-1">
<span class="material-symbols-outlined text-[15px]">timer</span>
<span>接单后 45 分钟内</span>
</div>
<div class="text-[11px] text-red-600 font-mono font-medium mt-0.5">剩余 1.2 小时超时</div>
</td>
<td class="py-3.5 px-3 font-mono font-bold text-slate-900">
¥ 550.00
</td>
<td class="py-3.5 px-3 text-center">
<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[12px] font-medium bg-amber-50 text-amber-700 border border-amber-200">
<span class="w-1.5 h-1.5 rounded-full bg-amber-500"></span>待指派
</span>
</td>
<td class="py-3.5 px-4 text-center sticky right-0 bg-white/95">
<div class="flex items-center justify-center gap-2">
<button class="text-primary hover:text-blue-700 font-semibold text-[12.5px]">指派电工</button>
<span class="text-slate-200">|</span>
<button class="text-slate-600 hover:text-slate-900 text-[12.5px]">编辑</button>
<span class="text-slate-200">|</span>
<button class="text-slate-400 hover:text-red-600 text-[12.5px]">取消</button>
</div>
</td>
</tr>
<!-- Row 2: 现场勘察中 (WH-SURV-2026-0303) 加急 -->
<tr class="hover:bg-blue-50/40 transition-colors">
<td class="py-3.5 px-4 text-center">
<input class="rounded border-slate-300 text-primary focus:ring-0 w-4 h-4 cursor-pointer" type="checkbox"/>
</td>
<td class="py-3.5 px-3">
<div class="flex items-center gap-1.5">
<span class="font-mono font-bold text-primary hover:underline cursor-pointer">WH-SURV-2026-0303</span>
</div>
<div class="mt-1">
<span class="text-[10.5px] px-1.5 py-0.5 rounded bg-amber-100 text-amber-800 font-semibold border border-amber-200">加急工单</span>
</div>
</td>
<td class="py-3.5 px-3">
<div class="font-semibold text-slate-900 leading-snug line-clamp-1">华中师范大学南湖校区 · 二期环网柜勘察测绘</div>
<div class="text-[11.5px] text-slate-400 mt-0.5">签约客户:湖北科教基建投资有限公司</div>
</td>
<td class="py-3.5 px-3">
<div class="flex items-start gap-1 text-[12.5px] text-slate-700">
<span class="material-symbols-outlined text-[15px] text-slate-400 shrink-0 mt-0.5">pin_drop</span>
<span>南湖校区 10kV 开闭所及 2# 箱变</span>
</div>
</td>
<td class="py-3.5 px-3">
<div class="font-mono font-semibold text-slate-800">24 回路</div>
<div class="text-[11.5px] text-slate-500">10kV 环网柜 / 630kVA</div>
</td>
<td class="py-3.5 px-3">
<div class="font-medium text-slate-800">万工</div>
<div class="font-mono text-[11.5px] text-slate-400">135****4829</div>
</td>
<td class="py-3.5 px-3">
<div class="flex items-center gap-2">
<div class="w-6 h-6 rounded-full bg-primary/10 text-primary flex items-center justify-center font-bold text-[11px]">赵</div>
<div>
<div class="font-semibold text-slate-800 text-[12.5px]">赵志强</div>
<div class="text-[11px] text-slate-400">距1.8km · <span class="text-amber-600 font-semibold">★4.98</span></div>
</div>
</div>
</td>
<td class="py-3.5 px-3">
<div class="text-slate-800 font-medium text-[12.5px]">履约测绘中</div>
<div class="text-[11.5px] text-emerald-600 font-mono">已完成 18/24 回路</div>
</td>
<td class="py-3.5 px-3 font-mono font-bold text-slate-900">
¥ 680.00
</td>
<td class="py-3.5 px-3 text-center">
<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[12px] font-medium bg-blue-50 text-blue-700 border border-blue-200">
<span class="w-1.5 h-1.5 rounded-full bg-blue-500 animate-pulse"></span>现场勘察中
</span>
</td>
<td class="py-3.5 px-4 text-center sticky right-0 bg-white/95">
<div class="flex items-center justify-center gap-2">
<button class="text-primary hover:text-blue-700 font-semibold text-[12.5px]">进度跟踪</button>
<span class="text-slate-200">|</span>
<button class="text-slate-600 hover:text-slate-900 text-[12.5px]">改派</button>
<span class="text-slate-200">|</span>
<button class="text-slate-600 hover:text-amber-600 text-[12.5px]">催办</button>
</div>
</td>
</tr>
<!-- Row 3: 待技术复核 (WH-SURV-2026-0304) 常规 -->
<tr class="hover:bg-blue-50/40 transition-colors">
<td class="py-3.5 px-4 text-center">
<input class="rounded border-slate-300 text-primary focus:ring-0 w-4 h-4 cursor-pointer" type="checkbox"/>
</td>
<td class="py-3.5 px-3">
<div class="flex items-center gap-1.5">
<span class="font-mono font-bold text-primary hover:underline cursor-pointer">WH-SURV-2026-0304</span>
</div>
<div class="mt-1">
<span class="text-[10.5px] px-1.5 py-0.5 rounded bg-slate-100 text-slate-600 font-medium border border-slate-200">常规工单</span>
</div>
</td>
<td class="py-3.5 px-3">
<div class="font-semibold text-slate-900 leading-snug line-clamp-1">光谷微电子产业园 · 3号配电站扩容测绘</div>
<div class="text-[11.5px] text-slate-400 mt-0.5">签约客户:光谷微电子联合实验室</div>
</td>
<td class="py-3.5 px-3">
<div class="flex items-start gap-1 text-[12.5px] text-slate-700">
<span class="material-symbols-outlined text-[15px] text-slate-400 shrink-0 mt-0.5">pin_drop</span>
<span>高新大道999号 研发楼B座负一层配电间</span>
</div>
</td>
<td class="py-3.5 px-3">
<div class="font-mono font-semibold text-slate-800">56 回路</div>
<div class="text-[11.5px] text-slate-500">双电源 ATS / 630A</div>
</td>
<td class="py-3.5 px-3">
<div class="font-medium text-slate-800">刘经理</div>
<div class="font-mono text-[11.5px] text-slate-400">139****1124</div>
</td>
<td class="py-3.5 px-3">
<div class="flex items-center gap-2">
<div class="w-6 h-6 rounded-full bg-purple-100 text-purple-700 flex items-center justify-center font-bold text-[11px]">孙</div>
<div>
<div class="font-semibold text-slate-800 text-[12.5px]">孙明远</div>
<div class="text-[11px] text-slate-400">距4.2km · <span class="text-amber-600 font-semibold">★4.92</span></div>
</div>
</div>
</td>
<td class="py-3.5 px-3">
<div class="text-slate-800 font-medium text-[12.5px]">已完成回传</div>
<div class="text-[11.5px] text-purple-600 font-mono">16张图纸待复核</div>
</td>
<td class="py-3.5 px-3 font-mono font-bold text-slate-900">
¥ 820.00
</td>
<td class="py-3.5 px-3 text-center">
<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[12px] font-medium bg-purple-50 text-purple-700 border border-purple-200">
<span class="w-1.5 h-1.5 rounded-full bg-purple-500"></span>待技术复核
</span>
</td>
<td class="py-3.5 px-4 text-center sticky right-0 bg-white/95">
<div class="flex items-center justify-center gap-2">
<button class="text-purple-600 hover:text-purple-800 font-bold text-[12.5px]">审核测绘成果</button>
<span class="text-slate-200">|</span>
<button class="text-slate-600 hover:text-slate-900 text-[12.5px]">取证照片</button>
</div>
</td>
</tr>
<!-- Row 4: 待指派 (WH-SURV-2026-0305) 常规 -->
<tr class="hover:bg-blue-50/40 transition-colors">
<td class="py-3.5 px-4 text-center">
<input class="rounded border-slate-300 text-primary focus:ring-0 w-4 h-4 cursor-pointer" type="checkbox"/>
</td>
<td class="py-3.5 px-3">
<div class="flex items-center gap-1.5">
<span class="font-mono font-bold text-primary hover:underline cursor-pointer">WH-SURV-2026-0305</span>
</div>
<div class="mt-1">
<span class="text-[10.5px] px-1.5 py-0.5 rounded bg-slate-100 text-slate-600 font-medium border border-slate-200">常规工单</span>
</div>
</td>
<td class="py-3.5 px-3">
<div class="font-semibold text-slate-900 leading-snug line-clamp-1">汉口新华医院 · 急诊楼供电后备发电机进线勘察</div>
<div class="text-[11.5px] text-slate-400 mt-0.5">签约客户:汉口新华医疗投资集团</div>
</td>
<td class="py-3.5 px-3">
<div class="flex items-start gap-1 text-[12.5px] text-slate-700">
<span class="material-symbols-outlined text-[15px] text-slate-400 shrink-0 mt-0.5">pin_drop</span>
<span>急诊综合大楼 B2柴发机房及母线桥架</span>
</div>
</td>
<td class="py-3.5 px-3">
<div class="font-mono font-semibold text-slate-800">18 回路</div>
<div class="text-[11.5px] text-slate-500">400V 应急发电机母排</div>
</td>
<td class="py-3.5 px-3">
<div class="font-medium text-slate-800">黄主管</div>
<div class="font-mono text-[11.5px] text-slate-400">136****9012</div>
</td>
<td class="py-3.5 px-3">
<div class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-slate-100 border border-slate-200 text-slate-600">
<span class="material-symbols-outlined text-[15px] text-slate-400">schedule</span>
<span class="font-medium text-[12px]">待指定电工</span>
<span class="text-[11px] text-primary underline cursor-pointer ml-0.5">智能派单</span>
</div>
</td>
<td class="py-3.5 px-3">
<div class="text-slate-800 font-medium text-[12.5px]">24小时内到场</div>
<div class="text-[11px] text-slate-500 font-mono">剩余 19.5 小时</div>
</td>
<td class="py-3.5 px-3 font-mono font-bold text-slate-900">
¥ 420.00
</td>
<td class="py-3.5 px-3 text-center">
<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[12px] font-medium bg-amber-50 text-amber-700 border border-amber-200">
<span class="w-1.5 h-1.5 rounded-full bg-amber-500"></span>待指派
</span>
</td>
<td class="py-3.5 px-4 text-center sticky right-0 bg-white/95">
<div class="flex items-center justify-center gap-2">
<button class="text-primary hover:text-blue-700 font-semibold text-[12.5px]">指派电工</button>
<span class="text-slate-200">|</span>
<button class="text-slate-600 hover:text-slate-900 text-[12.5px]">编辑</button>
<span class="text-slate-200">|</span>
<button class="text-slate-400 hover:text-red-600 text-[12.5px]">取消</button>
</div>
</td>
</tr>
<!-- Row 5: 勘察完成/已归档 (WH-SURV-2026-0288) 常规 -->
<tr class="hover:bg-blue-50/40 transition-colors">
<td class="py-3.5 px-4 text-center">
<input class="rounded border-slate-300 text-primary focus:ring-0 w-4 h-4 cursor-pointer" type="checkbox"/>
</td>
<td class="py-3.5 px-3">
<div class="flex items-center gap-1.5">
<span class="font-mono font-bold text-primary hover:underline cursor-pointer">WH-SURV-2026-0288</span>
</div>
<div class="mt-1">
<span class="text-[10.5px] px-1.5 py-0.5 rounded bg-slate-100 text-slate-600 font-medium border border-slate-200">常规工单</span>
</div>
</td>
<td class="py-3.5 px-3">
<div class="font-semibold text-slate-900 leading-snug line-clamp-1">东西湖现代冷链物流港 · 02号智能仓冷风机配电</div>
<div class="text-[11.5px] text-slate-400 mt-0.5">签约客户:湖北现代冷链物流发展有限公司</div>
</td>
<td class="py-3.5 px-3">
<div class="flex items-start gap-1 text-[12.5px] text-slate-700">
<span class="material-symbols-outlined text-[15px] text-slate-400 shrink-0 mt-0.5">pin_drop</span>
<span>冷链1号库东侧冷机房控制柜</span>
</div>
</td>
<td class="py-3.5 px-3">
<div class="font-mono font-semibold text-slate-800">32 回路</div>
<div class="text-[11.5px] text-slate-500">变频动力柜 / 250A</div>
</td>
<td class="py-3.5 px-3">
<div class="font-medium text-slate-800">王工</div>
<div class="font-mono text-[11.5px] text-slate-400">137****6632</div>
</td>
<td class="py-3.5 px-3">
<div class="flex items-center gap-2">
<div class="w-6 h-6 rounded-full bg-emerald-100 text-emerald-800 flex items-center justify-center font-bold text-[11px]">王</div>
<div>
<div class="font-semibold text-slate-800 text-[12.5px]">王建国</div>
<div class="text-[11px] text-slate-400">履约完成 · <span class="text-amber-600 font-semibold">★4.95</span></div>
</div>
</div>
</td>
<td class="py-3.5 px-3">
<div class="text-emerald-700 font-medium text-[12.5px] flex items-center gap-1">
<span class="material-symbols-outlined text-[15px]">verified</span>
<span>SLA 100% 达标</span>
</div>
<div class="text-[11px] text-slate-400 font-mono">已归档至项目库</div>
</td>
<td class="py-3.5 px-3 font-mono font-bold text-slate-900">
¥ 620.00
</td>
<td class="py-3.5 px-3 text-center">
<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[12px] font-medium bg-emerald-50 text-emerald-700 border border-emerald-200">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>已归档
</span>
</td>
<td class="py-3.5 px-4 text-center sticky right-0 bg-white/95">
<div class="flex items-center justify-center gap-2">
<button class="text-primary hover:text-blue-700 font-medium text-[12.5px]">生成方案</button>
<span class="text-slate-200">|</span>
<button class="text-slate-600 hover:text-slate-900 text-[12.5px]">查看报告</button>
<span class="text-slate-200">|</span>
<button class="text-slate-400 hover:text-slate-600 text-[12.5px]">详情</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Table Footer & Ant Design Pagination -->
<div class="px-5 py-4 border-t border-border-rule bg-surface flex flex-col md:flex-row items-center justify-between gap-4">
<!-- Batch Actions -->
<div class="flex items-center gap-3">
<span class="text-[12.5px] text-slate-500">
已选中 <strong class="text-primary font-mono font-bold">0</strong> 项
</span>
<button class="px-3 py-1.5 rounded-lg border border-slate-200 hover:bg-slate-50 text-slate-600 text-[12px] font-medium transition-colors disabled:opacity-50" disabled="">
批量指定电工
</button>
<button class="px-3 py-1.5 rounded-lg border border-slate-200 hover:bg-slate-50 text-slate-600 text-[12px] font-medium transition-colors disabled:opacity-50" disabled="">
批量打印现场作业票
</button>
</div>
<!-- Pagination Controls -->
<div class="flex items-center gap-2 text-[13px]">
<span class="text-slate-500 text-[12.5px]">共 170 条</span>
<!-- Page Size Selector -->
<select class="py-1 px-2.5 border border-slate-200 rounded-lg text-[12px] text-slate-700 bg-slate-50 focus:outline-none focus:border-primary">
<option value="10">10 条/页</option>
<option value="20">20 条/页</option>
<option value="50">50 条/页</option>
</select>
<!-- Page Buttons -->
<div class="flex items-center gap-1 ml-2">
<button class="w-8 h-8 rounded-lg border border-slate-200 flex items-center justify-center text-slate-400 hover:bg-slate-50 disabled:opacity-40" disabled="">
<span class="material-symbols-outlined text-[16px]">chevron_left</span>
</button>
<button class="w-8 h-8 rounded-lg bg-primary text-white font-bold flex items-center justify-center text-[12.5px] shadow-sm">
1
</button>
<button class="w-8 h-8 rounded-lg border border-slate-200 text-slate-600 hover:bg-slate-50 flex items-center justify-center text-[12.5px]">
2
</button>
<button class="w-8 h-8 rounded-lg border border-slate-200 text-slate-600 hover:bg-slate-50 flex items-center justify-center text-[12.5px]">
3
</button>
<span class="text-slate-400 px-1 font-mono text-[12px]">...</span>
<button class="w-8 h-8 rounded-lg border border-slate-200 text-slate-600 hover:bg-slate-50 flex items-center justify-center text-[12.5px]">
17
</button>
<button class="w-8 h-8 rounded-lg border border-slate-200 flex items-center justify-center text-slate-600 hover:bg-slate-50">
<span class="material-symbols-outlined text-[16px]">chevron_right</span>
</button>
</div>
<!-- Quick Jumper -->
<div class="flex items-center gap-1.5 ml-2 text-slate-500 text-[12.5px]">
<span>跳至</span>
<input class="w-12 py-1 px-1 text-center font-mono border border-slate-200 rounded-lg text-[12px] focus:outline-none focus:border-primary" type="text" value="1"/>
<span>页</span>
</div>
</div>
</div>
</div>
</main>
</div>
</body></html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 495 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,108 @@
<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8"><meta content="width=device-width, initial-scale=1.0" name="viewport"><link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=hankenGrotesk:wght@100..900&amp;display=swap" rel="stylesheet"><style>@layer base{html,body{margin:0;padding:0;}body{overscroll-behavior:none;}main>:first-child{margin-top:0!important;}main>:last-child{margin-bottom:0!important;}}::-webkit-scrollbar{display:none;}</style><script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script><script id="tailwind-config">tailwind.config = { darkMode: "class", theme: { extend: { "colors": { "on-error": "#ffffff", "border-rule": "#dbe4ee", "secondary-fixed": "#eaddff", "on-surface": "#0d1c2c", "tertiary": "#00685f", "secondary": "#712ae2", "on-primary-fixed-variant": "#0040a1", "on-tertiary": "#ffffff", "surface-container-low": "#eef4ff", "secondary-fixed-dim": "#d2bbff", "primary-container": "#136bfb", "primary-fixed": "#dae2ff", "on-secondary-fixed-variant": "#5a00c6", "on-tertiary-fixed-variant": "#005049", "tertiary-container": "#008378", "surface": "#ffffff", "text-muted": "#5b6b7e", "on-background": "#0d1c2c", "primary": "#0054cd", "outline-variant": "#c2c6d8", "surface-container-highest": "#d5e4fa", "on-primary-container": "#fefcff", "background": "#f8f9ff", "on-secondary-fixed": "#25005a", "surface-container-high": "#dbe9ff", "surface-container-lowest": "#ffffff", "bg-page": "#f4f7fb", "surface-container": "#e5efff", "on-primary": "#ffffff", "tertiary-fixed": "#89f5e7", "status-warn": "#e0852b", "tertiary-fixed-dim": "#6bd8cb", "outline": "#727787", "accent-subtle": "rgba(29,111,255,0.08)", "on-primary-fixed": "#001847", "inverse-primary": "#b2c5ff", "inverse-surface": "#233142", "on-secondary-container": "#fffbff", "surface-bright": "#f8f9ff", "on-tertiary-container": "#f4fffc", "on-tertiary-fixed": "#00201d", "primary-fixed-dim": "#b2c5ff", "inverse-on-surface": "#e9f1ff", "status-error": "#dc2626", "surface-tint": "#0056d2", "on-secondary": "#ffffff", "error": "#ba1a1a", "on-error-container": "#93000a", "surface-dim": "#ccdbf1", "surface-variant": "#d5e4fa", "secondary-container": "#8a4cfc", "error-container": "#ffdad6", "on-surface-variant": "#424655" }, "borderRadius": { "DEFAULT": "0.25rem", "lg": "0.5rem", "xl": "0.75rem", "full": "9999px" }, "spacing": { "lg": "24px", "base": "4px", "xs": "8px", "container-max": "1200px", "sm": "12px", "xl": "40px", "md": "16px", "gutter": "16px" }, "fontFamily": { "body-md": ["hankenGrotesk"], "caption-sm": ["hankenGrotesk"], "title-sm": ["hankenGrotesk"], "data-mono": ["jetbrainsMono", "monospace"], "label-bold": ["hankenGrotesk"], "headline-md": ["hankenGrotesk"], "headline-md-mobile": ["hankenGrotesk"], "display-lg": ["hankenGrotesk"] }, "fontSize": { "body-md": ["15px", { "lineHeight": "24px", "fontWeight": "400" }], "caption-sm": ["12px", { "lineHeight": "14px", "fontWeight": "700" }], "title-sm": ["17px", { "lineHeight": "24px", "fontWeight": "700" }], "data-mono": ["12px", { "lineHeight": "16px", "fontWeight": "500" }], "label-bold": ["13px", { "lineHeight": "16px", "fontWeight": "700" }], "headline-md": ["23px", { "lineHeight": "28px", "fontWeight": "700" }], "headline-md-mobile": ["20px", { "lineHeight": "26px", "fontWeight": "700" }], "display-lg": ["32px", { "lineHeight": "40px", "fontWeight": "700" }] } } } };</script></head><body class="bg-bg-page font-body-md text-body-md text-on-surface antialiased flex min-h-screen overflow-x-hidden"><!-- Minimal Top Global Navbar -->
<aside class="w-64 bg-[#0f172a] text-slate-300 flex-shrink-0 flex flex-col justify-between min-h-screen z-30 shadow-xl border-r border-slate-800">
<div>
<!-- Brand Logo -->
<div class="h-16 flex items-center px-lg border-b border-slate-800/80 gap-sm bg-[#0b1120]">
<img alt="XT-POWER Logo" class="h-8 w-auto object-contain" src="https://lh3.googleusercontent.com/aida/AEtjO1WEv-XpuvR6vI25Vg_b5kVa78sajEgqBSsbRjAXtXERG5rnQ0hjJNA7KEvKDG35aTm9975DUcxd1r__Mn0LJ4roIr35JhgLX6FmEDm6uma7hvkH3ptsQUHddesMFTsq5j4A62SGpFYFltxzOmw2H5UHfickwkRfcVPrXmm6JgC_LKoALuxUNakbkFkEh7HiTo1D8nhRvkWnILfZKZHl0yIujUYdbhhBgTw36BARKf_hYtvKLzYjV2tVjek">
<div class="flex flex-col">
<span class="font-title-sm text-white font-bold tracking-tight text-[15px] leading-tight">鑫潼智电</span>
<span class="font-data-mono text-[10px] text-slate-400 leading-tight tracking-wider">XT-POWER OS <span class="text-primary-fixed">v3.8</span></span>
</div>
</div>
<!-- Navigation Links -->
<nav class="p-sm space-y-xs text-[13px]">
<a href="#" class="flex items-center gap-sm px-sm py-xs rounded-lg text-slate-400 hover:text-white hover:bg-slate-800/60 transition-colors">
<span class="material-symbols-outlined text-[18px]">dashboard</span>
<span class="">运营工作台</span>
</a>
<a href="#" class="flex items-center gap-sm px-sm py-xs rounded-lg text-slate-400 hover:text-white hover:bg-slate-800/60 transition-colors">
<span class="material-symbols-outlined text-[18px]">contacts</span>
<span class="">客户管理</span>
</a>
<!-- Active Menu Group: 项目管理 -->
<div class="space-y-xs pt-xs">
<div class="flex items-center justify-between px-sm py-xs rounded-lg bg-slate-800/80 text-white font-medium">
<div class="flex items-center gap-sm">
<span class="material-symbols-outlined text-[18px] text-primary-fixed">engineering</span>
<span class="">项目管理</span>
</div>
<span class="material-symbols-outlined text-[16px] text-slate-400">expand_more</span>
</div>
<div class="pl-8 pr-xs space-y-xs border-l border-slate-800 ml-4 py-xs">
<a href="#" class="block px-xs py-base rounded text-slate-400 hover:text-white hover:bg-slate-800/40 text-[12px] transition-colors">全周期工程看板</a>
<a href="#" class="flex items-center justify-between px-xs py-base rounded bg-primary/20 text-primary-fixed font-bold text-[12px] border-l-2 border-primary pl-2">现场勘察派单 <span class="w-1.5 h-1.5 rounded-full bg-primary-fixed"></span></a>
<a href="#" class="block px-xs py-base rounded text-slate-400 hover:text-white hover:bg-slate-800/40 text-[12px] transition-colors">项目合同管理</a>
<a href="#" class="block px-xs py-base rounded text-slate-400 hover:text-white hover:bg-slate-800/40 text-[12px] transition-colors">工程安装派单</a>
<a href="#" class="block px-xs py-base rounded text-slate-400 hover:text-white hover:bg-slate-800/40 text-[12px] transition-colors">工程竣工验收</a>
</div>
</div>
<a href="#" class="flex items-center gap-sm px-sm py-xs rounded-lg text-slate-400 hover:text-white hover:bg-slate-800/60 transition-colors">
<span class="material-symbols-outlined text-[18px]">assignment</span>
<span class="">运营派单</span>
</a>
<a href="#" class="flex items-center gap-sm px-sm py-xs rounded-lg text-slate-400 hover:text-white hover:bg-slate-800/60 transition-colors">
<span class="material-symbols-outlined text-[18px]">grid_view</span>
<span class="">设备监控</span>
</a>
<a href="#" class="flex items-center gap-sm px-sm py-xs rounded-lg text-slate-400 hover:text-white hover:bg-slate-800/60 transition-colors">
<span class="material-symbols-outlined text-[18px]">bar_chart</span>
<span class="">统计报表</span>
</a>
<a href="#" class="flex items-center gap-sm px-sm py-xs rounded-lg text-slate-400 hover:text-white hover:bg-slate-800/60 transition-colors">
<span class="material-symbols-outlined text-[18px]">settings</span>
<span class="">系统管理</span>
</a>
</nav>
</div>
<!-- Sidebar Footer / Cluster Status -->
<div class="p-md border-t border-slate-800/80 bg-[#0b1120]/60">
<div class="flex items-center justify-between text-[11px] text-slate-400">
<div class="flex items-center gap-base">
<span class="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span>
<span class="">华东区 · 枢纽调度</span>
</div>
<span class="font-data-mono text-emerald-400 font-bold">正常</span>
</div>
</div>
</aside><main class="flex-1 min-w-0 bg-bg-page flex flex-col min-h-screen"><header class="h-16 w-full bg-surface border-b border-border-rule sticky top-0 z-40 px-lg flex items-center justify-between shadow-[0_1px_4px_rgba(0,0,0,0.03)]">
<!-- Breadcrumbs & Quick Search -->
<div class="flex items-center gap-md">
<nav class="flex items-center gap-xs text-caption-sm">
<a class="text-text-muted hover:text-primary transition-colors" href="#">项目管理</a>
<span class="material-symbols-outlined text-[14px] text-text-muted">chevron_right</span>
<span class="text-text-muted">现场勘察派单</span>
<span class="material-symbols-outlined text-[14px] text-text-muted">chevron_right</span>
<span class="font-data-mono font-bold text-primary px-sm py-base rounded-full bg-accent-subtle text-[11px]">WH-SURV-2026-0302</span>
</nav>
<div class="relative hidden md:block">
<input type="text" placeholder="全局搜索电表/断路器/派单编号..." class="w-64 pl-8 pr-md py-base bg-surface-container-low text-on-surface rounded-full text-caption-sm border border-border-rule focus:outline-none focus:ring-1 focus:ring-primary">
<span class="material-symbols-outlined absolute left-2.5 top-1/2 -translate-y-1/2 text-[16px] text-text-muted pointer-events-none">search</span>
</div>
</div>
<!-- Status badge, Notification, and User profile -->
<div class="flex items-center gap-md">
<div class="hidden sm:flex items-center gap-base bg-tertiary/10 text-tertiary px-sm py-base rounded-full text-caption-sm">
<span class="w-2 h-2 rounded-full bg-tertiary animate-pulse"></span>
<span class="font-medium">ICC 客资直通已联通</span>
</div>
<button class="relative p-base rounded-full text-text-muted hover:text-on-surface hover:bg-surface-container-low transition-colors" title="通知" type="button">
<span class="material-symbols-outlined text-[20px]">notifications</span>
<span class="absolute top-1 right-1 w-2 h-2 rounded-full bg-error"></span>
</button>
<button class="p-base rounded-full text-text-muted hover:text-on-surface hover:bg-surface-container-low transition-colors" title="帮助与SOP手册" type="button">
<span class="material-symbols-outlined text-[20px]">help_outline</span>
</button>
<div class="h-5 w-[1px] bg-border-rule"></div>
<div class="flex items-center gap-sm">
<div class="w-8 h-8 rounded-full bg-primary-container text-on-primary flex items-center justify-center font-bold text-caption-sm shadow-sm">李</div>
<div class="flex flex-col text-left">
<span class="font-label-bold text-on-surface text-caption-sm leading-tight">李工 <span class="text-[11px] font-normal text-text-muted">主控工程师</span></span>
<span class="font-data-mono text-[10px] text-text-muted leading-tight">调度中心 · 席位04</span>
</div>
</div>
</div>
</header><div class="space-y-md max-w-5xl mx-auto w-full px-lg py-md"><!-- Wizard Header & Step Indicator --><div class="bg-surface rounded-xl p-md shadow-sm border border-border-rule/60"><div class="flex flex-col sm:flex-row sm:items-center justify-between gap-xs pb-sm border-b border-border-rule/60 mb-sm"><div><h1 class="font-headline-md text-headline-md text-on-surface tracking-tight">现场勘察派单向导</h1><p class="font-body-md text-caption-sm text-text-muted">标准化派发配电房现场测绘任务,向共享电工推单并回传拓扑参数</p></div><span class="px-sm py-base rounded-full bg-surface-container-low text-primary font-data-mono text-caption-sm self-start sm:self-auto font-medium">STEP 01 / 03</span></div><!-- 3-Step Horizontal Progress Bar --><div class="grid grid-cols-1 md:grid-cols-3 gap-xs"><div class="flex items-center gap-sm p-sm rounded-lg bg-primary/5 border border-primary/20"><div class="w-8 h-8 rounded-full bg-primary text-on-primary flex items-center justify-center font-bold text-caption-sm shrink-0 shadow-sm">1</div><div class="min-w-0"><div class="font-label-bold text-caption-sm text-primary">基本信息与现场环境</div><div class="text-[11px] text-text-muted truncate">目标客户、区域与回路</div></div></div><div class="flex items-center gap-sm p-sm rounded-lg bg-surface-container-lowest border border-border-rule/50 opacity-75"><div class="w-8 h-8 rounded-full bg-surface-container text-on-surface-variant flex items-center justify-center font-bold text-caption-sm shrink-0">2</div><div class="min-w-0"><div class="font-label-bold text-caption-sm text-on-surface">技术要点与回执标准</div><div class="text-[11px] text-text-muted truncate">标准SOP模版、拍照指标</div></div></div><div class="flex items-center gap-sm p-sm rounded-lg bg-surface-container-lowest border border-border-rule/50 opacity-75"><div class="w-8 h-8 rounded-full bg-surface-container text-on-surface-variant flex items-center justify-center font-bold text-caption-sm shrink-0">3</div><div class="min-w-0"><div class="font-label-bold text-caption-sm text-on-surface">指派电工与服务报酬</div><div class="text-[11px] text-text-muted truncate">匹配电工、LBS距离与费率</div></div></div></div></div><!-- Active Wizard Step Container --><div class="space-y-md"><!-- STEP 1: Basic Information & Environment --><div class="bg-surface rounded-xl p-lg shadow-sm border border-border-rule/60 space-y-md"><div class="flex items-center justify-between pb-xs border-b border-border-rule/50"><div class="flex items-center gap-xs"><span class="material-symbols-outlined text-primary text-[22px]">corporate_fare</span><h2 class="font-title-sm text-title-sm text-on-surface font-bold">第 1 步:勘察基本信息与现场环境</h2></div><span class="font-caption-sm text-caption-sm text-text-muted">已自动关联 ICC 客资档案</span></div><div class="grid grid-cols-1 md:grid-cols-2 gap-md"><div><label class="block font-label-bold text-caption-sm text-on-surface mb-xs">关联客户及工程单位 <span class="text-error">*</span></label><div class="relative"><select class="w-full px-md py-xs bg-surface-container-low text-on-surface rounded-lg text-body-md focus:outline-none focus:ring-1 focus:ring-primary border border-border-rule appearance-none"><option selected="">武汉软件工程职业学院(实训中心)</option><option>华中师范大学南湖校区二期</option><option>光谷微电子产业园动力站</option></select><span class="material-symbols-outlined absolute right-sm top-1/2 -translate-y-1/2 text-text-muted pointer-events-none">arrow_drop_down</span></div></div><div><label class="block font-label-bold text-caption-sm text-on-surface mb-xs">勘察任务标题 <span class="text-error">*</span></label><input class="w-full px-md py-xs bg-surface-container-low text-on-surface rounded-lg text-body-md focus:outline-none focus:ring-1 focus:ring-primary border border-border-rule font-medium" type="text" value="实训楼智能配电改造一期工程现场测绘"></div></div><div class="grid grid-cols-1 md:grid-cols-3 gap-md"><div><label class="block font-label-bold text-caption-sm text-on-surface mb-xs">具体勘察物理区域 <span class="text-error">*</span></label><input class="w-full px-md py-xs bg-surface-container-low text-on-surface rounded-lg text-body-md focus:outline-none focus:ring-1 focus:ring-primary border border-border-rule" type="text" value="实训主楼 1F低压室 / 2~3F强电井"></div><div><label class="block font-label-bold text-caption-sm text-on-surface mb-xs">主进线规约与断路器容量</label><input class="w-full px-md py-xs bg-surface-container-low text-on-surface rounded-lg text-body-md focus:outline-none focus:ring-1 focus:ring-primary border border-border-rule font-data-mono" type="text" value="380V / 400A MCCB (SCB10-630kVA)"></div><div><label class="block font-label-bold text-caption-sm text-on-surface mb-xs">预估分支测绘回路Line truncated
</body></html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB