前端服务共享

This commit is contained in:
2025-12-11 14:21:36 +08:00
parent fa3dbe0496
commit 5ee9770747
46 changed files with 3732 additions and 1782 deletions

View File

@@ -31,7 +31,7 @@ log() {
# 数据库连接信息(可通过环境变量覆盖)
DB_HOST=${POSTGRES_HOST:-"localhost"}
DB_PORT=${POSTGRES_PORT:-"5432"}
DB_NAME=${POSTGRES_DB:-"urban-lifeline"}
DB_NAME=${POSTGRES_DB:-"urban_lifeline"}
DB_USER=${POSTGRES_USER:-"postgres"}
DB_PASSWORD=${POSTGRES_PASSWORD:-"postgres"}

View File

@@ -12,12 +12,13 @@ CREATE DATABASE urban_lifeline
LC_CTYPE 'zh_CN.UTF-8';
-- 连接到新创建的数据库
\c urban-lifeline;
\c urban_lifeline;
-- -- 创建扩展(如果需要)
CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- UUID 支持
CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- 文本搜索支持
CREATE EXTENSION IF NOT EXISTS "btree_gist"; -- GiST 索引支持
CREATE EXTENSION IF NOT EXISTS "vector"; -- 向量
-- 设置搜索路径(可选,但建议设置)
-- ALTER DATABASE urban-lifeline SET search_path TO sys, public;

View File

@@ -158,7 +158,10 @@ CREATE TABLE sys.tb_sys_view (
url VARCHAR(255) DEFAULT NULL, -- 视图URL
component VARCHAR(255) DEFAULT NULL, -- 视图组件
icon VARCHAR(100) DEFAULT NULL, -- 视图图标
type INTEGER DEFAULT 0, -- 视图类型
type INTEGER DEFAULT 0, -- 视图类型0=目录 1=菜单页面 2=按钮
view_type VARCHAR(20) DEFAULT 'route', -- 页面类型route=路由页面 iframe=嵌入页面
iframe_url VARCHAR(500) DEFAULT NULL, -- iframe URL仅当view_type=iframe时有效
service VARCHAR(20) DEFAULT 'platform', -- 所属服务platform=平台应用 bidding=招标应用 workcase=客服应用
layout VARCHAR(100) DEFAULT NULL, -- 布局组件路径名称
order_num INTEGER DEFAULT 0, -- 视图排序号
dept_path VARCHAR(255) DEFAULT NULL, -- 部门全路径
@@ -172,7 +175,11 @@ CREATE TABLE sys.tb_sys_view (
PRIMARY KEY (view_id),
UNIQUE (optsn)
);
COMMENT ON TABLE sys.tb_sys_view IS '视图表';
-- 创建索引
CREATE INDEX idx_sys_view_parent ON sys.tb_sys_view USING btree (parent_id) WHERE deleted = false;
CREATE INDEX idx_sys_view_service ON sys.tb_sys_view USING btree (service) WHERE deleted = false;
CREATE INDEX idx_sys_view_type ON sys.tb_sys_view USING btree (view_type) WHERE deleted = false;
COMMENT ON TABLE sys.tb_sys_view IS '视图表(菜单表)';
COMMENT ON COLUMN sys.tb_sys_view.optsn IS '流水号';
COMMENT ON COLUMN sys.tb_sys_view.view_id IS '视图ID';
COMMENT ON COLUMN sys.tb_sys_view.name IS '视图名称';
@@ -180,7 +187,10 @@ COMMENT ON COLUMN sys.tb_sys_view.parent_id IS '父视图ID';
COMMENT ON COLUMN sys.tb_sys_view.url IS '视图URL';
COMMENT ON COLUMN sys.tb_sys_view.component IS '视图组件';
COMMENT ON COLUMN sys.tb_sys_view.icon IS '视图图标';
COMMENT ON COLUMN sys.tb_sys_view.type IS '视图类型';
COMMENT ON COLUMN sys.tb_sys_view.type IS '视图类型0=目录 1=菜单页面 2=按钮';
COMMENT ON COLUMN sys.tb_sys_view.view_type IS '页面类型route=路由页面 iframe=嵌入页面';
COMMENT ON COLUMN sys.tb_sys_view.iframe_url IS 'iframe URL仅当view_type=iframe时有效';
COMMENT ON COLUMN sys.tb_sys_view.service IS '所属服务platform=平台应用 bidding=招标应用 workcase=客服应用';
COMMENT ON COLUMN sys.tb_sys_view.layout IS '布局组件路径名称';
COMMENT ON COLUMN sys.tb_sys_view.order_num IS '视图排序号';
COMMENT ON COLUMN sys.tb_sys_view.description IS '视图描述';

View File

@@ -31,7 +31,7 @@
\i createTableBidding.sql
-- 8. 智能客服业务模块
\i createTableCustomerService.sql
\i createTableWorkcase.sql
-- 9. 智能体模块(暂不启用)
-- \i createTableAgent.sql

View File

@@ -59,7 +59,7 @@ INSERT INTO sys.tb_sys_module (
('MODULE-0006', 'module_bidding', '招投标', '招投标业务管理',
'system', NULL, now(), false),
('MODULE-0007', 'module_customer_service', '智能客服', '客服工单管理',
('MODULE-0007', 'module_workcase', '智能客服', '客服工单管理',
'system', NULL, now(), false);
-- =============================
@@ -144,6 +144,18 @@ INSERT INTO sys.tb_sys_permission (
('PERM-0401', 'perm_log_view', '日志查看', 'log:log:view', '查看系统日志', 'module_system',
true, 'system', NULL, now(), false),
('PERM-0402', 'perm_log_export', '日志导出', 'log:log:export', '导出系统日志数据', 'module_system',
true, 'system', NULL, now(), false),
-- 平台基础菜单访问权限(所有登录用户都有)
('PERM-0501', 'perm_platform_home', '工作台访问', 'platform:home:view', '访问平台工作台', 'module_system',
true, 'system', NULL, now(), false),
('PERM-0502', 'perm_platform_chat', 'AI助手访问', 'platform:chat:view', '访问AI助手', 'module_system',
true, 'system', NULL, now(), false),
('PERM-0503', 'perm_platform_bidding', '招标助手访问', 'platform:bidding:view', '访问招标助手iframe', 'module_bidding',
true, 'system', NULL, now(), false),
('PERM-0504', 'perm_platform_workcase', '泰豪小电访问', 'platform:workcase:view', '访问泰豪小电客服iframe', 'module_workcase',
true, 'system', NULL, now(), false),
('PERM-0505', 'perm_platform_workflow', '智能体编排访问', 'platform:workflow:view', '访问智能体编排iframe', 'module_system',
true, 'system', NULL, now(), false);
-- =============================
@@ -151,36 +163,84 @@ INSERT INTO sys.tb_sys_permission (
-- =============================
INSERT INTO sys.tb_sys_view (
optsn, view_id, name, parent_id, url, component, icon, type,
layout, order_num, description, creator, create_time, deleted
view_type, iframe_url, service, layout, order_num, description,
creator, create_time, deleted
) VALUES
-- =========================
-- 平台应用菜单 (platform)
-- =========================
-- 一级菜单
('VIEW-0001', 'view_system', '系统管理', NULL, '/system', NULL, 'Settings', 0,
'MainLayout', 100, '系统管理菜单', 'system', now(), false),
('VIEW-P001', 'view_platform_home', '工作台', NULL, '/home', 'Home', 'Grid', 1,
'route', NULL, 'platform', 'SidebarLayout', 10, '平台工作台首页', 'system', now(), false),
('VIEW-0002', 'view_business', '业务管理', NULL, '/business', NULL, 'Briefcase', 0,
'MainLayout', 200, '业务管理菜单', 'system', now(), false),
('VIEW-P002', 'view_platform_chat', 'AI助手', NULL, '/chat', 'Chat', 'ChatDotRound', 1,
'route', NULL, 'platform', 'SidebarLayout', 20, 'AI智能对话助手', 'system', now(), false),
-- iframe 嵌入菜单
('VIEW-P003', 'view_platform_bidding', '招标助手', NULL, NULL, NULL, 'Document', 1,
'iframe', 'http://localhost:5002', 'platform', 'SidebarLayout', 30, '招标应用iframe', 'system', now(), false),
('VIEW-P004', 'view_platform_workcase', '泰豪小电', NULL, NULL, NULL, 'Service', 1,
'iframe', 'http://localhost:5003', 'platform', 'SidebarLayout', 40, '客服应用iframe', 'system', now(), false),
('VIEW-P005', 'view_platform_workflow', '智能体编排', NULL, NULL, NULL, 'Connection', 1,
'iframe', 'http://localhost:3000', 'platform', 'SidebarLayout', 50, 'Dify智能体编排iframe', 'system', now(), false),
-- 系统管理目录
('VIEW-P100', 'view_system', '系统管理', NULL, '/system', NULL, 'Settings', 0,
'route', NULL, 'platform', 'SidebarLayout', 100, '系统管理目录', 'system', now(), false),
-- 系统管理子菜单
('VIEW-0101', 'view_user', '用户管理', 'view_system', '/system/user', 'system/UserList', 'Users', 1,
'MainLayout', 10, '用户管理页面', 'system', now(), false),
('VIEW-P101', 'view_user', '用户管理', 'view_system', '/system/user', 'system/UserList', 'Users', 1,
'route', NULL, 'platform', 'SidebarLayout', 10, '用户管理页面', 'system', now(), false),
('VIEW-0102', 'view_role', '角色管理', 'view_system', '/system/role', 'system/RoleList', 'Shield', 1,
'MainLayout', 20, '角色管理页面', 'system', now(), false),
('VIEW-P102', 'view_role', '角色管理', 'view_system', '/system/role', 'system/RoleList', 'Shield', 1,
'route', NULL, 'platform', 'SidebarLayout', 20, '角色管理页面', 'system', now(), false),
('VIEW-0103', 'view_dept', '部门管理', 'view_system', '/system/dept', 'system/DeptList', 'Building', 1,
'MainLayout', 30, '部门管理页面', 'system', now(), false),
('VIEW-P103', 'view_dept', '部门管理', 'view_system', '/system/dept', 'system/DeptList', 'Building', 1,
'route', NULL, 'platform', 'SidebarLayout', 30, '部门管理页面', 'system', now(), false),
('VIEW-0104', 'view_permission', '权限管理', 'view_system', '/system/permission', 'system/PermissionList', 'Lock', 1,
'MainLayout', 40, '权限管理页面', 'system', now(), false),
('VIEW-P104', 'view_permission', '权限管理', 'view_system', '/system/permission', 'system/PermissionList', 'Lock', 1,
'route', NULL, 'platform', 'SidebarLayout', 40, '权限管理页面', 'system', now(), false),
('VIEW-0105', 'view_config', '配置管理', 'view_system', '/system/config', 'system/ConfigList', 'Settings', 1,
'MainLayout', 50, '配置管理页面', 'system', now(), false),
('VIEW-P105', 'view_config', '配置管理', 'view_system', '/system/config', 'system/ConfigList', 'Settings', 1,
'route', NULL, 'platform', 'SidebarLayout', 50, '配置管理页面', 'system', now(), false),
('VIEW-0106', 'view_file', '文件管理', 'view_system', '/system/file', 'system/FileList', 'FileText', 1,
'MainLayout', 60, '文件管理页面', 'system', now(), false),
('VIEW-P106', 'view_file', '文件管理', 'view_system', '/system/file', 'system/FileList', 'FileText', 1,
'route', NULL, 'platform', 'SidebarLayout', 60, '文件管理页面', 'system', now(), false),
('VIEW-0107', 'view_message', '消息管理', 'view_system', '/system/message', 'system/MessageList', 'Mail', 1,
'MainLayout', 70, '消息管理页面', 'system', now(), false);
('VIEW-P107', 'view_message', '消息管理', 'view_system', '/system/message', 'system/MessageList', 'Mail', 1,
'route', NULL, 'platform', 'SidebarLayout', 70, '消息管理页面', 'system', now(), false),
-- =========================
-- 招标应用菜单 (bidding)
-- =========================
('VIEW-B001', 'view_bidding_home', '首页', NULL, '/home', 'Home', 'House', 1,
'route', NULL, 'bidding', 'DefaultLayout', 10, '招标应用首页', 'system', now(), false),
('VIEW-B002', 'view_bidding_list', '招标列表', NULL, '/bidding/list', 'bidding/List', 'List', 1,
'route', NULL, 'bidding', 'DefaultLayout', 20, '招标项目列表', 'system', now(), false),
('VIEW-B003', 'view_bidding_detail', '招标详情', NULL, '/bidding/detail', 'bidding/Detail', 'Document', 1,
'route', NULL, 'bidding', 'DefaultLayout', 30, '招标项目详情', 'system', now(), false),
('VIEW-B004', 'view_bidding_offer', '投标管理', NULL, '/bidding/offer', 'bidding/Offer', 'Edit', 1,
'route', NULL, 'bidding', 'DefaultLayout', 40, '投标管理页面', 'system', now(), false),
-- =========================
-- 客服应用菜单 (workcase)
-- =========================
('VIEW-W001', 'view_workcase_home', '首页', NULL, '/home', 'Home', 'House', 1,
'route', NULL, 'workcase', 'DefaultLayout', 10, '客服应用首页', 'system', now(), false),
('VIEW-W002', 'view_workcase_list', '工单列表', NULL, '/workcase/list', 'workcase/List', 'Tickets', 1,
'route', NULL, 'workcase', 'DefaultLayout', 20, '工单列表页面', 'system', now(), false),
('VIEW-W003', 'view_workcase_detail', '工单详情', NULL, '/workcase/detail', 'workcase/Detail', 'Document', 1,
'route', NULL, 'workcase', 'DefaultLayout', 30, '工单详情页面', 'system', now(), false),
('VIEW-W004', 'view_workcase_chat', '智能客服', NULL, '/workcase/chat', 'workcase/Chat', 'ChatDotRound', 1,
'route', NULL, 'workcase', 'DefaultLayout', 40, '智能客服聊天', 'system', now(), false);
-- =============================
-- 6. 角色权限关联(超级管理员拥有所有权限)
@@ -199,7 +259,7 @@ SELECT
FROM sys.tb_sys_permission
WHERE deleted = false;
-- 系统管理员权限(除了用户删除外的系统管理权限)
-- 系统管理员权限(除了用户删除外的系统管理权限 + 所有平台基础菜单
INSERT INTO sys.tb_sys_role_permission (
optsn, role_id, permission_id, creator, dept_path, create_time, deleted
)
@@ -214,73 +274,94 @@ SELECT
FROM sys.tb_sys_permission
WHERE deleted = false
AND code NOT IN ('system:user:delete', 'system:role:delete', 'system:dept:delete')
AND module_id IN ('module_system', 'module_file', 'module_message', 'module_config');
AND (
module_id IN ('module_system', 'module_file', 'module_message', 'module_config', 'module_bidding', 'module_workcase')
OR code LIKE 'platform:%:view' -- 包含所有平台基础菜单权限
);
-- 普通用户权限(基础查看和文件操作)
-- 普通用户权限(基础查看和文件操作 + 平台基础菜单访问
INSERT INTO sys.tb_sys_role_permission (
optsn, role_id, permission_id, creator, dept_path, create_time, deleted
) VALUES
('RP-U-0001', 'role_user', 'perm_user_view', 'system', NULL, now(), false),
('RP-U-0002', 'role_user', 'perm_file_view', 'system', NULL, now(), false),
('RP-U-0003', 'role_user', 'perm_file_upload', 'system', NULL, now(), false),
('RP-U-0004', 'role_user', 'perm_file_download', 'system', NULL, now(), false),
('RP-U-0005', 'role_user', 'perm_message_view', 'system', NULL, now(), false),
('RP-U-0006', 'role_user', 'perm_config_view', 'system', NULL, now(), false);
-- 平台基础菜单访问权限
('RP-U-0001', 'role_user', 'perm_platform_home', 'system', NULL, now(), false),
('RP-U-0002', 'role_user', 'perm_platform_chat', 'system', NULL, now(), false),
('RP-U-0003', 'role_user', 'perm_platform_bidding', 'system', NULL, now(), false),
('RP-U-0004', 'role_user', 'perm_platform_workcase', 'system', NULL, now(), false),
('RP-U-0005', 'role_user', 'perm_platform_workflow', 'system', NULL, now(), false),
-- 系统功能权限
('RP-U-0011', 'role_user', 'perm_user_view', 'system', NULL, now(), false),
('RP-U-0012', 'role_user', 'perm_file_view', 'system', NULL, now(), false),
('RP-U-0013', 'role_user', 'perm_file_upload', 'system', NULL, now(), false),
('RP-U-0014', 'role_user', 'perm_file_download', 'system', NULL, now(), false),
('RP-U-0015', 'role_user', 'perm_message_view', 'system', NULL, now(), false),
('RP-U-0016', 'role_user', 'perm_config_view', 'system', NULL, now(), false);
-- 访客权限(仅查看)
-- 访客权限(仅查看 + 基础菜单访问
INSERT INTO sys.tb_sys_role_permission (
optsn, role_id, permission_id, creator, dept_path, create_time, deleted
) VALUES
('RP-G-0001', 'role_guest', 'perm_user_view', 'system', NULL, now(), false),
('RP-G-0002', 'role_guest', 'perm_file_view', 'system', NULL, now(), false),
('RP-G-0003', 'role_guest', 'perm_message_view', 'system', NULL, now(), false);
-- 平台基础菜单访问权限
('RP-G-0001', 'role_guest', 'perm_platform_home', 'system', NULL, now(), false),
('RP-G-0002', 'role_guest', 'perm_platform_chat', 'system', NULL, now(), false),
-- 系统功能权限(仅查看)
('RP-G-0011', 'role_guest', 'perm_user_view', 'system', NULL, now(), false),
('RP-G-0012', 'role_guest', 'perm_file_view', 'system', NULL, now(), false),
('RP-G-0013', 'role_guest', 'perm_message_view', 'system', NULL, now(), false);
-- =============================
-- 7. 视图权限关联
-- =============================
-- 将视图与对应模块的权限关联
-- 将视图与对应模块的权限关联(使用新的 view_id
INSERT INTO sys.tb_sys_view_permission (
optsn, view_id, permission_id, creator, dept_path, create_time, deleted
) VALUES
-- 用户管理视图关联用户权限
('VP-0001', 'view_user', 'perm_user_view', 'system', NULL, now(), false),
('VP-0002', 'view_user', 'perm_user_create', 'system', NULL, now(), false),
('VP-0003', 'view_user', 'perm_user_edit', 'system', NULL, now(), false),
('VP-0004', 'view_user', 'perm_user_delete', 'system', NULL, now(), false),
('VP-0005', 'view_user', 'perm_user_export', 'system', NULL, now(), false),
-- 平台基础菜单权限关联(所有登录用户都可访问)
('VP-P001', 'VIEW-P001', 'perm_platform_home', 'system', NULL, now(), false),
('VP-P002', 'VIEW-P002', 'perm_platform_chat', 'system', NULL, now(), false),
('VP-P003', 'VIEW-P003', 'perm_platform_bidding', 'system', NULL, now(), false),
('VP-P004', 'VIEW-P004', 'perm_platform_workcase', 'system', NULL, now(), false),
('VP-P005', 'VIEW-P005', 'perm_platform_workflow', 'system', NULL, now(), false),
-- 角色管理视图关联角色权限
('VP-0011', 'view_role', 'perm_role_view', 'system', NULL, now(), false),
('VP-0012', 'view_role', 'perm_role_create', 'system', NULL, now(), false),
('VP-0013', 'view_role', 'perm_role_edit', 'system', NULL, now(), false),
('VP-0014', 'view_role', 'perm_role_delete', 'system', NULL, now(), false),
('VP-0015', 'view_role', 'perm_role_export', 'system', NULL, now(), false),
-- 用户管理视图关联用户权限VIEW-P101
('VP-0001', 'VIEW-P101', 'perm_user_view', 'system', NULL, now(), false),
('VP-0002', 'VIEW-P101', 'perm_user_create', 'system', NULL, now(), false),
('VP-0003', 'VIEW-P101', 'perm_user_edit', 'system', NULL, now(), false),
('VP-0004', 'VIEW-P101', 'perm_user_delete', 'system', NULL, now(), false),
('VP-0005', 'VIEW-P101', 'perm_user_export', 'system', NULL, now(), false),
-- 部门管理视图关联部门权限
('VP-0021', 'view_dept', 'perm_dept_view', 'system', NULL, now(), false),
('VP-0022', 'view_dept', 'perm_dept_create', 'system', NULL, now(), false),
('VP-0023', 'view_dept', 'perm_dept_edit', 'system', NULL, now(), false),
('VP-0024', 'view_dept', 'perm_dept_delete', 'system', NULL, now(), false),
('VP-0025', 'view_dept', 'perm_dept_export', 'system', NULL, now(), false),
-- 角色管理视图关联角色权限VIEW-P102
('VP-0011', 'VIEW-P102', 'perm_role_view', 'system', NULL, now(), false),
('VP-0012', 'VIEW-P102', 'perm_role_create', 'system', NULL, now(), false),
('VP-0013', 'VIEW-P102', 'perm_role_edit', 'system', NULL, now(), false),
('VP-0014', 'VIEW-P102', 'perm_role_delete', 'system', NULL, now(), false),
('VP-0015', 'VIEW-P102', 'perm_role_export', 'system', NULL, now(), false),
-- 权限管理视图关联权限管理权限
('VP-0031', 'view_permission', 'perm_permission_view', 'system', NULL, now(), false),
('VP-0032', 'view_permission', 'perm_permission_manage', 'system', NULL, now(), false),
-- 部门管理视图关联部门权限VIEW-P103
('VP-0021', 'VIEW-P103', 'perm_dept_view', 'system', NULL, now(), false),
('VP-0022', 'VIEW-P103', 'perm_dept_create', 'system', NULL, now(), false),
('VP-0023', 'VIEW-P103', 'perm_dept_edit', 'system', NULL, now(), false),
('VP-0024', 'VIEW-P103', 'perm_dept_delete', 'system', NULL, now(), false),
('VP-0025', 'VIEW-P103', 'perm_dept_export', 'system', NULL, now(), false),
-- 配置管理视图关联配置权限
('VP-0041', 'view_config', 'perm_config_view', 'system', NULL, now(), false),
('VP-0042', 'view_config', 'perm_config_edit', 'system', NULL, now(), false),
('VP-0043', 'view_config', 'perm_config_export', 'system', NULL, now(), false),
-- 权限管理视图关联权限管理权限VIEW-P104
('VP-0031', 'VIEW-P104', 'perm_permission_view', 'system', NULL, now(), false),
('VP-0032', 'VIEW-P104', 'perm_permission_manage', 'system', NULL, now(), false),
-- 文件管理视图关联文件权限
('VP-0051', 'view_file', 'perm_file_view', 'system', NULL, now(), false),
('VP-0052', 'view_file', 'perm_file_upload', 'system', NULL, now(), false),
('VP-0053', 'view_file', 'perm_file_download', 'system', NULL, now(), false),
('VP-0054', 'view_file', 'perm_file_delete', 'system', NULL, now(), false),
('VP-0055', 'view_file', 'perm_file_export', 'system', NULL, now(), false),
-- 配置管理视图关联配置权限VIEW-P105
('VP-0041', 'VIEW-P105', 'perm_config_view', 'system', NULL, now(), false),
('VP-0042', 'VIEW-P105', 'perm_config_edit', 'system', NULL, now(), false),
('VP-0043', 'VIEW-P105', 'perm_config_export', 'system', NULL, now(), false),
-- 消息管理视图关联消息权限
('VP-0061', 'view_message', 'perm_message_view', 'system', NULL, now(), false),
('VP-0062', 'view_message', 'perm_message_send', 'system', NULL, now(), false),
('VP-0063', 'view_message', 'perm_message_manage', 'system', NULL, now(), false),
('VP-0064', 'view_message', 'perm_message_export', 'system', NULL, now(), false);
-- 文件管理视图关联文件权限VIEW-P106
('VP-0051', 'VIEW-P106', 'perm_file_view', 'system', NULL, now(), false),
('VP-0052', 'VIEW-P106', 'perm_file_upload', 'system', NULL, now(), false),
('VP-0053', 'VIEW-P106', 'perm_file_download', 'system', NULL, now(), false),
('VP-0054', 'VIEW-P106', 'perm_file_delete', 'system', NULL, now(), false),
('VP-0055', 'VIEW-P106', 'perm_file_export', 'system', NULL, now(), false),
-- 消息管理视图关联消息权限VIEW-P107
('VP-0061', 'VIEW-P107', 'perm_message_view', 'system', NULL, now(), false),
('VP-0062', 'VIEW-P107', 'perm_message_send', 'system', NULL, now(), false),
('VP-0063', 'VIEW-P107', 'perm_message_manage', 'system', NULL, now(), false),
('VP-0064', 'VIEW-P107', 'perm_message_export', 'system', NULL, now(), false);

View File

@@ -12,7 +12,7 @@ urban-lifeline:
- /** # 认证服务的所有接口都放行
security:
aes:
secret-key: 1234567890qwer
secret-key: MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI= # Base64 编码32字节256位
# ================== Spring ==================
spring:
application:

View File

@@ -36,9 +36,18 @@ public class TbSysViewDTO extends BaseDTO {
@Schema(description = "图标")
private String icon;
@Schema(description = "类型")
@Schema(description = "类型0=目录 1=菜单页面 2=按钮")
private Integer type;
@Schema(description = "页面类型route=路由页面 iframe=嵌入页面")
private String viewType;
@Schema(description = "iframe URL仅当viewType=iframe时有效")
private String iframeUrl;
@Schema(description = "所属服务platform=平台应用 bidding=招标应用 workcase=客服应用")
private String service;
@Schema(description = "布局")
private String layout;

View File

@@ -37,9 +37,9 @@ spring:
- id: auth-service
uri: lb://auth-service
predicates:
- Path=/auth/**
- Path=/urban-lifeline/auth/**
filters:
- RewritePath=/auth/(?<segment>.*), /urban-lifeline/auth/$\{segment}
# 不需要重写,直接转发保持原路径
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100
@@ -49,73 +49,55 @@ spring:
- id: system-service
uri: lb://system-service
predicates:
- Path=/system/**
filters:
- RewritePath=/system/(?<segment>.*), /urban-lifeline/system/$\{segment}
- Path=/urban-lifeline/system/**
# ==================== 日志服务路由 ====================
- id: log-service
uri: lb://log-service
predicates:
- Path=/log/**
filters:
- RewritePath=/log/(?<segment>.*), /urban-lifeline/log/$\{segment}
- Path=/urban-lifeline/log/**
# ==================== 文件服务路由 ====================
- id: file-service
uri: lb://file-service
predicates:
- Path=/file/**
filters:
- RewritePath=/file/(?<segment>.*), /urban-lifeline/file/$\{segment}
- Path=/urban-lifeline/file/**
# ==================== 消息服务路由 ====================
- id: message-service
uri: lb://message-service
predicates:
- Path=/message/**
filters:
- RewritePath=/message/(?<segment>.*), /urban-lifeline/message/$\{segment}
- Path=/urban-lifeline/message/**
# ==================== 招投标服务路由 ====================
- id: bidding-service
uri: lb://bidding-service
predicates:
- Path=/bidding/**
filters:
- RewritePath=/bidding/(?<segment>.*), /urban-lifeline/bidding/$\{segment}
- Path=/urban-lifeline/bidding/**
# ==================== 平台服务路由 ====================
- id: platform-service
uri: lb://platform-service
predicates:
- Path=/platform/**
filters:
- RewritePath=/platform/(?<segment>.*), /urban-lifeline/platform/$\{segment}
- Path=/urban-lifeline/platform/**
# ==================== 工单服务路由 ====================
- id: workcase-service
uri: lb://workcase-service
predicates:
- Path=/workcase/**
filters:
- RewritePath=/workcase/(?<segment>.*), /urban-lifeline/workcase/$\{segment}
- Path=/urban-lifeline/workcase/**
# ==================== 定时任务服务路由 ====================
- id: crontab-service
uri: lb://crontab-service
predicates:
- Path=/crontab/**
filters:
- RewritePath=/crontab/(?<segment>.*), /urban-lifeline/crontab/$\{segment}
- Path=/urban-lifeline/crontab/**
# ==================== AI Agent 服务路由 ====================
- id: agent-service
uri: lb://agent-service
predicates:
- Path=/agent/**
filters:
- RewritePath=/agent/(?<segment>.*), /urban-lifeline/agent/$\{segment}
- Path=/urban-lifeline/agent/**
# 全局跨域配置
globalcors:
@@ -175,7 +157,7 @@ auth:
- /error
security:
aes:
secret-key: 1234567890qwer
secret-key: MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI= # Base64 编码32字节256位
# Actuator 监控端点
management:
endpoints:

View File

@@ -12,6 +12,9 @@
<result column="component" property="component" jdbcType="VARCHAR"/>
<result column="icon" property="icon" jdbcType="VARCHAR"/>
<result column="type" property="type" jdbcType="INTEGER"/>
<result column="view_type" property="viewType" jdbcType="VARCHAR"/>
<result column="iframe_url" property="iframeUrl" jdbcType="VARCHAR"/>
<result column="service" property="service" jdbcType="VARCHAR"/>
<result column="layout" property="layout" jdbcType="VARCHAR"/>
<result column="order_num" property="orderNum" jdbcType="INTEGER"/>
<result column="description" property="description" jdbcType="VARCHAR"/>
@@ -29,7 +32,7 @@
<!-- 基础列 -->
<sql id="Base_Column_List">
view_id, name, parent_id, url, component, icon, type, layout, order_num, description,
view_id, name, parent_id, url, component, icon, type, view_type, iframe_url, service, layout, order_num, description,
optsn, creator, updater, dept_path, remark, create_time, update_time, delete_time, deleted
</sql>
@@ -44,11 +47,14 @@
layout,
order_num,
optsn,
<!-- 可选字段parent_id, url, component, icon, description 及基础字段 -->
<!-- 可选字段parent_id, url, component, icon, view_type, iframe_url, service, description 及基础字段 -->
<if test="parentId != null and parentId != ''">parent_id,</if>
<if test="url != null and url != ''">url,</if>
<if test="component != null and component != ''">component,</if>
<if test="icon != null and icon != ''">icon,</if>
<if test="viewType != null and viewType != ''">view_type,</if>
<if test="iframeUrl != null and iframeUrl != ''">iframe_url,</if>
<if test="service != null and service != ''">service,</if>
<if test="description != null and description != ''">description,</if>
<if test="creator != null and creator != ''">creator,</if>
<if test="deptPath != null and deptPath != ''">dept_path,</if>
@@ -72,6 +78,9 @@
<if test="url != null and url != ''">#{url},</if>
<if test="component != null and component != ''">#{component},</if>
<if test="icon != null and icon != ''">#{icon},</if>
<if test="viewType != null and viewType != ''">#{viewType},</if>
<if test="iframeUrl != null and iframeUrl != ''">#{iframeUrl},</if>
<if test="service != null and service != ''">#{service},</if>
<if test="description != null and description != ''">#{description},</if>
<if test="creator != null and creator != ''">#{creator},</if>
<if test="deptPath != null and deptPath != ''">#{deptPath},</if>
@@ -105,6 +114,15 @@
<if test="type != null">
type = #{type},
</if>
<if test="viewType != null">
view_type = #{viewType},
</if>
<if test="iframeUrl != null">
iframe_url = #{iframeUrl},
</if>
<if test="service != null">
service = #{service},
</if>
<if test="layout != null">
layout = #{layout},
</if>
@@ -168,6 +186,12 @@
<if test="filter.type != null">
AND type = #{filter.type}
</if>
<if test="filter.viewType != null and filter.viewType != ''">
AND view_type = #{filter.viewType}
</if>
<if test="filter.service != null and filter.service != ''">
AND service = #{filter.service}
</if>
<if test="filter.deptPath != null and filter.deptPath != ''">
AND dept_path LIKE CONCAT(#{filter.deptPath}, '%')
</if>
@@ -194,6 +218,12 @@
<if test="filter.type != null">
AND type = #{filter.type}
</if>
<if test="filter.viewType != null and filter.viewType != ''">
AND view_type = #{filter.viewType}
</if>
<if test="filter.service != null and filter.service != ''">
AND service = #{filter.service}
</if>
<if test="filter.deptPath != null and filter.deptPath != ''">
AND dept_path LIKE CONCAT(#{filter.deptPath}, '%')
</if>
@@ -220,6 +250,12 @@
<if test="filter.type != null">
AND type = #{filter.type}
</if>
<if test="filter.viewType != null and filter.viewType != ''">
AND view_type = #{filter.viewType}
</if>
<if test="filter.service != null and filter.service != ''">
AND service = #{filter.service}
</if>
<if test="filter.deptPath != null and filter.deptPath != ''">
AND dept_path LIKE CONCAT(#{filter.deptPath}, '%')
</if>

View File

@@ -1,6 +1,8 @@
# Urban Lifeline Web 微前端项目
基于 Import Maps 的微前端架构,包含共享模块和多个业务应用。
基于 **@module-federation/vite** 的微前端架构,包含共享模块和多个业务应用。
> ✨ **v2.0 升级**:已从 `@originjs/vite-plugin-federation` 迁移到官方维护的 `@module-federation/vite`,获得更好的开发体验和稳定性!
## 📦 项目结构
@@ -19,40 +21,43 @@ urbanLifelineWeb/
### 1. 安装依赖
```bash
# 一键安装所有包的依赖
npm run install:all
# 使用 pnpm 安装(推荐)
pnpm install
# 或单独安装
npm run install:shared
npm run install:platform
npm run install:bidding
npm run install:workcase
# 或使用 install.bat 脚本
install.bat
```
### 2. 启动开发服务器
```bash
# 同时启动所有服务
npm run dev:all
# 方式1: 使用启动脚本Windows推荐
start-all.bat
# 或单独启动
npm run dev:shared # http://localhost:5000
npm run dev:platform # http://localhost:5001
npm run dev:bidding # http://localhost:5002
npm run dev:workcase # http://localhost:5003
# 方式2: 使用 pnpm 并行启动所有服务
pnpm run dev
# 方式3: 手动启动(需要两个终端)
# 终端1 - 启动 shared
cd packages/shared
pnpm run dev # http://localhost:5000
# 终端2 - 启动 platform
cd packages/platform
pnpm run dev # http://localhost:5001
```
### 3. 构建生产版本
```bash
# 构建所有应用
npm run build:all
pnpm run build
# 或单独构建
npm run build:shared
npm run build:platform
npm run build:bidding
npm run build:workcase
cd packages/shared && pnpm run build
cd packages/platform && pnpm run build
cd packages/bidding && pnpm run build
cd packages/workcase && pnpm run build
```
## 🌐 端口分配
@@ -73,24 +78,144 @@ npm run build:workcase
- **路由**: Vue Router 4.5
- **工具库**: VueUse
## 📝 开发说明
## 📝 Module Federation 配置
### Import Maps
### Shared 配置 (packages/shared/vite.config.ts)
本项目使用 Import Maps 实现模块共享:
```typescript
import { federation } from '@module-federation/vite'
```html
<script type="importmap">
{
"imports": {
"@shared/components": "http://localhost:5000/shared/components.js",
"@shared/utils": "http://localhost:5000/shared/utils.js",
"@shared/api": "http://localhost:5000/shared/api.js"
export default defineConfig({
plugins: [
federation({
name: 'shared',
filename: 'remoteEntry.js',
exposes: {
'./FileUpload': './src/components/fileupload/FileUpload.vue',
'./DynamicFormItem': './src/components/dynamicFormItem/DynamicFormItem.vue',
'./api': './src/api/index.ts',
'./authAPI': './src/api/auth/auth.ts',
'./fileAPI': './src/api/file/file.ts',
'./utils': './src/utils/index.ts',
'./types': './src/types/index.ts',
'./components': './src/components/index.ts'
},
shared: {
vue: {},
'vue-router': {},
'element-plus': {},
'@element-plus/icons-vue': {},
axios: {}
}
})
],
server: {
port: 5000,
strictPort: true, // 关键:锁定端口,避免漂移
host: true,
cors: true
}
})
```
### Platform 配置 (packages/platform/vite.config.ts)
```typescript
import { federation } from '@module-federation/vite'
export default defineConfig({
plugins: [
federation({
name: 'platform',
remotes: {
shared: {
type: 'module', // 关键:必须指定 ES module 类型
name: 'shared',
entry: 'http://localhost:5000/remoteEntry.js'
}
},
shared: {
vue: {},
'vue-router': {},
'element-plus': {},
axios: {}
}
})
]
})
```
### 使用示例
```typescript
// 在 platform 中导入 shared 的组件
import FileUpload from 'shared/FileUpload'
import DynamicFormItem from 'shared/DynamicFormItem'
import { authAPI } from 'shared/authAPI'
import { getAesInstance } from 'shared/utils'
import type { LoginParam, SysUserVO } from 'shared/types'
// 在组件中使用
<template>
<FileUpload mode="cover" v-model:coverImg="image" />
<DynamicFormItem :config="config" v-model="value" />
</template>
<script setup lang="ts">
// 调用 API
const result = await authAPI.login(params)
</script>
```
### 优势
-**开发模式直接支持**:无需构建,直接 dev 即可
-**完整的热更新**:修改 shared 代码自动更新到 platform
-**自动路径处理**:无需关心 shared 内部的 `@/` 路径
-**依赖去重**vue、element-plus 等只加载一次
-**企业级稳定**:官方维护,支持 Vite 6
## 🎯 开发工作流
### 日常开发
1. **启动服务**(每天开始)
```bash
# 使用脚本一键启动
start-all.bat
# 或手动启动
cd packages/shared && pnpm run dev # 终端1
cd packages/platform && pnpm run dev # 终端2
```
2. **修改代码**
- 修改 shared 组件 → 保存 → 自动热更新 ✨
- 修改 platform 页面 → 保存 → 自动热更新 ✨
3. **结束开发**
- 按 Ctrl+C 停止所有服务
### 添加新的共享模块
1. **在 shared 中创建组件**
```bash
# 例如创建新组件
packages/shared/src/components/mynew/MyNewComponent.vue
```
2. **在 shared/vite.config.ts 中暴露**
```typescript
exposes: {
'./MyNewComponent': './src/components/mynew/MyNewComponent.vue'
}
```
3. **在 platform 中使用**
```typescript
import MyNewComponent from 'shared/MyNewComponent'
```
### API 代理
所有应用的 `/api` 请求会代理到后端服务 `http://localhost:8080`
@@ -101,7 +226,8 @@ npm run build:workcase
## 🔥 注意事项
1. **启动顺序**: 建议先启动 `shared` 服务,再启动其他应用
2. **端口占用**: 确保 5000-5003 端口未被占
3. **Node 版本**: 建议使用 Node.js 18+
4. **依赖安装**: 首次运行前必须执行 `npm run install:all`
1. **包管理器**: 必须使用 pnpm项目使用 pnpm workspace
2. **启动顺序**: 必须先启动 `shared` 服务5000端口再启动其他应
3. **端口占用**: 确保 5000-5003 端口未被占用shared 使用 strictPort 模式
4. **Node 版本**: 建议使用 Node.js 18+
5. **Module Federation**: remoteEntry.js 路径为 `http://localhost:5000/remoteEntry.js`(不是 /assets/

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,21 @@ const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
export default defineConfig({
plugins: [vue(), vueJsx()],
plugins: [
vue({
script: {
defineModel: true,
propsDestructure: true
}
}),
vueJsx()
],
define: {
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: true,
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: true
},
resolve: {
alias: {

View File

@@ -0,0 +1,130 @@
// Windows temporarily needs this file, https://github.com/module-federation/vite/issues/68
import {loadShare} from "@module-federation/runtime";
const importMap = {
"element-plus": async () => {
let pkg = await import("__mf__virtual/platform__prebuild__element_mf_2_plus__prebuild__.js");
return pkg;
}
,
"vue": async () => {
let pkg = await import("__mf__virtual/platform__prebuild__vue__prebuild__.js");
return pkg;
}
,
"vue-router": async () => {
let pkg = await import("__mf__virtual/platform__prebuild__vue_mf_2_router__prebuild__.js");
return pkg;
}
}
const usedShared = {
"element-plus": {
name: "element-plus",
version: "2.12.0",
scope: ["default"],
loaded: false,
from: "platform",
async get () {
if (false) {
throw new Error(`Shared module '${"element-plus"}' must be provided by host`);
}
usedShared["element-plus"].loaded = true
const {"element-plus": pkgDynamicImport} = importMap
const res = await pkgDynamicImport()
const exportModule = {...res}
// All npm packages pre-built by vite will be converted to esm
Object.defineProperty(exportModule, "__esModule", {
value: true,
enumerable: false
})
return function () {
return exportModule
}
},
shareConfig: {
singleton: false,
requiredVersion: "^2.12.0",
}
}
,
"vue": {
name: "vue",
version: "3.5.25",
scope: ["default"],
loaded: false,
from: "platform",
async get () {
if (false) {
throw new Error(`Shared module '${"vue"}' must be provided by host`);
}
usedShared["vue"].loaded = true
const {"vue": pkgDynamicImport} = importMap
const res = await pkgDynamicImport()
const exportModule = {...res}
// All npm packages pre-built by vite will be converted to esm
Object.defineProperty(exportModule, "__esModule", {
value: true,
enumerable: false
})
return function () {
return exportModule
}
},
shareConfig: {
singleton: false,
requiredVersion: "^3.5.25",
}
}
,
"vue-router": {
name: "vue-router",
version: "4.6.3",
scope: ["default"],
loaded: false,
from: "platform",
async get () {
if (false) {
throw new Error(`Shared module '${"vue-router"}' must be provided by host`);
}
usedShared["vue-router"].loaded = true
const {"vue-router": pkgDynamicImport} = importMap
const res = await pkgDynamicImport()
const exportModule = {...res}
// All npm packages pre-built by vite will be converted to esm
Object.defineProperty(exportModule, "__esModule", {
value: true,
enumerable: false
})
return function () {
return exportModule
}
},
shareConfig: {
singleton: false,
requiredVersion: "^4.6.3",
}
}
}
const usedRemotes = [
{
entryGlobalName: "http://localhost:5000/remoteEntry.js",
name: "shared",
type: "var",
entry: "http://localhost:5000/remoteEntry.js",
shareScope: "default",
}
]
export {
usedShared,
usedRemotes
}

View File

@@ -0,0 +1,9 @@
# Platform 应用环境变量示例
# 复制此文件为 .env.local 或 .env.production 使用
# API 基础地址Gateway 地址)
VITE_API_BASE_URL=http://localhost:8180
# AES 加密密钥(可选,默认使用配置文件中的密钥)
# 生产环境建议通过环境变量配置
# VUE_APP_AES_SECRET_KEY=1234567890qwer

View File

@@ -9,22 +9,8 @@
<!-- 加载运行时配置(必须在其他脚本之前加载) -->
<script src="/app-config.js"></script>
<!-- Import Maps 配置 - 只配置共享模块 -->
<script type="importmap">
{
"imports": {
"@shared/components": "/shared/components.js",
"@shared/utils": "/shared/utils.js",
"@shared/api": "/shared/api.js",
"@shared/composables": "/shared/composables.js",
"@shared/types": "/shared/types.js"
}
}
</script>
<!-- 预加载关键模块 -->
<link rel="modulepreload" href="/shared/components.js">
<link rel="modulepreload" href="/shared/utils.js">
<!-- Module Federation - 预加载远程入口 -->
<link rel="modulepreload" href="http://localhost:5000/remoteEntry.js">
</head>
<body>
<div id="app"></div>

View File

@@ -21,6 +21,7 @@
"@types/node": "^22.0.0",
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"@module-federation/vite": "^1.9.3",
"typescript": "^5.7.2",
"vite": "^6.0.3",
"vue-tsc": "^2.2.0"

View File

@@ -28,6 +28,9 @@ dependencies:
version: 4.6.3(vue@3.5.25)
devDependencies:
'@module-federation/vite':
specifier: ^1.9.3
version: 1.9.3
'@types/node':
specifier: ^22.0.0
version: 22.19.1
@@ -596,10 +599,61 @@ packages:
'@jridgewell/sourcemap-codec': 1.5.5
dev: true
/@module-federation/error-codes@0.21.6:
resolution: {integrity: sha512-MLJUCQ05KnoVl8xd6xs9a5g2/8U+eWmVxg7xiBMeR0+7OjdWUbHwcwgVFatRIwSZvFgKHfWEiI7wsU1q1XbTRQ==}
dev: true
/@module-federation/runtime-core@0.21.6:
resolution: {integrity: sha512-5Hd1Y5qp5lU/aTiK66lidMlM/4ji2gr3EXAtJdreJzkY+bKcI5+21GRcliZ4RAkICmvdxQU5PHPL71XmNc7Lsw==}
dependencies:
'@module-federation/error-codes': 0.21.6
'@module-federation/sdk': 0.21.6
dev: true
/@module-federation/runtime@0.21.6:
resolution: {integrity: sha512-+caXwaQqwTNh+CQqyb4mZmXq7iEemRDrTZQGD+zyeH454JAYnJ3s/3oDFizdH6245pk+NiqDyOOkHzzFQorKhQ==}
dependencies:
'@module-federation/error-codes': 0.21.6
'@module-federation/runtime-core': 0.21.6
'@module-federation/sdk': 0.21.6
dev: true
/@module-federation/sdk@0.21.6:
resolution: {integrity: sha512-x6hARETb8iqHVhEsQBysuWpznNZViUh84qV2yE7AD+g7uIzHKiYdoWqj10posbo5XKf/147qgWDzKZoKoEP2dw==}
dev: true
/@module-federation/vite@1.9.3:
resolution: {integrity: sha512-MV6XI3FX6okEMJ7FdmvFmYuu7DygRoLljKT8atrBwFhlttsgBbswpqMj4P4Fs/X+pFmbIi/ntFzVhsrG0qQnGQ==}
dependencies:
'@module-federation/runtime': 0.21.6
'@module-federation/sdk': 0.21.6
'@rollup/pluginutils': 5.3.0
defu: 6.1.4
estree-walker: 2.0.2
magic-string: 0.30.21
pathe: 1.1.2
transitivePeerDependencies:
- rollup
dev: true
/@rolldown/pluginutils@1.0.0-beta.53:
resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==}
dev: true
/@rollup/pluginutils@5.3.0:
resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
peerDependenciesMeta:
rollup:
optional: true
dependencies:
'@types/estree': 1.0.8
estree-walker: 2.0.2
picomatch: 4.0.3
dev: true
/@rollup/rollup-android-arm-eabi@4.53.3:
resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==}
cpu: [arm]
@@ -1146,6 +1200,10 @@ packages:
ms: 2.1.3
dev: true
/defu@6.1.4:
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
dev: true
/delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
@@ -1464,6 +1522,10 @@ packages:
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
dev: true
/pathe@1.1.2:
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
dev: true
/picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="50" fill="#ffe4c4"/>
<circle cx="50" cy="38" r="18" fill="#ffd1a1"/>
<ellipse cx="50" cy="75" rx="28" ry="20" fill="#ff6b6b"/>
<circle cx="38" cy="35" r="3" fill="#333"/>
<circle cx="62" cy="35" r="3" fill="#333"/>
<path d="M42 48 Q50 55 58 48" stroke="#333" stroke-width="2" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 399 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect width="100" height="100" rx="20" fill="#7c3aed"/>
<path d="M30 70V40h10v30H30zm15-30h10v30H45V40zm15 0h10v30H60V40z" fill="white"/>
<rect x="25" y="30" width="50" height="5" rx="2" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 278 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -0,0 +1,27 @@
<template>
<router-view />
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
onMounted(() => {
console.log('Platform App Mounted')
})
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
#app {
width: 100%;
height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</style>

View File

@@ -0,0 +1,254 @@
# Platform 应用配置说明
## AES 加密配置
### 密钥配置
**配置文件**`src/config/index.ts`
```typescript
export const AES_SECRET_KEY = '1234567890qwer'
```
**注意事项**
1. ✅ 密钥已配置为 `1234567890qwer`,与后端保持一致
2. ⚠️ 该密钥与后端 `application.yml` 中的 `security.aes.secret-key` 必须相同
3. 🔒 生产环境应从环境变量或配置中心获取,不要硬编码
### 对应后端配置
**Gateway** (`gateway/src/main/resources/application.yml`):
```yaml
security:
aes:
secret-key: 1234567890qwer
```
**Auth Service** (`auth/src/main/resources/application.yml`):
```yaml
security:
aes:
secret-key: 1234567890qwer
```
## 使用示例
### 1. 登录时加密密码
```typescript
import { authAPI } from '@shared/api/auth'
import { getAesInstance } from '@shared/utils'
async function handleLogin(username: string, password: string) {
try {
// 1. 获取 AES 加密实例
const aes = getAesInstance()
// 2. 加密密码
const encryptedPassword = await aes.encryptPassword(password)
// 3. 发送登录请求
const response = await authAPI.login({
username,
password: encryptedPassword, // 使用加密后的密码
loginType: 'password'
})
if (response.data.success) {
console.log('登录成功')
// 保存 token 等操作
}
} catch (error) {
console.error('登录失败:', error)
}
}
```
### 2. 注册时加密手机号和密码
```typescript
import { authAPI } from '@shared/api/auth'
import { getAesInstance } from '@shared/utils'
async function handleRegister(phone: string, password: string, smsCode: string, sessionId: string) {
try {
const aes = getAesInstance()
// 加密敏感信息
const encryptedPhone = await aes.encryptPhone(phone)
const encryptedPassword = await aes.encryptPassword(password)
// 发送注册请求
const response = await authAPI.register({
registerType: 'phone',
phone: encryptedPhone,
password: encryptedPassword,
confirmPassword: encryptedPassword,
smsCode,
smsSessionId: sessionId
})
if (response.data.success) {
console.log('注册成功')
}
} catch (error) {
console.error('注册失败:', error)
}
}
```
### 3. 数据脱敏显示
```typescript
import { AesUtils } from '@shared/utils'
// 显示脱敏手机号
const phone = '13812345678'
const maskedPhone = AesUtils.maskPhone(phone)
console.log(maskedPhone) // 输出138****5678
// 显示脱敏身份证号
const idCard = '110101199001011234'
const maskedIdCard = AesUtils.maskIdCard(idCard)
console.log(maskedIdCard) // 输出110101********1234
```
## 初始化流程
### 应用启动时自动初始化
**文件**`src/main.ts`
```typescript
import { AES_SECRET_KEY } from './config'
import { initAesEncrypt } from '@shared/utils'
async function initApp() {
// 初始化 AES 加密工具
await initAesEncrypt(AES_SECRET_KEY)
// ... 其他初始化操作
}
initApp()
```
### 初始化状态检查
```typescript
import { getAesInstance } from '@shared/utils'
try {
const aes = getAesInstance()
console.log('✅ AES 加密工具已初始化')
} catch (error) {
console.error('❌ AES 加密工具未初始化:', error)
}
```
## 加密流程图
```
用户输入密码
前端 AES 加密 (1234567890qwer)
发送加密后的密码
Gateway (不解密,直接转发)
Auth Service 接收
AES 解密 (1234567890qwer)
BCrypt 再次加密
存入数据库
```
## 安全建议
### 开发环境
- ✅ 使用固定密钥 `1234567890qwer`
- ✅ 密钥在代码中配置
### 生产环境
- 🔒 从环境变量获取密钥
- 🔒 使用配置中心Nacos
- 🔒 定期轮换密钥
- 🔒 密钥长度至少 32 字符
### 示例:从环境变量获取
```typescript
// 生产环境配置
export const AES_SECRET_KEY = process.env.VUE_APP_AES_SECRET_KEY || '1234567890qwer'
```
## 故障排查
### 问题:登录时提示"密码错误"
**可能原因**:前后端密钥不一致
**排查步骤**
1. 检查前端配置:`src/config/index.ts` 中的 `AES_SECRET_KEY`
2. 检查后端配置:`application.yml` 中的 `security.aes.secret-key`
3. 确保两者完全一致
**解决方案**
```bash
# 前端
export const AES_SECRET_KEY = '1234567890qwer'
# 后端
security:
aes:
secret-key: 1234567890qwer
```
### 问题:"AES 加密工具未初始化"
**原因**`initAesEncrypt()` 未被调用
**解决**:检查 `main.ts` 中是否正确调用初始化函数
### 问题:加密后的数据无法解密
**可能原因**
1. 密钥不正确
2. 数据被篡改
3. Base64 编码问题
**调试方法**
```typescript
const aes = getAesInstance()
const original = 'test123'
const encrypted = await aes.encrypt(original)
const decrypted = await aes.decrypt(encrypted)
console.log(original === decrypted) // 应该输出 true
```
## API 参考
### 配置项
| 配置项 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| `AES_SECRET_KEY` | `string` | `'1234567890qwer'` | AES 加密密钥 |
| `API_BASE_URL` | `string` | `'http://localhost:8180'` | API 基础地址 |
| `APP_CONFIG.name` | `string` | `'泰豪电源 AI 数智化平台'` | 应用名称 |
| `APP_CONFIG.version` | `string` | `'1.0.0'` | 应用版本 |
### 环境变量
| 变量名 | 说明 | 示例 |
|--------|------|------|
| `VITE_API_BASE_URL` | API 基础地址 | `https://api.example.com` |
| `VUE_APP_AES_SECRET_KEY` | AES 密钥(生产) | `your-secret-key-32-chars-long` |
## 更多信息
- AES 加密工具详细文档:`@shared/utils/crypto/README.md`
- Auth API 文档:`@shared/api/auth/auth.ts`
- 后端 AES 实现:`urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/crypto/AesEncryptUtil.java`

View File

@@ -0,0 +1,24 @@
/**
* Platform 应用配置
*/
/**
* AES 加密密钥(与后端保持一致)
* 对应后端配置security.aes.secret-key
* Base64 编码的 32 字节密钥256 位)
*/
export const AES_SECRET_KEY = 'MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=' // Base64 编码,解码后是 "12345678901234567890123456789012" (32字节)
/**
* API 基础地址
*/
export const API_BASE_URL = (import.meta as any).env?.VITE_API_BASE_URL || 'http://localhost:8180'
/**
* 应用配置
*/
export const APP_CONFIG = {
name: '泰豪电源 AI 数智化平台',
version: '1.0.0',
copyright: '泰豪电源'
}

View File

@@ -0,0 +1,214 @@
# SidebarLayout 侧边栏布局组件
## 功能特性
### 🎯 核心功能
- **侧边栏菜单导航**:左侧固定侧边栏,支持折叠/展开
- **双模式内容区**
- **路由模式**:普通路由页面渲染
- **iframe 模式**:嵌入外部应用(招标助手、泰豪小电、智能体编排)
- **用户信息展示**:底部用户头像和下拉菜单
- **响应式设计**:支持移动端适配
### 📱 菜单配置
```typescript
const menuItems: MenuItem[] = [
{
key: 'home',
label: '工作台',
icon: 'Grid',
path: '/home',
type: 'route'
},
{
key: 'bidding',
label: '招标助手',
icon: 'Document',
iframeUrl: 'http://localhost:5002',
type: 'iframe'
},
{
key: 'service',
label: '泰豪小电',
icon: 'Service',
iframeUrl: 'http://localhost:5003',
type: 'iframe'
},
{
key: 'workflow',
label: '智能体编排',
icon: 'Connection',
iframeUrl: 'http://localhost:3000', // Dify 地址
type: 'iframe'
}
]
```
## 使用方式
### 在路由中使用
```typescript
// router/index.ts
import { SidebarLayout } from '@/layouts'
const routes = [
{
path: '/',
component: SidebarLayout,
children: [
{
path: '/home',
component: () => import('@/views/Home.vue')
},
{
path: '/chat',
component: () => import('@/views/Chat.vue')
}
]
}
]
```
### 在 App.vue 中使用
```vue
<template>
<SidebarLayout />
</template>
<script setup lang="ts">
import { SidebarLayout } from '@/layouts'
</script>
```
## 菜单项类型
```typescript
interface MenuItem {
key: string // 唯一标识
label: string // 显示名称
icon: string // Element Plus 图标组件名
path?: string // 路由路径route 类型必需)
iframeUrl?: string // iframe URLiframe 类型必需)
type: 'route' | 'iframe' // 菜单类型
}
```
## iframe 应用说明
### 1. 招标助手 (Bidding)
- **端口**5002
- **URL**http://localhost:5002
- **说明**:招投标业务管理系统
### 2. 泰豪小电 (Service)
- **端口**5003
- **URL**http://localhost:5003
- **说明**:智能客服工单管理系统
### 3. 智能体编排 (Workflow)
- **端口**3000
- **URL**http://localhost:3000
- **说明**Dify 智能体编排界面
## 样式自定义
### 主题色调整
```scss
// 修改侧边栏背景色
.sidebar {
background: #F0EAF4; // 当前淡紫色背景
}
// 修改激活项颜色
.nav-item.active {
background: rgba(124, 58, 237, 0.15);
color: #7c3aed;
}
```
### 侧边栏宽度
```scss
.sidebar {
width: 220px; // 展开宽度
&.collapsed {
width: 64px; // 折叠宽度
}
}
```
## 功能说明
### 侧边栏折叠
- 点击头部箭头图标可切换折叠/展开状态
- 折叠后只显示图标,展开后显示图标+文字
### iframe 加载
- 自动显示加载中状态
- 支持刷新按钮重新加载
- 显示当前应用标题
### 用户操作
- **个人中心**:跳转到 /profile
- **系统设置**:跳转到 /settings
- **退出登录**:跳转到 /login
## 注意事项
1. **跨域问题**:确保 iframe 应用允许被嵌入
```nginx
# nginx 配置
add_header X-Frame-Options "SAMEORIGIN";
# 或者
add_header Content-Security-Policy "frame-ancestors 'self' http://localhost:5001";
```
2. **端口配置**:确保对应服务已启动
- platform: 5001
- bidding: 5002
- workcase: 5003
- dify: 3000
3. **路由同步**iframe 模式不会改变浏览器 URL
4. **通信机制**:如需与 iframe 通信,使用 postMessage API
## 扩展建议
### 添加新菜单项
```typescript
// 在 menuItems 数组中添加
{
key: 'new-app',
label: '新应用',
icon: 'Plus',
iframeUrl: 'http://localhost:5004',
type: 'iframe'
}
```
### 动态菜单加载
```typescript
// 从 API 获取菜单配置
const loadMenus = async () => {
const response = await fetch('/api/menus')
menuItems.value = await response.json()
}
```
### 权限控制
```typescript
const menuItems = computed(() => {
return allMenuItems.filter(item =>
hasPermission(item.key)
)
})
```

View File

@@ -0,0 +1,264 @@
.sidebar-layout {
display: flex;
width: 100%;
height: 100vh;
overflow: hidden;
}
// ==================== 侧边栏 ====================
.sidebar {
width: 220px;
height: 100%;
background: #F0EAF4;
display: flex;
flex-direction: column;
color: #333;
flex-shrink: 0;
transition: width 0.3s ease;
border-right: 1px solid rgba(0, 0, 0, 0.08);
&.collapsed {
width: 64px;
.sidebar-header {
padding: 16px 12px;
justify-content: center;
.logo {
justify-content: center;
}
.collapse-btn {
position: static;
margin-left: 0;
}
}
.nav-item {
justify-content: center;
padding: 12px;
}
.user-section {
justify-content: center;
padding: 16px 12px;
}
}
}
// 侧边栏头部
.sidebar-header {
padding: 16px 20px;
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
display: flex;
align-items: center;
justify-content: space-between;
}
.collapse-btn {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
cursor: pointer;
color: #888;
transition: all 0.2s;
&:hover {
background: rgba(124, 58, 237, 0.1);
color: #7c3aed;
}
}
.logo {
display: flex;
align-items: center;
gap: 10px;
.logo-img {
width: 40px;
height: 40px;
border-radius: 6px;
object-fit: contain;
background: #fff;
padding: 2px;
}
.logo-text {
font-size: 16px;
font-weight: 600;
color: #333;
}
}
// 导航菜单
.nav-menu {
flex: 1;
overflow-y: auto;
padding: 12px 0;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 4px;
}
}
.nav-section {
padding: 8px 0;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 20px;
margin-bottom: 4px;
cursor: pointer;
transition: all 0.2s ease;
color: #555;
font-size: 14px;
&:hover {
background: rgba(124, 58, 237, 0.1);
color: #7c3aed;
}
&.active {
background: rgba(124, 58, 237, 0.15);
color: #7c3aed;
font-weight: 500;
}
.el-icon {
font-size: 18px;
flex-shrink: 0;
}
span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
// 用户信息
.user-section {
padding: 16px 20px;
border-top: 1px solid rgba(0, 0, 0, 0.08);
cursor: pointer;
transition: background 0.2s;
&:hover {
background: rgba(124, 58, 237, 0.05);
}
.user-info-wrapper {
display: flex;
align-items: center;
gap: 12px;
}
.user-avatar {
flex-shrink: 0;
}
.user-name {
font-size: 14px;
font-weight: 500;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
// ==================== 主内容区 ====================
.main-content {
flex: 1;
height: 100%;
overflow: hidden;
background: #fff;
position: relative;
}
// iframe 容器
.iframe-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
position: relative;
}
.iframe-header {
height: 56px;
padding: 0 24px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e5e7eb;
background: #fafafa;
flex-shrink: 0;
}
.iframe-title {
font-size: 16px;
font-weight: 600;
color: #333;
}
.content-iframe {
flex: 1;
width: 100%;
height: 100%;
border: none;
background: #fff;
}
.iframe-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
color: #7c3aed;
font-size: 14px;
z-index: 10;
.el-icon {
font-size: 32px;
}
}
// ==================== 响应式 ====================
@media (max-width: 768px) {
.sidebar {
width: 64px;
&:not(.collapsed) {
width: 220px;
position: fixed;
left: 0;
top: 0;
z-index: 1000;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
}
}
.iframe-header {
padding: 0 16px;
.iframe-title {
font-size: 14px;
}
}
}

View File

@@ -0,0 +1,248 @@
<template>
<div class="sidebar-layout">
<!-- 侧边栏 -->
<aside class="sidebar" :class="{ collapsed: collapsed }">
<div class="sidebar-header">
<div class="logo">
<img src="/logo.jpg" alt="Logo" class="logo-img" />
<span v-if="!collapsed" class="logo-text">城市生命线</span>
</div>
<div class="collapse-btn" @click="toggleSidebar">
<el-icon>
<DArrowLeft v-if="!collapsed" />
<DArrowRight v-else />
</el-icon>
</div>
</div>
<nav class="nav-menu">
<div class="nav-section">
<div
v-for="item in menuItems"
:key="item.key"
class="nav-item"
:class="{ active: activeMenu === item.key }"
@click="handleMenuClick(item)"
>
<el-icon><component :is="item.icon" /></el-icon>
<span v-if="!collapsed">{{ item.label }}</span>
</div>
</div>
</nav>
<!-- 用户信息 -->
<el-dropdown class="user-section" trigger="click" @command="handleUserCommand">
<div class="user-info-wrapper">
<div class="user-avatar">
<el-avatar :size="36" src="/avatar.svg" @error="handleAvatarError" />
</div>
<span v-if="!collapsed" class="user-name">{{ userName }}</span>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile">
<el-icon><User /></el-icon>
个人中心
</el-dropdown-item>
<el-dropdown-item command="settings" divided>
<el-icon><Setting /></el-icon>
系统设置
</el-dropdown-item>
<el-dropdown-item command="logout" divided>
<el-icon><SwitchButton /></el-icon>
退出登录
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</aside>
<!-- 主内容区 -->
<main class="main-content">
<!-- iframe 模式 -->
<div v-if="currentIframeUrl" class="iframe-container">
<div class="iframe-header">
<span class="iframe-title">{{ currentMenuItem?.label }}</span>
<el-button
text
@click="handleRefreshIframe"
:icon="Refresh"
>
刷新
</el-button>
</div>
<iframe
ref="iframeRef"
:src="currentIframeUrl"
class="content-iframe"
frameborder="0"
@load="handleIframeLoad"
/>
<div v-if="iframeLoading" class="iframe-loading">
<el-icon class="is-loading"><Loading /></el-icon>
<span>加载中...</span>
</div>
</div>
<!-- 路由模式 -->
<router-view v-else />
</main>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import {
ChatDotRound,
Grid,
Connection,
Document,
Service,
DArrowLeft,
DArrowRight,
User,
Setting,
SwitchButton,
Refresh,
Loading
} from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
interface MenuItem {
key: string
label: string
icon: string
path?: string
iframeUrl?: string
type: 'route' | 'iframe'
}
const router = useRouter()
const route = useRoute()
// 状态管理
const collapsed = ref(false)
const activeMenu = ref('home')
const iframeLoading = ref(false)
const iframeRef = ref<HTMLIFrameElement>()
const userName = ref('管理员')
// 菜单配置
const menuItems: MenuItem[] = [
{
key: 'home',
label: '工作台',
icon: 'Grid',
path: '/home',
type: 'route'
},
{
key: 'bidding',
label: '招标助手',
icon: 'Document',
iframeUrl: 'http://localhost:5002',
type: 'iframe'
},
{
key: 'service',
label: '泰豪小电',
icon: 'Service',
iframeUrl: 'http://localhost:5003',
type: 'iframe'
},
{
key: 'workflow',
label: '智能体编排',
icon: 'Connection',
iframeUrl: 'http://localhost:3000',
type: 'iframe'
},
{
key: 'chat',
label: 'AI助手',
icon: 'ChatDotRound',
path: '/chat',
type: 'route'
}
]
// 当前菜单项
const currentMenuItem = computed(() => {
return menuItems.find(item => item.key === activeMenu.value)
})
// 当前 iframe URL
const currentIframeUrl = computed(() => {
return currentMenuItem.value?.type === 'iframe'
? currentMenuItem.value.iframeUrl
: null
})
// 切换侧边栏
const toggleSidebar = () => {
collapsed.value = !collapsed.value
}
// 处理菜单点击
const handleMenuClick = (item: MenuItem) => {
activeMenu.value = item.key
if (item.type === 'route' && item.path) {
router.push(item.path)
} else if (item.type === 'iframe') {
iframeLoading.value = true
// iframe 模式不需要路由跳转
}
}
// iframe 加载完成
const handleIframeLoad = () => {
iframeLoading.value = false
}
// 刷新 iframe
const handleRefreshIframe = () => {
if (iframeRef.value) {
iframeLoading.value = true
iframeRef.value.src = iframeRef.value.src
}
}
// 用户头像加载错误
const handleAvatarError = () => {
return true
}
// 用户操作
const handleUserCommand = (command: string) => {
switch (command) {
case 'profile':
router.push('/profile')
break
case 'settings':
router.push('/settings')
break
case 'logout':
ElMessage.success('退出成功')
router.push('/login')
break
}
}
// 监听路由变化,同步激活菜单
watch(
() => route.path,
(newPath) => {
const menuItem = menuItems.find(item => item.path === newPath)
if (menuItem) {
activeMenu.value = menuItem.key
}
},
{ immediate: true }
)
</script>
<style lang="scss" scoped>
@import url("./SidebarLayout.scss");
</style>

View File

@@ -0,0 +1 @@
export { default as SidebarLayout } from "./SidebarLayout/SidebarLayout.vue";

View File

@@ -0,0 +1,47 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router/'
import { AES_SECRET_KEY } from './config'
import { initAesEncrypt } from 'shared/utils'
// 异步初始化应用
async function initApp() {
// 1. 初始化 AES 加密工具
try {
await initAesEncrypt(AES_SECRET_KEY)
console.log('✅ AES 加密工具初始化成功')
} catch (error) {
console.error('❌ AES 加密工具初始化失败:', error)
}
// 2. 创建 Vue 应用
const app = createApp(App)
// 3. 注册 Pinia
const pinia = createPinia()
app.use(pinia)
// 4. 注册 Element Plus
app.use(ElementPlus)
// 5. 注册所有 Element Plus 图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
// 6. 注册路由
app.use(router)
// 7. 挂载应用
app.mount('#app')
console.log('✅ Platform 应用启动成功')
}
// 启动应用
initApp()

View File

@@ -0,0 +1,60 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { SidebarLayout } from '../layouts'
import { TokenManager } from 'shared/api'
const routes: RouteRecordRaw[] = [
{
path: '/',
redirect: '/home'
},
{
path: '/login',
name: 'Login',
component: () => import('../views/public/Login.vue'),
meta: {
title: '登录',
requiresAuth: false // 不需要登录
}
},
{
path: '/home',
name: 'Home',
component: SidebarLayout,
meta: {
title: '首页',
requiresAuth: true // 需要登录
}
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// 路由守卫
router.beforeEach((to, from, next) => {
// 设置页面标题
if (to.meta.title) {
document.title = `${to.meta.title} - 泰豪电源 AI 数智化平台`
}
// 检查是否需要登录
const requiresAuth = to.meta.requiresAuth !== false // 默认需要登录
const hasToken = TokenManager.hasToken()
if (requiresAuth && !hasToken) {
// 需要登录但未登录,跳转到登录页
next({
path: '/login',
query: { redirect: to.fullPath } // 保存原始路径
})
} else if (to.path === '/login' && hasToken) {
// 已登录但访问登录页,跳转到首页
next('/home')
} else {
next()
}
})
export default router

View File

@@ -0,0 +1,54 @@
/**
* Shared Module Federation 类型声明
* 用于 TypeScript 识别远程模块
*/
declare module 'shared/FileUpload' {
import { DefineComponent } from 'vue'
const FileUpload: DefineComponent<{}, {}, any>
export default FileUpload
}
declare module 'shared/DynamicFormItem' {
import { DefineComponent } from 'vue'
const DynamicFormItem: DefineComponent<{}, {}, any>
export default DynamicFormItem
}
declare module 'shared/api' {
export const api: any
export const TokenManager: any
}
declare module 'shared/authAPI' {
export const authAPI: any
}
declare module 'shared/fileAPI' {
export const fileAPI: any
}
declare module 'shared/utils' {
export const initAesEncrypt: any
export const getAesInstance: any
export const formatFileSize: any
export const isImageFile: any
export const getFileTypeIcon: any
export const isValidFileType: any
export const getFilePreviewUrl: any
}
declare module 'shared/types' {
export type LoginParam = any
export type LoginDomain = any
export type SysUserVO = any
export type TbSysFileDTO = any
export type SysConfigVO = any
export type ResultDomain<T = any> = any
}
declare module 'shared/components' {
export const FileUpload: any
export const DynamicFormItem: any
}

View File

@@ -1,156 +0,0 @@
<script setup lang="ts">
/**
* Import Maps 使用示例
*
* 通过 HTML 中的 <script type="importmap"> 配置,
* 可以直接从 HTTP URL 导入共享组件
*/
// ✅ 直接导入!浏览器会自动从 http://localhost/shared/components.js 加载
import { UlTable } from '@shared/components'
import { http } from '@shared/utils'
import { authApi } from '@shared/api'
import { useTable } from '@shared/composables'
// 类型定义
interface User {
id: string
username: string
email: string
createTime: string
}
// 使用共享的 useTable 组合式函数
const {
loading,
tableData,
pagination,
handlePageChange,
handleSizeChange,
refresh
} = useTable<User>({
fetchData: async (params) => {
// 使用共享的 API 函数
return await authApi.getUserList(params)
}
})
// 表格列配置
const columns = [
{ prop: 'username', label: '用户名', minWidth: 150 },
{ prop: 'email', label: '邮箱', minWidth: 200 },
{ prop: 'createTime', label: '创建时间', width: 180 }
]
// 测试 HTTP 请求
const testRequest = async () => {
try {
// 使用共享的 http 工具
const result = await http.get('/api/test')
console.log('请求结果:', result)
} catch (error) {
console.error('请求失败:', error)
}
}
</script>
<template>
<div class="import-maps-example">
<div class="header">
<h1>Import Maps 示例</h1>
<p class="description">
共享组件从 <code>http://localhost/shared/</code> 动态加载<br>
无需打包浏览器原生 ES Module 支持<br>
真正的运行时共享所有应用使用同一份代码
</p>
<el-button type="primary" @click="testRequest">
测试 HTTP 请求
</el-button>
</div>
<!-- 使用从 HTTP 加载的共享组件 -->
<UlTable
:data="tableData"
:columns="columns"
:loading="loading"
:pagination="pagination"
@page-change="handlePageChange"
@size-change="handleSizeChange"
/>
<div class="info">
<h3>📦 当前加载的模块</h3>
<ul>
<li><code>@shared/components</code> http://localhost/shared/components.js</li>
<li><code>@shared/utils</code> http://localhost/shared/utils.js</li>
<li><code>@shared/api</code> http://localhost/shared/api.js</li>
<li><code>@shared/composables</code> http://localhost/shared/composables.js</li>
</ul>
<h3>🔍 如何查看</h3>
<ol>
<li>打开浏览器开发者工具 (F12)</li>
<li>切换到 Network 标签页</li>
<li>筛选 JS 类型</li>
<li>刷新页面可以看到从 /shared/ 加载的模块</li>
</ol>
<h3> 优势</h3>
<ul>
<li> 真正的代码共享所有应用共用一份</li>
<li> 支持热更新修改共享组件所有应用自动更新</li>
<li> 减小构建体积共享代码不打包到业务应用</li>
<li> 浏览器缓存共享模块只下载一次</li>
</ul>
</div>
</div>
</template>
<style scoped>
.import-maps-example {
padding: 24px;
}
.header {
margin-bottom: 24px;
}
.description {
color: #666;
line-height: 1.8;
margin: 16px 0;
}
.description code {
background: #f0f0f0;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Courier New', monospace;
}
.info {
margin-top: 32px;
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
.info h3 {
margin-top: 20px;
margin-bottom: 12px;
color: #409eff;
}
.info ul, .info ol {
line-height: 2;
color: #606266;
}
.info code {
background: #fff;
padding: 2px 8px;
border-radius: 3px;
font-family: 'Courier New', monospace;
color: #e6a23c;
}
</style>

View File

@@ -1,24 +1,256 @@
<template>
<div>
<div class="login-container">
<div class="login-card">
<div class="login-header">
<h1 class="login-title">泰豪电源 AI 数智化平台</h1>
<p class="login-subtitle">Urban Lifeline Platform</p>
</div>
<el-form
ref="loginFormRef"
:model="loginForm"
:rules="loginRules"
class="login-form"
@submit.prevent="handleLogin"
>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="请输入用户名"
size="large"
prefix-icon="User"
clearable
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="请输入密码"
size="large"
prefix-icon="Lock"
show-password
@keyup.enter="handleLogin"
/>
</el-form-item>
<el-form-item>
<el-checkbox v-model="loginForm.rememberMe">
记住我
</el-checkbox>
</el-form-item>
<el-form-item>
<el-button
type="primary"
size="large"
:loading="loading"
class="login-button"
@click="handleLogin"
>
{{ loading ? '登录中...' : '登录' }}
</el-button>
</el-form-item>
</el-form>
<div class="login-footer">
<span class="footer-link">忘记密码</span>
<span class="footer-divider">|</span>
<span class="footer-link">注册账号</span>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive } from "vue"
import type { LoginParam} from "@shared/types/auth"
import { authAPI } from "@shared/api/auth/auth"
import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import type { LoginParam } from 'shared/types'
import { authAPI } from 'shared/authAPI'
import { getAesInstance } from 'shared/utils'
import { TokenManager } from 'shared/api'
// 路由
const router = useRouter()
const loginParam = reactive<LoginParam>({
loginType: "password"
// 表单引用
const loginFormRef = ref<FormInstance>()
// 加载状态
const loading = ref(false)
// 登录表单
const loginForm = reactive({
username: '',
password: '',
rememberMe: false
})
async function login(){
// const result = await authAPI.login(loginParam)
// 表单验证规则
const loginRules: FormRules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '用户名长度为3-20个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码至少6个字符', trigger: 'blur' }
]
}
/**
* 处理登录
*/
async function handleLogin() {
if (!loginFormRef.value) return
try {
// 1. 表单验证
await loginFormRef.value.validate()
// 2. 显示加载状态
loading.value = true
// 3. 获取 AES 加密实例
const aes = getAesInstance()
// 4. 加密密码
const encryptedPassword = await aes.encryptPassword(loginForm.password)
// 5. 构建登录参数
const loginParam: LoginParam = {
username: loginForm.username,
password: encryptedPassword, // 使用加密后的密码
loginType: 'password'
}
// 6. 调用登录接口
const response = await authAPI.login(loginParam)
// 7. 检查登录结果
if (response.data.success && response.data.data) {
const loginData = response.data.data
// 8. 保存 Token
if (loginData.token) {
TokenManager.setToken(loginData.token, loginForm.rememberMe)
}
// 9. 显示成功消息
ElMessage.success('登录成功!')
// 10. 跳转到首页
router.push('/home')
} else {
// 登录失败
ElMessage.error(response.data.message || '登录失败,请检查用户名和密码')
}
} catch (error: any) {
console.error('登录失败:', error)
// 显示错误消息
if (error.response) {
// HTTP 错误
ElMessage.error(error.response.data?.message || '登录失败,请稍后重试')
} else if (error.message) {
// 其他错误
ElMessage.error(error.message)
} else {
ElMessage.error('登录失败,请检查网络连接')
}
} finally {
// 隐藏加载状态
loading.value = false
}
}
</script>
<style lang="scss" scoped>
.login-container {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
}
.login-card {
width: 100%;
max-width: 420px;
padding: 40px;
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.login-header {
text-align: center;
margin-bottom: 40px;
}
.login-title {
font-size: 24px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.login-subtitle {
font-size: 14px;
color: #999;
}
.login-form {
:deep(.el-form-item) {
margin-bottom: 24px;
}
:deep(.el-input__wrapper) {
padding: 12px 16px;
}
}
.login-button {
width: 100%;
height: 44px;
font-size: 16px;
font-weight: 500;
border-radius: 8px;
}
.login-footer {
display: flex;
align-items: center;
justify-content: center;
margin-top: 24px;
font-size: 14px;
color: #999;
}
.footer-link {
cursor: pointer;
transition: color 0.3s;
&:hover {
color: var(--el-color-primary);
}
}
.footer-divider {
margin: 0 12px;
}
// 响应式设计
@media (max-width: 480px) {
.login-card {
padding: 30px 20px;
}
.login-title {
font-size: 20px;
}
}
</style>

View File

@@ -16,8 +16,7 @@
"noEmit": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@shared/*": ["../shared/src/*"]
"@/*": ["src/*"]
}
},
"include": [

View File

@@ -1,14 +1,44 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import { federation } from '@module-federation/vite'
import { resolve, dirname } from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
export default defineConfig({
plugins: [vue(), vueJsx()],
plugins: [
vue({
script: {
defineModel: true,
propsDestructure: true
}
}),
vueJsx(),
federation({
name: 'platform',
remotes: {
shared: {
type: 'module',
name: 'shared',
entry: 'http://localhost:5000/remoteEntry.js'
}
},
shared: {
vue: {},
'vue-router': {},
'element-plus': {},
axios: {}
}
})
],
define: {
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: true,
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: true
},
resolve: {
alias: {
@@ -25,11 +55,6 @@ export default defineConfig({
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/api/, '')
},
// 代理共享模块请求到 shared 服务
'/shared': {
target: 'http://localhost:5000',
changeOrigin: true
}
}
},

View File

@@ -0,0 +1,192 @@
// Windows temporarily needs this file, https://github.com/module-federation/vite/issues/68
import {loadShare} from "@module-federation/runtime";
const importMap = {
"@element-plus/icons-vue": async () => {
let pkg = await import("__mf__virtual/shared__prebuild___mf_0_element_mf_2_plus_mf_1_icons_mf_2_vue__prebuild__.js");
return pkg;
}
,
"axios": async () => {
let pkg = await import("__mf__virtual/shared__prebuild__axios__prebuild__.js");
return pkg;
}
,
"element-plus": async () => {
let pkg = await import("__mf__virtual/shared__prebuild__element_mf_2_plus__prebuild__.js");
return pkg;
}
,
"vue": async () => {
let pkg = await import("__mf__virtual/shared__prebuild__vue__prebuild__.js");
return pkg;
}
,
"vue-router": async () => {
let pkg = await import("__mf__virtual/shared__prebuild__vue_mf_2_router__prebuild__.js");
return pkg;
}
}
const usedShared = {
"@element-plus/icons-vue": {
name: "@element-plus/icons-vue",
version: "2.3.2",
scope: ["default"],
loaded: false,
from: "shared",
async get () {
if (false) {
throw new Error(`Shared module '${"@element-plus/icons-vue"}' must be provided by host`);
}
usedShared["@element-plus/icons-vue"].loaded = true
const {"@element-plus/icons-vue": pkgDynamicImport} = importMap
const res = await pkgDynamicImport()
const exportModule = {...res}
// All npm packages pre-built by vite will be converted to esm
Object.defineProperty(exportModule, "__esModule", {
value: true,
enumerable: false
})
return function () {
return exportModule
}
},
shareConfig: {
singleton: false,
requiredVersion: "^2.3.2",
}
}
,
"axios": {
name: "axios",
version: "1.13.2",
scope: ["default"],
loaded: false,
from: "shared",
async get () {
if (false) {
throw new Error(`Shared module '${"axios"}' must be provided by host`);
}
usedShared["axios"].loaded = true
const {"axios": pkgDynamicImport} = importMap
const res = await pkgDynamicImport()
const exportModule = {...res}
// All npm packages pre-built by vite will be converted to esm
Object.defineProperty(exportModule, "__esModule", {
value: true,
enumerable: false
})
return function () {
return exportModule
}
},
shareConfig: {
singleton: false,
requiredVersion: "^1.13.2",
}
}
,
"element-plus": {
name: "element-plus",
version: "2.12.0",
scope: ["default"],
loaded: false,
from: "shared",
async get () {
if (false) {
throw new Error(`Shared module '${"element-plus"}' must be provided by host`);
}
usedShared["element-plus"].loaded = true
const {"element-plus": pkgDynamicImport} = importMap
const res = await pkgDynamicImport()
const exportModule = {...res}
// All npm packages pre-built by vite will be converted to esm
Object.defineProperty(exportModule, "__esModule", {
value: true,
enumerable: false
})
return function () {
return exportModule
}
},
shareConfig: {
singleton: false,
requiredVersion: "^2.12.0",
}
}
,
"vue": {
name: "vue",
version: "3.5.25",
scope: ["default"],
loaded: false,
from: "shared",
async get () {
if (false) {
throw new Error(`Shared module '${"vue"}' must be provided by host`);
}
usedShared["vue"].loaded = true
const {"vue": pkgDynamicImport} = importMap
const res = await pkgDynamicImport()
const exportModule = {...res}
// All npm packages pre-built by vite will be converted to esm
Object.defineProperty(exportModule, "__esModule", {
value: true,
enumerable: false
})
return function () {
return exportModule
}
},
shareConfig: {
singleton: false,
requiredVersion: "^3.5.25",
}
}
,
"vue-router": {
name: "vue-router",
version: "4.6.3",
scope: ["default"],
loaded: false,
from: "shared",
async get () {
if (false) {
throw new Error(`Shared module '${"vue-router"}' must be provided by host`);
}
usedShared["vue-router"].loaded = true
const {"vue-router": pkgDynamicImport} = importMap
const res = await pkgDynamicImport()
const exportModule = {...res}
// All npm packages pre-built by vite will be converted to esm
Object.defineProperty(exportModule, "__esModule", {
value: true,
enumerable: false
})
return function () {
return exportModule
}
},
shareConfig: {
singleton: false,
requiredVersion: "^4.6.3",
}
}
}
const usedRemotes = [
]
export {
usedShared,
usedRemotes
}

View File

@@ -5,24 +5,24 @@
"private": true,
"scripts": {
"dev": "vite",
"dev:demo": "vite",
"build": "run-p build:*",
"build:esm": "vite build --mode esm",
"build:federation": "vite build --mode federation",
"preview": "vite preview"
"build": "vite build",
"preview": "vite preview",
"serve": "node server.js"
},
"dependencies": {
"vue": "^3.5.13",
"ofetch": "^1.4.1",
"vue-router": "^4.5.0",
"@element-plus/icons-vue": "^2.3.2",
"cors": "^2.8.5",
"element-plus": "^2.12.0",
"@element-plus/icons-vue": "^2.3.2"
"express": "^4.18.2",
"ofetch": "^1.4.1",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@module-federation/vite": "^1.9.3",
"@types/node": "^20.10.0",
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"@originjs/vite-plugin-federation": "^1.3.6",
"npm-run-all": "^4.1.5",
"sass": "^1.80.6",
"sass-embedded": "^1.80.6",
@@ -30,7 +30,7 @@
"vite": "^6.0.3"
},
"peerDependencies": {
"vue": "^3.5.13",
"typescript": ">=5.0.0"
"typescript": ">=5.0.0",
"vue": "^3.5.13"
}
}

View File

@@ -8,9 +8,15 @@ dependencies:
'@element-plus/icons-vue':
specifier: ^2.3.2
version: 2.3.2(vue@3.5.25)
cors:
specifier: ^2.8.5
version: 2.8.5
element-plus:
specifier: ^2.12.0
version: 2.12.0(vue@3.5.25)
express:
specifier: ^4.18.2
version: 4.22.1
ofetch:
specifier: ^1.4.1
version: 1.5.1
@@ -22,9 +28,9 @@ dependencies:
version: 4.6.3(vue@3.5.25)
devDependencies:
'@originjs/vite-plugin-federation':
specifier: ^1.3.6
version: 1.4.1
'@module-federation/vite':
specifier: ^1.9.3
version: 1.9.3
'@types/node':
specifier: ^20.10.0
version: 20.19.25
@@ -603,12 +609,41 @@ packages:
'@jridgewell/sourcemap-codec': 1.5.5
dev: true
/@originjs/vite-plugin-federation@1.4.1:
resolution: {integrity: sha512-Uo08jW5pj1t58OUKuZNkmzcfTN2pqeVuAWCCiKf/75/oll4Efq4cHOqSE1FXMlvwZNGDziNdDyBbQ5IANem3CQ==}
engines: {node: '>=14.0.0', pnpm: '>=7.0.1'}
/@module-federation/error-codes@0.21.6:
resolution: {integrity: sha512-MLJUCQ05KnoVl8xd6xs9a5g2/8U+eWmVxg7xiBMeR0+7OjdWUbHwcwgVFatRIwSZvFgKHfWEiI7wsU1q1XbTRQ==}
dev: true
/@module-federation/runtime-core@0.21.6:
resolution: {integrity: sha512-5Hd1Y5qp5lU/aTiK66lidMlM/4ji2gr3EXAtJdreJzkY+bKcI5+21GRcliZ4RAkICmvdxQU5PHPL71XmNc7Lsw==}
dependencies:
estree-walker: 3.0.3
magic-string: 0.27.0
'@module-federation/error-codes': 0.21.6
'@module-federation/sdk': 0.21.6
dev: true
/@module-federation/runtime@0.21.6:
resolution: {integrity: sha512-+caXwaQqwTNh+CQqyb4mZmXq7iEemRDrTZQGD+zyeH454JAYnJ3s/3oDFizdH6245pk+NiqDyOOkHzzFQorKhQ==}
dependencies:
'@module-federation/error-codes': 0.21.6
'@module-federation/runtime-core': 0.21.6
'@module-federation/sdk': 0.21.6
dev: true
/@module-federation/sdk@0.21.6:
resolution: {integrity: sha512-x6hARETb8iqHVhEsQBysuWpznNZViUh84qV2yE7AD+g7uIzHKiYdoWqj10posbo5XKf/147qgWDzKZoKoEP2dw==}
dev: true
/@module-federation/vite@1.9.3:
resolution: {integrity: sha512-MV6XI3FX6okEMJ7FdmvFmYuu7DygRoLljKT8atrBwFhlttsgBbswpqMj4P4Fs/X+pFmbIi/ntFzVhsrG0qQnGQ==}
dependencies:
'@module-federation/runtime': 0.21.6
'@module-federation/sdk': 0.21.6
'@rollup/pluginutils': 5.3.0
defu: 6.1.4
estree-walker: 2.0.2
magic-string: 0.30.21
pathe: 1.1.2
transitivePeerDependencies:
- rollup
dev: true
/@parcel/watcher-android-arm64@2.5.1:
@@ -764,6 +799,20 @@ packages:
resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==}
dev: true
/@rollup/pluginutils@5.3.0:
resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
peerDependenciesMeta:
rollup:
optional: true
dependencies:
'@types/estree': 1.0.8
estree-walker: 2.0.2
picomatch: 4.0.3
dev: true
/@rollup/rollup-android-arm-eabi@4.53.3:
resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==}
cpu: [arm]
@@ -1142,6 +1191,14 @@ packages:
- vue
dev: false
/accepts@1.3.8:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
dependencies:
mime-types: 2.1.35
negotiator: 0.6.3
dev: false
/ansi-styles@3.2.1:
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
engines: {node: '>=4'}
@@ -1157,6 +1214,10 @@ packages:
is-array-buffer: 3.0.5
dev: true
/array-flatten@1.1.1:
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
dev: false
/arraybuffer.prototype.slice@1.0.4:
resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
engines: {node: '>= 0.4'}
@@ -1195,6 +1256,26 @@ packages:
hasBin: true
dev: true
/body-parser@1.20.4:
resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
dependencies:
bytes: 3.1.2
content-type: 1.0.5
debug: 2.6.9
depd: 2.0.0
destroy: 1.2.0
http-errors: 2.0.1
iconv-lite: 0.4.24
on-finished: 2.4.1
qs: 6.14.0
raw-body: 2.5.3
type-is: 1.6.18
unpipe: 1.0.0
transitivePeerDependencies:
- supports-color
dev: false
/brace-expansion@1.1.12:
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
dependencies:
@@ -1227,13 +1308,17 @@ packages:
resolution: {integrity: sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==}
dev: true
/bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
dev: false
/call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
dependencies:
es-errors: 1.3.0
function-bind: 1.1.2
dev: true
/call-bind@1.0.8:
resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==}
@@ -1251,7 +1336,6 @@ packages:
dependencies:
call-bind-apply-helpers: 1.0.2
get-intrinsic: 1.3.0
dev: true
/caniuse-lite@1.0.30001759:
resolution: {integrity: sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==}
@@ -1291,10 +1375,39 @@ packages:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
dev: true
/content-disposition@0.5.4:
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
engines: {node: '>= 0.6'}
dependencies:
safe-buffer: 5.2.1
dev: false
/content-type@1.0.5:
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
engines: {node: '>= 0.6'}
dev: false
/convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
dev: true
/cookie-signature@1.0.7:
resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==}
dev: false
/cookie@0.7.2:
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
engines: {node: '>= 0.6'}
dev: false
/cors@2.8.5:
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
engines: {node: '>= 0.10'}
dependencies:
object-assign: 4.1.1
vary: 1.1.2
dev: false
/cross-spawn@6.0.6:
resolution: {integrity: sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==}
engines: {node: '>=4.8'}
@@ -1340,6 +1453,17 @@ packages:
resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
dev: false
/debug@2.6.9:
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dependencies:
ms: 2.0.0
dev: false
/debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
@@ -1370,10 +1494,24 @@ packages:
object-keys: 1.1.1
dev: true
/defu@6.1.4:
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
dev: true
/depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
dev: false
/destr@2.0.5:
resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==}
dev: false
/destroy@1.2.0:
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
dev: false
/detect-libc@1.0.3:
resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
engines: {node: '>=0.10'}
@@ -1389,7 +1527,10 @@ packages:
call-bind-apply-helpers: 1.0.2
es-errors: 1.3.0
gopd: 1.2.0
dev: true
/ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
dev: false
/electron-to-chromium@1.5.266:
resolution: {integrity: sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg==}
@@ -1419,6 +1560,16 @@ packages:
- '@vue/composition-api'
dev: false
/encodeurl@1.0.2:
resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
engines: {node: '>= 0.8'}
dev: false
/encodeurl@2.0.0:
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
engines: {node: '>= 0.8'}
dev: false
/entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
@@ -1492,19 +1643,16 @@ packages:
/es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'}
dev: true
/es-errors@1.3.0:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
dev: true
/es-object-atoms@1.1.1:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
dependencies:
es-errors: 1.3.0
dev: true
/es-set-tostringtag@2.1.0:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
@@ -1564,6 +1712,10 @@ packages:
engines: {node: '>=6'}
dev: true
/escape-html@1.0.3:
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
dev: false
/escape-string-regexp@1.0.5:
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
engines: {node: '>=0.8.0'}
@@ -1572,11 +1724,49 @@ packages:
/estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
/estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
/etag@1.8.1:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
dev: false
/express@4.22.1:
resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==}
engines: {node: '>= 0.10.0'}
dependencies:
'@types/estree': 1.0.8
dev: true
accepts: 1.3.8
array-flatten: 1.1.1
body-parser: 1.20.4
content-disposition: 0.5.4
content-type: 1.0.5
cookie: 0.7.2
cookie-signature: 1.0.7
debug: 2.6.9
depd: 2.0.0
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
finalhandler: 1.3.2
fresh: 0.5.2
http-errors: 2.0.1
merge-descriptors: 1.0.3
methods: 1.1.2
on-finished: 2.4.1
parseurl: 1.3.3
path-to-regexp: 0.1.12
proxy-addr: 2.0.7
qs: 6.14.0
range-parser: 1.2.1
safe-buffer: 5.2.1
send: 0.19.1
serve-static: 1.16.2
setprototypeof: 1.2.0
statuses: 2.0.2
type-is: 1.6.18
utils-merge: 1.0.1
vary: 1.1.2
transitivePeerDependencies:
- supports-color
dev: false
/fdir@6.5.0(picomatch@4.0.3):
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
@@ -1599,6 +1789,21 @@ packages:
dev: true
optional: true
/finalhandler@1.3.2:
resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==}
engines: {node: '>= 0.8'}
dependencies:
debug: 2.6.9
encodeurl: 2.0.0
escape-html: 1.0.3
on-finished: 2.4.1
parseurl: 1.3.3
statuses: 2.0.2
unpipe: 1.0.0
transitivePeerDependencies:
- supports-color
dev: false
/for-each@0.3.5:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'}
@@ -1606,6 +1811,16 @@ packages:
is-callable: 1.2.7
dev: true
/forwarded@0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'}
dev: false
/fresh@0.5.2:
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
engines: {node: '>= 0.6'}
dev: false
/fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -1616,7 +1831,6 @@ packages:
/function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
dev: true
/function.prototype.name@1.1.8:
resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==}
@@ -1658,7 +1872,6 @@ packages:
has-symbols: 1.1.0
hasown: 2.0.2
math-intrinsics: 1.1.0
dev: true
/get-proto@1.0.1:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
@@ -1666,7 +1879,6 @@ packages:
dependencies:
dunder-proto: 1.0.1
es-object-atoms: 1.1.1
dev: true
/get-symbol-description@1.1.0:
resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
@@ -1688,7 +1900,6 @@ packages:
/gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
dev: true
/graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
@@ -1725,7 +1936,6 @@ packages:
/has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'}
dev: true
/has-tostringtag@1.0.2:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
@@ -1739,16 +1949,48 @@ packages:
engines: {node: '>= 0.4'}
dependencies:
function-bind: 1.1.2
dev: true
/hosted-git-info@2.8.9:
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
dev: true
/http-errors@2.0.0:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'}
dependencies:
depd: 2.0.0
inherits: 2.0.4
setprototypeof: 1.2.0
statuses: 2.0.1
toidentifier: 1.0.1
dev: false
/http-errors@2.0.1:
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
engines: {node: '>= 0.8'}
dependencies:
depd: 2.0.0
inherits: 2.0.4
setprototypeof: 1.2.0
statuses: 2.0.2
toidentifier: 1.0.1
dev: false
/iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
dependencies:
safer-buffer: 2.1.2
dev: false
/immutable@5.1.4:
resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==}
dev: true
/inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
dev: false
/internal-slot@1.1.0:
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
engines: {node: '>= 0.4'}
@@ -1758,6 +2000,11 @@ packages:
side-channel: 1.1.0
dev: true
/ipaddr.js@1.9.1:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'}
dev: false
/is-array-buffer@3.0.5:
resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
engines: {node: '>= 0.4'}
@@ -2015,13 +2262,6 @@ packages:
yallist: 3.1.1
dev: true
/magic-string@0.27.0:
resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==}
engines: {node: '>=12'}
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
dev: true
/magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
dependencies:
@@ -2030,7 +2270,11 @@ packages:
/math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
dev: true
/media-typer@0.3.0:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'}
dev: false
/memoize-one@6.0.0:
resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
@@ -2041,6 +2285,15 @@ packages:
engines: {node: '>= 0.10.0'}
dev: true
/merge-descriptors@1.0.3:
resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==}
dev: false
/methods@1.1.2:
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
engines: {node: '>= 0.6'}
dev: false
/micromatch@4.0.8:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
@@ -2051,21 +2304,47 @@ packages:
dev: true
optional: true
/mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
dev: false
/mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
dependencies:
mime-db: 1.52.0
dev: false
/mime@1.6.0:
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
engines: {node: '>=4'}
hasBin: true
dev: false
/minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
dependencies:
brace-expansion: 1.1.12
dev: true
/ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
dev: false
/ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
dev: true
/nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
/negotiator@0.6.3:
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
engines: {node: '>= 0.6'}
dev: false
/nice-try@1.0.5:
resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==}
dev: true
@@ -2113,10 +2392,14 @@ packages:
string.prototype.padend: 3.1.6
dev: true
/object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
dev: false
/object-inspect@1.13.4:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'}
dev: true
/object-keys@1.1.1:
resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
@@ -2143,6 +2426,13 @@ packages:
ufo: 1.6.1
dev: false
/on-finished@2.4.1:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'}
dependencies:
ee-first: 1.1.1
dev: false
/own-keys@1.0.1:
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
engines: {node: '>= 0.4'}
@@ -2160,6 +2450,11 @@ packages:
json-parse-better-errors: 1.0.2
dev: true
/parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
dev: false
/path-key@2.0.1:
resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==}
engines: {node: '>=4'}
@@ -2169,6 +2464,10 @@ packages:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
dev: true
/path-to-regexp@0.1.12:
resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==}
dev: false
/path-type@3.0.0:
resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==}
engines: {node: '>=4'}
@@ -2176,6 +2475,10 @@ packages:
pify: 3.0.0
dev: true
/pathe@1.1.2:
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
dev: true
/picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -2215,6 +2518,36 @@ packages:
picocolors: 1.1.1
source-map-js: 1.2.1
/proxy-addr@2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'}
dependencies:
forwarded: 0.2.0
ipaddr.js: 1.9.1
dev: false
/qs@6.14.0:
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
engines: {node: '>=0.6'}
dependencies:
side-channel: 1.1.0
dev: false
/range-parser@1.2.1:
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
engines: {node: '>= 0.6'}
dev: false
/raw-body@2.5.3:
resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==}
engines: {node: '>= 0.8'}
dependencies:
bytes: 3.1.2
http-errors: 2.0.1
iconv-lite: 0.4.24
unpipe: 1.0.0
dev: false
/read-pkg@3.0.0:
resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==}
engines: {node: '>=4'}
@@ -2314,6 +2647,10 @@ packages:
isarray: 2.0.5
dev: true
/safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
dev: false
/safe-push-apply@1.0.0:
resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==}
engines: {node: '>= 0.4'}
@@ -2331,6 +2668,10 @@ packages:
is-regex: 1.2.1
dev: true
/safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
dev: false
/sass-embedded-all-unknown@1.93.3:
resolution: {integrity: sha512-3okGgnE41eg+CPLtAPletu6nQ4N0ij7AeW+Sl5Km4j29XcmqZQeFwYjHe1AlKTEgLi/UAONk1O8i8/lupeKMbw==}
cpu: ['!arm', '!arm64', '!riscv64', '!x64']
@@ -2571,6 +2912,60 @@ packages:
hasBin: true
dev: true
/send@0.19.0:
resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==}
engines: {node: '>= 0.8.0'}
dependencies:
debug: 2.6.9
depd: 2.0.0
destroy: 1.2.0
encodeurl: 1.0.2
escape-html: 1.0.3
etag: 1.8.1
fresh: 0.5.2
http-errors: 2.0.0
mime: 1.6.0
ms: 2.1.3
on-finished: 2.4.1
range-parser: 1.2.1
statuses: 2.0.1
transitivePeerDependencies:
- supports-color
dev: false
/send@0.19.1:
resolution: {integrity: sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==}
engines: {node: '>= 0.8.0'}
dependencies:
debug: 2.6.9
depd: 2.0.0
destroy: 1.2.0
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
fresh: 0.5.2
http-errors: 2.0.0
mime: 1.6.0
ms: 2.1.3
on-finished: 2.4.1
range-parser: 1.2.1
statuses: 2.0.1
transitivePeerDependencies:
- supports-color
dev: false
/serve-static@1.16.2:
resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==}
engines: {node: '>= 0.8.0'}
dependencies:
encodeurl: 2.0.0
escape-html: 1.0.3
parseurl: 1.3.3
send: 0.19.0
transitivePeerDependencies:
- supports-color
dev: false
/set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
engines: {node: '>= 0.4'}
@@ -2602,6 +2997,10 @@ packages:
es-object-atoms: 1.1.1
dev: true
/setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
dev: false
/shebang-command@1.2.0:
resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==}
engines: {node: '>=0.10.0'}
@@ -2625,7 +3024,6 @@ packages:
dependencies:
es-errors: 1.3.0
object-inspect: 1.13.4
dev: true
/side-channel-map@1.0.1:
resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
@@ -2635,7 +3033,6 @@ packages:
es-errors: 1.3.0
get-intrinsic: 1.3.0
object-inspect: 1.13.4
dev: true
/side-channel-weakmap@1.0.2:
resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
@@ -2646,7 +3043,6 @@ packages:
get-intrinsic: 1.3.0
object-inspect: 1.13.4
side-channel-map: 1.0.1
dev: true
/side-channel@1.1.0:
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
@@ -2657,7 +3053,6 @@ packages:
side-channel-list: 1.0.0
side-channel-map: 1.0.1
side-channel-weakmap: 1.0.2
dev: true
/source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
@@ -2685,6 +3080,16 @@ packages:
resolution: {integrity: sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==}
dev: true
/statuses@2.0.1:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'}
dev: false
/statuses@2.0.2:
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
engines: {node: '>= 0.8'}
dev: false
/stop-iteration-iterator@1.1.0:
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
engines: {node: '>= 0.4'}
@@ -2788,10 +3193,23 @@ packages:
dev: true
optional: true
/toidentifier@1.0.1:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
dev: false
/tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
dev: true
/type-is@1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
dependencies:
media-typer: 0.3.0
mime-types: 2.1.35
dev: false
/typed-array-buffer@1.0.3:
resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
engines: {node: '>= 0.4'}
@@ -2860,6 +3278,11 @@ packages:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
dev: true
/unpipe@1.0.0:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'}
dev: false
/update-browserslist-db@1.2.2(browserslist@4.28.1):
resolution: {integrity: sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==}
hasBin: true
@@ -2871,6 +3294,11 @@ packages:
picocolors: 1.1.1
dev: true
/utils-merge@1.0.1:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
dev: false
/validate-npm-package-license@3.0.4:
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
dependencies:
@@ -2882,6 +3310,11 @@ packages:
resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==}
dev: true
/vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
dev: false
/vite@6.4.1(@types/node@20.19.25)(sass-embedded@1.93.3)(sass@1.94.2):
resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}

View File

@@ -0,0 +1,73 @@
/**
* Shared 模块静态文件服务器
* 提供构建后的 ES Module 文件供其他应用使用
*
* 使用方式:
* 1. npm run build:esm # 先构建
* 2. node server.js # 启动服务器
*/
import express from 'express'
import cors from 'cors'
import path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const app = express()
const PORT = 5000
// 启用 CORS
app.use(cors())
// 静态文件服务:/shared/* -> dist/esm/*
app.use('/shared', express.static(path.join(__dirname, 'dist/esm'), {
setHeaders: (res, filepath) => {
// 设置正确的 MIME 类型
if (filepath.endsWith('.js')) {
res.setHeader('Content-Type', 'application/javascript; charset=utf-8')
}
// 允许跨域
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS')
}
}))
// 健康检查
app.get('/health', (req, res) => {
res.json({
status: 'ok',
port: PORT,
modules: ['components', 'utils', 'api', 'composables', 'types']
})
})
// 模块列表
app.get('/modules', (req, res) => {
res.json({
modules: {
components: '/shared/components.js',
utils: '/shared/utils.js',
api: '/shared/api.js',
composables: '/shared/composables.js',
types: '/shared/types.js'
}
})
})
// 启动服务器
app.listen(PORT, () => {
console.log(`\n🚀 Shared 模块服务器已启动!`)
console.log(``)
console.log(`📦 提供以下模块:`)
console.log(` - http://localhost:${PORT}/shared/components.js`)
console.log(` - http://localhost:${PORT}/shared/utils.js`)
console.log(` - http://localhost:${PORT}/shared/api.js`)
console.log(` - http://localhost:${PORT}/shared/composables.js`)
console.log(` - http://localhost:${PORT}/shared/types.js`)
console.log(``)
console.log(`🔍 健康检查http://localhost:${PORT}/health`)
console.log(`📋 模块列表http://localhost:${PORT}/modules`)
console.log(``)
})

View File

@@ -1,6 +1,92 @@
import { api } from '@/api/index'
import type { LoginParam, LoginDomain } from '@/types'
/**
* 认证 API
* 通过 Gateway (8180) 访问 Auth Service (8181)
* 路由规则:/urban-lifeline/auth/** → auth-service/urban-lifeline/auth/**
*/
export const authAPI = {
baseUrl: "/auth",
baseUrl: "/urban-lifeline/auth",
/**
* 用户登录
* @param loginParam 登录参数
* @returns 登录结果(包含 token 和用户信息)
*/
login(loginParam: LoginParam) {
return api.post<LoginDomain>(`${this.baseUrl}/login`, loginParam)
},
/**
* 用户登出
* @returns 登出结果
*/
logout() {
return api.post<LoginDomain>(`${this.baseUrl}/logout`)
},
/**
* 获取验证码(统一接口)
* @param loginParam 登录参数(包含验证码类型)
* @returns 验证码结果
*/
getCaptcha(loginParam: LoginParam) {
return api.post<LoginDomain>(`${this.baseUrl}/captcha`, loginParam)
},
/**
* 刷新 Token
* @returns 新的登录信息
*/
refreshToken() {
return api.post<LoginDomain>(`${this.baseUrl}/refresh`)
},
/**
* 发送邮箱验证码
* @param email 邮箱地址
* @returns 发送结果
*/
sendEmailCode(email: string) {
return api.post<LoginDomain>(`${this.baseUrl}/send-email-code`, { email })
},
/**
* 发送短信验证码
* @param phone 手机号
* @returns 发送结果
*/
sendSmsCode(phone: string) {
return api.post<LoginDomain>(`${this.baseUrl}/send-sms-code`, { phone })
},
/**
* 用户注册
* @param registerData 注册数据
* @returns 注册结果(成功后自动登录,返回 token
*/
register(registerData: {
registerType: 'username' | 'phone' | 'email'
username?: string
phone?: string
email?: string
password: string
confirmPassword: string
smsCode?: string
emailCode?: string
smsSessionId?: string
emailSessionId?: string
studentId?: string
}) {
return api.post<LoginDomain>(`${this.baseUrl}/register`, registerData)
},
/**
* 健康检查
* @returns 健康状态
*/
health() {
return api.get<string>(`${this.baseUrl}/health`)
}
}

View File

@@ -0,0 +1,274 @@
# 加密工具
## AES-256-GCM 加密
### 概述
前端 AES 加密工具,与后端 `AesEncryptUtil` 保持一致,用于敏感信息传输加密。
### 使用场景
1. **密码传输**:登录时加密密码
2. **敏感信息**:加密手机号、身份证号等
### 快速开始
#### 1. 初始化(应用启动时)
```typescript
import { initAesEncrypt } from '@/utils/crypto'
// 在 main.ts 中初始化
const AES_SECRET_KEY = '1234567890qwer' // 与后端配置保持一致
await initAesEncrypt(AES_SECRET_KEY)
```
#### 2. 使用加密
```typescript
import { getAesInstance } from '@/utils/crypto'
// 获取加密实例
const aes = getAesInstance()
// 加密密码
const encryptedPassword = await aes.encryptPassword('myPassword123')
// 加密手机号
const encryptedPhone = await aes.encryptPhone('13812345678')
```
### 完整示例
#### 登录时加密密码
```typescript
import { authAPI } from '@/api/auth'
import { getAesInstance } from '@/utils/crypto'
async function login(username: string, password: string) {
try {
// 加密密码
const aes = getAesInstance()
const encryptedPassword = await aes.encryptPassword(password)
// 发送登录请求
const response = await authAPI.login({
username,
password: encryptedPassword,
loginType: 'password'
})
return response.data
} catch (error) {
console.error('登录失败:', error)
throw error
}
}
```
#### 手机号注册
```typescript
import { authAPI } from '@/api/auth'
import { getAesInstance } from '@/utils/crypto'
async function register(phone: string, password: string, smsCode: string) {
try {
const aes = getAesInstance()
// 加密密码
const encryptedPassword = await aes.encryptPassword(password)
// 加密手机号
const encryptedPhone = await aes.encryptPhone(phone)
// 发送注册请求
const response = await authAPI.register({
registerType: 'phone',
phone: encryptedPhone,
password: encryptedPassword,
confirmPassword: encryptedPassword,
smsCode,
smsSessionId: 'session-id-from-captcha'
})
return response.data
} catch (error) {
console.error('注册失败:', error)
throw error
}
}
```
### 工具函数
#### 数据脱敏
```typescript
import { AesUtils } from '@/utils/crypto'
// 脱敏手机号
const masked = AesUtils.maskPhone('13812345678')
// 输出138****5678
// 脱敏身份证号
const maskedId = AesUtils.maskIdCard('110101199001011234')
// 输出110101********1234
// 脱敏邮箱
const maskedEmail = AesUtils.maskEmail('test@example.com')
// 输出t***@example.com
```
### API 参考
#### AesEncryptUtil 类
| 方法 | 参数 | 返回值 | 说明 |
|------|------|--------|------|
| `init()` | - | `Promise<void>` | 初始化密钥(必须先调用) |
| `encrypt(plaintext)` | `string` | `Promise<string>` | 加密字符串 |
| `decrypt(ciphertext)` | `string` | `Promise<string>` | 解密字符串 |
| `encryptPassword(password)` | `string` | `Promise<string>` | 加密密码 |
| `encryptPhone(phone)` | `string` | `Promise<string>` | 加密手机号 |
| `decryptPhone(encrypted)` | `string` | `Promise<string>` | 解密手机号 |
| `encryptIdCard(idCard)` | `string` | `Promise<string>` | 加密身份证号 |
| `decryptIdCard(encrypted)` | `string` | `Promise<string>` | 解密身份证号 |
#### AesUtils 静态方法
| 方法 | 参数 | 返回值 | 说明 |
|------|------|--------|------|
| `maskPhone(phone)` | `string` | `string` | 脱敏手机号 |
| `maskIdCard(idCard)` | `string` | `string` | 脱敏身份证号 |
| `maskEmail(email)` | `string` | `string` | 脱敏邮箱 |
#### 全局函数
| 函数 | 参数 | 返回值 | 说明 |
|------|------|--------|------|
| `initAesEncrypt(secretKey)` | `string` | `Promise<void>` | 初始化 AES 加密(应用启动时调用) |
| `getAesInstance()` | - | `AesEncryptUtil` | 获取 AES 加密实例 |
| `createAesEncrypt(secretKey)` | `string` | `Promise<AesEncryptUtil>` | 创建新的 AES 实例 |
### 配置说明
#### 密钥配置
前端密钥必须与后端配置保持一致:
**后端配置application.yml**
```yaml
security:
aes:
secret-key: 1234567890qwer
```
**前端配置**
```typescript
const AES_SECRET_KEY = '1234567890qwer' // 与后端保持一致
await initAesEncrypt(AES_SECRET_KEY)
```
#### 算法参数
| 参数 | 值 | 说明 |
|------|-----|------|
| 算法 | AES-256-GCM | 高强度加密算法 |
| 密钥长度 | 256 bits | AES-256 |
| IV 长度 | 12 bytes | GCM 推荐长度 |
| Tag 长度 | 128 bits | GCM 认证标签 |
| 编码 | Base64 | 密文编码格式 |
### 安全注意事项
1. **密钥管理**
- 密钥不要硬编码在代码中
- 生产环境从配置中心或环境变量获取
- 定期轮换密钥
2. **HTTPS 传输**
- 生产环境必须使用 HTTPS
- 加密只是额外保障,不能替代 HTTPS
3. **密码安全**
- 密码传输前加密
- 后端再次使用 BCrypt 等算法加密存储
- 前端加密防止明文传输被截获
4. **错误处理**
- 捕获加密失败异常
- 不要在错误信息中暴露敏感信息
### 浏览器兼容性
使用 Web Crypto API支持以下浏览器
- Chrome 37+
- Firefox 34+
- Safari 11+
- Edge 79+
不支持 IE 浏览器。
### 与后端对接
#### 数据流程
```
前端 ----[加密数据]----> Gateway -----> Auth Service
(AES-256-GCM) (AES-256-GCM)
[解密] → [BCrypt] → 数据库
```
#### 示例对比
**前端加密**
```typescript
const encrypted = await aes.encrypt('13812345678')
// 输出Base64([IV(12字节)][密文])
```
**后端解密**
```java
String decrypted = aesEncryptUtil.decrypt(encrypted)
// 输出:'13812345678'
```
### 故障排查
#### 1. "AES 密钥未初始化"
**原因**:未调用 `initAesEncrypt()`
**解决**:在 `main.ts` 中初始化
```typescript
await initAesEncrypt(AES_SECRET_KEY)
```
#### 2. "AES 解密失败"
**原因**:前后端密钥不一致
**解决**:检查前后端密钥配置是否相同
#### 3. 类型错误
**原因**TypeScript 类型问题
**解决**:确保使用最新版本的工具类
### 性能优化
1. **密钥复用**:使用单例模式,避免重复初始化
2. **异步处理**:加密操作是异步的,注意使用 `await`
3. **批量加密**:如需加密多个字段,可并行处理
```typescript
// 并行加密
const [encryptedPhone, encryptedPassword] = await Promise.all([
aes.encryptPhone(phone),
aes.encryptPassword(password)
])
```

View File

@@ -0,0 +1,322 @@
/**
* AES-256-GCM 加密工具(兼容 H5/小程序/App
* 与后端 AesEncryptUtil 保持一致
*
* 使用场景:
* - 密码传输前加密
* - 敏感信息(手机号、身份证号)加密
*
* @author yslg
* @since 2025-12-10
*/
/**
* AES 加密配置
*/
interface AesConfig {
algorithm: string
keySize: number
ivLength: number
tagLength: number
}
const AES_CONFIG: AesConfig = {
algorithm: 'AES-GCM',
keySize: 256,
ivLength: 12, // GCM 推荐 IV 长度
tagLength: 128 // GCM 认证标签长度
}
const getCrypto = (): Crypto => {
return window.crypto
}
/**
* AES 加密工具类
*/
export class AesEncryptUtil {
private secretKey: CryptoKey | null = null
private secretKeyString: string
/**
* 构造函数
* @param secretKeyString Base64 编码的密钥32字节AES-256
*/
constructor(secretKeyString: string) {
this.secretKeyString = secretKeyString
}
/**
* 初始化密钥(异步)
*/
async init(): Promise<void> {
if (!this.secretKeyString) {
throw new Error('AES 密钥未配置')
}
try {
// Base64 解码密钥
const keyData = this.base64ToArrayBuffer(this.secretKeyString)
// 校验密钥长度AES-256 必须是 32 字节)
if (keyData.byteLength !== 32) {
throw new Error(`AES 密钥长度错误需32字节实际${keyData.byteLength}字节`)
}
// 导入密钥(跨端兼容)
this.secretKey = await getCrypto().subtle.importKey(
'raw',
keyData,
{ name: AES_CONFIG.algorithm },
false,
['encrypt', 'decrypt']
)
} catch (error) {
throw new Error(`AES 密钥初始化失败: ${error}`)
}
}
/**
* 加密字符串
* @param plaintext 明文
* @returns Base64 编码的密文(包含 IV
*/
async encrypt(plaintext: string): Promise<string> {
if (!plaintext) {
return plaintext
}
if (!this.secretKey) {
throw new Error('AES 密钥未初始化,请先调用 init()')
}
try {
// 生成随机 IV跨端兼容
const iv = getCrypto().getRandomValues(new Uint8Array(AES_CONFIG.ivLength))
// 将明文转为 ArrayBuffer
const encoder = new TextEncoder()
const data = encoder.encode(plaintext)
// 加密
const ciphertext = await getCrypto().subtle.encrypt(
{
name: AES_CONFIG.algorithm,
iv: iv,
tagLength: AES_CONFIG.tagLength
},
this.secretKey,
data
)
// 将 IV 和密文组合:[IV(12字节)][密文]
const combined = new Uint8Array(iv.length + ciphertext.byteLength)
combined.set(iv, 0)
combined.set(new Uint8Array(ciphertext), iv.length)
// Base64 编码
return this.arrayBufferToBase64(combined)
} catch (error) {
throw new Error(`AES 加密失败: ${error}`)
}
}
/**
* 解密字符串
* @param ciphertext Base64 编码的密文(包含 IV
* @returns 明文
*/
async decrypt(ciphertext: string): Promise<string> {
if (!ciphertext) {
return ciphertext
}
if (!this.secretKey) {
throw new Error('AES 密钥未初始化,请先调用 init()')
}
try {
// Base64 解码
const combinedBuffer = this.base64ToArrayBuffer(ciphertext)
const combined = new Uint8Array(combinedBuffer)
// 校验 IV 长度
if (combined.length < AES_CONFIG.ivLength) {
throw new Error('密文格式错误IV 长度不足')
}
// 提取 IV 和密文
const iv = combined.slice(0, AES_CONFIG.ivLength)
const data = combined.slice(AES_CONFIG.ivLength)
// 解密
const plaintext = await getCrypto().subtle.decrypt(
{
name: AES_CONFIG.algorithm,
iv: iv,
tagLength: AES_CONFIG.tagLength
},
this.secretKey,
data
)
// 将 ArrayBuffer 转为字符串
const decoder = new TextDecoder()
return decoder.decode(plaintext)
} catch (error) {
throw new Error(`AES 解密失败: ${error}`)
}
}
/**
* 加密密码(用于登录等场景)
*/
async encryptPassword(password: string): Promise<string> {
return this.encrypt(password)
}
/**
* 加密手机号
*/
async encryptPhone(phone: string): Promise<string> {
return this.encrypt(phone)
}
/**
* 解密手机号
*/
async decryptPhone(encryptedPhone: string): Promise<string> {
return this.decrypt(encryptedPhone)
}
/**
* 加密身份证号
*/
async encryptIdCard(idCard: string): Promise<string> {
return this.encrypt(idCard)
}
/**
* 解密身份证号
*/
async decryptIdCard(encryptedIdCard: string): Promise<string> {
return this.decrypt(encryptedIdCard)
}
// ============ 工具方法 ============
/**
* Base64 转 ArrayBuffer
*/
private base64ToArrayBuffer(base64: string): ArrayBuffer {
// 处理 Base64 填充字符
base64 = base64.replace(/-/g, '+').replace(/_/g, '/')
const padLength = (4 - (base64.length % 4)) % 4
base64 += '='.repeat(padLength)
const binaryString = atob(base64)
const bytes = new Uint8Array(binaryString.length)
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i)
}
return bytes.buffer
}
/**
* ArrayBuffer 转 Base64
*/
private arrayBufferToBase64(buffer: Uint8Array): string {
let binary = ''
const len = buffer.byteLength
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(buffer[i])
}
return btoa(binary)
}
}
/**
* 静态工具方法
*/
export class AesUtils {
/**
* 脱敏显示手机号
* 例如13812345678 -> 138****5678
*/
static maskPhone(phone: string): string {
if (!phone || phone.length < 11) {
return phone
}
return phone.substring(0, 3) + '****' + phone.substring(7)
}
/**
* 脱敏显示身份证号
* 例如110101199001011234 -> 110101********1234
*/
static maskIdCard(idCard: string): string {
if (!idCard || idCard.length < 18) {
return idCard
}
return idCard.substring(0, 6) + '********' + idCard.substring(14)
}
/**
* 脱敏显示邮箱
* 例如test@example.com -> t***@example.com
*/
static maskEmail(email: string): string {
if (!email || !email.includes('@')) {
return email
}
const [username, domain] = email.split('@')
if (username.length <= 1) {
return email
}
return username[0] + '***@' + domain
}
/**
* 生成 AES-256 Base64 密钥(工具方法,用于后端/测试)
*/
static generateAes256KeyBase64(): string {
const crypto = getCrypto()
const keyBytes = crypto.getRandomValues(new Uint8Array(32)) // 32字节=256位
let binary = ''
for (let i = 0; i < keyBytes.length; i++) {
binary += String.fromCharCode(keyBytes[i])
}
return btoa(binary)
}
}
/**
* 创建 AES 加密实例的工厂函数
* @param secretKey Base64 编码的密钥32字节
*/
export async function createAesEncrypt(secretKey: string): Promise<AesEncryptUtil> {
const aes = new AesEncryptUtil(secretKey)
await aes.init()
return aes
}
// 导出单例(需要在应用启动时初始化)
let aesInstance: AesEncryptUtil | null = null
/**
* 获取 AES 加密实例
*/
export function getAesInstance(): AesEncryptUtil {
if (!aesInstance) {
throw new Error('AES 加密工具未初始化,请先调用 initAesEncrypt()')
}
return aesInstance
}
/**
* 初始化 AES 加密工具(在应用启动时调用)
* @param secretKey Base64 编码的密钥32字节与后端配置保持一致
*/
export async function initAesEncrypt(secretKey: string): Promise<void> {
aesInstance = await createAesEncrypt(secretKey)
}

View File

@@ -0,0 +1 @@
export * from './aes'

View File

@@ -3,3 +3,4 @@
*/
export * from './file'
export * from './crypto'

View File

@@ -1,108 +1,111 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import { federation } from '@module-federation/vite'
import { resolve, dirname } from 'path'
import { fileURLToPath } from 'url'
// ES 模块中获取 __dirname
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
/**
* ES Module 构建配置
* 用于 Import Maps 方案
* Module Federation 构建配置(@module-federation/vite
* 官方维护版本,支持 Vite 6 + 开发模式热更新
*
* 策略:将 Vue、Element Plus 等依赖打包进共享模块
* 业务应用只需引入 @shared/*,无需关心底层依赖
* 优势:
* - ✅ 完整支持 Vite 开发模式
* - ✅ dev 模式能生成 remoteEntry.js
* - ✅ 自动处理内部路径别名 (@/)
* - ✅ 真正的生产可用版本
*/
export default defineConfig({
plugins: [vue(), vueJsx()],
define: {
__VUE_PROD_DEVTOOLS__: true,
// __VUE_OPTIONS_API__: true, // 确保启用 Options API
// __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: true
plugins: [
vue({
script: {
defineModel: true,
propsDestructure: true
}
}),
vueJsx(),
federation({
name: 'shared',
filename: 'remoteEntry.js',
// 暴露的模块
exposes: {
// 通用组件
'./FileUpload': './src/components/fileupload/FileUpload.vue',
'./DynamicFormItem': './src/components/dynamicFormItem/DynamicFormItem.vue',
// API 模块
'./api': './src/api/index.ts',
'./authAPI': './src/api/auth/auth.ts',
'./fileAPI': './src/api/file/file.ts',
// Utils 模块
'./utils': './src/utils/index.ts',
// Types 模块
'./types': './src/types/index.ts',
// 整体导出
'./components': './src/components/index.ts'
},
resolve: {
alias: [
{ find: '@', replacement: resolve(__dirname, 'src') }
// 共享依赖(重要:避免重复加载)
shared: {
vue: {},
'vue-router': {},
'element-plus': {},
'@element-plus/icons-vue': {},
axios: {}
}
})
],
define: {
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: true,
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: true
},
resolve: {
alias: {
'@': resolve(__dirname, 'src')
},
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
},
build: {
lib: {
entry: {
components: resolve(__dirname, 'src/components/index.ts'),
utils: resolve(__dirname, 'src/utils/index.ts'),
api: resolve(__dirname, 'src/api/index.ts'),
composables: resolve(__dirname, 'src/composables/index.ts'),
types: resolve(__dirname, 'src/types/index.ts')
},
formats: ['es'], // 仅构建 ES Module
fileName: (format, entryName) => `${entryName}.js`
},
rollupOptions: {
// ⚠️ 不外部化依赖,将它们打包进共享模块
// 这样业务应用只需引入 @shared/* 即可
external: [],
output: {
// 保持 ES Module 格式
format: 'es',
// 导出命名导出
exports: 'named',
// 生成 sourcemap
sourcemap: true,
// 分块策略:将大的依赖分离出来
manualChunks(id) {
// Vue 核心
if (id.includes('node_modules/vue/') ||
id.includes('node_modules/@vue/')) {
return 'vue-core'
}
// Vue Router
if (id.includes('node_modules/vue-router/')) {
return 'vue-router'
}
// Pinia
if (id.includes('node_modules/pinia/')) {
return 'pinia'
}
// Element Plus
if (id.includes('node_modules/element-plus/')) {
return 'element-plus'
}
// VueUse
if (id.includes('node_modules/@vueuse/')) {
return 'vueuse'
}
}
}
},
// 输出目录
outDir: 'dist/esm',
emptyOutDir: true,
// 目标浏览器
target: 'esnext',
// 不压缩(开发环境)
minify: false,
// 启用代码分割
cssCodeSplit: true
cssCodeSplit: false,
sourcemap: true,
rollupOptions: {
output: {
format: 'es'
}
}
},
// 开发服务器配置
server: {
port: 5000,
strictPort: true,
host: true,
cors: true,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
}
},
preview: {
port: 5000,
host: true,
cors: true,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
// 'Content-Type': 'application/javascript; charset=utf-8'
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
}
}
})

View File

@@ -8,7 +8,21 @@ const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
export default defineConfig({
plugins: [vue(), vueJsx()],
plugins: [
vue({
script: {
defineModel: true,
propsDestructure: true
}
}),
vueJsx()
],
define: {
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: true,
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: true
},
resolve: {
alias: {

View File

@@ -13,4 +13,4 @@ echo 按 Ctrl+C 停止所有服务
echo ====================================
echo.
npm run dev:all
pnpm run dev