配置统计卡片与导出修改

main
kangwenjing 2026-04-30 15:06:53 +08:00
parent 3a11b86a62
commit 9e149a1adf
132 changed files with 9756 additions and 2341 deletions

3
.gitignore vendored
View File

@ -4,10 +4,13 @@
backend/.idea/
frontend/dist/
frontend1/dist/
backend/target/
frontend/node_modules/
frontend1/node_modules/
frontend/.cert/
frontend1/.cert/
*.log

View File

@ -3,6 +3,7 @@ package com.unis.crm;
import com.unis.crm.config.WecomProperties;
import com.unis.crm.config.InternalAuthProperties;
import com.unis.crm.config.OmsProperties;
import java.util.TimeZone;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@ -16,6 +17,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
public class UnisCrmBackendApplication {
public static void main(String[] args) {
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"));
SpringApplication.run(UnisCrmBackendApplication.class, args);
}
}

View File

@ -0,0 +1,105 @@
package com.unis.crm.common;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import javax.sql.DataSource;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
@Component
public class DashboardAnalyticsSchemaInitializer implements ApplicationRunner {
private final DataSource dataSource;
public DashboardAnalyticsSchemaInitializer(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public void run(ApplicationArguments args) {
try (Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement()) {
statement.execute("""
create table if not exists dashboard_analytics_panel_config (
id bigint generated by default as identity primary key,
tenant_id bigint not null,
enabled boolean not null default false,
title varchar(100) not null default '',
subtitle varchar(255),
empty_state_text varchar(255),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint uk_dashboard_analytics_panel_tenant unique (tenant_id)
)
""");
statement.execute("""
create table if not exists dashboard_analytics_card_config (
id bigint generated by default as identity primary key,
tenant_id bigint not null,
card_key varchar(100) not null,
title varchar(100) not null,
subtitle varchar(255),
render_type varchar(20) not null default 'metric',
sql_template text not null,
value_field varchar(100) not null default 'value',
description_field varchar(100),
category_field varchar(100),
color_field varchar(100),
display_text_config text,
unit varchar(20),
value_type varchar(20) not null default 'number',
sort_direction varchar(20) not null default 'sql',
display_limit integer,
layout_type varchar(20) not null default 'vertical',
full_row boolean not null default false,
link_path varchar(255),
sort_order integer not null default 0,
enabled boolean not null default true,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint uk_dashboard_analytics_card_tenant_key unique (tenant_id, card_key)
)
""");
statement.execute("""
create index if not exists idx_dashboard_analytics_card_tenant_sort
on dashboard_analytics_card_config (tenant_id, sort_order asc, id asc)
""");
statement.execute("""
alter table dashboard_analytics_card_config
add column if not exists render_type varchar(20) not null default 'metric'
""");
statement.execute("""
alter table dashboard_analytics_card_config
add column if not exists category_field varchar(100)
""");
statement.execute("""
alter table dashboard_analytics_card_config
add column if not exists color_field varchar(100)
""");
statement.execute("""
alter table dashboard_analytics_card_config
add column if not exists display_text_config text
""");
statement.execute("""
alter table dashboard_analytics_card_config
add column if not exists sort_direction varchar(20) not null default 'sql'
""");
statement.execute("""
alter table dashboard_analytics_card_config
add column if not exists display_limit integer
""");
statement.execute("""
alter table dashboard_analytics_card_config
add column if not exists layout_type varchar(20) not null default 'vertical'
""");
statement.execute("""
alter table dashboard_analytics_card_config
add column if not exists full_row boolean not null default false
""");
} catch (SQLException exception) {
throw new IllegalStateException("Failed to initialize dashboard analytics schema", exception);
}
}
}

View File

@ -32,8 +32,12 @@ public class OpportunitySchemaInitializer implements ApplicationRunner {
try (Statement statement = connection.createStatement()) {
statement.execute("alter table crm_opportunity add column if not exists pre_sales_id bigint");
statement.execute("alter table crm_opportunity add column if not exists pre_sales_name varchar(100)");
statement.execute("alter table crm_opportunity add column if not exists latest_progress text");
statement.execute("alter table crm_opportunity add column if not exists next_plan text");
statement.execute("alter table crm_opportunity add column if not exists archived_at timestamptz");
statement.execute("create index if not exists idx_crm_opportunity_archived_at on crm_opportunity(archived_at)");
statement.execute("comment on column crm_opportunity.latest_progress is '项目最新进展'");
statement.execute("comment on column crm_opportunity.next_plan is '下一步销售计划'");
statement.execute("comment on column crm_opportunity.archived_at is '归档时间'");
}
ensureArchivedAtStorage(connection);

View File

@ -0,0 +1,47 @@
package com.unis.crm.controller;
import com.unis.crm.common.ApiResponse;
import com.unis.crm.dto.dashboard.DashboardAnalyticsCardDTO;
import com.unis.crm.dto.dashboard.DashboardAnalyticsPanelDTO;
import com.unis.crm.dto.dashboardanalytics.DashboardAnalyticsConfigDTO;
import com.unis.crm.service.DashboardAnalyticsConfigService;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/sys/api/admin")
public class DashboardAnalyticsAdminController {
private final DashboardAnalyticsConfigService dashboardAnalyticsConfigService;
public DashboardAnalyticsAdminController(DashboardAnalyticsConfigService dashboardAnalyticsConfigService) {
this.dashboardAnalyticsConfigService = dashboardAnalyticsConfigService;
}
@GetMapping("/dashboard-analytics-config")
public ApiResponse<DashboardAnalyticsConfigDTO> getConfig(@RequestParam(value = "tenantId", required = false) Long tenantId) {
return ApiResponse.success(dashboardAnalyticsConfigService.getConfig(tenantId));
}
@PutMapping("/dashboard-analytics-config")
public ApiResponse<Boolean> updateConfig(@Valid @RequestBody DashboardAnalyticsConfigDTO payload) {
return ApiResponse.success(dashboardAnalyticsConfigService.saveConfig(payload));
}
@GetMapping("/dashboard-analytics-config/preview")
public ApiResponse<DashboardAnalyticsPanelDTO> preview(@RequestParam(value = "tenantId", required = false) Long tenantId) {
return ApiResponse.success(dashboardAnalyticsConfigService.preview(tenantId));
}
@GetMapping("/dashboard-analytics-config/preview/card-detail")
public ApiResponse<DashboardAnalyticsCardDTO> previewCardDetail(
@RequestParam(value = "tenantId", required = false) Long tenantId,
@RequestParam("cardKey") String cardKey) {
return ApiResponse.success(dashboardAnalyticsConfigService.previewCard(tenantId, cardKey));
}
}

View File

@ -1,6 +1,7 @@
package com.unis.crm.controller;
import com.unis.crm.common.ApiResponse;
import com.unis.crm.dto.dashboard.DashboardAnalyticsCardDTO;
import com.unis.crm.dto.dashboard.DashboardHomeDTO;
import com.unis.crm.service.DashboardService;
import com.unisbase.common.annotation.Log;
@ -38,4 +39,11 @@ public class DashboardController {
dashboardService.completeTodo(userId, todoId);
return ApiResponse.success(null);
}
@GetMapping("/analytics-cards/{cardKey}")
public ApiResponse<DashboardAnalyticsCardDTO> getAnalyticsCardDetail(
@RequestHeader("X-User-Id") @Min(1) Long userId,
@PathVariable("cardKey") String cardKey) {
return ApiResponse.success(dashboardService.getAnalyticsCardDetail(userId, cardKey));
}
}

View File

@ -0,0 +1,176 @@
package com.unis.crm.dto.dashboard;
public class DashboardAnalyticsCardDTO {
private Long id;
private String cardKey;
private String title;
private String subtitle;
private String renderType;
private String description;
private String value;
private String valueText;
private String valueType;
private String unit;
private String displayTextConfig;
private String linkPath;
private String layoutType;
private Boolean fullRow;
private Integer sortOrder;
private String errorMessage;
private Integer totalCount;
private Boolean hasMore;
private java.util.List<DashboardAnalyticsChartPointDTO> chartData;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCardKey() {
return cardKey;
}
public void setCardKey(String cardKey) {
this.cardKey = cardKey;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getSubtitle() {
return subtitle;
}
public void setSubtitle(String subtitle) {
this.subtitle = subtitle;
}
public String getRenderType() {
return renderType;
}
public void setRenderType(String renderType) {
this.renderType = renderType;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getValueText() {
return valueText;
}
public void setValueText(String valueText) {
this.valueText = valueText;
}
public String getValueType() {
return valueType;
}
public void setValueType(String valueType) {
this.valueType = valueType;
}
public String getUnit() {
return unit;
}
public void setUnit(String unit) {
this.unit = unit;
}
public String getDisplayTextConfig() {
return displayTextConfig;
}
public void setDisplayTextConfig(String displayTextConfig) {
this.displayTextConfig = displayTextConfig;
}
public String getLinkPath() {
return linkPath;
}
public void setLinkPath(String linkPath) {
this.linkPath = linkPath;
}
public String getLayoutType() {
return layoutType;
}
public void setLayoutType(String layoutType) {
this.layoutType = layoutType;
}
public Boolean getFullRow() {
return fullRow;
}
public void setFullRow(Boolean fullRow) {
this.fullRow = fullRow;
}
public Integer getSortOrder() {
return sortOrder;
}
public void setSortOrder(Integer sortOrder) {
this.sortOrder = sortOrder;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
public Integer getTotalCount() {
return totalCount;
}
public void setTotalCount(Integer totalCount) {
this.totalCount = totalCount;
}
public Boolean getHasMore() {
return hasMore;
}
public void setHasMore(Boolean hasMore) {
this.hasMore = hasMore;
}
public java.util.List<DashboardAnalyticsChartPointDTO> getChartData() {
return chartData;
}
public void setChartData(java.util.List<DashboardAnalyticsChartPointDTO> chartData) {
this.chartData = chartData;
}
}

View File

@ -0,0 +1,68 @@
package com.unis.crm.dto.dashboard;
public class DashboardAnalyticsChartPointDTO {
private String label;
private String value;
private String valueText;
private String secondaryValue;
private String secondaryValueText;
private String description;
private String color;
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getValueText() {
return valueText;
}
public void setValueText(String valueText) {
this.valueText = valueText;
}
public String getSecondaryValue() {
return secondaryValue;
}
public void setSecondaryValue(String secondaryValue) {
this.secondaryValue = secondaryValue;
}
public String getSecondaryValueText() {
return secondaryValueText;
}
public void setSecondaryValueText(String secondaryValueText) {
this.secondaryValueText = secondaryValueText;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}

View File

@ -0,0 +1,52 @@
package com.unis.crm.dto.dashboard;
import java.util.List;
public class DashboardAnalyticsPanelDTO {
private Boolean enabled;
private String title;
private String subtitle;
private String emptyStateText;
private List<DashboardAnalyticsCardDTO> cards;
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getSubtitle() {
return subtitle;
}
public void setSubtitle(String subtitle) {
this.subtitle = subtitle;
}
public String getEmptyStateText() {
return emptyStateText;
}
public void setEmptyStateText(String emptyStateText) {
this.emptyStateText = emptyStateText;
}
public List<DashboardAnalyticsCardDTO> getCards() {
return cards;
}
public void setCards(List<DashboardAnalyticsCardDTO> cards) {
this.cards = cards;
}
}

View File

@ -9,29 +9,36 @@ public class DashboardHomeDTO {
private String jobTitle;
private String deptName;
private Long onboardingDays;
private Boolean statsCardVisible;
private Boolean todoCardVisible;
private Boolean activityCardVisible;
private Boolean analyticsCardVisible;
private List<DashboardStatDTO> stats;
private List<DashboardTodoDTO> todos;
private List<DashboardActivityDTO> activities;
private DashboardAnalyticsPanelDTO analyticsPanel;
public DashboardHomeDTO() {
}
public DashboardHomeDTO(Long userId, String realName, String jobTitle, String deptName, Long onboardingDays,
Boolean todoCardVisible, Boolean activityCardVisible,
Boolean statsCardVisible, Boolean todoCardVisible, Boolean activityCardVisible,
Boolean analyticsCardVisible,
List<DashboardStatDTO> stats, List<DashboardTodoDTO> todos,
List<DashboardActivityDTO> activities) {
List<DashboardActivityDTO> activities, DashboardAnalyticsPanelDTO analyticsPanel) {
this.userId = userId;
this.realName = realName;
this.jobTitle = jobTitle;
this.deptName = deptName;
this.onboardingDays = onboardingDays;
this.statsCardVisible = statsCardVisible;
this.todoCardVisible = todoCardVisible;
this.activityCardVisible = activityCardVisible;
this.analyticsCardVisible = analyticsCardVisible;
this.stats = stats;
this.todos = todos;
this.activities = activities;
this.analyticsPanel = analyticsPanel;
}
public Long getUserId() {
@ -74,6 +81,14 @@ public class DashboardHomeDTO {
this.onboardingDays = onboardingDays;
}
public Boolean getStatsCardVisible() {
return statsCardVisible;
}
public void setStatsCardVisible(Boolean statsCardVisible) {
this.statsCardVisible = statsCardVisible;
}
public Boolean getTodoCardVisible() {
return todoCardVisible;
}
@ -90,6 +105,14 @@ public class DashboardHomeDTO {
this.activityCardVisible = activityCardVisible;
}
public Boolean getAnalyticsCardVisible() {
return analyticsCardVisible;
}
public void setAnalyticsCardVisible(Boolean analyticsCardVisible) {
this.analyticsCardVisible = analyticsCardVisible;
}
public List<DashboardStatDTO> getStats() {
return stats;
}
@ -113,4 +136,12 @@ public class DashboardHomeDTO {
public void setActivities(List<DashboardActivityDTO> activities) {
this.activities = activities;
}
public DashboardAnalyticsPanelDTO getAnalyticsPanel() {
return analyticsPanel;
}
public void setAnalyticsPanel(DashboardAnalyticsPanelDTO analyticsPanel) {
this.analyticsPanel = analyticsPanel;
}
}

View File

@ -0,0 +1,229 @@
package com.unis.crm.dto.dashboardanalytics;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
public class DashboardAnalyticsCardConfigDTO {
private Long id;
@NotBlank(message = "卡片编码不能为空")
@Size(max = 100, message = "卡片编码长度不能超过100")
private String cardKey;
@NotBlank(message = "卡片标题不能为空")
@Size(max = 100, message = "卡片标题长度不能超过100")
private String title;
@Size(max = 255, message = "卡片副标题长度不能超过255")
private String subtitle;
@NotBlank(message = "展示类型不能为空")
@Size(max = 20, message = "展示类型长度不能超过20")
private String renderType;
@NotBlank(message = "查询 SQL 不能为空")
private String sqlTemplate;
@NotBlank(message = "数值字段不能为空")
@Size(max = 100, message = "数值字段长度不能超过100")
private String valueField;
@Size(max = 100, message = "说明字段长度不能超过100")
private String descriptionField;
@Size(max = 100, message = "分类字段长度不能超过100")
private String categoryField;
@Size(max = 100, message = "颜色字段长度不能超过100")
private String colorField;
private String displayTextConfig;
@Size(max = 20, message = "单位长度不能超过20")
private String unit;
@NotBlank(message = "展示类型不能为空")
@Size(max = 20, message = "展示类型长度不能超过20")
private String valueType;
@Size(max = 20, message = "数据排序方式长度不能超过20")
private String sortDirection;
private Integer displayLimit;
@Size(max = 20, message = "布局方式长度不能超过20")
private String layoutType;
private Boolean fullRow;
@Size(max = 255, message = "跳转路径长度不能超过255")
private String linkPath;
@NotNull(message = "排序不能为空")
private Integer sortOrder;
@NotNull(message = "启用状态不能为空")
private Boolean enabled;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCardKey() {
return cardKey;
}
public void setCardKey(String cardKey) {
this.cardKey = cardKey;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getSubtitle() {
return subtitle;
}
public void setSubtitle(String subtitle) {
this.subtitle = subtitle;
}
public String getRenderType() {
return renderType;
}
public void setRenderType(String renderType) {
this.renderType = renderType;
}
public String getSqlTemplate() {
return sqlTemplate;
}
public void setSqlTemplate(String sqlTemplate) {
this.sqlTemplate = sqlTemplate;
}
public String getValueField() {
return valueField;
}
public void setValueField(String valueField) {
this.valueField = valueField;
}
public String getDescriptionField() {
return descriptionField;
}
public void setDescriptionField(String descriptionField) {
this.descriptionField = descriptionField;
}
public String getCategoryField() {
return categoryField;
}
public void setCategoryField(String categoryField) {
this.categoryField = categoryField;
}
public String getColorField() {
return colorField;
}
public void setColorField(String colorField) {
this.colorField = colorField;
}
public String getDisplayTextConfig() {
return displayTextConfig;
}
public void setDisplayTextConfig(String displayTextConfig) {
this.displayTextConfig = displayTextConfig;
}
public String getUnit() {
return unit;
}
public void setUnit(String unit) {
this.unit = unit;
}
public String getValueType() {
return valueType;
}
public void setValueType(String valueType) {
this.valueType = valueType;
}
public String getLinkPath() {
return linkPath;
}
public String getLayoutType() {
return layoutType;
}
public void setLayoutType(String layoutType) {
this.layoutType = layoutType;
}
public Boolean getFullRow() {
return fullRow;
}
public void setFullRow(Boolean fullRow) {
this.fullRow = fullRow;
}
public Integer getDisplayLimit() {
return displayLimit;
}
public void setDisplayLimit(Integer displayLimit) {
this.displayLimit = displayLimit;
}
public String getSortDirection() {
return sortDirection;
}
public void setSortDirection(String sortDirection) {
this.sortDirection = sortDirection;
}
public void setLinkPath(String linkPath) {
this.linkPath = linkPath;
}
public Integer getSortOrder() {
return sortOrder;
}
public void setSortOrder(Integer sortOrder) {
this.sortOrder = sortOrder;
}
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
}

View File

@ -0,0 +1,75 @@
package com.unis.crm.dto.dashboardanalytics;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.util.ArrayList;
import java.util.List;
public class DashboardAnalyticsConfigDTO {
private Long tenantId;
@NotNull(message = "首页经营分析启用状态不能为空")
private Boolean enabled;
@Size(max = 100, message = "模块标题长度不能超过100")
private String title;
@Size(max = 255, message = "模块副标题长度不能超过255")
private String subtitle;
@Size(max = 255, message = "空状态文案长度不能超过255")
private String emptyStateText;
@Valid
private List<DashboardAnalyticsCardConfigDTO> cards = new ArrayList<>();
public Long getTenantId() {
return tenantId;
}
public void setTenantId(Long tenantId) {
this.tenantId = tenantId;
}
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getSubtitle() {
return subtitle;
}
public void setSubtitle(String subtitle) {
this.subtitle = subtitle;
}
public String getEmptyStateText() {
return emptyStateText;
}
public void setEmptyStateText(String emptyStateText) {
this.emptyStateText = emptyStateText;
}
public List<DashboardAnalyticsCardConfigDTO> getCards() {
return cards;
}
public void setCards(List<DashboardAnalyticsCardConfigDTO> cards) {
this.cards = cards;
}
}

View File

@ -41,6 +41,8 @@ public class CreateOpportunityRequest {
private Long salesExpansionId;
private Long channelExpansionId;
private String competitorName;
private String latestProgress;
private String nextPlan;
private String description;
public Long getId() {
@ -163,6 +165,22 @@ public class CreateOpportunityRequest {
this.competitorName = competitorName;
}
public String getLatestProgress() {
return latestProgress;
}
public void setLatestProgress(String latestProgress) {
this.latestProgress = latestProgress;
}
public String getNextPlan() {
return nextPlan;
}
public void setNextPlan(String nextPlan) {
this.nextPlan = nextPlan;
}
public String getDescription() {
return description;
}

View File

@ -139,6 +139,11 @@ public interface WorkMapper {
@Param("content") String content,
@Param("nextAction") String nextAction);
int updateOpportunitySnapshot(
@Param("opportunityId") Long opportunityId,
@Param("latestProgress") String latestProgress,
@Param("nextPlan") String nextPlan);
int deleteTodosByBiz(@Param("userId") Long userId, @Param("bizType") String bizType, @Param("bizId") Long bizId);
int deleteTodosByBizType(@Param("userId") Long userId, @Param("bizType") String bizType);

View File

@ -1,5 +1,6 @@
package com.unis.crm.service;
import com.unis.crm.dto.dashboard.DashboardAnalyticsCardDTO;
import com.unis.crm.dto.dashboard.DashboardHomeDTO;
public interface DashboardService {
@ -7,4 +8,6 @@ public interface DashboardService {
DashboardHomeDTO getHome(Long userId);
void completeTodo(Long userId, Long todoId);
DashboardAnalyticsCardDTO getAnalyticsCardDetail(Long userId, String cardKey);
}

View File

@ -1,12 +1,16 @@
package com.unis.crm.service.impl;
import com.unis.crm.common.BusinessException;
import com.unis.crm.common.UnauthorizedException;
import com.unis.crm.dto.dashboard.DashboardActivityDTO;
import com.unis.crm.dto.dashboard.DashboardAnalyticsCardDTO;
import com.unis.crm.dto.dashboard.DashboardAnalyticsPanelDTO;
import com.unis.crm.dto.dashboard.DashboardHomeDTO;
import com.unis.crm.dto.dashboard.DashboardStatDTO;
import com.unis.crm.dto.dashboard.DashboardTodoDTO;
import com.unis.crm.dto.dashboard.UserWelcomeDTO;
import com.unis.crm.mapper.DashboardMapper;
import com.unis.crm.service.DashboardAnalyticsConfigService;
import com.unis.crm.service.DashboardService;
import com.unisbase.service.SysPermissionService;
import com.unisbase.spi.UnisBaseTenantProvider;
@ -25,15 +29,20 @@ public class DashboardServiceImpl implements DashboardService {
private static final String DASHBOARD_TODO_CARD_VIEW_PERMISSION = "dashboard_todo_card:view";
private static final String DASHBOARD_ACTIVITY_CARD_VIEW_PERMISSION = "dashboard_activity_card:view";
private static final String DASHBOARD_ANALYTICS_CARD_VIEW_PERMISSION = "dashboard_analytics_card:view";
private static final String DASHBOARD_STATS_CARD_VIEW_PERMISSION = "dashboard_stats_card:view";
private final DashboardMapper dashboardMapper;
private final DashboardAnalyticsConfigService dashboardAnalyticsConfigService;
private final SysPermissionService sysPermissionService;
private final UnisBaseTenantProvider tenantProvider;
public DashboardServiceImpl(DashboardMapper dashboardMapper,
DashboardAnalyticsConfigService dashboardAnalyticsConfigService,
SysPermissionService sysPermissionService,
UnisBaseTenantProvider tenantProvider) {
this.dashboardMapper = dashboardMapper;
this.dashboardAnalyticsConfigService = dashboardAnalyticsConfigService;
this.sysPermissionService = sysPermissionService;
this.tenantProvider = tenantProvider;
}
@ -56,10 +65,18 @@ public class DashboardServiceImpl implements DashboardService {
DashboardStatDTO monthlyChannelStat = dashboardMapper.selectMonthlyChannelStat(userId);
addStatIfPresent(stats, monthlyChannelStat);
Set<String> permissionCodes = loadPermissionCodes(userId);
boolean statsCardVisible = permissionCodes.contains(DASHBOARD_STATS_CARD_VIEW_PERMISSION);
boolean todoCardVisible = permissionCodes.contains(DASHBOARD_TODO_CARD_VIEW_PERMISSION);
boolean activityCardVisible = permissionCodes.contains(DASHBOARD_ACTIVITY_CARD_VIEW_PERMISSION);
boolean analyticsCardVisible = permissionCodes.contains(DASHBOARD_ANALYTICS_CARD_VIEW_PERMISSION);
if (!statsCardVisible) {
stats = List.of();
}
List<DashboardTodoDTO> todos = todoCardVisible ? dashboardMapper.selectTodos(userId) : List.of();
List<DashboardActivityDTO> activities = activityCardVisible ? loadLatestActivities(userId) : List.of();
DashboardAnalyticsPanelDTO analyticsPanel = analyticsCardVisible
? dashboardAnalyticsConfigService.getDashboardPanel(userId)
: null;
enrichActivityTimeText(activities);
long onboardingDays = 0;
@ -74,11 +91,14 @@ public class DashboardServiceImpl implements DashboardService {
user.getJobTitle(),
user.getDeptName(),
onboardingDays,
statsCardVisible,
todoCardVisible,
activityCardVisible,
analyticsCardVisible,
stats,
todos,
activities
activities,
analyticsPanel
);
}
@ -96,6 +116,32 @@ public class DashboardServiceImpl implements DashboardService {
}
}
@Override
public DashboardAnalyticsCardDTO getAnalyticsCardDetail(Long userId, String cardKey) {
if (userId == null) {
throw new UnauthorizedException("未获取到当前登录用户,禁止查询他人数据");
}
Set<String> permissionCodes = loadPermissionCodes(userId);
if (!permissionCodes.contains(DASHBOARD_ANALYTICS_CARD_VIEW_PERMISSION)) {
throw new UnauthorizedException("无权查看经营分析卡片详情");
}
try {
return dashboardAnalyticsConfigService.getDashboardCardDetail(userId, cardKey);
} catch (Exception exception) {
DashboardAnalyticsPanelDTO panel = dashboardAnalyticsConfigService.getDashboardPanel(userId);
DashboardAnalyticsCardDTO fallbackCard = panel.getCards() == null
? null
: panel.getCards().stream()
.filter(item -> item != null && cardKey != null && cardKey.equals(item.getCardKey()))
.findFirst()
.orElse(null);
if (fallbackCard != null) {
return fallbackCard;
}
throw exception;
}
}
private Set<String> loadPermissionCodes(Long userId) {
try {
Long tenantId = tenantProvider.getCurrentTenantId();

View File

@ -504,7 +504,7 @@ public class ExpansionServiceImpl implements ExpansionService {
request.setChannelName(normalizeRequiredText(request.getChannelName(), "渠道名称不能为空"));
request.setProvince(normalizeRequiredText(request.getProvince(), "请选择省份"));
request.setCity(normalizeRequiredText(request.getCity(), "请选择市"));
request.setCertificationLevel(normalizeRequiredText(request.getCertificationLevel(), "请选择认证级别"));
request.setCertificationLevel(normalizeRequiredText(request.getCertificationLevel(), "请选择汇智内部认证级别"));
request.setOfficeAddress(normalizeRequiredText(request.getOfficeAddress(), "请填写办公地址"));
request.setChannelIndustry(normalizeRequiredText(request.getChannelIndustry(), "请选择聚焦行业"));
request.setAnnualRevenue(requirePositiveAmount(request.getAnnualRevenue(), "请填写年度营业额(万元)"));
@ -533,7 +533,7 @@ public class ExpansionServiceImpl implements ExpansionService {
request.setChannelName(normalizeRequiredText(request.getChannelName(), "渠道名称不能为空"));
request.setProvince(normalizeRequiredText(request.getProvince(), "请选择省份"));
request.setCity(normalizeRequiredText(request.getCity(), "请选择市"));
request.setCertificationLevel(normalizeRequiredText(request.getCertificationLevel(), "请选择认证级别"));
request.setCertificationLevel(normalizeRequiredText(request.getCertificationLevel(), "请选择汇智内部认证级别"));
request.setOfficeAddress(normalizeRequiredText(request.getOfficeAddress(), "请填写办公地址"));
request.setChannelIndustry(normalizeRequiredText(request.getChannelIndustry(), "请选择聚焦行业"));
request.setAnnualRevenue(requirePositiveAmount(request.getAnnualRevenue(), "请填写年度营业额(万元)"));

View File

@ -288,7 +288,9 @@ public class OpportunityServiceImpl implements OpportunityService {
}
if (isBlank(followUp.getNextAction())) {
String nextPlan = extractFollowUpField(followUp.getContent(), "后续规划");
String nextPlan = firstNonBlank(
extractFollowUpField(followUp.getContent(), "下一步销售计划"),
extractFollowUpField(followUp.getContent(), "后续规划"));
if (!isBlank(nextPlan)) {
followUp.setNextAction(nextPlan);
}
@ -358,9 +360,16 @@ public class OpportunityServiceImpl implements OpportunityService {
request.setSource("主动开发");
}
request.setCompetitorName(normalizeRequiredText(request.getCompetitorName(), "请选择竞争对手"));
request.setLatestProgress(normalizeSnapshotText(request.getLatestProgress()));
request.setNextPlan(normalizeSnapshotText(request.getNextPlan()));
validateOperatorRelations(request.getOperatorName(), request.getSalesExpansionId(), request.getChannelExpansionId());
}
private String normalizeSnapshotText(String value) {
String normalized = normalizeOptionalText(value);
return normalized == null ? "" : normalized;
}
private void ensureOpportunityNotArchived(Long userId, Long opportunityId, String message) {
if (Boolean.TRUE.equals(opportunityMapper.selectArchived(userId, opportunityId))) {
throw new BusinessException(message);

View File

@ -75,6 +75,8 @@ public class WorkServiceImpl implements WorkService {
private static final Pattern PLAN_ITEMS_METADATA_PATTERN = Pattern.compile("\\[\\[WORK_PLAN_ITEMS]](.*?)\\[\\[/WORK_PLAN_ITEMS]]", Pattern.DOTALL);
private static final String ONLY_SEE_ROLE_CODE = "only_see";
private static final String WORK_REPORT_FOLLOW_UP_TYPE = "工作日报";
private static final String LEGACY_NEXT_PLAN_LABEL = "后续规划";
private static final String OPPORTUNITY_NEXT_PLAN_LABEL = "下一步销售计划";
private static final int EXPORT_LIMIT = 5000;
private static final String TENCENT_COORD_TYPE_GPS = "1";
private static final String TENCENT_COORD_TYPE_GCJ02 = "2";
@ -856,11 +858,19 @@ public class WorkServiceImpl implements WorkService {
editorText = buildEditorText(item.getBizType(), objectName, extractLineFieldValues(item));
}
Map<String, String> fieldValues = parseEditorTextFields(editorText);
if ("opportunity".equals(item.getBizType())) {
String nextPlan = firstNonBlank(
fieldValues.get(OPPORTUNITY_NEXT_PLAN_LABEL),
fieldValues.get(LEGACY_NEXT_PLAN_LABEL));
if (nextPlan != null) {
fieldValues.put(OPPORTUNITY_NEXT_PLAN_LABEL, nextPlan);
}
}
item.setEditorText(buildEditorText(item.getBizType(), objectName, fieldValues));
if ("sales".equals(item.getBizType()) || "channel".equals(item.getBizType())) {
item.setVisitStartTime(buildReportDateTimeText(item.getWorkDate()));
item.setEvaluationContent(fieldValues.get("沟通内容"));
item.setNextPlan(fieldValues.get("后续规划"));
item.setNextPlan(fieldValues.get(LEGACY_NEXT_PLAN_LABEL));
item.setContent(buildExpansionLineContent(item));
return;
}
@ -868,7 +878,7 @@ public class WorkServiceImpl implements WorkService {
item.setEvaluationContent(null);
item.setCommunicationTime(null);
item.setCommunicationContent(null);
item.setNextPlan(fieldValues.get("后续规划"));
item.setNextPlan(fieldValues.get(OPPORTUNITY_NEXT_PLAN_LABEL));
item.setContent(buildOpportunityLineContent(item));
}
@ -884,7 +894,7 @@ public class WorkServiceImpl implements WorkService {
parts.add("沟通内容:" + communication);
}
if (nextPlan != null) {
parts.add("后续规划" + nextPlan);
parts.add(LEGACY_NEXT_PLAN_LABEL + "" + nextPlan);
}
return String.join("\n", parts);
}
@ -901,7 +911,7 @@ public class WorkServiceImpl implements WorkService {
parts.add("项目最新进展:" + latestProgress);
}
if (nextPlan != null) {
parts.add("后续规划" + nextPlan);
parts.add(OPPORTUNITY_NEXT_PLAN_LABEL + "" + nextPlan);
}
return String.join("\n", parts);
}
@ -913,11 +923,11 @@ public class WorkServiceImpl implements WorkService {
}
if ("sales".equals(item.getBizType()) || "channel".equals(item.getBizType())) {
fieldValues.put("沟通内容", normalizeOptionalText(item.getEvaluationContent()));
fieldValues.put("后续规划", normalizeOptionalText(item.getNextPlan()));
fieldValues.put(LEGACY_NEXT_PLAN_LABEL, normalizeOptionalText(item.getNextPlan()));
return fieldValues;
}
fieldValues.put("项目最新进展", normalizeOptionalText(item.getLatestProgress()));
fieldValues.put("后续规划", normalizeOptionalText(item.getNextPlan()));
fieldValues.put(OPPORTUNITY_NEXT_PLAN_LABEL, normalizeOptionalText(item.getNextPlan()));
return fieldValues;
}
@ -969,9 +979,9 @@ public class WorkServiceImpl implements WorkService {
private List<String> getEditorFieldLabels(String bizType) {
if ("opportunity".equals(bizType)) {
return List.of("项目最新进展", "后续规划");
return List.of("项目最新进展", OPPORTUNITY_NEXT_PLAN_LABEL);
}
return List.of("沟通内容", "后续规划");
return List.of("沟通内容", LEGACY_NEXT_PLAN_LABEL);
}
private String getBizTypeLabel(String bizType) {
@ -1065,7 +1075,11 @@ public class WorkServiceImpl implements WorkService {
followUpTime,
WORK_REPORT_FOLLOW_UP_TYPE,
item.getContent(),
resolveOpportunityNextAction(item, request));
resolveOpportunityNextAction(item));
workMapper.updateOpportunitySnapshot(
item.getBizId(),
normalizeSnapshotText(item.getLatestProgress()),
normalizeSnapshotText(item.getNextPlan()));
continue;
}
workMapper.deleteDailyReportExpansionFollowUps(
@ -1088,12 +1102,41 @@ public class WorkServiceImpl implements WorkService {
}
}
private String resolveOpportunityNextAction(WorkReportLineItemRequest item, CreateWorkDailyReportRequest request) {
private String resolveOpportunityNextAction(WorkReportLineItemRequest item) {
String nextAction = item == null ? null : normalizeOptionalText(item.getNextPlan());
if (nextAction != null) {
return nextAction;
}
return request == null ? null : normalizeOptionalText(request.getTomorrowPlan());
if (item == null) {
return null;
}
return firstNonBlank(
extractSingleLineField(item.getContent(), OPPORTUNITY_NEXT_PLAN_LABEL),
extractSingleLineField(item.getContent(), LEGACY_NEXT_PLAN_LABEL));
}
private String extractSingleLineField(String content, String label) {
String normalizedContent = normalizeOptionalText(content);
String normalizedLabel = normalizeOptionalText(label);
if (normalizedContent == null || normalizedLabel == null) {
return null;
}
String prefix = normalizedLabel + "";
int startIndex = normalizedContent.indexOf(prefix);
if (startIndex < 0) {
return null;
}
int valueStart = startIndex + prefix.length();
int lineEnd = normalizedContent.indexOf('\n', valueStart);
String value = lineEnd >= 0
? normalizedContent.substring(valueStart, lineEnd)
: normalizedContent.substring(valueStart);
return normalizeOptionalText(value);
}
private String normalizeSnapshotText(String value) {
String normalized = normalizeOptionalText(value);
return normalized == null ? "" : normalized;
}
private OffsetDateTime resolveOffsetDateTime(String value) {

View File

@ -4,6 +4,8 @@ server:
spring:
application:
name: unis-crm-backend
jackson:
time-zone: Asia/Shanghai
servlet:
multipart:
max-file-size: 20MB
@ -13,6 +15,8 @@ spring:
username: postgres
password: 199628
driver-class-name: org.postgresql.Driver
hikari:
connection-init-sql: SET TIME ZONE 'Asia/Shanghai'
data:
redis:
host: 127.0.0.1

View File

@ -112,7 +112,7 @@
o.pre_sales_id as preSalesId,
coalesce(o.pre_sales_name, '') as preSalesName,
coalesce(o.competitor_name, '') as competitorName,
coalesce((
coalesce(o.latest_progress, (
select
case
when f.content like '项目最新进展:%' then
@ -125,10 +125,12 @@
order by f.followup_time desc, f.id desc
limit 1
), '') as latestProgress,
coalesce((
coalesce(o.next_plan, (
select
case
when coalesce(nullif(btrim(f.next_action), ''), '') &lt;&gt; '' then f.next_action
when f.content like '%下一步销售计划:%' then
split_part(split_part(f.content, '下一步销售计划:', 2), E'\n', 1)
when f.content like '%后续规划:%' then
split_part(split_part(f.content, '后续规划:', 2), E'\n', 1)
else ''
@ -137,6 +139,7 @@
where f.opportunity_id = o.id
and (
coalesce(nullif(btrim(f.next_action), ''), '') &lt;&gt; ''
or f.content like '%下一步销售计划:%'
or f.content like '%后续规划:%'
)
order by f.followup_time desc, f.id desc
@ -260,7 +263,7 @@
o.pre_sales_id as preSalesId,
coalesce(o.pre_sales_name, '') as preSalesName,
coalesce(o.competitor_name, '') as competitorName,
coalesce((
coalesce(o.latest_progress, (
select
case
when f.content like '项目最新进展:%' then
@ -273,10 +276,12 @@
order by f.followup_time desc, f.id desc
limit 1
), '') as latestProgress,
coalesce((
coalesce(o.next_plan, (
select
case
when coalesce(nullif(btrim(f.next_action), ''), '') &lt;&gt; '' then f.next_action
when f.content like '%下一步销售计划:%' then
split_part(split_part(f.content, '下一步销售计划:', 2), E'\n', 1)
when f.content like '%后续规划:%' then
split_part(split_part(f.content, '后续规划:', 2), E'\n', 1)
else ''
@ -285,6 +290,7 @@
where f.opportunity_id = o.id
and (
coalesce(nullif(btrim(f.next_action), ''), '') &lt;&gt; ''
or f.content like '%下一步销售计划:%'
or f.content like '%后续规划:%'
)
order by f.followup_time desc, f.id desc
@ -405,6 +411,8 @@
sales_expansion_id,
channel_expansion_id,
competitor_name,
latest_progress,
next_plan,
pushed_to_oms,
oms_push_time,
description,
@ -428,6 +436,8 @@
#{request.salesExpansionId},
#{request.channelExpansionId},
#{request.competitorName},
#{request.latestProgress},
#{request.nextPlan},
false,
null,
#{request.description},
@ -550,6 +560,8 @@
sales_expansion_id = #{request.salesExpansionId},
channel_expansion_id = #{request.channelExpansionId},
competitor_name = #{request.competitorName},
latest_progress = #{request.latestProgress},
next_plan = #{request.nextPlan},
description = #{request.description},
status = case
when #{request.stage} = 'won' then 'won'

View File

@ -628,6 +628,14 @@
)
</insert>
<update id="updateOpportunitySnapshot">
update crm_opportunity
set latest_progress = #{latestProgress},
next_plan = #{nextPlan},
updated_at = now()
where id = #{opportunityId}
</update>
<delete id="deleteTodosByBiz">
delete from work_todo
where user_id = #{userId}

View File

@ -3,6 +3,7 @@ package com.unis.crm.service.impl;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@ -112,6 +113,32 @@ class OpportunityServiceImplTest {
verify(opportunityMapper).insertCustomer(any(), eq(1L), eq("客户B"), any());
}
@Test
void createOpportunity_shouldPersistEditableSnapshotFields() {
when(opportunityMapper.selectOwnedCustomerIdByName(1L, "客户A")).thenReturn(200L);
when(opportunityMapper.insertOpportunity(eq(1L), eq(200L), any(CreateOpportunityRequest.class))).thenAnswer(invocation -> {
CreateOpportunityRequest request = invocation.getArgument(2);
request.setId(10L);
return 1;
});
when(opportunityMapper.selectOpportunityOmsPushData(1L, 10L)).thenReturn(opportunityPushData("客户A"));
when(opportunityMapper.selectCurrentUserAccount(1L)).thenReturn(currentUserAccount());
when(omsClient.ensureUserExists("zhangsan", "张三")).thenReturn(omsUser());
when(omsClient.createProject(any(OpportunityOmsPushDataDTO.class), eq("99"))).thenReturn("OPP-CODE-002");
when(opportunityMapper.updateOpportunityCode(1L, 10L, "OPP-CODE-002")).thenReturn(1);
CreateOpportunityRequest request = updateRequest("客户A");
request.setLatestProgress(" 已完成需求沟通 ");
request.setNextPlan(" 下周提交报价 ");
Long result = opportunityService.createOpportunity(1L, request);
assertEquals(10L, result);
verify(opportunityMapper).insertOpportunity(eq(1L), eq(200L), argThat(payload ->
"已完成需求沟通".equals(payload.getLatestProgress())
&& "下周提交报价".equals(payload.getNextPlan())));
}
private OpportunityCustomerSnapshotDTO customerSnapshot(Long customerId, String customerName) {
OpportunityCustomerSnapshotDTO dto = new OpportunityCustomerSnapshotDTO();
dto.setCustomerId(customerId);

View File

@ -10,10 +10,14 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.unis.crm.common.BusinessException;
import com.unis.crm.dto.work.CreateWorkCheckInRequest;
import com.unis.crm.dto.work.CreateWorkDailyReportRequest;
import com.unis.crm.dto.work.WorkDailyReportDTO;
import com.unis.crm.dto.work.WorkCheckInExportDTO;
import com.unis.crm.dto.work.WorkDailyReportExportDTO;
import com.unis.crm.dto.work.WorkHistoryItemDTO;
import com.unis.crm.dto.work.WorkHistoryPageDTO;
import com.unis.crm.dto.work.WorkReportLineItemRequest;
import com.unis.crm.dto.work.WorkTomorrowPlanItemRequest;
import java.time.LocalDate;
import com.unis.crm.mapper.ProfileMapper;
import com.unis.crm.mapper.WorkMapper;
import com.unis.crm.service.ReportReminderService;
@ -25,6 +29,10 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.web.MockMultipartFile;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
@ExtendWith(MockitoExtension.class)
class WorkServiceImplTest {
@ -163,8 +171,8 @@ class WorkServiceImplTest {
void exportDailyReports_shouldCleanMetadataAndFilterByBizType() {
WorkDailyReportExportDTO opportunityReport = new WorkDailyReportExportDTO();
opportunityReport.setWorkContent("""
1. 2026-04-02 A
[[WORK_REPORT_LINES]][{"workDate":"2026-04-02","bizType":"opportunity","bizId":1,"bizName":"项目A","content":"项目最新进展:推进中\\n后续规划:继续推进","latestProgress":"推进中","nextPlan":"继续推进","editorText":"@商机 项目A\\n# 项目最新进展:推进中\\n# 后续规划:继续推进"}][[/WORK_REPORT_LINES]]
1. 2026-04-02 A
[[WORK_REPORT_LINES]][{"workDate":"2026-04-02","bizType":"opportunity","bizId":1,"bizName":"项目A","content":"项目最新进展:推进中\\n下一步销售计划:继续推进","latestProgress":"推进中","nextPlan":"继续推进","editorText":"@商机 项目A\\n# 项目最新进展:推进中\\n# 下一步销售计划:继续推进"}][[/WORK_REPORT_LINES]]
""");
opportunityReport.setTomorrowPlan("""
1.
@ -185,13 +193,58 @@ class WorkServiceImplTest {
List<WorkDailyReportExportDTO> result = workService.exportDailyReports(17L, null, null, null, null, "opportunity", null);
assertEquals(1, result.size());
assertEquals("1. 2026-04-02 跟进商机“项目A”项目最新进展推进中后续规划:继续推进", result.get(0).getWorkContent());
assertEquals("1. 2026-04-02 跟进商机“项目A”项目最新进展推进中下一步销售计划:继续推进", result.get(0).getWorkContent());
assertEquals(1, result.get(0).getLineItems().size());
assertEquals("项目A", result.get(0).getLineItems().get(0).getBizName());
assertEquals(1, result.get(0).getPlanItems().size());
assertEquals("跟进客户", result.get(0).getPlanItems().get(0).getContent());
}
@Test
void saveDailyReport_shouldNotUseTomorrowPlanAsOpportunityNextAction() {
CreateWorkDailyReportRequest request = new CreateWorkDailyReportRequest();
WorkReportLineItemRequest lineItem = new WorkReportLineItemRequest();
lineItem.setWorkDate("2026-04-28");
lineItem.setBizType("opportunity");
lineItem.setBizId(101L);
lineItem.setBizName("项目A");
lineItem.setEditorText("""
@ A
#
#
""");
lineItem.setContent("项目最新进展:推进中");
request.setLineItems(List.of(lineItem));
WorkTomorrowPlanItemRequest planItem = new WorkTomorrowPlanItemRequest();
planItem.setContent("这是整篇日报的明日计划");
request.setPlanItems(List.of(planItem));
request.setWorkContent("占位");
request.setTomorrowPlan("这是整篇日报的明日计划");
request.setSourceType("manual");
WorkDailyReportDTO currentReport = new WorkDailyReportDTO();
currentReport.setDate("2026-04-28");
currentReport.setSubmitTime("2026-04-28 10:00");
when(workMapper.insertDailyReport(eq(17L), any(CreateWorkDailyReportRequest.class), any(LocalDate.class), any()))
.thenReturn(1);
when(workMapper.selectTodayReportId(eq(17L), any(LocalDate.class)))
.thenReturn(null, 501L);
when(workMapper.selectTodayReport(eq(17L), any(LocalDate.class)))
.thenReturn(currentReport);
workService.saveDailyReport(17L, request);
verify(workMapper).insertLegacyOpportunityFollowUp(
eq(101L),
eq(17L),
any(),
eq("工作日报"),
eq("项目最新进展:推进中"),
eq(null));
}
private WorkHistoryItemDTO historyItem(Long id, String type, String date, String time, String content) {
WorkHistoryItemDTO item = new WorkHistoryItemDTO();
item.setId(id);

View File

@ -0,0 +1,756 @@
# 后台配置 SQL 驱动的经营分析方案
## 1. 目标
本方案满足你明确提出的目标:
- 统计定义在后台管理配置
- H5 端直接按配置展示
- 取数 SQL 也可以在后台管理配置
- 后续口径变化时,尽量不改前端代码
- 同时兼顾 PC 端与 H5 / 企业微信端
这套方案的核心思想是:
- `前端不写死报表`
- `后台管理端维护报表定义`
- `后端读取配置 SQL 执行`
- `H5 / PC 统一走配置渲染`
## 2. 结论先说
这个方案是可行的,但建议不要做“完全无限制的任意 SQL 执行”,而要做:
- `后台可配置 SQL`
- `后端受控执行 SQL`
- `带参数模板`
- `带权限注入`
- `带版本管理`
- `带审核发布`
也就是说,不是直接把数据库裸露给后台管理员,而是做一个“`可配置 SQL 报表平台`”。
## 3. 适合你的整体架构
建议拆成 4 个部分:
### 3.1 报表定义后台
给管理员使用,用于配置:
- 报表名称
- 适用端
- 筛选条件
- 图表类型
- 指标字段
- 维度字段
- SQL 模板
- 参数说明
- 发布版本
### 3.2 报表执行引擎
后端新增一个报表引擎服务,职责是:
- 读取报表配置
- 校验参数
- 替换 SQL 模板变量
- 自动追加权限条件
- 执行 SQL
- 返回统一结果结构
### 3.3 H5 / PC 动态渲染层
前端不再写死每个图表,而是:
- 先请求报表页面配置
- 再请求每个组件的数据
- 根据配置渲染 KPI、柱状图、折线图、饼图、表格
### 3.4 配置治理层
用于保证 SQL 配置安全可控:
- 草稿/已发布版本
- 审核流
- 执行日志
- 回滚版本
- 性能限制
## 4. 最推荐的实现模式
你要的是“SQL 可配置”,这里推荐两个级别。
### 4.1 级别一SQL 模板可配置
这是最推荐的。
后台配置的不是完全自由 SQL而是带占位符的模板 SQL例如
```sql
select
owner_user_id as ownerUserId,
sum(amount) as actualAmount
from crm_opportunity
where status = 'won'
and won_at between ${startDate} and ${endDate}
${dataScopeClause}
${orgFilterClause}
group by owner_user_id
order by actualAmount desc
```
特点:
- 可配灵活
- 后端可控
- 容易注入权限
- 易于参数校验
### 4.2 级别二:完全自由 SQL
不建议默认开放。
如果一定要支持,也建议:
- 仅超级管理员可用
- 仅允许 `select`
- 禁止 `update/delete/insert/drop`
- 禁止多语句
- 禁止访问敏感表
- 强制走只读数据源
- 强制超时和行数限制
## 5. 业务上如何满足“销售完成业绩”和“区域业绩”
这两个场景很适合做成后台配置化报表。
### 5.1 销售完成业绩
建议报表结构:
- KPI
- 销售目标
- 实际完成
- 完成率
- 同比/环比
- 图表:
- 销售排行
- 趋势图
- 部门分布
- 明细:
- 销售员、目标值、完成值、完成率、区域、趋势
### 5.2 区域业绩
建议报表结构:
- KPI
- 区域目标
- 区域实际
- 区域完成率
- 图表:
- 区域排名
- 区域趋势
- 区域构成
- 明细:
- 大区、省区、城市、销售数、目标、实际、完成率
这两类都可以通过后台 SQL 配置实现。
## 6. 推荐的数据模型
建议新增以下表。
### 6.1 `analytics_report`
存报表主定义。
建议字段:
- `id`
- `report_code`
- `report_name`
- `report_category`
- `description`
- `status`
- `terminal_scope`
- `default_time_granularity`
- `visible`
- `created_by`
- `updated_by`
- `created_at`
- `updated_at`
说明:
- `terminal_scope` 用于区分 `pc / h5 / both`
### 6.2 `analytics_report_version`
存报表版本。
建议字段:
- `id`
- `report_id`
- `version_no`
- `version_status`
- `change_log`
- `published_by`
- `published_at`
- `config_json`
- `created_at`
说明:
- 一个报表允许多个版本
- H5 默认读已发布版本
### 6.3 `analytics_widget`
存一个报表下的组件定义。
建议字段:
- `id`
- `report_version_id`
- `widget_code`
- `widget_name`
- `widget_type`
- `title`
- `subtitle`
- `layout_json`
- `props_json`
- `data_source_id`
- `sort_order`
- `visible`
### 6.4 `analytics_data_source`
存数据源定义,也就是 SQL 配置核心。
建议字段:
- `id`
- `source_code`
- `source_name`
- `source_type`
- `dataset_code`
- `query_sql`
- `count_sql`
- `parameter_schema_json`
- `result_schema_json`
- `timeout_ms`
- `max_rows`
- `cache_ttl_seconds`
- `enabled`
- `created_at`
- `updated_at`
### 6.5 `analytics_filter_def`
存筛选器定义。
建议字段:
- `id`
- `report_version_id`
- `filter_code`
- `filter_name`
- `filter_type`
- `data_type`
- `default_value_json`
- `options_source_type`
- `options_source_config`
- `sort_order`
- `visible`
### 6.6 `analytics_sql_publish_log`
存 SQL 发布日志。
建议字段:
- `id`
- `data_source_id`
- `version_no`
- `old_sql`
- `new_sql`
- `change_summary`
- `operator_id`
- `created_at`
### 6.7 `analytics_query_log`
存执行日志。
建议字段:
- `id`
- `report_code`
- `widget_code`
- `data_source_id`
- `request_user_id`
- `request_params_json`
- `final_sql_preview`
- `duration_ms`
- `row_count`
- `success`
- `error_message`
- `created_at`
## 7. 配置中心怎么设计
后台建议做成 5 个页面。
### 7.1 报表管理
配置:
- 报表基本信息
- 展示端
- 菜单归属
- 是否发布
### 7.2 页面布局管理
配置:
- KPI 卡片
- 图表组件
- 表格组件
- H5 是否显示
- PC 是否显示
- 顺序与显隐
### 7.3 SQL 数据源管理
配置:
- SQL 模板
- 参数定义
- 字段映射
- 结果预览
- 缓存策略
- 执行限制
### 7.4 筛选器管理
配置:
- 时间筛选
- 区域筛选
- 销售筛选
- 部门筛选
- 自定义枚举
### 7.5 发布与回滚
配置:
- 草稿
- 预览
- 发布
- 回滚历史版本
## 8. SQL 配置方式建议
这是关键设计点。
### 8.1 SQL 模板结构
建议使用模板 SQL不要直接存“拼好的最终 SQL”。
示例:
```sql
select
region_code as regionCode,
region_name as regionName,
sum(actual_amount) as actualAmount,
sum(target_amount) as targetAmount,
case when sum(target_amount) = 0 then 0
else round(sum(actual_amount) / sum(target_amount) * 100, 2)
end as completionRate
from analytics_sales_performance_fact
where stat_date between ${startDate} and ${endDate}
${orgFilterClause}
${regionFilterClause}
${salesFilterClause}
${dataScopeClause}
group by region_code, region_name
order by actualAmount desc
limit ${limit}
```
### 8.2 参数定义
每条 SQL 必须配参数定义。
示例:
```json
[
{ "name": "startDate", "type": "date", "required": true },
{ "name": "endDate", "type": "date", "required": true },
{ "name": "orgId", "type": "long", "required": false },
{ "name": "regionCode", "type": "string", "required": false },
{ "name": "salesUserId", "type": "long", "required": false },
{ "name": "limit", "type": "int", "required": false, "defaultValue": 50 }
]
```
### 8.3 建议支持的占位符
后端统一支持如下占位符:
- `${startDate}`
- `${endDate}`
- `${limit}`
- `${offset}`
- `${dataScopeClause}`
- `${orgFilterClause}`
- `${regionFilterClause}`
- `${salesFilterClause}`
### 8.4 动态条件注入方式
例如 `regionCode` 为空时,不拼条件;
有值时自动生成:
```sql
and region_code = :regionCode
```
不建议把这种逻辑写死在前端。
## 9. 前端展示怎么实现
虽然 SQL 在后台配置,但前端还是要有统一协议。
### 9.1 H5 与 PC 共用配置协议
前端请求:
- 页面定义
- 筛选器定义
- Widget 定义
- Widget 数据
### 9.2 Widget 类型建议
支持以下组件:
- `kpi`
- `line`
- `bar`
- `stack-bar`
- `pie`
- `table`
- `rank-list`
### 9.3 H5 展示规则
建议在配置里加上:
- `showOnPc`
- `showOnH5`
- `h5SortOrder`
- `pcSortOrder`
- `h5ChartType`
- `pcChartType`
这样同一个报表可以:
- PC 显示表格 + 图表
- H5 只显示 KPI + Top10
### 9.4 H5 页面渲染流程
1. 获取报表定义
2. 获取筛选项定义
3. 渲染页面骨架
4. 请求组件数据
5. 渲染图表
## 10. 后端执行引擎设计
建议新增一个 `AnalyticsQueryService`
职责:
- 加载已发布报表版本
- 解析筛选条件
- 校验参数类型
- 生成最终 SQL
- 注入权限条件
- 执行查询
- 做缓存
- 记录日志
### 10.1 执行流程
1. 读取报表配置
2. 读取组件配置
3. 读取数据源 SQL 模板
4. 根据请求参数构造过滤条件
5. 自动注入数据权限
6. 生成最终 SQL
7. 执行查询
8. 转成统一结果结构
### 10.2 统一返回结构
建议:
```json
{
"reportCode": "sales-performance",
"title": "销售完成业绩",
"filters": [],
"widgets": [
{
"widgetCode": "sales-rank",
"widgetType": "bar",
"title": "销售业绩排名",
"columns": ["ownerName", "actualAmount"],
"rows": []
}
]
}
```
## 11. 权限与安全控制
这是这个方案能不能上线的关键。
### 11.1 禁止直接执行危险 SQL
必须限制:
- 只允许 `select`
- 禁止 `insert`
- 禁止 `update`
- 禁止 `delete`
- 禁止 `drop`
- 禁止 `alter`
- 禁止多语句执行
### 11.2 必须强制数据权限注入
即使后台配置 SQL也不能信任配置人手动写权限条件。
必须由后端统一追加:
- 当前租户过滤
- 当前用户数据范围
- 组织权限范围
### 11.3 建议使用只读数据源
报表执行引擎单独使用只读数据库账号。
### 11.4 超时与数据量限制
每条数据源定义都要支持:
- 最大执行时长
- 最大返回行数
- 最大导出行数
### 11.5 SQL 审核
建议发布前做 SQL 检查:
- 关键词校验
- 表白名单校验
- 字段白名单校验
- explain 成本校验
## 12. 针对“销售完成业绩”和“区域业绩”的配置方式
### 12.1 销售完成业绩推荐做法
建议先沉淀一个业绩事实层表或视图:
- `analytics_sales_performance_fact`
字段建议:
- `stat_date`
- `sales_user_id`
- `sales_user_name`
- `org_id`
- `org_name`
- `region_code`
- `region_name`
- `target_amount`
- `actual_amount`
- `won_amount`
- `contract_amount`
- `payment_amount`
- `adjust_amount`
好处:
- 前端怎么展示都行
- 后台 SQL 简单很多
- 口径变更时只改事实层生成逻辑或切换版本
### 12.2 区域业绩推荐做法
建议同样通过事实层统一:
- 客户区域
- 项目区域
- 销售归属区域
如果口径会改,可以做多个字段:
- `customer_region_code`
- `project_region_code`
- `owner_region_code`
后台 SQL 通过配置选择用哪个字段聚合。
## 13. 推荐的“SQL 可配置”分层
为了避免后期越来越乱,建议把 SQL 分成两层。
### 13.1 第一层:事实层 SQL
由研发维护。
职责:
- 把复杂业务逻辑沉淀成事实表或视图
- 处理复杂口径、去重、归属、调整项
### 13.2 第二层:展示层 SQL
由后台管理配置。
职责:
- 从事实层表/视图做汇总
- 做 KPI、排行、趋势、区域分组
这是最适合你的方案。
原因:
- 真正复杂的口径不适合完全交给后台人员写
- 但展示层统计完全可以后台配置
- H5 端可以完全动态渲染
## 14. 如果你坚持“所有 SQL 都后台可配”
也可以做,但我建议加 3 个保护措施。
### 14.1 只允许访问白名单对象
例如只允许查:
- `analytics_*`
- `crm_*`
- `work_*`
- 部分 `sys_*` 维表
### 14.2 强制 SQL 发布审核
流程建议:
- 编辑草稿
- 测试预览
- 提交审核
- 审核通过后发布
### 14.3 强制版本切换
H5 / PC 永远只读“已发布版本”,不读草稿。
## 15. API 设计建议
### 15.1 后台管理端
- `GET /api/admin/analytics/reports`
- `POST /api/admin/analytics/reports`
- `PUT /api/admin/analytics/reports/{id}`
- `GET /api/admin/analytics/reports/{id}/versions`
- `POST /api/admin/analytics/data-sources`
- `PUT /api/admin/analytics/data-sources/{id}`
- `POST /api/admin/analytics/data-sources/{id}/preview`
- `POST /api/admin/analytics/reports/{id}/publish`
### 15.2 前台 H5 / PC
- `GET /api/analytics/reports/{reportCode}`
- `POST /api/analytics/reports/{reportCode}/query`
## 16. 实施步骤建议
### 第一阶段
目标:
- 先把基础平台搭起来
内容:
- 报表定义表
- 数据源定义表
- Widget 定义表
- H5 动态渲染页
- PC 动态渲染页
- SQL 安全校验
### 第二阶段
目标:
- 上线销售完成业绩、区域业绩
内容:
- 销售业绩事实表
- 区域业绩事实表
- 报表管理页
- SQL 配置预览
- 发布与回滚
### 第三阶段
目标:
- 真正进入“后台可运营”
内容:
- 版本对比
- 审核流
- 执行监控
- 性能报警
- 缓存策略
## 17. 最终建议
如果你想实现:
- 后台管理定义
- H5 自动展示
- SQL 可配置
- 后期口径频繁修改
那么我最推荐的是:
1. 做一个 `报表定义 + SQL 数据源 + Widget 配置` 的后台管理平台
2. H5 与 PC 统一走动态渲染
3. SQL 采用“模板化 + 参数化 + 权限注入 + 发布审核”
4. 真正复杂的业绩口径优先沉淀为事实层视图或事实表
5. 展示层 SQL 再交给后台配置
一句话总结:
`这个方案完全可以做而且很适合你当前“口径复杂、变化频繁、H5要快速展示”的场景但最佳实践不是裸放任意 SQL而是做受控的后台 SQL 配置平台。`

View File

@ -222,6 +222,8 @@ erDiagram
| `product_type` | varchar(100) | 产品类型 |
| `source` | varchar(50) | 商机来源 |
| `competitor_name` | varchar(200) | 竞品名称 |
| `latest_progress` | text | 项目最新进展 |
| `next_plan` | text | 下一步销售计划 |
| `archived` | boolean | 是否归档 |
| `pushed_to_oms` | boolean | 是否已推送 OMS |
| `oms_push_time` | timestamptz | 推送时间 |
@ -341,7 +343,7 @@ erDiagram
| `channel_name` | varchar(200) | 渠道名称 |
| `office_address` | varchar(255) | 办公地址 |
| `channel_industry` | varchar(100) | 聚焦行业 |
| `certification_level` | varchar(100) | 认证级别 |
| `certification_level` | varchar(100) | 汇智内部认证级别 |
| `annual_revenue` | numeric(18,2) | 年营收 |
| `staff_size` | integer | 人员规模 |
| `contact_established_date` | date | 建立联系日期 |
@ -655,4 +657,3 @@ erDiagram
- `docs/system-construction.md`
- `docs/technical-system-planning.md`
- `docs/deployment-guide.md`

View File

@ -0,0 +1,207 @@
# 首页相关能力 SQL 发布说明
本文档用于指导已有生产环境执行本次首页相关能力升级 SQL。
对应正式脚本:
- `sql/upgrade_dashboard_analytics_prod_pg17.sql`
## 1. 适用范围
适用于以下场景:
- 已存在业务数据的老环境
- 需要补齐首页经营分析配置能力
- 需要补齐首页卡片权限
- 需要补齐商机快照字段
不适用于以下场景:
- 全新初始化数据库
全新环境请直接执行:
```bash
psql -d your_database -f sql/init_full_pg17.sql
```
## 2. 本次变更内容
主升级脚本 `sql/upgrade_dashboard_analytics_prod_pg17.sql` 会补齐:
- 新建 `dashboard_analytics_panel_config`
- 新建 `dashboard_analytics_card_config`
- 补齐首页权限分组与卡片权限:
- `menu:frontend-home`
- `dashboard_todo_card:view`
- `dashboard_activity_card:view`
- `dashboard_stats_card:view`
- `dashboard_analytics_card:view`
- `menu:dashboard-analytics-settings`
- `dashboard_analytics_config:view`
- `dashboard_analytics_config:update`
- `dashboard_analytics_config:preview`
- 给 `crm_opportunity` 增加字段:
- `latest_progress`
- `next_plan`
- 修正 `crm_channel_expansion.certification_level` 字段注释为“汇智内部认证级别”
- 修复平台菜单权限串租户问题,并补齐 `system` 父目录权限
这份脚本不会做的事情:
- 不插入默认经营分析业务卡片
- 不创建目标值业务表
- 不导入业务数据
## 3. 上线执行顺序
### 3.1 执行前准备
建议先完成以下检查:
- 确认当前数据库为 PostgreSQL 17 兼容环境
- 确认业务高峰期外执行
- 备份本次涉及表的结构和关键权限数据:
- `crm_opportunity`
- `crm_channel_expansion`
- `sys_permission`
- `sys_role_permission`
- `dashboard_analytics_panel_config`
- `dashboard_analytics_card_config`
建议至少保留一份备份:
```bash
pg_dump -d your_database -t crm_opportunity -t crm_channel_expansion -t sys_permission -t sys_role_permission > pre_dashboard_upgrade.sql
```
如果环境里还没有经营分析表,`pg_dump` 忽略这两张新表即可。
### 3.2 正常执行顺序
生产环境标准执行顺序如下:
1. 执行正式升级脚本
```bash
psql -d your_database -f sql/upgrade_dashboard_analytics_prod_pg17.sql
```
2. 执行完成后做结构校验
```sql
select column_name
from information_schema.columns
where table_name = 'crm_opportunity'
and column_name in ('latest_progress', 'next_plan');
```
```sql
select table_name
from information_schema.tables
where table_name in ('dashboard_analytics_panel_config', 'dashboard_analytics_card_config');
```
3. 做权限校验
```sql
select code, name, is_deleted
from sys_permission
where code in (
'menu:frontend-home',
'dashboard_todo_card:view',
'dashboard_activity_card:view',
'dashboard_stats_card:view',
'dashboard_analytics_card:view',
'menu:dashboard-analytics-settings',
'dashboard_analytics_config:view',
'dashboard_analytics_config:update',
'dashboard_analytics_config:preview'
)
order by code, perm_id;
```
4. 做页面冒烟验证
- 首页正常打开
- 待办卡片正常显示
- 最新动态卡片正常显示
- 指标卡正常显示
- 有权限账号能进入“首页经营分析配置”页面
- 商机新增/编辑页面可正常保存“项目最新进展”“下一步销售计划”
## 4. 验收重点
上线后建议重点验收以下内容:
- `crm_opportunity` 能正常读写 `latest_progress`、`next_plan`
- 老商机详情能正常打开,不因新字段为空报错
- 首页待办、动态、指标、经营分析卡片权限符合预期
- 管理员角色拥有首页经营分析配置菜单与按钮权限
- 普通租户角色不会误拿平台菜单权限
- 经营分析配置页能打开,但若未配置卡片,页面为空属于预期现象
## 5. 风险说明
### 5.1 权限可见性变化风险
本次脚本会创建、修复并去重首页相关权限,同时会给管理员类角色补权限。
风险点:
- 某些历史脏数据环境中,原来依赖重复权限记录的角色可见性可能发生变化
- 若角色本身不属于管理员类,脚本不会自动补齐首页经营分析配置权限
建议:
- 上线后用管理员账号和普通业务账号各验证一次首页与系统管理菜单
### 5.2 经营分析页“空白”风险
本次只补配置表和权限,不会写默认卡片配置。
风险点:
- 脚本执行成功后,经营分析区域可能为空
这不是故障,表示表结构已就绪,但后台尚未配置业务卡片。
### 5.3 商机字段新增后的数据回填预期
`latest_progress``next_plan` 为新增字段,历史数据不会被批量回填。
风险点:
- 旧商机初次查看时,这两个字段可能为空
当前代码已经保留从历史跟进记录兜底读取的兼容逻辑,因此一般不会影响旧数据查看。
### 5.4 平台菜单权限纠偏风险
本次正式升级脚本已包含平台菜单权限纠偏逻辑。
风险点:
- 若某些环境历史上错误发放了平台菜单权限,执行后这些错误权限会被收回
建议:
- 执行前备份 `sys_role_permission`
- 执行后重点验证租户管理员、平台管理员两类账号的菜单可见性
## 6. 回滚建议
本次脚本以“新增字段、建表、权限修复”为主,不建议直接无审查回滚。
建议回滚策略:
1. 若只是经营分析权限异常,优先人工修复 `sys_permission` / `sys_role_permission`
2. 若只是经营分析配置有误,清理 `dashboard_analytics_panel_config` / `dashboard_analytics_card_config` 即可
3. 若必须整体回退,请基于执行前备份恢复,不建议直接手写反向 SQL 在线回滚
## 7. 推荐发布口径
可向实施或运维同事同步以下结论:
- 本次生产升级主入口只有一份:`sql/upgrade_dashboard_analytics_prod_pg17.sql`
- 旧的重复升级脚本已删除,不需要再单独执行首页卡片权限历史脚本

View File

@ -908,6 +908,42 @@
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
"license": "BSD-3-Clause"
},
"node_modules/@reduxjs/toolkit": {
"version": "2.11.2",
"resolved": "https://registry.npmmirror.com/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
"integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"@standard-schema/utils": "^0.3.0",
"immer": "^11.0.0",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
"reselect": "^5.1.0"
},
"peerDependencies": {
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-redux": {
"optional": true
}
}
},
"node_modules/@reduxjs/toolkit/node_modules/immer": {
"version": "11.1.4",
"resolved": "https://registry.npmmirror.com/immer/-/immer-11.1.4.tgz",
"integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
@ -1263,6 +1299,18 @@
"win32"
]
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"license": "MIT"
},
"node_modules/@standard-schema/utils": {
"version": "0.3.0",
"resolved": "https://registry.npmmirror.com/@standard-schema/utils/-/utils-0.3.0.tgz",
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
"license": "MIT"
},
"node_modules/@tailwindcss/node": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz",
@ -1593,6 +1641,69 @@
"@types/node": "*"
}
},
"node_modules/@types/d3-array": {
"version": "3.2.2",
"resolved": "https://registry.npmmirror.com/@types/d3-array/-/d3-array-3.2.2.tgz",
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
"license": "MIT"
},
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmmirror.com/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
"license": "MIT"
},
"node_modules/@types/d3-ease": {
"version": "3.0.2",
"resolved": "https://registry.npmmirror.com/@types/d3-ease/-/d3-ease-3.0.2.tgz",
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
"license": "MIT"
},
"node_modules/@types/d3-interpolate": {
"version": "3.0.4",
"resolved": "https://registry.npmmirror.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"license": "MIT",
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-path": {
"version": "3.1.1",
"resolved": "https://registry.npmmirror.com/@types/d3-path/-/d3-path-3.1.1.tgz",
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
"license": "MIT"
},
"node_modules/@types/d3-scale": {
"version": "4.0.9",
"resolved": "https://registry.npmmirror.com/@types/d3-scale/-/d3-scale-4.0.9.tgz",
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
"license": "MIT",
"dependencies": {
"@types/d3-time": "*"
}
},
"node_modules/@types/d3-shape": {
"version": "3.1.8",
"resolved": "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.8.tgz",
"integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
"license": "MIT",
"dependencies": {
"@types/d3-path": "*"
}
},
"node_modules/@types/d3-time": {
"version": "3.0.4",
"resolved": "https://registry.npmmirror.com/@types/d3-time/-/d3-time-3.0.4.tgz",
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
"license": "MIT"
},
"node_modules/@types/d3-timer": {
"version": "3.0.2",
"resolved": "https://registry.npmmirror.com/@types/d3-timer/-/d3-timer-3.0.2.tgz",
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@ -1701,6 +1812,12 @@
"@types/node": "*"
}
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmmirror.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
"license": "MIT"
},
"node_modules/@vitejs/plugin-react": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz",
@ -2356,6 +2473,127 @@
"node": ">= 8"
}
},
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.4.tgz",
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"license": "ISC",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-format": {
"version": "3.1.2",
"resolved": "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.2.tgz",
"integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-path": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/d3-path/-/d3-path-3.1.0.tgz",
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"license": "ISC",
"dependencies": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-shape": {
"version": "3.2.0",
"resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz",
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
"license": "ISC",
"dependencies": {
"d3-path": "^3.1.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/d3-time/-/d3-time-3.1.0.tgz",
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"license": "ISC",
"dependencies": {
"d3-array": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time-format": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-4.1.0.tgz",
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"license": "ISC",
"dependencies": {
"d3-time": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
@ -2398,6 +2636,12 @@
}
}
},
"node_modules/decimal.js-light": {
"version": "2.5.1",
"resolved": "https://registry.npmmirror.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
"license": "MIT"
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@ -2585,6 +2829,16 @@
"node": ">= 0.4"
}
},
"node_modules/es-toolkit": {
"version": "1.46.0",
"resolved": "https://registry.npmmirror.com/es-toolkit/-/es-toolkit-1.46.0.tgz",
"integrity": "sha512-IToJ6ct9OLl5zz6WsC/1vZEwfSZ7Myil+ygl5Tf30Xjn9AEkzNB4kqp2G7VUJKF1DtTx/ra5M5KLlXvzOg51BA==",
"license": "MIT",
"workspaces": [
"docs",
"benchmarks"
]
},
"node_modules/esbuild": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
@ -2651,6 +2905,12 @@
"node": ">= 0.6"
}
},
"node_modules/eventemitter3": {
"version": "5.0.4",
"resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.4.tgz",
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
"license": "MIT"
},
"node_modules/exceljs": {
"version": "4.4.0",
"resolved": "https://registry.npmmirror.com/exceljs/-/exceljs-4.4.0.tgz",
@ -3266,6 +3526,16 @@
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/immer": {
"version": "10.2.0",
"resolved": "https://registry.npmmirror.com/immer/-/immer-10.2.0.tgz",
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz",
@ -3283,6 +3553,15 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@ -4377,6 +4656,36 @@
"react": "^19.2.4"
}
},
"node_modules/react-is": {
"version": "19.2.5",
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-19.2.5.tgz",
"integrity": "sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==",
"license": "MIT",
"peer": true
},
"node_modules/react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmmirror.com/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT",
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
},
"peerDependencies": {
"@types/react": "^18.2.25 || ^19",
"react": "^18.0 || ^19",
"redux": "^5.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"redux": {
"optional": true
}
}
},
"node_modules/react-refresh": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
@ -4472,6 +4781,57 @@
"node": ">=10"
}
},
"node_modules/recharts": {
"version": "3.8.1",
"resolved": "https://registry.npmmirror.com/recharts/-/recharts-3.8.1.tgz",
"integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==",
"license": "MIT",
"workspaces": [
"www"
],
"dependencies": {
"@reduxjs/toolkit": "^1.9.0 || 2.x.x",
"clsx": "^2.1.1",
"decimal.js-light": "^2.5.1",
"es-toolkit": "^1.39.3",
"eventemitter3": "^5.0.1",
"immer": "^10.1.1",
"react-redux": "8.x.x || 9.x.x",
"reselect": "5.1.1",
"tiny-invariant": "^1.3.3",
"use-sync-external-store": "^1.2.2",
"victory-vendor": "^37.0.2"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/redux": {
"version": "5.0.1",
"resolved": "https://registry.npmmirror.com/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT"
},
"node_modules/redux-thunk": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/redux-thunk/-/redux-thunk-3.1.0.tgz",
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
"license": "MIT",
"peerDependencies": {
"redux": "^5.0.0"
}
},
"node_modules/reselect": {
"version": "5.1.1",
"resolved": "https://registry.npmmirror.com/reselect/-/reselect-5.1.1.tgz",
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
"license": "MIT"
},
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
@ -4948,6 +5308,12 @@
"node": ">=6"
}
},
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmmirror.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@ -5137,6 +5503,15 @@
"browserslist": ">= 4.21.0"
}
},
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -5170,6 +5545,28 @@
"node": ">= 0.8"
}
},
"node_modules/victory-vendor": {
"version": "37.3.6",
"resolved": "https://registry.npmmirror.com/victory-vendor/-/victory-vendor-37.3.6.tgz",
"integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
"license": "MIT AND ISC",
"dependencies": {
"@types/d3-array": "^3.0.3",
"@types/d3-ease": "^3.0.0",
"@types/d3-interpolate": "^3.0.1",
"@types/d3-scale": "^4.0.2",
"@types/d3-shape": "^3.1.0",
"@types/d3-time": "^3.0.0",
"@types/d3-timer": "^3.0.0",
"d3-array": "^3.1.6",
"d3-ease": "^3.0.1",
"d3-interpolate": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-shape": "^3.1.0",
"d3-time": "^3.0.0",
"d3-timer": "^3.0.1"
}
},
"node_modules/vite": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",

View File

@ -1,91 +1,100 @@
{
"hash": "4ed97220",
"hash": "11a137f3",
"configHash": "4d48f89c",
"lockfileHash": "25980767",
"browserHash": "0ac37406",
"lockfileHash": "3435c45d",
"browserHash": "01835e8f",
"optimized": {
"react": {
"src": "../../react/index.js",
"file": "react.js",
"fileHash": "5499e597",
"fileHash": "2fe36264",
"needsInterop": true
},
"react-dom": {
"src": "../../react-dom/index.js",
"file": "react-dom.js",
"fileHash": "ee512445",
"fileHash": "3e554bb3",
"needsInterop": true
},
"react/jsx-dev-runtime": {
"src": "../../react/jsx-dev-runtime.js",
"file": "react_jsx-dev-runtime.js",
"fileHash": "b416d357",
"fileHash": "640ca040",
"needsInterop": true
},
"react/jsx-runtime": {
"src": "../../react/jsx-runtime.js",
"file": "react_jsx-runtime.js",
"fileHash": "d2d6e3f0",
"fileHash": "e2fb61c6",
"needsInterop": true
},
"clsx": {
"src": "../../clsx/dist/clsx.mjs",
"file": "clsx.js",
"fileHash": "48844b26",
"fileHash": "36d60f13",
"needsInterop": false
},
"date-fns": {
"src": "../../date-fns/index.js",
"file": "date-fns.js",
"fileHash": "e0776950",
"fileHash": "204ad576",
"needsInterop": false
},
"lucide-react": {
"src": "../../lucide-react/dist/esm/lucide-react.js",
"file": "lucide-react.js",
"fileHash": "2b6331af",
"fileHash": "13822394",
"needsInterop": false
},
"motion/react": {
"src": "../../motion/dist/es/react.mjs",
"file": "motion_react.js",
"fileHash": "6ad27c2c",
"fileHash": "baa20c1b",
"needsInterop": false
},
"react-dom/client": {
"src": "../../react-dom/client.js",
"file": "react-dom_client.js",
"fileHash": "105dfe43",
"fileHash": "49e567cb",
"needsInterop": true
},
"react-router-dom": {
"src": "../../react-router-dom/dist/index.mjs",
"file": "react-router-dom.js",
"fileHash": "8926882a",
"fileHash": "92bb81d7",
"needsInterop": false
},
"tailwind-merge": {
"src": "../../tailwind-merge/dist/bundle-mjs.mjs",
"file": "tailwind-merge.js",
"fileHash": "2c815557",
"fileHash": "196a6373",
"needsInterop": false
},
"date-fns/locale": {
"src": "../../date-fns/locale.js",
"file": "date-fns_locale.js",
"fileHash": "00bef214",
"fileHash": "c028f096",
"needsInterop": false
},
"exceljs": {
"src": "../../exceljs/dist/exceljs.min.js",
"file": "exceljs.js",
"fileHash": "7f44c50c",
"fileHash": "4f4c428f",
"needsInterop": true
},
"recharts": {
"src": "../../recharts/es6/index.js",
"file": "recharts.js",
"fileHash": "05557fbc",
"needsInterop": false
}
},
"chunks": {
"chunk-7VK2ROTQ": {
"file": "chunk-7VK2ROTQ.js"
"chunk-ZFXKT4LN": {
"file": "chunk-ZFXKT4LN.js"
},
"chunk-U7P2NEEE": {
"file": "chunk-U7P2NEEE.js"
},
"chunk-5MXL5BYH": {
"file": "chunk-5MXL5BYH.js"

View File

@ -1,22 +1,9 @@
import {
clsx,
clsx_default
} from "./chunk-U7P2NEEE.js";
import "./chunk-2TUXWMP5.js";
// node_modules/clsx/dist/clsx.mjs
function r(e) {
var t, f, n = "";
if ("string" == typeof e || "number" == typeof e) n += e;
else if ("object" == typeof e) if (Array.isArray(e)) {
var o = e.length;
for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
} else for (f in e) e[f] && (n && (n += " "), n += f);
return n;
}
function clsx() {
for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
return n;
}
var clsx_default = clsx;
export {
clsx,
clsx_default as default
};
//# sourceMappingURL=clsx.js.map

View File

@ -1,7 +1,7 @@
{
"version": 3,
"sources": ["../../clsx/dist/clsx.mjs"],
"sourcesContent": ["function r(e){var t,f,n=\"\";if(\"string\"==typeof e||\"number\"==typeof e)n+=e;else if(\"object\"==typeof e)if(Array.isArray(e)){var o=e.length;for(t=0;t<o;t++)e[t]&&(f=r(e[t]))&&(n&&(n+=\" \"),n+=f)}else for(f in e)e[f]&&(n&&(n+=\" \"),n+=f);return n}export function clsx(){for(var e,t,f=0,n=\"\",o=arguments.length;f<o;f++)(e=arguments[f])&&(t=r(e))&&(n&&(n+=\" \"),n+=t);return n}export default clsx;"],
"mappings": ";;;AAAA,SAAS,EAAE,GAAE;AAAC,MAAI,GAAE,GAAE,IAAE;AAAG,MAAG,YAAU,OAAO,KAAG,YAAU,OAAO,EAAE,MAAG;AAAA,WAAU,YAAU,OAAO,EAAE,KAAG,MAAM,QAAQ,CAAC,GAAE;AAAC,QAAI,IAAE,EAAE;AAAO,SAAI,IAAE,GAAE,IAAE,GAAE,IAAI,GAAE,CAAC,MAAI,IAAE,EAAE,EAAE,CAAC,CAAC,OAAK,MAAI,KAAG,MAAK,KAAG;AAAA,EAAE,MAAM,MAAI,KAAK,EAAE,GAAE,CAAC,MAAI,MAAI,KAAG,MAAK,KAAG;AAAG,SAAO;AAAC;AAAQ,SAAS,OAAM;AAAC,WAAQ,GAAE,GAAE,IAAE,GAAE,IAAE,IAAG,IAAE,UAAU,QAAO,IAAE,GAAE,IAAI,EAAC,IAAE,UAAU,CAAC,OAAK,IAAE,EAAE,CAAC,OAAK,MAAI,KAAG,MAAK,KAAG;AAAG,SAAO;AAAC;AAAC,IAAO,eAAQ;",
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}

View File

@ -28,7 +28,7 @@ import {
setDefaultOptions,
startOfWeek,
toDate
} from "./chunk-7VK2ROTQ.js";
} from "./chunk-ZFXKT4LN.js";
import {
__publicField
} from "./chunk-2TUXWMP5.js";

View File

@ -21,6 +21,7 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.13.1",
"recharts": "^3.8.1",
"tailwind-merge": "^3.5.0",
"vite": "^6.2.0"
},
@ -913,6 +914,42 @@
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
"license": "BSD-3-Clause"
},
"node_modules/@reduxjs/toolkit": {
"version": "2.11.2",
"resolved": "https://registry.npmmirror.com/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
"integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"@standard-schema/utils": "^0.3.0",
"immer": "^11.0.0",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
"reselect": "^5.1.0"
},
"peerDependencies": {
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-redux": {
"optional": true
}
}
},
"node_modules/@reduxjs/toolkit/node_modules/immer": {
"version": "11.1.4",
"resolved": "https://registry.npmmirror.com/immer/-/immer-11.1.4.tgz",
"integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
@ -1244,6 +1281,18 @@
"win32"
]
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"license": "MIT"
},
"node_modules/@standard-schema/utils": {
"version": "0.3.0",
"resolved": "https://registry.npmmirror.com/@standard-schema/utils/-/utils-0.3.0.tgz",
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
"license": "MIT"
},
"node_modules/@tailwindcss/node": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz",
@ -1563,6 +1612,69 @@
"@types/node": "*"
}
},
"node_modules/@types/d3-array": {
"version": "3.2.2",
"resolved": "https://registry.npmmirror.com/@types/d3-array/-/d3-array-3.2.2.tgz",
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
"license": "MIT"
},
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmmirror.com/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
"license": "MIT"
},
"node_modules/@types/d3-ease": {
"version": "3.0.2",
"resolved": "https://registry.npmmirror.com/@types/d3-ease/-/d3-ease-3.0.2.tgz",
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
"license": "MIT"
},
"node_modules/@types/d3-interpolate": {
"version": "3.0.4",
"resolved": "https://registry.npmmirror.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"license": "MIT",
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-path": {
"version": "3.1.1",
"resolved": "https://registry.npmmirror.com/@types/d3-path/-/d3-path-3.1.1.tgz",
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
"license": "MIT"
},
"node_modules/@types/d3-scale": {
"version": "4.0.9",
"resolved": "https://registry.npmmirror.com/@types/d3-scale/-/d3-scale-4.0.9.tgz",
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
"license": "MIT",
"dependencies": {
"@types/d3-time": "*"
}
},
"node_modules/@types/d3-shape": {
"version": "3.1.8",
"resolved": "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.8.tgz",
"integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
"license": "MIT",
"dependencies": {
"@types/d3-path": "*"
}
},
"node_modules/@types/d3-time": {
"version": "3.0.4",
"resolved": "https://registry.npmmirror.com/@types/d3-time/-/d3-time-3.0.4.tgz",
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
"license": "MIT"
},
"node_modules/@types/d3-timer": {
"version": "3.0.2",
"resolved": "https://registry.npmmirror.com/@types/d3-timer/-/d3-timer-3.0.2.tgz",
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@ -1671,6 +1783,12 @@
"@types/node": "*"
}
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmmirror.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
"license": "MIT"
},
"node_modules/@vitejs/plugin-react": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz",
@ -2326,6 +2444,127 @@
"node": ">= 8"
}
},
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.4.tgz",
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"license": "ISC",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-format": {
"version": "3.1.2",
"resolved": "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.2.tgz",
"integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-path": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/d3-path/-/d3-path-3.1.0.tgz",
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"license": "ISC",
"dependencies": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-shape": {
"version": "3.2.0",
"resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz",
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
"license": "ISC",
"dependencies": {
"d3-path": "^3.1.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/d3-time/-/d3-time-3.1.0.tgz",
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"license": "ISC",
"dependencies": {
"d3-array": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time-format": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-4.1.0.tgz",
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"license": "ISC",
"dependencies": {
"d3-time": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
@ -2368,6 +2607,12 @@
}
}
},
"node_modules/decimal.js-light": {
"version": "2.5.1",
"resolved": "https://registry.npmmirror.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
"license": "MIT"
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@ -2555,6 +2800,16 @@
"node": ">= 0.4"
}
},
"node_modules/es-toolkit": {
"version": "1.46.0",
"resolved": "https://registry.npmmirror.com/es-toolkit/-/es-toolkit-1.46.0.tgz",
"integrity": "sha512-IToJ6ct9OLl5zz6WsC/1vZEwfSZ7Myil+ygl5Tf30Xjn9AEkzNB4kqp2G7VUJKF1DtTx/ra5M5KLlXvzOg51BA==",
"license": "MIT",
"workspaces": [
"docs",
"benchmarks"
]
},
"node_modules/esbuild": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
@ -2621,6 +2876,12 @@
"node": ">= 0.6"
}
},
"node_modules/eventemitter3": {
"version": "5.0.4",
"resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.4.tgz",
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
"license": "MIT"
},
"node_modules/exceljs": {
"version": "4.4.0",
"resolved": "https://registry.npmmirror.com/exceljs/-/exceljs-4.4.0.tgz",
@ -3236,6 +3497,16 @@
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/immer": {
"version": "10.2.0",
"resolved": "https://registry.npmmirror.com/immer/-/immer-10.2.0.tgz",
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz",
@ -3253,6 +3524,15 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@ -4337,6 +4617,36 @@
"react": "^19.2.4"
}
},
"node_modules/react-is": {
"version": "19.2.5",
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-19.2.5.tgz",
"integrity": "sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==",
"license": "MIT",
"peer": true
},
"node_modules/react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmmirror.com/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT",
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
},
"peerDependencies": {
"@types/react": "^18.2.25 || ^19",
"react": "^18.0 || ^19",
"redux": "^5.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"redux": {
"optional": true
}
}
},
"node_modules/react-refresh": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
@ -4432,6 +4742,57 @@
"node": ">=10"
}
},
"node_modules/recharts": {
"version": "3.8.1",
"resolved": "https://registry.npmmirror.com/recharts/-/recharts-3.8.1.tgz",
"integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==",
"license": "MIT",
"workspaces": [
"www"
],
"dependencies": {
"@reduxjs/toolkit": "^1.9.0 || 2.x.x",
"clsx": "^2.1.1",
"decimal.js-light": "^2.5.1",
"es-toolkit": "^1.39.3",
"eventemitter3": "^5.0.1",
"immer": "^10.1.1",
"react-redux": "8.x.x || 9.x.x",
"reselect": "5.1.1",
"tiny-invariant": "^1.3.3",
"use-sync-external-store": "^1.2.2",
"victory-vendor": "^37.0.2"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/redux": {
"version": "5.0.1",
"resolved": "https://registry.npmmirror.com/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT"
},
"node_modules/redux-thunk": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/redux-thunk/-/redux-thunk-3.1.0.tgz",
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
"license": "MIT",
"peerDependencies": {
"redux": "^5.0.0"
}
},
"node_modules/reselect": {
"version": "5.1.1",
"resolved": "https://registry.npmmirror.com/reselect/-/reselect-5.1.1.tgz",
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
"license": "MIT"
},
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
@ -4908,6 +5269,12 @@
"node": ">=6"
}
},
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmmirror.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@ -5097,6 +5464,15 @@
"browserslist": ">= 4.21.0"
}
},
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -5130,6 +5506,28 @@
"node": ">= 0.8"
}
},
"node_modules/victory-vendor": {
"version": "37.3.6",
"resolved": "https://registry.npmmirror.com/victory-vendor/-/victory-vendor-37.3.6.tgz",
"integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
"license": "MIT AND ISC",
"dependencies": {
"@types/d3-array": "^3.0.3",
"@types/d3-ease": "^3.0.0",
"@types/d3-interpolate": "^3.0.1",
"@types/d3-scale": "^4.0.2",
"@types/d3-shape": "^3.1.0",
"@types/d3-time": "^3.0.0",
"@types/d3-timer": "^3.0.0",
"d3-array": "^3.1.6",
"d3-ease": "^3.0.1",
"d3-interpolate": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-shape": "^3.1.0",
"d3-time": "^3.0.0",
"d3-timer": "^3.0.1"
}
},
"node_modules/vite": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",

View File

@ -24,6 +24,7 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.13.1",
"recharts": "^3.8.1",
"tailwind-merge": "^3.5.0",
"vite": "^6.2.0"
},

View File

@ -0,0 +1,564 @@
import { useId, useMemo } from "react";
import {
Area,
AreaChart,
Bar,
BarChart,
CartesianGrid,
Cell,
Funnel,
FunnelChart,
LabelList,
Pie,
PieChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import type { DashboardAnalyticsCard } from "@/lib/auth";
import { useIsMobileViewport } from "@/hooks/useIsMobileViewport";
type ChartPoint = NonNullable<DashboardAnalyticsCard["chartData"]>[number];
type DisplayTextConfig = Partial<{
emptyText: string;
unnamedLabel: string;
peakPrefix: string;
ratioPrefix: string;
sharePrefix: string;
centerLabel: string;
tableLabelHeader: string;
tableValueHeader: string;
categoryOptions: Array<{
label?: string;
color?: string;
description?: string;
}>;
}>;
type NormalizedPoint = {
label: string;
value: number;
rawValue: string;
valueText: string;
secondaryValueText?: string;
description?: string;
color: string;
initials: string;
};
const CHART_COLORS = ["#2563eb", "#7c3aed", "#db2777", "#f59e0b", "#10b981", "#0ea5e9", "#f97316", "#8b5cf6"];
const RANKING_BADGE_CLASSES = [
"bg-blue-100 text-blue-600",
"bg-purple-100 text-purple-600",
"bg-rose-100 text-rose-600",
"bg-emerald-100 text-emerald-600",
"bg-amber-100 text-amber-600",
];
function toNumber(value?: string) {
const parsed = Number(value ?? "");
return Number.isFinite(parsed) ? parsed : 0;
}
function parseDisplayTextConfig(raw?: string): DisplayTextConfig {
if (!raw) {
return {};
}
try {
const parsed = JSON.parse(raw);
return parsed && typeof parsed === "object" ? parsed as DisplayTextConfig : {};
} catch {
return {};
}
}
function formatPercent(ratio: number) {
const percent = ratio * 100;
return `${percent.toFixed(percent >= 10 ? 1 : 2).replace(/\.0+$/, "")}%`;
}
function formatRatioLabel(ratio: number, prefix: string) {
return `${prefix} ${formatPercent(ratio)}`;
}
function getSignedNumber(text?: string) {
if (!text) {
return Number.NaN;
}
const match = text.match(/[+-]?\d+(?:\.\d+)?/);
return match ? Number.parseFloat(match[0]) : Number.NaN;
}
function getPercentTone(text?: string) {
if (!text?.includes("%")) {
return "default" as const;
}
const value = getSignedNumber(text);
if (!Number.isFinite(value) || value === 0) {
return "neutral" as const;
}
return value > 0 ? "positive" as const : "negative" as const;
}
function getPercentToneFromRatio(ratio: number) {
if (!Number.isFinite(ratio) || ratio === 0) {
return "neutral" as const;
}
return ratio > 0 ? "positive" as const : "negative" as const;
}
function getPercentTextClass(text?: string, fallback = "text-slate-900") {
const tone = getPercentTone(text);
if (tone === "positive") {
return "text-emerald-500";
}
if (tone === "negative") {
return "text-rose-500";
}
if (tone === "neutral") {
return "text-slate-400";
}
return fallback;
}
function getPercentTextClassFromRatio(ratio: number, fallback = "text-slate-400") {
const tone = getPercentToneFromRatio(ratio);
if (tone === "positive") {
return "text-emerald-500";
}
if (tone === "negative") {
return "text-rose-500";
}
if (tone === "neutral") {
return "text-slate-400";
}
return fallback;
}
function getPercentFillColor(text?: string, fallback = "#0f172a") {
const tone = getPercentTone(text);
if (tone === "positive") {
return "#10b981";
}
if (tone === "negative") {
return "#f43f5e";
}
if (tone === "neutral") {
return "#94a3b8";
}
return fallback;
}
function formatRankingSecondaryLabel(
secondaryValueText: string | undefined,
ratio: number,
prefix: string | undefined,
) {
const normalizedPrefix = prefix?.trim();
if (secondaryValueText) {
return normalizedPrefix ? `${normalizedPrefix} ${secondaryValueText}` : secondaryValueText;
}
return formatRatioLabel(ratio, normalizedPrefix || "占首位");
}
function getInitials(label: string) {
const trimmed = label.trim();
if (!trimmed) {
return "--";
}
if (/[\u4e00-\u9fa5]/.test(trimmed)) {
return trimmed.slice(0, 1);
}
const parts = trimmed.split(/\s+/).filter(Boolean);
if (parts.length >= 2) {
return `${parts[0][0]}${parts[1][0]}`.toUpperCase();
}
return trimmed.slice(0, 2).toUpperCase();
}
function formatAxisValue(value: number) {
const absolute = Math.abs(value);
if (absolute >= 1000000) {
return `${(value / 1000000).toFixed(1).replace(/\.0$/, "")}M`;
}
if (absolute >= 10000) {
return `${(value / 10000).toFixed(1).replace(/\.0$/, "")}w`;
}
if (absolute >= 1000) {
return `${(value / 1000).toFixed(1).replace(/\.0$/, "")}k`;
}
return `${value}`;
}
function getChartHeightClass(renderType: DashboardAnalyticsCard["renderType"], mobile: boolean, expanded?: boolean) {
if (renderType === "funnel") {
return expanded ? "h-[340px]" : mobile ? "h-[260px]" : "h-[300px]";
}
if (renderType === "pie" || renderType === "ring") {
return expanded ? "h-[340px]" : mobile ? "h-[300px]" : "h-[320px]";
}
return expanded ? "h-[320px]" : mobile ? "h-[240px]" : "h-64";
}
function normalizePoints(points: ChartPoint[], texts: DisplayTextConfig, allowTextOnly?: boolean) {
const categoryOptionMap = new Map(
(texts.categoryOptions ?? [])
.filter((item) => Boolean(item?.label?.trim()))
.map((item) => [item.label!.trim(), item])
);
return points
.map((point, index) => {
const label = point.label?.trim() || texts.unnamedLabel || "未命名";
const option = categoryOptionMap.get(label);
return {
label,
value: toNumber(point.value),
rawValue: point.value || "0",
valueText: point.valueText || point.value || "0",
secondaryValueText: point.secondaryValueText || point.secondaryValue || undefined,
description: point.description || option?.description || undefined,
color: point.color || option?.color || CHART_COLORS[index % CHART_COLORS.length],
initials: getInitials(label),
} satisfies NormalizedPoint;
})
.filter((point) => allowTextOnly || point.value >= 0);
}
function EmptyState({ text }: { text?: string }) {
return (
<div className="flex min-h-[220px] items-center justify-center rounded-[24px] border border-dashed border-slate-200 bg-slate-50/60 px-6 text-center text-sm text-slate-400">
{text || "暂无图表数据"}
</div>
);
}
function AnalyticsTooltip({
active,
payload,
prefix,
mode,
total,
}: {
active?: boolean;
payload?: Array<{ payload?: NormalizedPoint }>;
prefix: string;
mode: "peak" | "share";
total: number;
}) {
const point = payload?.[0]?.payload;
if (!active || !point) {
return null;
}
const ratio = total > 0 ? Math.max(point.value, 0) / total : 0;
const ratioLabel = formatRatioLabel(ratio, prefix);
return (
<div className="rounded-2xl border border-slate-100 bg-white px-4 py-3 shadow-[0_10px_15px_-3px_rgb(0_0_0_/_0.1)]">
<div className="text-xs font-bold text-slate-700">{point.label}</div>
<div className={`mt-1 text-sm font-semibold ${getPercentTextClass(point.valueText, "text-slate-900")}`}>{point.valueText}</div>
<div className={`mt-1 text-[11px] ${getPercentTextClassFromRatio(ratio)}`}>
{mode === "peak" ? ratioLabel : ratioLabel}
</div>
</div>
);
}
function ChartLegend({ points }: { points: NormalizedPoint[] }) {
return (
<div className="mt-5 flex flex-wrap justify-center gap-x-4 gap-y-2">
{points.map((point) => (
<div key={point.label} className="inline-flex items-center gap-2 text-[10px] font-medium text-slate-500">
<span className="h-2.5 w-2.5 rounded-full" style={{ backgroundColor: point.color }} />
<span>{point.label}</span>
</div>
))}
</div>
);
}
function LineChartCard({
points,
texts,
mobile,
expanded,
}: {
points: NormalizedPoint[];
texts: DisplayTextConfig;
mobile: boolean;
expanded?: boolean;
}) {
const gradientId = useId();
const max = Math.max(...points.map((point) => point.value), 0);
return (
<div className={getChartHeightClass("line", mobile, expanded)}>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={points} margin={{ top: 12, right: 10, left: mobile ? -24 : -12, bottom: 0 }}>
<defs>
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#2563eb" stopOpacity={0.1} />
<stop offset="95%" stopColor="#2563eb" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid vertical={false} stroke="#e2e8f0" strokeDasharray="4 6" />
<XAxis dataKey="label" axisLine={false} tickLine={false} tick={{ fontSize: 10, fill: "#9ca3af" }} />
<YAxis
axisLine={false}
tickLine={false}
tick={{ fontSize: 10, fill: "#9ca3af" }}
width={mobile ? 28 : 36}
tickFormatter={formatAxisValue}
/>
<Tooltip content={<AnalyticsTooltip prefix={texts.peakPrefix || "相对峰值"} mode="peak" total={max} />} />
<Area type="monotone" dataKey="value" stroke="#2563eb" strokeWidth={3} fillOpacity={1} fill={`url(#${gradientId})`} />
</AreaChart>
</ResponsiveContainer>
</div>
);
}
function BarChartCard({
points,
texts,
mobile,
expanded,
}: {
points: NormalizedPoint[];
texts: DisplayTextConfig;
mobile: boolean;
expanded?: boolean;
}) {
const max = Math.max(...points.map((point) => point.value), 0);
return (
<div className={getChartHeightClass("bar", mobile, expanded)}>
<ResponsiveContainer width="100%" height="100%">
<BarChart data={points} margin={{ top: 12, right: 10, left: mobile ? -24 : -12, bottom: 0 }}>
<CartesianGrid vertical={false} stroke="#e2e8f0" strokeDasharray="4 6" />
<XAxis dataKey="label" axisLine={false} tickLine={false} tick={{ fontSize: 10, fill: "#9ca3af" }} />
<YAxis
axisLine={false}
tickLine={false}
tick={{ fontSize: 10, fill: "#9ca3af" }}
width={mobile ? 28 : 36}
tickFormatter={formatAxisValue}
/>
<Tooltip content={<AnalyticsTooltip prefix={texts.peakPrefix || "相对峰值"} mode="peak" total={max} />} cursor={{ fill: "transparent" }} />
<Bar dataKey="value" radius={[6, 6, 0, 0]} barSize={mobile ? 18 : 24}>
{points.map((point) => (
<Cell key={point.label} fill={point.color} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
);
}
function PieChartCard({
points,
texts,
ring,
mobile,
expanded,
}: {
points: NormalizedPoint[];
texts: DisplayTextConfig;
ring?: boolean;
mobile: boolean;
expanded?: boolean;
}) {
const total = points.reduce((sum, point) => sum + point.value, 0);
const primaryRatio = total > 0 ? points[0]?.value / total : 0;
const centerTitle = ring
? texts.centerLabel || (points.length === 2 ? points[0]?.label || "达成率" : "总计")
: "";
const centerValue = points.length === 2 ? formatPercent(primaryRatio) : formatAxisValue(total);
return (
<div className={getChartHeightClass(ring ? "ring" : "pie", mobile, expanded)}>
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={points}
cx="50%"
cy={ring ? "46%" : "44%"}
innerRadius={ring ? (mobile ? 52 : 60) : 0}
outerRadius={mobile ? 76 : 84}
paddingAngle={ring ? 5 : 2}
dataKey="value"
nameKey="label"
label={ring ? false : ({ value }) => `${value}`}
labelLine={!ring}
>
{points.map((point) => (
<Cell key={point.label} fill={point.color} />
))}
</Pie>
<Tooltip content={<AnalyticsTooltip prefix={texts.sharePrefix || "占比"} mode="share" total={total} />} />
{ring ? (
<>
<text x="50%" y="42%" textAnchor="middle" dominantBaseline="middle" className="fill-slate-400 text-[10px] font-semibold">
{centerTitle}
</text>
<text x="50%" y="52%" textAnchor="middle" dominantBaseline="middle" className="fill-slate-800 text-xl font-bold">
<tspan fill={getPercentFillColor(centerValue, "#0f172a")}>{centerValue}</tspan>
</text>
</>
) : null}
</PieChart>
</ResponsiveContainer>
<ChartLegend points={points} />
</div>
);
}
function FunnelChartCard({
points,
mobile,
expanded,
}: {
points: NormalizedPoint[];
mobile: boolean;
expanded?: boolean;
}) {
const funnelData = points.map((point) => ({
...point,
fill: point.color,
name: point.label,
}));
return (
<div className={getChartHeightClass("funnel", mobile, expanded)}>
<ResponsiveContainer width="100%" height="100%">
<FunnelChart>
<Tooltip content={<AnalyticsTooltip prefix="步骤占比" mode="share" total={Math.max(...points.map((point) => point.value), 0)} />} />
<Funnel dataKey="value" data={funnelData} isAnimationActive={false}>
<LabelList position="right" fill="#64748b" stroke="none" dataKey="name" className="text-[10px]" />
</Funnel>
</FunnelChart>
</ResponsiveContainer>
</div>
);
}
function RankingChartCard({
points,
texts,
}: {
points: NormalizedPoint[];
texts: DisplayTextConfig;
}) {
return (
<div className="space-y-4">
{points.map((point, index) => (
<div key={point.label} className="flex items-center justify-between rounded-2xl p-3 transition-colors hover:bg-slate-50">
<div className="flex items-center gap-3">
<span className="w-4 text-xs font-bold text-slate-400">{index + 1}</span>
<div className={`flex h-8 w-8 items-center justify-center rounded-full text-xs font-bold ${RANKING_BADGE_CLASSES[index % RANKING_BADGE_CLASSES.length]}`}>
{point.initials}
</div>
<div className="min-w-0">
<p className="truncate text-sm font-bold text-slate-800">{point.label}</p>
<p className="truncate text-[10px] text-slate-400">{point.description || "成交效率优秀"}</p>
</div>
</div>
<div className="shrink-0 pl-3 text-right">
<p className={`text-sm font-bold ${getPercentTextClass(point.valueText, "text-slate-900")}`}>{point.valueText}</p>
<p
className={`text-[10px] font-bold ${
point.secondaryValueText
? getPercentTextClass(point.secondaryValueText, "text-slate-400")
: getPercentTextClassFromRatio(points[0]?.value ? point.value / points[0].value : 0)
}`}
>
{formatRankingSecondaryLabel(
point.secondaryValueText,
points[0]?.value ? point.value / points[0].value : 0,
texts.ratioPrefix,
)}
</p>
</div>
</div>
))}
</div>
);
}
function TableCard({
points,
texts,
}: {
points: NormalizedPoint[];
texts: DisplayTextConfig;
}) {
return (
<div className="overflow-hidden rounded-[24px] border border-slate-100">
<div className="grid grid-cols-[minmax(0,1.2fr)_auto_auto] gap-4 border-b border-slate-100 bg-slate-50 px-4 py-3 text-[10px] font-bold uppercase tracking-wider text-slate-400">
<span>{texts.tableLabelHeader || "项目"}</span>
<span className="hidden sm:block"></span>
<span className="text-right">{texts.tableValueHeader || "数值"}</span>
</div>
<div className="divide-y divide-slate-100 bg-white">
{points.map((point, index) => (
<div key={`${point.label}-${index}`} className="grid grid-cols-[minmax(0,1.2fr)_auto] gap-4 px-4 py-3.5 sm:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)_auto]">
<div className="min-w-0">
<div className="truncate text-sm font-bold text-slate-800">{point.label}</div>
<div className="mt-1 text-[10px] text-slate-400 sm:hidden">{point.description || "-"}</div>
</div>
<div className="hidden min-w-0 sm:block">
<div className="truncate text-xs text-slate-400">{point.description || "-"}</div>
</div>
<div className={`text-right text-sm font-bold ${getPercentTextClass(point.valueText, "text-slate-900")}`}>{point.valueText}</div>
</div>
))}
</div>
</div>
);
}
export default function DashboardAnalyticsChart({
card,
expanded,
}: {
card: DashboardAnalyticsCard;
expanded?: boolean;
}) {
const isMobile = useIsMobileViewport();
const renderType = card.renderType || "metric";
const texts = parseDisplayTextConfig(card.displayTextConfig);
const chartPoints = useMemo(
() => normalizePoints(card.chartData ?? [], texts, renderType === "table"),
[card.chartData, renderType, texts],
);
if (renderType === "table") {
return chartPoints.length ? <TableCard points={chartPoints} texts={texts} /> : <EmptyState text={texts.emptyText} />;
}
if (!chartPoints.length) {
return <EmptyState text={texts.emptyText} />;
}
if (renderType === "line") {
return <LineChartCard points={chartPoints} texts={texts} mobile={isMobile} expanded={expanded} />;
}
if (renderType === "bar") {
return <BarChartCard points={chartPoints} texts={texts} mobile={isMobile} expanded={expanded} />;
}
if (renderType === "pie") {
return <PieChartCard points={chartPoints} texts={texts} mobile={isMobile} expanded={expanded} />;
}
if (renderType === "ring") {
return <PieChartCard points={chartPoints} texts={texts} ring mobile={isMobile} expanded={expanded} />;
}
if (renderType === "ranking") {
return <RankingChartCard points={chartPoints} texts={texts} />;
}
if (renderType === "funnel") {
return <FunnelChartCard points={chartPoints} mobile={isMobile} expanded={expanded} />;
}
return <EmptyState text={texts.emptyText} />;
}

View File

@ -143,7 +143,7 @@ export function validateChannelForm(form: CreateChannelExpansionPayload, channel
errors.channelIndustry = "请选择聚焦行业";
}
if (!form.certificationLevel?.trim()) {
errors.certificationLevel = "请选择认证级别";
errors.certificationLevel = "请选择汇智内部认证级别";
}
if (!form.annualRevenue || form.annualRevenue <= 0) {
errors.annualRevenue = "请填写年度营业额";
@ -474,11 +474,11 @@ export function QuickChannelForm({
{fieldErrors.channelIndustry ? <p className="text-xs text-rose-500">{fieldErrors.channelIndustry}</p> : null}
</label>
<label className="space-y-2">
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">{requiredMark}</span>
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">{requiredMark}</span>
<AdaptiveSelect
value={form.certificationLevel || ""}
placeholder="请选择"
sheetTitle="认证级别"
sheetTitle="汇智内部认证级别"
options={[
{ value: "", label: "请选择" },
...certificationLevelOptions.map((option) => ({

View File

@ -456,6 +456,54 @@ select {
padding: 1.25rem;
}
.crm-dashboard-panel-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.75rem;
margin-bottom: 1rem;
}
.crm-dashboard-panel-heading {
display: flex;
min-width: 0;
flex-direction: column;
gap: 0.3rem;
}
.crm-dashboard-panel-title {
font-size: 1rem;
line-height: 1.3;
font-weight: 600;
color: #0f172a;
}
.crm-dashboard-panel-subtitle {
font-size: 0.75rem;
line-height: 1.6;
color: #94a3b8;
}
.crm-dashboard-panel-meta {
flex-shrink: 0;
font-size: 0.75rem;
line-height: 1.5;
color: #94a3b8;
}
.crm-dashboard-card-summary {
margin-top: 0.875rem;
padding-top: 0.75rem;
border-top: 1px solid rgba(226, 232, 240, 0.72);
display: flex;
align-items: center;
justify-content: center;
text-align: center;
font-size: 0.6875rem;
line-height: 1.4;
color: #94a3b8;
}
.crm-filter-bar {
border-radius: 1rem;
border: 1px solid #e2e8f0;
@ -582,6 +630,20 @@ select {
color: #94a3b8;
}
.dark .crm-dashboard-panel-title {
color: #f8fafc;
}
.dark .crm-dashboard-panel-subtitle,
.dark .crm-dashboard-panel-meta {
color: #94a3b8;
}
.dark .crm-dashboard-card-summary {
border-top-color: rgba(51, 65, 85, 0.75);
color: #64748b;
}
.dark .crm-filter-bar {
border-color: rgba(51, 65, 85, 0.8);
background: rgba(15, 23, 42, 0.46);
@ -656,6 +718,26 @@ select {
padding: 1.5rem;
}
.crm-dashboard-panel-header {
margin-bottom: 1.125rem;
gap: 1rem;
}
.crm-dashboard-panel-title {
font-size: 1.125rem;
}
.crm-dashboard-panel-subtitle,
.crm-dashboard-panel-meta {
font-size: 0.8125rem;
}
.crm-dashboard-card-summary {
margin-top: 1rem;
padding-top: 0.875rem;
font-size: 0.75rem;
}
.crm-modal-stack {
gap: 1.5rem;
}

View File

@ -111,17 +111,58 @@ export interface DashboardActivity {
timeText?: string;
}
export interface DashboardAnalyticsCard {
id?: number;
cardKey?: string;
title?: string;
subtitle?: string;
renderType?: "metric" | "line" | "bar" | "pie" | "ring" | "funnel" | "ranking" | "table";
description?: string;
value?: string;
valueText?: string;
valueType?: string;
unit?: string;
displayTextConfig?: string;
linkPath?: string;
layoutType?: "vertical" | "horizontal";
fullRow?: boolean;
sortOrder?: number;
errorMessage?: string;
totalCount?: number;
hasMore?: boolean;
chartData?: Array<{
label?: string;
value?: string;
valueText?: string;
secondaryValue?: string;
secondaryValueText?: string;
description?: string;
color?: string;
}>;
}
export interface DashboardAnalyticsPanel {
enabled?: boolean;
title?: string;
subtitle?: string;
emptyStateText?: string;
cards?: DashboardAnalyticsCard[];
}
export interface DashboardHome {
userId?: number;
realName?: string;
jobTitle?: string;
deptName?: string;
onboardingDays?: number;
statsCardVisible?: boolean;
todoCardVisible?: boolean;
activityCardVisible?: boolean;
analyticsCardVisible?: boolean;
stats?: DashboardStat[];
todos?: DashboardTodo[];
activities?: DashboardActivity[];
analyticsPanel?: DashboardAnalyticsPanel;
}
export interface ProfileOverview {
@ -434,6 +475,8 @@ export interface CreateOpportunityPayload {
salesExpansionId?: number;
channelExpansionId?: number;
competitorName?: string;
latestProgress?: string;
nextPlan?: string;
pushedToOms?: boolean;
description?: string;
}
@ -967,6 +1010,10 @@ export async function getDashboardHome() {
return request<DashboardHome>("/api/dashboard/home", undefined, true);
}
export async function getDashboardAnalyticsCardDetail(cardKey: string) {
return request<DashboardAnalyticsCard>(`/api/dashboard/analytics-cards/${encodeURIComponent(cardKey)}`, undefined, true);
}
export async function completeDashboardTodo(todoId: string) {
return request<void>(`/api/dashboard/todos/${todoId}/complete`, {
method: "POST",

View File

@ -1,10 +1,51 @@
import { useEffect, useMemo, useState } from "react";
import { BarChart3, Building2, Check, TrendingUp, Users } from "lucide-react";
import {
Activity,
ArrowDownRight,
ArrowUpRight,
BadgeDollarSign,
BarChart3,
BriefcaseBusiness,
Building2,
CalendarDays,
Check,
CircleDollarSign,
ClipboardList,
DollarSign,
FileText,
HandCoins,
Landmark,
Megaphone,
MoreVertical,
Package,
PhoneCall,
PieChart,
Rocket,
ShoppingCart,
Store,
Target,
TrendingUp,
Trophy,
Users,
Wallet,
X,
Zap,
} from "lucide-react";
import { motion } from "motion/react";
import { useNavigate } from "react-router-dom";
import { completeDashboardTodo, getDashboardHome, type DashboardActivity, type DashboardHome, type DashboardStat, type DashboardTodo } from "@/lib/auth";
import {
completeDashboardTodo,
getDashboardAnalyticsCardDetail,
getDashboardHome,
type DashboardActivity,
type DashboardAnalyticsCard,
type DashboardHome,
type DashboardStat,
type DashboardTodo
} from "@/lib/auth";
import { useIsMobileViewport } from "@/hooks/useIsMobileViewport";
import { useIsWecomBrowser } from "@/hooks/useIsWecomBrowser";
import DashboardAnalyticsChart from "@/components/dashboard/DashboardAnalyticsChart";
const DASHBOARD_PREVIEW_COUNT = 5;
const DASHBOARD_HISTORY_PREVIEW_COUNT = 3;
@ -28,6 +69,42 @@ const amountMetricKeys = new Set<(typeof baseStats)[number]["metricKey"]>([
"pushedOmsProjects",
"monthlyWonOpportunities",
]);
type AnalyticsCardDisplayConfig = Partial<{
horizontalColumns: number;
metricIconKey: string;
}>;
type MetricIconComponent =
| typeof Activity
| typeof BadgeDollarSign
| typeof BarChart3
| typeof BriefcaseBusiness
| typeof Building2
| typeof CalendarDays
| typeof CircleDollarSign
| typeof ClipboardList
| typeof DollarSign
| typeof FileText
| typeof HandCoins
| typeof Landmark
| typeof Megaphone
| typeof Package
| typeof PhoneCall
| typeof PieChart
| typeof Rocket
| typeof ShoppingCart
| typeof Store
| typeof Target
| typeof TrendingUp
| typeof Trophy
| typeof Users
| typeof Wallet
| typeof Zap;
type MetricVisual = {
key: string;
icon: MetricIconComponent;
iconClassName: string;
backgroundClassName: string;
};
function formatStatDisplay(metricKey: (typeof baseStats)[number]["metricKey"], value?: number) {
const numericValue = value ?? 0;
@ -55,6 +132,178 @@ function getStatValueClass(valueText: string) {
return "text-[20px] min-[380px]:text-[22px] min-[430px]:text-[24px] min-[520px]:text-[34px]";
}
function isChartAnalyticsCard(card: DashboardAnalyticsCard) {
return card.renderType !== undefined && card.renderType !== "metric";
}
function supportsAnalyticsDetail(card: DashboardAnalyticsCard) {
return Boolean(card?.hasMore) && card.renderType !== "metric";
}
function getAnalyticsCardPreviewSummary(card: DashboardAnalyticsCard) {
const visibleCount = card.chartData?.length ?? 0;
const totalCount = card.totalCount ?? visibleCount;
if (!card.hasMore || totalCount <= visibleCount) {
return "";
}
return `已展示 ${visibleCount} / 共 ${totalCount}`;
}
function parseAnalyticsDisplayConfig(raw?: string): AnalyticsCardDisplayConfig {
if (!raw) {
return {};
}
try {
const parsed = JSON.parse(raw);
return parsed && typeof parsed === "object" ? parsed as AnalyticsCardDisplayConfig : {};
} catch {
return {};
}
}
function resolveHorizontalColumns(card: DashboardAnalyticsCard) {
const config = parseAnalyticsDisplayConfig(card.displayTextConfig);
const horizontalColumns = Number(config.horizontalColumns);
if (Number.isFinite(horizontalColumns)) {
return Math.max(1, Math.min(4, horizontalColumns));
}
return 2;
}
const METRIC_ICON_LIBRARY: Array<{
key: string;
icon: MetricIconComponent;
iconClassName: string;
backgroundClassName: string;
}> = [
{ key: "dollar-sign", icon: DollarSign, iconClassName: "text-blue-600", backgroundClassName: "bg-blue-50" },
{ key: "circle-dollar-sign", icon: CircleDollarSign, iconClassName: "text-cyan-600", backgroundClassName: "bg-cyan-50" },
{ key: "badge-dollar-sign", icon: BadgeDollarSign, iconClassName: "text-indigo-600", backgroundClassName: "bg-indigo-50" },
{ key: "hand-coins", icon: HandCoins, iconClassName: "text-teal-600", backgroundClassName: "bg-teal-50" },
{ key: "wallet", icon: Wallet, iconClassName: "text-blue-600", backgroundClassName: "bg-blue-50" },
{ key: "shopping-cart", icon: ShoppingCart, iconClassName: "text-amber-600", backgroundClassName: "bg-amber-50" },
{ key: "store", icon: Store, iconClassName: "text-orange-600", backgroundClassName: "bg-orange-50" },
{ key: "package", icon: Package, iconClassName: "text-yellow-600", backgroundClassName: "bg-yellow-50" },
{ key: "users", icon: Users, iconClassName: "text-purple-600", backgroundClassName: "bg-purple-50" },
{ key: "building-2", icon: Building2, iconClassName: "text-violet-600", backgroundClassName: "bg-violet-50" },
{ key: "trending-up", icon: TrendingUp, iconClassName: "text-emerald-600", backgroundClassName: "bg-emerald-50" },
{ key: "bar-chart-3", icon: BarChart3, iconClassName: "text-sky-600", backgroundClassName: "bg-sky-50" },
{ key: "pie-chart", icon: PieChart, iconClassName: "text-fuchsia-600", backgroundClassName: "bg-fuchsia-50" },
{ key: "target", icon: Target, iconClassName: "text-rose-600", backgroundClassName: "bg-rose-50" },
{ key: "trophy", icon: Trophy, iconClassName: "text-yellow-700", backgroundClassName: "bg-yellow-50" },
{ key: "briefcase-business", icon: BriefcaseBusiness, iconClassName: "text-slate-600", backgroundClassName: "bg-slate-100" },
{ key: "landmark", icon: Landmark, iconClassName: "text-stone-600", backgroundClassName: "bg-stone-100" },
{ key: "megaphone", icon: Megaphone, iconClassName: "text-pink-600", backgroundClassName: "bg-pink-50" },
{ key: "phone-call", icon: PhoneCall, iconClassName: "text-green-600", backgroundClassName: "bg-green-50" },
{ key: "clipboard-list", icon: ClipboardList, iconClassName: "text-slate-600", backgroundClassName: "bg-slate-100" },
{ key: "file-text", icon: FileText, iconClassName: "text-zinc-600", backgroundClassName: "bg-zinc-100" },
{ key: "calendar-days", icon: CalendarDays, iconClassName: "text-red-600", backgroundClassName: "bg-red-50" },
{ key: "activity", icon: Activity, iconClassName: "text-lime-600", backgroundClassName: "bg-lime-50" },
{ key: "zap", icon: Zap, iconClassName: "text-yellow-600", backgroundClassName: "bg-yellow-50" },
{ key: "rocket", icon: Rocket, iconClassName: "text-violet-600", backgroundClassName: "bg-violet-50" },
];
const METRIC_ICON_VISUALS: Record<string, MetricVisual> = Object.fromEntries(
METRIC_ICON_LIBRARY.map((item) => [item.key, item] as const)
);
const LEGACY_METRIC_ICON_KEY_MAP: Record<string, string> = {
revenue: "dollar-sign",
customers: "users",
orders: "shopping-cart",
growth: "trending-up",
analytics: "bar-chart-3",
channel: "building-2",
};
function resolveConfiguredMetricIconKey(metricIconKey?: string) {
if (!metricIconKey) {
return "";
}
if (METRIC_ICON_VISUALS[metricIconKey]) {
return metricIconKey;
}
return LEGACY_METRIC_ICON_KEY_MAP[metricIconKey] || "";
}
function getAnalyticsCardLayoutClass(card: DashboardAnalyticsCard) {
if (card.renderType === "table" || card.fullRow) {
return "col-span-12";
}
if (card.layoutType !== "horizontal") {
return "col-span-12";
}
switch (resolveHorizontalColumns(card)) {
case 1:
return "col-span-12";
case 3:
return "col-span-6 min-[420px]:col-span-4";
case 4:
return "col-span-6 min-[420px]:col-span-4 min-[520px]:col-span-3";
case 2:
default:
return "col-span-6";
}
}
function getAnalyticsMetricVisual(card: DashboardAnalyticsCard, index: number) {
const configuredIconKey = resolveConfiguredMetricIconKey(parseAnalyticsDisplayConfig(card.displayTextConfig).metricIconKey);
if (configuredIconKey && METRIC_ICON_VISUALS[configuredIconKey]) {
return METRIC_ICON_VISUALS[configuredIconKey];
}
const text = `${card.title || ""}${card.subtitle || ""}${card.description || ""}`.toLowerCase();
const visualOptions = [
METRIC_ICON_VISUALS["dollar-sign"],
METRIC_ICON_VISUALS.users,
METRIC_ICON_VISUALS["shopping-cart"],
METRIC_ICON_VISUALS["trending-up"],
] as const;
if (text.includes("金额") || text.includes("营收") || text.includes("收入") || text.includes("业绩") || text.includes("回款") || text.includes("销售额")) {
return METRIC_ICON_VISUALS["dollar-sign"];
}
if (text.includes("用户") || text.includes("客户") || text.includes("成员") || text.includes("人员")) {
return METRIC_ICON_VISUALS.users;
}
if (text.includes("订单") || text.includes("签约") || text.includes("成交") || text.includes("商机")) {
return METRIC_ICON_VISUALS["shopping-cart"];
}
if (text.includes("渠道") || text.includes("组织") || text.includes("区域") || text.includes("网点")) {
return METRIC_ICON_VISUALS["building-2"];
}
if (text.includes("分析") || text.includes("排行") || text.includes("报表") || text.includes("统计")) {
return METRIC_ICON_VISUALS["bar-chart-3"];
}
if (text.includes("达成") || text.includes("增长") || text.includes("转化") || text.includes("完成率") || text.includes("效率")) {
return METRIC_ICON_VISUALS["trending-up"];
}
return visualOptions[index % visualOptions.length];
}
function getAnalyticsMetricFootnote(card: DashboardAnalyticsCard) {
const text = card.description?.trim() || card.subtitle?.trim() || "";
if (!text) {
return null;
}
const normalized = text.toLowerCase();
const numericMatch = normalized.match(/[+-]?\d+(?:\.\d+)?%?/);
const numericValue = numericMatch ? Number.parseFloat(numericMatch[0].replace("%", "")) : Number.NaN;
const positive = normalized.includes("+")
|| normalized.includes("增长")
|| normalized.includes("提升")
|| normalized.includes("上升")
|| (Number.isFinite(numericValue) && numericValue > 0);
const negative = normalized.includes("-")
|| normalized.includes("下降")
|| normalized.includes("下滑")
|| normalized.includes("减少")
|| (Number.isFinite(numericValue) && numericValue < 0);
return {
text,
tone: positive ? "up" : negative ? "down" : "neutral",
} as const;
}
export default function Dashboard() {
const navigate = useNavigate();
const isMobileViewport = useIsMobileViewport();
@ -66,6 +315,9 @@ export default function Dashboard() {
const [showAllHistoryTodos, setShowAllHistoryTodos] = useState(false);
const [historyExpanded, setHistoryExpanded] = useState(false);
const [completingTodoId, setCompletingTodoId] = useState<string | null>(null);
const [detailCard, setDetailCard] = useState<DashboardAnalyticsCard | null>(null);
const [detailLoading, setDetailLoading] = useState(false);
const [detailError, setDetailError] = useState<string>("");
useEffect(() => {
let cancelled = false;
@ -120,10 +372,11 @@ export default function Dashboard() {
const visibleActivities = showAllActivities ? activities : activities.slice(0, DASHBOARD_PREVIEW_COUNT);
const hasMoreActivities = activities.length > DASHBOARD_PREVIEW_COUNT && activities[0]?.id !== 0;
const hasMoreHistoryTodos = historyTodos.length > DASHBOARD_HISTORY_PREVIEW_COUNT;
const showStatsCard = home.statsCardVisible !== false;
const showTodoCard = home.todoCardVisible !== false;
const showActivityCard = home.activityCardVisible !== false;
const dashboardPanelCount = Number(showTodoCard) + Number(showActivityCard);
const showAnalyticsCard = home.analyticsCardVisible !== false && home.analyticsPanel?.enabled === true;
const analyticsCards = (home.analyticsPanel?.cards ?? []).filter((item) => !item.errorMessage);
const handleCompleteTodo = async (todoId: string) => {
if (!todoId || completingTodoId === todoId) {
return;
@ -171,6 +424,41 @@ export default function Dashboard() {
}
};
const handleAnalyticsCardClick = (card: DashboardAnalyticsCard) => {
if (!supportsAnalyticsDetail(card)) {
return;
}
void openAnalyticsCardDetail(card);
};
const openAnalyticsCardDetail = async (card: DashboardAnalyticsCard) => {
if (!card?.cardKey || !supportsAnalyticsDetail(card)) {
return;
}
setDetailLoading(true);
setDetailError("");
setDetailCard({
...card,
chartData: [],
});
try {
const data = await getDashboardAnalyticsCardDetail(card.cardKey);
setDetailCard(data);
} catch (error) {
setDetailError(error instanceof Error ? error.message : "加载完整卡片详情失败");
} finally {
setDetailLoading(false);
}
};
const closeAnalyticsDetail = () => {
setDetailCard(null);
setDetailError("");
setDetailLoading(false);
};
const dashboardPanelGridClassName = "grid-cols-1";
return (
<div className="crm-page-stack">
<header className="crm-page-header">
@ -198,6 +486,7 @@ export default function Dashboard() {
animate={{ opacity: 1 }}
transition={{ duration: disableMobileMotion ? 0 : 0.24, ease: "easeOut" }}
>
{showStatsCard ? (
<div className="grid grid-cols-2 gap-x-3 gap-y-4 min-[520px]:grid-cols-2 sm:gap-4 xl:grid-cols-4">
{stats.map((stat, i) => {
const display = formatStatDisplay(stat.metricKey, stat.value);
@ -242,42 +531,46 @@ export default function Dashboard() {
);
})}
</div>
) : null}
<div className={`mt-5 grid gap-5 md:mt-6 ${dashboardPanelCount > 1 ? "md:grid-cols-2" : "md:grid-cols-1"} md:gap-6`}>
<div className={`${showStatsCard ? "mt-5 md:mt-6 " : ""}grid gap-5 ${dashboardPanelGridClassName} md:gap-6`}>
{showTodoCard ? (
<motion.div
initial={disableMobileMotion ? false : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={disableMobileMotion ? { duration: 0 } : { delay: 0.4 }}
className="crm-card crm-card-pad-lg rounded-2xl"
className="overflow-hidden rounded-[30px] border border-slate-100 bg-white p-5 shadow-[0_16px_38px_-30px_rgba(15,23,42,0.14)]"
>
<div className="mb-3 flex items-center justify-between sm:mb-4">
<h2 className="text-base font-semibold text-slate-900 dark:text-white sm:text-lg"></h2>
<span className="text-[11px] text-slate-400 dark:text-slate-500 sm:text-xs">
<div className="crm-dashboard-panel-header">
<div className="crm-dashboard-panel-heading">
<h2 className="crm-dashboard-panel-title"></h2>
<p className="crm-dashboard-panel-subtitle"></p>
</div>
<span className="crm-dashboard-panel-meta">
{pendingTodos.length + historyTodos.length}
</span>
</div>
<div className="crm-section-stack sm:gap-5">
<div className="crm-card-subtle rounded-xl p-3 sm:p-4">
<div className="rounded-[24px] border border-slate-100 bg-slate-50/70 p-4">
<div className="mb-2 flex items-center gap-2 sm:mb-3">
<span className="crm-pill crm-pill-neutral"></span>
<span className="rounded-full border border-slate-200 bg-white px-3 py-1 text-[11px] font-semibold text-slate-500"></span>
<span className="text-xs text-slate-400 dark:text-slate-500">{pendingTodos.length} </span>
</div>
{pendingTodos.length ? (
<ul className="space-y-2.5 sm:space-y-3">
{pendingTodos.map((task: DashboardTodo) => (
<li key={task.id} className="group flex items-start gap-2.5 rounded-xl border border-white/80 bg-white px-3 py-2.5 transition-all hover:bg-slate-50 dark:border-slate-700/60 dark:bg-slate-900/40 dark:hover:bg-slate-800 sm:gap-3 sm:p-3">
<li key={task.id} className="group flex items-start gap-3 rounded-[20px] border border-slate-100 bg-white px-4 py-3 transition-all hover:border-slate-200 hover:bg-slate-50/70">
<button
type="button"
onClick={() => void handleCompleteTodo(task.id)}
disabled={completingTodoId === task.id}
className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full border-2 border-slate-300 text-transparent transition-colors hover:border-violet-500 disabled:cursor-not-allowed disabled:opacity-60 dark:border-slate-600 dark:hover:border-violet-400"
className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full border-2 border-slate-300 text-transparent transition-colors hover:border-blue-500 disabled:cursor-not-allowed disabled:opacity-60"
aria-label={`完成待办:${getTodoDisplayTitle(task)}`}
>
<Check className="h-3 w-3" />
</button>
<div className="min-w-0 flex-1">
<p className="truncate text-sm text-slate-700 transition-colors group-hover:text-slate-900 dark:text-slate-300 dark:group-hover:text-white">
<p className="truncate text-sm font-medium text-slate-700 transition-colors group-hover:text-slate-900">
{getTodoDisplayTitle(task)}
</p>
</div>
@ -291,17 +584,17 @@ export default function Dashboard() {
)}
</div>
<div className="crm-card-subtle rounded-xl p-3 sm:p-4">
<div className="rounded-[24px] border border-slate-100 bg-slate-50/70 p-4">
<button
type="button"
onClick={() => setHistoryExpanded((current) => !current)}
className="flex w-full items-center justify-between rounded-xl border border-slate-200 bg-white px-4 py-3 text-left transition-colors hover:border-violet-300 hover:bg-violet-50/50 dark:border-slate-700 dark:bg-slate-900/40 dark:hover:border-violet-600 dark:hover:bg-slate-800"
className="flex w-full items-center justify-between rounded-[20px] border border-slate-200 bg-white px-4 py-3 text-left transition-colors hover:border-slate-300 hover:bg-slate-50"
>
<div className="flex items-center gap-2">
<span className="crm-pill crm-pill-violet"></span>
<span className="rounded-full border border-slate-200 bg-white px-3 py-1 text-[11px] font-semibold text-slate-500"></span>
<span className="text-xs text-slate-400 dark:text-slate-500">{historyTodos.length} </span>
</div>
<span className="text-sm font-medium text-slate-500 dark:text-slate-400">
<span className="text-sm font-medium text-slate-500">
{historyExpanded ? "收起" : "展开"}
</span>
</button>
@ -310,12 +603,12 @@ export default function Dashboard() {
<>
<ul className="mt-3 space-y-2.5 sm:space-y-3">
{visibleHistoryTodos.map((task: DashboardTodo) => (
<li key={task.id} className="flex items-start gap-2.5 rounded-xl border border-white/80 bg-white px-3 py-2.5 dark:border-slate-700/60 dark:bg-slate-900/40 sm:gap-3 sm:p-3">
<li key={task.id} className="flex items-start gap-3 rounded-[20px] border border-slate-100 bg-white px-4 py-3">
<div className="mt-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-emerald-500 text-white">
<Check className="h-3 w-3" />
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-sm text-slate-500 line-through dark:text-slate-400">
<p className="truncate text-sm text-slate-500 line-through">
{getTodoDisplayTitle(task)}
</p>
</div>
@ -326,7 +619,7 @@ export default function Dashboard() {
<button
type="button"
onClick={() => setShowAllHistoryTodos((current) => !current)}
className="mt-3 w-full rounded-xl border border-dashed border-slate-200 px-3 py-2 text-sm font-medium text-slate-500 transition-colors hover:border-violet-300 hover:text-violet-600 dark:border-slate-700 dark:text-slate-400 dark:hover:border-violet-700 dark:hover:text-violet-400"
className="mt-3 w-full rounded-[18px] border border-dashed border-slate-200 px-3 py-2 text-sm font-medium text-slate-500 transition-colors hover:border-slate-300 hover:text-slate-700"
>
{showAllHistoryTodos ? "收起历史明细" : `展开更多 (${historyTodos.length})`}
</button>
@ -343,14 +636,150 @@ export default function Dashboard() {
</motion.div>
) : null}
{showAnalyticsCard ? (
<motion.div
initial={disableMobileMotion ? false : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={disableMobileMotion ? { duration: 0 } : { delay: 0.45 }}
>
{analyticsCards.length ? (
<div className={`grid grid-cols-12 gap-3 min-[420px]:gap-4 ${isMobileViewport ? "" : "sm:gap-4"}`}>
{analyticsCards.map((card, index) => {
const clickable = supportsAnalyticsDetail(card);
const chartCard = isChartAnalyticsCard(card);
const horizontalColumns = resolveHorizontalColumns(card);
const compactMobileCard = isMobileViewport && card.layoutType === "horizontal" && !card.fullRow && horizontalColumns >= 3;
const ultraCompactMobileCard = compactMobileCard && horizontalColumns >= 4;
const metricVisual = getAnalyticsMetricVisual(card, index);
const metricFootnote = getAnalyticsMetricFootnote(card);
const MetricIcon = metricVisual.icon;
const cardSummary = getAnalyticsCardPreviewSummary(card);
return (
<div
key={card.cardKey || card.id || card.title}
onClick={() => handleAnalyticsCardClick(card)}
onKeyDown={(event) => {
if (!clickable) {
return;
}
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
handleAnalyticsCardClick(card);
}
}}
role={clickable ? "button" : undefined}
tabIndex={clickable ? 0 : undefined}
className={`${getAnalyticsCardLayoutClass(card)} overflow-hidden border border-slate-100 bg-white text-left shadow-sm transition-all ${
clickable ? "cursor-pointer hover:-translate-y-0.5 hover:shadow-md" : ""
} ${
chartCard
? compactMobileCard
? "rounded-[24px] p-4"
: "rounded-[32px] p-6"
: ultraCompactMobileCard
? "rounded-[18px] p-3"
: compactMobileCard
? "rounded-[20px] p-4"
: "rounded-[24px] p-5"
}`}
>
{chartCard ? (
<>
<div className={`${compactMobileCard ? "mb-4 gap-2" : "mb-6 gap-4"} flex items-start justify-between`}>
<div className="min-w-0">
<h3 className={`${compactMobileCard ? "text-sm" : "text-base"} truncate font-bold text-slate-800`}>
{card.title || "未命名卡片"}
</h3>
{card.subtitle ? (
<p className={`${compactMobileCard ? "text-[9px]" : "text-[10px]"} mt-1 font-medium text-slate-400`}>
{card.subtitle}
</p>
) : null}
</div>
<span className={`${compactMobileCard ? "p-1.5" : "p-2"} rounded-xl text-slate-300`}>
<MoreVertical className="h-4 w-4" />
</span>
</div>
<DashboardAnalyticsChart card={card} />
{cardSummary ? (
<div className={`${compactMobileCard ? "mt-4 pt-3 text-[10px]" : "mt-5 pt-4 text-[11px]"} border-t border-slate-100 text-center text-slate-400`}>
{cardSummary}
</div>
) : null}
</>
) : (
<>
<div className={`${
ultraCompactMobileCard
? "mb-3 h-8 w-8 rounded-xl"
: compactMobileCard
? "mb-3 h-9 w-9 rounded-2xl"
: "mb-4 h-10 w-10 rounded-2xl"
} flex items-center justify-center ${metricVisual.backgroundClassName}`}>
<MetricIcon className={`${
ultraCompactMobileCard ? "h-4 w-4" : "h-[18px] w-[18px]"
} ${metricVisual.iconClassName}`} />
</div>
<p className={`${
ultraCompactMobileCard ? "text-[9px] leading-4" : "text-[10px]"
} line-clamp-2 font-bold uppercase tracking-wider text-slate-400`}>
{card.title || "未命名卡片"}
</p>
<h3 className={`${
ultraCompactMobileCard ? "mt-1 text-lg leading-tight" : compactMobileCard ? "mt-1 text-[19px]" : "mt-1 text-xl"
} font-bold text-slate-900`}>
{card.valueText || card.value || "0"}
</h3>
{metricFootnote ? (
<div
className={`${
ultraCompactMobileCard ? "mt-1.5 text-[9px]" : "mt-2 text-[10px]"
} inline-flex items-center gap-0.5 font-bold ${
metricFootnote.tone === "up"
? "text-emerald-500"
: metricFootnote.tone === "down"
? "text-rose-500"
: "text-slate-400"
}`}
>
{metricFootnote.tone === "up" ? <ArrowUpRight className="h-3 w-3" /> : null}
{metricFootnote.tone === "down" ? <ArrowDownRight className="h-3 w-3" /> : null}
<span>{metricFootnote.text}</span>
</div>
) : card.subtitle ? (
<p className={`${ultraCompactMobileCard ? "mt-1.5 text-[9px]" : "mt-2 text-[10px]"} font-medium text-slate-400`}>
{card.subtitle}
</p>
) : null}
</>
)}
</div>
);
})}
</div>
) : (
<div className="crm-empty-panel">
{home.analyticsPanel?.emptyStateText || "暂无可展示的经营分析卡片"}
</div>
)}
</motion.div>
) : null}
{showActivityCard ? (
<motion.div
initial={disableMobileMotion ? false : { opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={disableMobileMotion ? { duration: 0 } : { delay: 0.5 }}
className="crm-card crm-card-pad-lg rounded-2xl"
className="overflow-hidden rounded-[30px] border border-slate-100 bg-white p-5 shadow-[0_16px_38px_-30px_rgba(15,23,42,0.14)]"
>
<h2 className="mb-4 text-lg font-semibold text-slate-900 dark:text-white"></h2>
<div className="crm-dashboard-panel-header">
<div className="crm-dashboard-panel-heading">
<h2 className="crm-dashboard-panel-title"></h2>
<p className="crm-dashboard-panel-subtitle"></p>
</div>
<span className="crm-dashboard-panel-meta">{visibleActivities.length} </span>
</div>
<div className="rounded-[24px] border border-slate-100 bg-slate-50/70 p-4">
<div className="crm-section-stack sm:gap-5">
{visibleActivities.map((news: DashboardActivity, i: number) => (
<button
@ -358,7 +787,7 @@ export default function Dashboard() {
type="button"
onClick={() => handleActivityClick(news)}
disabled={!news?.bizId || !news?.bizType}
className="group flex w-full gap-4 rounded-xl border border-transparent px-2 py-2 text-left transition-colors hover:border-violet-100 hover:bg-violet-50/60 disabled:cursor-default disabled:hover:border-transparent disabled:hover:bg-transparent dark:hover:border-violet-900/40 dark:hover:bg-slate-900/50"
className="group flex w-full gap-4 rounded-[20px] border border-slate-100 bg-white px-4 py-3 text-left transition-colors hover:border-slate-200 hover:bg-slate-50/70 disabled:cursor-default disabled:hover:border-slate-100 disabled:hover:bg-white"
>
<div className="relative mt-1 flex h-3 w-3 shrink-0 items-center justify-center">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-violet-400 opacity-20"></span>
@ -366,17 +795,17 @@ export default function Dashboard() {
</div>
<div className="min-w-0 flex-1">
<div className="flex items-start justify-between gap-3">
<p className="text-sm font-medium text-slate-900 transition-colors group-hover:text-violet-700 dark:text-white dark:group-hover:text-violet-300">
<p className="text-sm font-semibold text-slate-900 transition-colors group-hover:text-slate-700">
{news.title || "无"}
</p>
{news.bizId ? (
<span className="shrink-0 rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-medium text-slate-500 group-hover:bg-violet-100 group-hover:text-violet-700 dark:bg-slate-800 dark:text-slate-400 dark:group-hover:bg-violet-500/10 dark:group-hover:text-violet-300">
<span className="shrink-0 rounded-full border border-slate-200 bg-white px-2.5 py-1 text-[10px] font-medium text-slate-500">
</span>
) : null}
</div>
<p className="crm-field-note mt-0.5 line-clamp-2">{news.content || "无"}</p>
<div className="mt-1 flex items-center gap-2 text-[10px] text-slate-400 dark:text-slate-500">
<div className="mt-1 flex items-center gap-2 text-[10px] text-slate-400">
<span>{news.timeText || "无"}</span>
{news.operatorName ? <span>· {news.operatorName}</span> : null}
</div>
@ -388,16 +817,63 @@ export default function Dashboard() {
<button
type="button"
onClick={() => setShowAllActivities(true)}
className="mt-4 w-full rounded-xl border border-dashed border-slate-200 px-3 py-2 text-sm font-medium text-slate-500 transition-colors hover:border-violet-300 hover:text-violet-600 dark:border-slate-700 dark:text-slate-400 dark:hover:border-violet-700 dark:hover:text-violet-400"
className="mt-4 w-full rounded-[18px] border border-dashed border-slate-200 px-3 py-2 text-sm font-medium text-slate-500 transition-colors hover:border-slate-300 hover:text-slate-700"
>
...
</button>
) : null}
</div>
</motion.div>
) : null}
</div>
</motion.div>
)}
{detailCard ? (
<div
className="fixed inset-0 z-50 flex items-end justify-center bg-slate-950/45 p-3 sm:items-center sm:p-6"
onClick={closeAnalyticsDetail}
>
<div
className="flex max-h-[88vh] w-full max-w-3xl flex-col overflow-hidden rounded-[28px] bg-white shadow-[0_28px_80px_-28px_rgba(15,23,42,0.45)]"
onClick={(event) => event.stopPropagation()}
>
<div className="flex min-h-[88px] items-start justify-between gap-4 border-b border-slate-200 px-5 py-5 sm:px-6">
<div className="min-w-0 space-y-1.5">
<h3 className="text-[17px] font-semibold text-slate-900 sm:text-[19px]">{detailCard.title || "经营分析详情"}</h3>
{detailCard.subtitle ? (
<p className="text-xs leading-5 text-slate-500 sm:text-sm">{detailCard.subtitle}</p>
) : null}
</div>
<button
type="button"
onClick={closeAnalyticsDetail}
className="rounded-full border border-slate-200 bg-white p-2 text-slate-500 transition-colors hover:border-slate-300 hover:text-slate-700"
aria-label="关闭经营分析详情"
>
<X className="h-4 w-4" />
</button>
</div>
<div className="overflow-y-auto px-5 py-5 sm:px-6 sm:py-6">
{detailLoading ? (
<div className="rounded-3xl bg-slate-50/80 p-4 sm:p-5">
<div className="crm-empty-panel">...</div>
</div>
) : detailError ? (
<div className="rounded-3xl bg-slate-50/80 p-4 sm:p-5">
<div className="rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-600">
{detailError}
</div>
</div>
) : (
<div className="rounded-3xl bg-slate-50/80 p-4 sm:p-5">
<DashboardAnalyticsChart card={detailCard} expanded />
</div>
)}
</div>
</div>
</div>
) : null}
</div>
);
}

View File

@ -43,6 +43,53 @@ type ExpansionExportFilters = {
establishedStartDate?: string;
establishedEndDate?: string;
hasRelatedProject?: string;
selectedSalesFields?: SalesExportFieldKey[];
selectedChannelFields?: ChannelExportFieldKey[];
};
type SalesExportFieldKey =
| "employeeNo"
| "name"
| "phone"
| "officeName"
| "dept"
| "title"
| "industry"
| "intent"
| "active"
| "hasExp"
| "relatedProjects"
| "relatedProjectAmount"
| "owner"
| "updatedAt"
| "followUps";
type ChannelExportFieldKey =
| "channelCode"
| "name"
| "province"
| "city"
| "officeAddress"
| "certificationLevel"
| "channelIndustry"
| "channelAttribute"
| "internalAttribute"
| "intent"
| "establishedDate"
| "revenue"
| "size"
| "hasDesktopExp"
| "relatedProjects"
| "relatedProjectAmount"
| "contacts"
| "notes"
| "owner"
| "updatedAt"
| "followUps";
type ExportColumnKind = "default" | "longText" | "project" | "contact" | "followup";
type ExportColumn<T, K extends string> = {
key: K;
label: string;
kind?: ExportColumnKind;
value: (item: T) => string;
};
type SalesCreateField =
| "employeeNo"
@ -78,6 +125,7 @@ function createEmptyChannelContact(): ChannelExpansionContact {
}
const CHANNEL_REVENUE_LABEL = "年度营业额(万元)";
const EXPANSION_EXPORT_PREFERENCES_STORAGE_KEY = "crm:expansion-export-preferences";
const defaultSalesForm: CreateSalesExpansionPayload = {
employeeNo: "",
@ -120,6 +168,33 @@ function normalizeOptionalText(value?: string) {
return trimmed ? trimmed : undefined;
}
function loadExpansionExportPreferences(): ExpansionExportFilters {
if (typeof window === "undefined") {
return {};
}
try {
const rawValue = window.localStorage.getItem(EXPANSION_EXPORT_PREFERENCES_STORAGE_KEY);
if (!rawValue) {
return {};
}
const parsed = JSON.parse(rawValue);
return typeof parsed === "object" && parsed ? parsed as ExpansionExportFilters : {};
} catch {
return {};
}
}
function persistExpansionExportPreferences(filters: ExpansionExportFilters) {
if (typeof window === "undefined") {
return;
}
try {
window.localStorage.setItem(EXPANSION_EXPORT_PREFERENCES_STORAGE_KEY, JSON.stringify(filters));
} catch {
// ignore storage failures
}
}
function dedupeExpansionItemsById<T extends { id?: number | string | null }>(items: T[]) {
const seenIds = new Set<number | string>();
return items.filter((item) => {
@ -400,110 +475,86 @@ function downloadExcelFile(filename: string, content: BlobPart) {
window.URL.revokeObjectURL(objectUrl);
}
function buildSalesExportHeaders(items: SalesExpansionItem[]) {
const headers = [
"工号",
"姓名",
"联系方式",
"代表处 / 办事处",
"所属部门",
"职务",
"所属行业",
"合作意向",
"销售是否在职",
"销售以前是否做过云桌面项目",
"跟进项目金额",
"项目信息",
];
const salesExportColumns: Array<ExportColumn<SalesExpansionItem, SalesExportFieldKey>> = [
{ key: "employeeNo", label: "工号", value: (item) => normalizeExportText(item.employeeNo) },
{ key: "name", label: "姓名", value: (item) => normalizeExportText(item.name) },
{ key: "phone", label: "联系方式", value: (item) => normalizeExportText(item.phone) },
{ key: "officeName", label: "代表处/办事处", value: (item) => normalizeExportText(item.officeName) },
{ key: "dept", label: "所属部门", value: (item) => normalizeExportText(item.dept) },
{ key: "title", label: "职务", value: (item) => normalizeExportText(item.title) },
{ key: "industry", label: "所属行业", value: (item) => normalizeExportText(item.industry) },
{ key: "intent", label: "合作意向", value: (item) => normalizeExportText(item.intent) },
{ key: "active", label: "销售是否在职", value: (item) => (item.active === null || item.active === undefined ? "" : item.active ? "是" : "否") },
{ key: "hasExp", label: "销售以前是否做过云桌面项目", value: (item) => formatExportBoolean(item.hasExp) },
{ key: "relatedProjects", label: "跟进的云桌面项目", kind: "project", value: (item) => formatExportProjectListCell(item.relatedProjects) },
{ key: "relatedProjectAmount", label: "跟进项目金额", value: (item) => normalizeExportText(formatRelatedProjectAmount(item.relatedProjects)) },
{ key: "owner", label: "创建人", value: (item) => normalizeExportText(item.owner) },
{ key: "updatedAt", label: "更新修改时间", value: (item) => normalizeExportText(item.updatedAt) },
{ key: "followUps", label: "跟进记录", kind: "followup", value: (item) => formatExportFollowUps(item.followUps) },
];
headers.push("创建人", "更新修改时间");
headers.push("跟进记录");
return headers;
const defaultSalesExportFields: SalesExportFieldKey[] = [
"employeeNo",
"name",
"phone",
"officeName",
"dept",
"title",
"industry",
"intent",
"active",
"hasExp",
"relatedProjects",
"followUps",
];
const channelExportColumns: Array<ExportColumn<ChannelExpansionItem, ChannelExportFieldKey>> = [
{ key: "name", label: "渠道名称", value: (item) => normalizeExportText(item.name) },
{ key: "province", label: "省份", value: (item) => normalizeExportText(item.province) },
{ key: "officeAddress", label: "办公地址", kind: "longText", value: (item) => normalizeExportText(item.officeAddress) },
{ key: "channelIndustry", label: "聚焦行业", value: (item) => normalizeExportText(item.channelIndustry) },
{ key: "channelAttribute", label: "渠道属性", value: (item) => normalizeExportText(item.channelAttribute) },
{ key: "internalAttribute", label: "新华三内部属性", value: (item) => normalizeExportText(item.internalAttribute) },
{ key: "intent", label: "合作意向", value: (item) => normalizeExportText(item.intent) },
{ key: "establishedDate", label: "建立联系时间", value: (item) => normalizeExportText(item.establishedDate) },
{ key: "revenue", label: "年度营业额", value: (item) => normalizeExportText(item.revenue) },
{ key: "size", label: "人员规模", value: (item) => (item.size ? `${item.size}` : "") },
{ key: "hasDesktopExp", label: "以前是否做过云桌面项目", value: (item) => formatExportBoolean(item.hasDesktopExp) },
{ key: "relatedProjects", label: "跟进的云桌面项目", kind: "project", value: (item) => formatExportProjectListCell(item.relatedProjects) },
{ key: "contacts", label: "人员信息", kind: "contact", value: (item) => formatExportContactListCell(item.contacts) },
{ key: "followUps", label: "跟进记录", kind: "followup", value: (item) => formatExportFollowUps(item.followUps) },
{ key: "channelCode", label: "编码", value: (item) => normalizeExportText(item.channelCode) },
{ key: "city", label: "市", value: (item) => normalizeExportText(item.city) },
{ key: "certificationLevel", label: "汇智内部认证级别", value: (item) => normalizeExportText(item.certificationLevel) },
{ key: "relatedProjectAmount", label: "跟进项目金额", value: (item) => normalizeExportText(formatRelatedProjectAmount(item.relatedProjects)) },
{ key: "notes", label: "备注说明", kind: "longText", value: (item) => normalizeExportText(item.notes) },
{ key: "owner", label: "创建人", value: (item) => normalizeExportText(item.owner) },
{ key: "updatedAt", label: "更新修改时间", value: (item) => normalizeExportText(item.updatedAt) },
];
const defaultChannelExportFields: ChannelExportFieldKey[] = [
"name",
"province",
"officeAddress",
"channelIndustry",
"channelAttribute",
"internalAttribute",
"intent",
"establishedDate",
"revenue",
"size",
"hasDesktopExp",
"relatedProjects",
"contacts",
"followUps",
];
function resolveSelectedExpansionFields<K extends string>(selectedFields: K[] | undefined, defaultFields: K[]) {
return selectedFields === undefined ? defaultFields : selectedFields;
}
function buildSalesExportData(items: SalesExpansionItem[]) {
return items.map((item) => {
const row = [
normalizeExportText(item.employeeNo),
normalizeExportText(item.name),
normalizeExportText(item.phone),
normalizeExportText(item.officeName),
normalizeExportText(item.dept),
normalizeExportText(item.title),
normalizeExportText(item.industry),
normalizeExportText(item.intent),
item.active === null || item.active === undefined ? "" : item.active ? "是" : "否",
formatExportBoolean(item.hasExp),
normalizeExportText(formatRelatedProjectAmount(item.relatedProjects)),
formatExportProjectListCell(item.relatedProjects),
];
row.push(
normalizeExportText(item.owner),
normalizeExportText(item.updatedAt),
);
row.push(formatExportFollowUps(item.followUps));
return row;
});
}
function buildChannelExportHeaders(items: ChannelExpansionItem[]) {
const headers = [
"编码",
"渠道名称",
"省份",
"市",
"办公地址",
"认证级别",
"聚焦行业",
"渠道属性",
"新华三内部属性",
"合作意向",
"建立联系时间",
CHANNEL_REVENUE_LABEL,
"人员规模",
"以前是否做过云桌面项目",
"跟进项目金额",
"项目信息",
"人员信息",
];
headers.push("备注说明");
headers.push("创建人", "更新修改时间");
headers.push("跟进记录");
return headers;
}
function buildChannelExportData(items: ChannelExpansionItem[]) {
return items.map((item) => {
const row = [
normalizeExportText(item.channelCode),
normalizeExportText(item.name),
normalizeExportText(item.province),
normalizeExportText(item.city),
normalizeExportText(item.officeAddress),
normalizeExportText(item.certificationLevel),
normalizeExportText(item.channelIndustry),
normalizeExportText(item.channelAttribute),
normalizeExportText(item.internalAttribute),
normalizeExportText(item.intent),
normalizeExportText(item.establishedDate),
normalizeExportText(item.revenue),
item.size ? `${item.size}` : "",
formatExportBoolean(item.hasDesktopExp),
normalizeExportText(formatRelatedProjectAmount(item.relatedProjects)),
formatExportProjectListCell(item.relatedProjects),
formatExportContactListCell(item.contacts),
];
row.push(normalizeExportText(item.notes));
row.push(
normalizeExportText(item.owner),
normalizeExportText(item.updatedAt),
);
row.push(formatExportFollowUps(item.followUps));
return row;
});
function buildExportRows<T, K extends string>(items: T[], columns: Array<ExportColumn<T, K>>) {
return items.map((item) => columns.map((column) => column.value(item)));
}
function validateSalesCreateForm(form: CreateSalesExpansionPayload) {
@ -557,7 +608,7 @@ function validateChannelForm(form: CreateChannelExpansionPayload, channelOtherOp
errors.officeAddress = "请填写办公地址";
}
if (!form.certificationLevel?.trim()) {
errors.certificationLevel = "请选择认证级别";
errors.certificationLevel = "请选择汇智内部认证级别";
}
if ((form.channelIndustry?.length ?? 0) <= 0) {
errors.channelIndustry = "请选择聚焦行业";
@ -775,6 +826,10 @@ function ExpansionExportFilterModal({
}) {
const [draftFilters, setDraftFilters] = useState<ExpansionExportFilters>(initialFilters);
const isSalesTab = activeTab === "sales";
const selectedSalesFields = resolveSelectedExpansionFields(draftFilters.selectedSalesFields, defaultSalesExportFields);
const selectedChannelFields = resolveSelectedExpansionFields(draftFilters.selectedChannelFields, defaultChannelExportFields);
const activeFieldOptions = isSalesTab ? salesExportColumns : channelExportColumns;
const selectedFieldKeys = isSalesTab ? selectedSalesFields : selectedChannelFields;
const hasDraftFilters = Boolean(
draftFilters.keyword
|| draftFilters.intent
@ -788,7 +843,29 @@ function ExpansionExportFilterModal({
|| draftFilters.establishedStartDate
|| draftFilters.establishedEndDate
|| draftFilters.hasRelatedProject,
);
) || (isSalesTab
? JSON.stringify(selectedSalesFields) !== JSON.stringify(defaultSalesExportFields)
: JSON.stringify(selectedChannelFields) !== JSON.stringify(defaultChannelExportFields));
const hasSelectedFields = selectedFieldKeys.length > 0;
const toggleField = (fieldKey: string) => {
if (isSalesTab) {
setDraftFilters((current) => {
const currentFields = resolveSelectedExpansionFields(current.selectedSalesFields, defaultSalesExportFields);
const nextFields = currentFields.includes(fieldKey as SalesExportFieldKey)
? currentFields.filter((item) => item !== fieldKey)
: [...currentFields, fieldKey as SalesExportFieldKey];
return { ...current, selectedSalesFields: nextFields };
});
return;
}
setDraftFilters((current) => {
const currentFields = resolveSelectedExpansionFields(current.selectedChannelFields, defaultChannelExportFields);
const nextFields = currentFields.includes(fieldKey as ChannelExportFieldKey)
? currentFields.filter((item) => item !== fieldKey)
: [...currentFields, fieldKey as ChannelExportFieldKey];
return { ...current, selectedChannelFields: nextFields };
});
};
const handleFilterChange = (key: keyof ExpansionExportFilters, value: string) => {
setDraftFilters((current) => ({ ...current, [key]: value }));
};
@ -815,7 +892,10 @@ function ExpansionExportFilterModal({
<div className="flex flex-col-reverse gap-3 sm:flex-row sm:justify-between">
<button
type="button"
onClick={() => setDraftFilters({})}
onClick={() => setDraftFilters((current) => ({
selectedSalesFields: resolveSelectedExpansionFields(current.selectedSalesFields, defaultSalesExportFields),
selectedChannelFields: resolveSelectedExpansionFields(current.selectedChannelFields, defaultChannelExportFields),
}))}
disabled={!hasDraftFilters}
className="crm-btn crm-btn-secondary disabled:cursor-not-allowed disabled:opacity-60"
>
@ -824,7 +904,7 @@ function ExpansionExportFilterModal({
<button
type="button"
onClick={() => onConfirm(draftFilters)}
disabled={exporting}
disabled={exporting || !hasSelectedFields}
className="crm-btn crm-btn-primary disabled:cursor-not-allowed disabled:opacity-60"
>
{exporting ? "导出中..." : "确认导出"}
@ -873,6 +953,60 @@ function ExpansionExportFilterModal({
onChange={(value) => handleFilterChange("hasRelatedProject", value)}
/>
</label>
<div className="space-y-3 rounded-2xl border border-slate-200 bg-slate-50 p-4 dark:border-slate-800 dark:bg-slate-900/40 sm:col-span-2">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div>
<p className="text-sm font-medium text-slate-700 dark:text-slate-300"></p>
<p className="text-xs text-slate-500 dark:text-slate-400"></p>
</div>
<div className="flex flex-wrap gap-2">
<button
type="button"
onClick={() => setDraftFilters((current) => ({
...current,
selectedSalesFields: isSalesTab ? salesExportColumns.map((column) => column.key) : current.selectedSalesFields,
selectedChannelFields: isSalesTab ? current.selectedChannelFields : channelExportColumns.map((column) => column.key),
}))}
className="rounded-full border border-slate-200 px-3 py-1 text-xs font-medium text-slate-600 transition-colors hover:bg-white dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800"
>
</button>
<button
type="button"
onClick={() => setDraftFilters((current) => ({
...current,
selectedSalesFields: isSalesTab ? defaultSalesExportFields : current.selectedSalesFields,
selectedChannelFields: isSalesTab ? current.selectedChannelFields : defaultChannelExportFields,
}))}
className="rounded-full border border-slate-200 px-3 py-1 text-xs font-medium text-slate-600 transition-colors hover:bg-white dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800"
>
</button>
</div>
</div>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
{activeFieldOptions.map((field) => {
const checked = selectedFieldKeys.includes(field.key);
return (
<label key={field.key} className={cn(
"flex items-center gap-3 rounded-xl border px-3 py-2 text-sm transition-colors",
checked
? "border-violet-200 bg-violet-50 text-violet-700 dark:border-violet-500/30 dark:bg-violet-500/10 dark:text-violet-300"
: "border-slate-200 bg-white text-slate-600 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-300",
)}>
<input
type="checkbox"
checked={checked}
onChange={() => toggleField(field.key)}
className="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500"
/>
<span>{field.label}</span>
</label>
);
})}
</div>
{!hasSelectedFields ? <p className="text-xs text-rose-500"></p> : null}
</div>
{isSalesTab ? (
<>
@ -932,12 +1066,12 @@ function ExpansionExportFilterModal({
/>
</label>
<label className="space-y-1.5">
<span className="text-sm font-medium text-slate-700 dark:text-slate-300"></span>
<span className="text-sm font-medium text-slate-700 dark:text-slate-300"></span>
<AdaptiveSelect
value={draftFilters.certificationLevel ?? ""}
options={toSearchableOptions(certificationLevelOptions, "全部认证级别")}
placeholder="全部认证级别"
sheetTitle="选择认证级别"
options={toSearchableOptions(certificationLevelOptions, "全部汇智内部认证级别")}
placeholder="全部汇智内部认证级别"
sheetTitle="选择汇智内部认证级别"
className="crm-input-box crm-input-text w-full border border-slate-200 bg-white outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500 dark:border-slate-800 dark:bg-slate-900/50"
onChange={(value) => handleFilterChange("certificationLevel", value)}
/>
@ -1018,7 +1152,7 @@ export default function Expansion() {
const [submitting, setSubmitting] = useState(false);
const [exporting, setExporting] = useState(false);
const [exportFilterOpen, setExportFilterOpen] = useState(false);
const [exportFilters, setExportFilters] = useState<ExpansionExportFilters>({});
const [exportFilters, setExportFilters] = useState<ExpansionExportFilters>(() => loadExpansionExportPreferences());
const [salesDuplicateChecking, setSalesDuplicateChecking] = useState(false);
const [channelDuplicateChecking, setChannelDuplicateChecking] = useState(false);
const [createError, setCreateError] = useState("");
@ -1633,21 +1767,33 @@ export default function Expansion() {
setExporting(true);
setExportError("");
setExportFilters(filters);
persistExpansionExportPreferences(filters);
try {
const overview = await getExpansionOverview("");
const exportSalesItems = dedupeExpansionItemsById(overview.salesItems ?? []).filter((item) => matchesSalesExportFilters(item, filters));
const exportChannelItems = dedupeExpansionItemsById(overview.channelItems ?? []).filter((item) => matchesChannelExportFilters(item, filters));
const exportItems = isSalesTab ? exportSalesItems : exportChannelItems;
const selectedSalesFieldKeys = resolveSelectedExpansionFields(filters.selectedSalesFields, defaultSalesExportFields);
const selectedChannelFieldKeys = resolveSelectedExpansionFields(filters.selectedChannelFields, defaultChannelExportFields);
const selectedFieldKeys = isSalesTab ? selectedSalesFieldKeys : selectedChannelFieldKeys;
if (exportItems.length <= 0) {
throw new Error(`当前筛选条件下暂无可导出的${isSalesTab ? "销售人员拓展" : "渠道拓展"}数据`);
}
if (selectedFieldKeys.length <= 0) {
throw new Error("请至少选择一个导出字段");
}
const ExcelJS = await import("exceljs");
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet(isSalesTab ? "销售人员拓展" : "渠道拓展");
const headers = isSalesTab ? buildSalesExportHeaders(exportSalesItems) : buildChannelExportHeaders(exportChannelItems);
const rows = isSalesTab ? buildSalesExportData(exportSalesItems) : buildChannelExportData(exportChannelItems);
const salesColumns = salesExportColumns.filter((column) => selectedSalesFieldKeys.includes(column.key));
const channelColumns = channelExportColumns.filter((column) => selectedChannelFieldKeys.includes(column.key));
const columns = isSalesTab ? salesColumns : channelColumns;
const headers = columns.map((column) => column.label);
const rows = isSalesTab
? buildExportRows(exportSalesItems, salesColumns)
: buildExportRows(exportChannelItems, channelColumns);
worksheet.addRow(headers);
rows.forEach((row) => {
@ -1655,60 +1801,44 @@ export default function Expansion() {
});
worksheet.views = [{ state: "frozen", ySplit: 1 }];
const projectInfoColumnIndex = headers.indexOf("项目信息") + 1;
const contactInfoColumnIndex = headers.indexOf("人员信息") + 1;
worksheet.getRow(1).height = 24;
worksheet.getRow(1).font = { bold: true };
worksheet.getRow(1).alignment = { vertical: "middle", horizontal: "center" };
const projectInfoColumnWidth = projectInfoColumnIndex > 0
? Math.min(
80,
Math.max(
16,
getExcelDisplayWidth(headers[projectInfoColumnIndex - 1] || "") + 2,
rows.reduce((maxWidth, row) => {
const cellValue = row[projectInfoColumnIndex - 1];
const longestLineWidth = typeof cellValue === "string"
? cellValue.split("\n").reduce((lineMax, line) => Math.max(lineMax, getExcelDisplayWidth(line)), 0)
: 0;
return Math.max(maxWidth, longestLineWidth + 2);
}, 0),
),
)
: 30;
const contactInfoColumnWidth = contactInfoColumnIndex > 0
? Math.min(
80,
Math.max(
16,
getExcelDisplayWidth(headers[contactInfoColumnIndex - 1] || "") + 2,
rows.reduce((maxWidth, row) => {
const cellValue = row[contactInfoColumnIndex - 1];
const longestLineWidth = typeof cellValue === "string"
? cellValue.split("\n").reduce((lineMax, line) => Math.max(lineMax, getExcelDisplayWidth(line)), 0)
: 0;
return Math.max(maxWidth, longestLineWidth + 2);
}, 0),
),
)
: 30;
headers.forEach((header, index) => {
const column = worksheet.getColumn(index + 1);
if (header === "跟进记录") {
column.width = 42;
} else if (header === "项目信息") {
column.width = projectInfoColumnWidth;
} else if (header === "人员信息") {
column.width = contactInfoColumnWidth;
} else if (header.includes("办公地址") || header.includes("备注")) {
column.width = 24;
} else if (header.includes("渠道属性") || header.includes("内部属性") || header.includes("聚焦行业")) {
column.width = 18;
} else {
column.width = 16;
const columnWidths = columns.map((column, index) => {
const columnValues = rows.map((row) => row[index]).filter((value): value is string => typeof value === "string");
if (column.kind === "followup") {
return 42;
}
if (column.kind === "project" || column.kind === "contact") {
return Math.min(
80,
Math.max(
16,
getExcelDisplayWidth(column.label) + 2,
columnValues.reduce((maxWidth, value) => {
const longestLineWidth = value.split("\n").reduce((lineMax, line) => Math.max(lineMax, getExcelDisplayWidth(line)), 0);
return Math.max(maxWidth, longestLineWidth + 2);
}, 0),
),
);
}
if (column.kind === "longText") {
return 24;
}
if (column.label.includes("渠道属性") || column.label.includes("内部属性") || column.label.includes("聚焦行业")) {
return 18;
}
return 16;
});
columns.forEach((columnConfig, index) => {
const column = worksheet.getColumn(index + 1);
column.width = columnWidths[index];
column.alignment = {
vertical: "top",
horizontal: "left",
wrapText: columnConfig.kind === "followup" || columnConfig.kind === "project" || columnConfig.kind === "contact",
};
});
worksheet.eachRow((row, rowNumber) => {
@ -1722,21 +1852,19 @@ export default function Expansion() {
cell.alignment = {
vertical: "top",
horizontal: rowNumber === 1 ? "center" : "left",
wrapText: headers[columnNumber - 1] === "跟进记录" || headers[columnNumber - 1] === "项目信息" || headers[columnNumber - 1] === "人员信息",
wrapText: rowNumber > 1 && (columns[columnNumber - 1]?.kind === "followup" || columns[columnNumber - 1]?.kind === "project" || columns[columnNumber - 1]?.kind === "contact"),
};
});
if (rowNumber > 1) {
const projectText = projectInfoColumnIndex > 0 ? row.getCell(projectInfoColumnIndex).value as string | null | undefined : "";
const projectLineCount = getExcelWrappedLineCount(
typeof projectText === "string" ? projectText : "",
projectInfoColumnWidth,
);
const contactText = contactInfoColumnIndex > 0 ? row.getCell(contactInfoColumnIndex).value as string | null | undefined : "";
const contactLineCount = getExcelWrappedLineCount(
typeof contactText === "string" ? contactText : "",
contactInfoColumnWidth,
);
row.height = Math.max(22, Math.max(projectLineCount, contactLineCount) * 16);
const wrappedLineCount = columns.reduce((maxLineCount, column, index) => {
if (column.kind !== "project" && column.kind !== "contact" && column.kind !== "followup") {
return maxLineCount;
}
const cellValue = row.getCell(index + 1).value;
const text = typeof cellValue === "string" ? cellValue : "";
return Math.max(maxLineCount, getExcelWrappedLineCount(text, columnWidths[index]));
}, 1);
row.height = Math.max(22, wrappedLineCount * 16);
}
});
@ -1977,11 +2105,11 @@ export default function Expansion() {
{fieldErrors?.channelIndustry ? <p className="text-xs text-rose-500">{fieldErrors.channelIndustry}</p> : null}
</label>
<label className="space-y-2">
<span className="text-sm font-medium text-slate-700 dark:text-slate-300"><RequiredMark /></span>
<span className="text-sm font-medium text-slate-700 dark:text-slate-300"><RequiredMark /></span>
<AdaptiveSelect
value={form.certificationLevel || ""}
placeholder="请选择"
sheetTitle="认证级别"
sheetTitle="汇智内部认证级别"
options={[
{ value: "", label: "请选择" },
...certificationLevelOptions.map((option) => ({
@ -2446,7 +2574,7 @@ export default function Expansion() {
<DetailItem label="编码" value={selectedItem.channelCode || "无"} />
<DetailItem label="省份" value={selectedItem.province || "无"} />
<DetailItem label="市" value={selectedItem.city || "无"} />
<DetailItem label="认证级别" value={selectedItem.certificationLevel || "无"} />
<DetailItem label="汇智内部认证级别" value={selectedItem.certificationLevel || "无"} />
<DetailItem label="办公地址" value={selectedItem.officeAddress || "无"} className="sm:col-span-2" />
<DetailItem label="聚焦行业" value={selectedItem.channelIndustry || "无"} icon={<Building2 className="h-3 w-3" />} />
<DetailItem label={CHANNEL_REVENUE_LABEL} value={selectedItem.revenue || "无"} />

View File

@ -64,6 +64,9 @@ const FALLBACK_OPPORTUNITY_TYPE_OPTIONS = [
const OPPORTUNITY_EXPANSION_OPTION_LIMIT = 20;
const OPPORTUNITY_EXPANSION_SEARCH_DEBOUNCE_MS = 300;
const OPPORTUNITY_NEXT_PLAN_LABEL = "下一步销售计划";
const LEGACY_OPPORTUNITY_NEXT_PLAN_LABEL = "后续规划";
const OPPORTUNITY_EXPORT_PREFERENCES_STORAGE_KEY = "crm:opportunity-export-preferences";
const COMPETITOR_OPTIONS = [
"深信服",
@ -90,6 +93,42 @@ type OpportunityExportFilters = {
operatorName?: string;
hasSalesExpansion?: string;
hasChannelExpansion?: string;
selectedFields?: OpportunityExportFieldKey[];
};
type OpportunityExportFieldKey =
| "code"
| "name"
| "projectLocation"
| "client"
| "type"
| "operatorName"
| "h3cOwner"
| "channelName"
| "stage"
| "confidence"
| "amount"
| "date"
| "competitorName"
| "latestProgress"
| "nextPlan"
| "followUps"
| "salesExpansionName"
| "salesExpansionIntent"
| "salesExpansionActive"
| "channelExpansionNameExtended"
| "channelExpansionIntent"
| "channelExpansionEstablishedDate"
| "preSalesName"
| "notes"
| "owner"
| "updatedAt"
| "archived"
| "pushedToOms";
type OpportunityExportColumn = {
key: OpportunityExportFieldKey;
label: string;
kind?: "default" | "longText" | "followup";
value: (item: OpportunityItem, relatedSales: SalesExpansionItem | null, relatedChannel: ChannelExpansionItem | null) => string;
};
type OpportunityField =
| "projectLocation"
@ -103,7 +142,9 @@ type OpportunityField =
| "confidencePct"
| "stage"
| "competitorName"
| "opportunityType";
| "opportunityType"
| "latestProgress"
| "nextPlan";
type QuickCreateType = "sales" | "channel";
const defaultForm: CreateOpportunityPayload = {
@ -121,6 +162,8 @@ const defaultForm: CreateOpportunityPayload = {
salesExpansionId: undefined,
channelExpansionId: undefined,
competitorName: "",
latestProgress: "",
nextPlan: "",
description: "",
};
@ -138,6 +181,33 @@ function formatOpportunityBoolean(value?: boolean, trueLabel = "是", falseLabel
return value ? trueLabel : falseLabel;
}
function loadOpportunityExportPreferences(): OpportunityExportFilters {
if (typeof window === "undefined") {
return {};
}
try {
const rawValue = window.localStorage.getItem(OPPORTUNITY_EXPORT_PREFERENCES_STORAGE_KEY);
if (!rawValue) {
return {};
}
const parsed = JSON.parse(rawValue);
return typeof parsed === "object" && parsed ? parsed as OpportunityExportFilters : {};
} catch {
return {};
}
}
function persistOpportunityExportPreferences(filters: OpportunityExportFilters) {
if (typeof window === "undefined") {
return;
}
try {
window.localStorage.setItem(OPPORTUNITY_EXPORT_PREFERENCES_STORAGE_KEY, JSON.stringify(filters));
} catch {
// ignore storage failures
}
}
function normalizeOpportunityExportText(value?: string | number | boolean | null) {
if (value === null || value === undefined) {
return "";
@ -171,6 +241,81 @@ function formatOpportunityExportFilenameTime(date = new Date()) {
return `${year}${month}${day}_${hours}${minutes}${seconds}`;
}
const opportunityExportColumns: OpportunityExportColumn[] = [
{ key: "code", label: "项目编码", value: (item) => normalizeOpportunityExportText(item.code) },
{ key: "name", label: "项目名称", value: (item) => normalizeOpportunityExportText(item.name) },
{ key: "projectLocation", label: "项目地", value: (item) => normalizeOpportunityExportText(item.projectLocation) },
{ key: "client", label: "最终用户", value: (item) => normalizeOpportunityExportText(item.client) },
{ key: "type", label: "建设类型", value: (item) => normalizeOpportunityExportText(item.type || "新建") },
{ key: "operatorName", label: "运作方", value: (item) => normalizeOpportunityExportText(item.operatorName) },
{ key: "h3cOwner", label: "新华三负责人", value: (item, relatedSales) => normalizeOpportunityExportText(item.salesExpansionName || relatedSales?.name) },
{ key: "channelName", label: "渠道名称", value: (item, _relatedSales, relatedChannel) => normalizeOpportunityExportText(item.channelExpansionName || relatedChannel?.name) },
{ key: "stage", label: "项目阶段", value: (item) => normalizeOpportunityExportText(item.stage) },
{ key: "confidence", label: "项目把握度", value: (item) => normalizeOpportunityExportText(item.confidence) },
{ key: "amount", label: "预计金额(元)", value: (item) => (item.amount === null || item.amount === undefined ? "" : `¥${formatAmount(item.amount)}`) },
{ key: "date", label: "预计下单时间", value: (item) => normalizeOpportunityExportText(item.date) },
{ key: "competitorName", label: "竞争对手", value: (item) => normalizeOpportunityExportText(item.competitorName) },
{ key: "latestProgress", label: "项目最新进展", kind: "longText", value: (item) => normalizeOpportunityExportText(item.latestProgress) },
{ key: "nextPlan", label: OPPORTUNITY_NEXT_PLAN_LABEL, kind: "longText", value: (item) => normalizeOpportunityExportText(item.nextPlan) },
{ key: "followUps", label: "跟进记录", kind: "followup", value: (item, relatedSales, relatedChannel) => buildOpportunityFollowUpExportText(item, relatedSales, relatedChannel) },
{ key: "salesExpansionName", label: "销售拓展人员姓名", value: (item, relatedSales) => normalizeOpportunityExportText(item.salesExpansionName || relatedSales?.name) },
{ key: "salesExpansionIntent", label: "销售拓展人员合作意向", value: (item, relatedSales) => normalizeOpportunityExportText(item.salesExpansionIntent || relatedSales?.intent) },
{ key: "salesExpansionActive", label: "销售拓展人员是否在职", value: (item, relatedSales) => {
const active = item.salesExpansionActive ?? relatedSales?.active;
return active === undefined ? "" : active ? "是" : "否";
} },
{ key: "channelExpansionNameExtended", label: "拓展渠道名称", value: (item, _relatedSales, relatedChannel) => normalizeOpportunityExportText(item.channelExpansionName || relatedChannel?.name) },
{ key: "channelExpansionIntent", label: "拓展渠道合作意向", value: (item, _relatedSales, relatedChannel) => normalizeOpportunityExportText(item.channelExpansionIntent || relatedChannel?.intent) },
{ key: "channelExpansionEstablishedDate", label: "拓展渠道建立联系时间", value: (item, _relatedSales, relatedChannel) => normalizeOpportunityExportText(item.channelExpansionEstablishedDate || relatedChannel?.establishedDate) },
{ key: "preSalesName", label: "售前", value: (item) => normalizeOpportunityExportText(item.preSalesName) },
{ key: "notes", label: "备注说明", kind: "longText", value: (item) => normalizeOpportunityExportText(item.notes) },
{ key: "owner", label: "创建人", value: (item) => normalizeOpportunityExportText(item.owner) },
{ key: "updatedAt", label: "更新修改时间", value: (item) => normalizeOpportunityExportText(item.updatedAt) },
{ key: "archived", label: "是否签单", value: (item) => formatOpportunityBoolean(item.archived, "已签单", "未签单") },
{ key: "pushedToOms", label: "是否推送OMS", value: (item) => formatOpportunityBoolean(item.pushedToOms, "已推送", "未推送") },
];
const defaultOpportunityExportFields: OpportunityExportFieldKey[] = [
"code",
"name",
"projectLocation",
"client",
"type",
"operatorName",
"h3cOwner",
"channelName",
"stage",
"confidence",
"amount",
"date",
"competitorName",
"latestProgress",
"nextPlan",
"followUps",
];
function resolveSelectedOpportunityFields(selectedFields: OpportunityExportFieldKey[] | undefined) {
return selectedFields === undefined ? defaultOpportunityExportFields : selectedFields;
}
function buildOpportunityFollowUpExportText(
item: OpportunityItem,
_relatedSales: SalesExpansionItem | null,
_relatedChannel: ChannelExpansionItem | null,
) {
return (item.followUps ?? [])
.map((record) => {
const summary = getOpportunityFollowUpSummary(record);
const lines = [
[normalizeOpportunityExportText(record.date), normalizeOpportunityExportText(record.user)].filter(Boolean).join(" / "),
normalizeOpportunityExportText(summary.communicationContent),
].filter(Boolean);
return lines.join("\n");
})
.filter(Boolean)
.join("\n\n");
}
function normalizeOpportunityExportFilterText(value?: string | number | boolean | null) {
return normalizeOpportunityExportText(value).toLowerCase();
}
@ -279,6 +424,8 @@ function toFormFromItem(item: OpportunityItem, confidenceOptions: OpportunityDic
channelExpansionId: item.channelExpansionId,
operatorName: item.operatorCode || item.operatorName || "",
competitorName: item.competitorName || "",
latestProgress: item.latestProgress || "",
nextPlan: item.nextPlan || "",
description: item.notes || "",
};
}
@ -670,6 +817,7 @@ function OpportunityExportFilterModal({
onConfirm: (filters: OpportunityExportFilters) => void;
}) {
const [draftFilters, setDraftFilters] = useState<OpportunityExportFilters>(initialFilters);
const selectedFields = resolveSelectedOpportunityFields(draftFilters.selectedFields);
const hasDraftFilters = Boolean(
draftFilters.keyword
|| draftFilters.expectedStartDate
@ -681,10 +829,20 @@ function OpportunityExportFilterModal({
|| draftFilters.operatorName
|| draftFilters.hasSalesExpansion
|| draftFilters.hasChannelExpansion,
);
) || JSON.stringify(selectedFields) !== JSON.stringify(defaultOpportunityExportFields);
const hasSelectedFields = selectedFields.length > 0;
const handleFilterChange = (key: keyof OpportunityExportFilters, value: string) => {
setDraftFilters((current) => ({ ...current, [key]: value }));
};
const toggleField = (fieldKey: OpportunityExportFieldKey) => {
setDraftFilters((current) => {
const currentFields = resolveSelectedOpportunityFields(current.selectedFields);
const nextFields = currentFields.includes(fieldKey)
? currentFields.filter((item) => item !== fieldKey)
: [...currentFields, fieldKey];
return { ...current, selectedFields: nextFields };
});
};
const renderOption = (option: OpportunityDictOption) => {
const value = option.value || option.label || "";
const label = option.label || option.value || "";
@ -710,7 +868,9 @@ function OpportunityExportFilterModal({
<div className="flex flex-col-reverse gap-3 sm:flex-row sm:justify-between">
<button
type="button"
onClick={() => setDraftFilters({})}
onClick={() => setDraftFilters((current) => ({
selectedFields: resolveSelectedOpportunityFields(current.selectedFields),
}))}
disabled={!hasDraftFilters}
className="crm-btn crm-btn-secondary disabled:cursor-not-allowed disabled:opacity-60"
>
@ -719,7 +879,7 @@ function OpportunityExportFilterModal({
<button
type="button"
onClick={() => onConfirm(draftFilters)}
disabled={exporting}
disabled={exporting || !hasSelectedFields}
className="crm-btn crm-btn-primary disabled:cursor-not-allowed disabled:opacity-60"
>
{exporting ? "导出中..." : "确认导出"}
@ -848,6 +1008,52 @@ function OpportunityExportFilterModal({
onChange={(value) => handleFilterChange("hasChannelExpansion", value)}
/>
</label>
<div className="space-y-3 rounded-2xl border border-slate-200 bg-slate-50 p-4 dark:border-slate-800 dark:bg-slate-900/40 sm:col-span-2">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div>
<p className="text-sm font-medium text-slate-700 dark:text-slate-300"></p>
<p className="text-xs text-slate-500 dark:text-slate-400"></p>
</div>
<div className="flex flex-wrap gap-2">
<button
type="button"
onClick={() => setDraftFilters((current) => ({ ...current, selectedFields: opportunityExportColumns.map((column) => column.key) }))}
className="rounded-full border border-slate-200 px-3 py-1 text-xs font-medium text-slate-600 transition-colors hover:bg-white dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800"
>
</button>
<button
type="button"
onClick={() => setDraftFilters((current) => ({ ...current, selectedFields: defaultOpportunityExportFields }))}
className="rounded-full border border-slate-200 px-3 py-1 text-xs font-medium text-slate-600 transition-colors hover:bg-white dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800"
>
</button>
</div>
</div>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
{opportunityExportColumns.map((field) => {
const checked = selectedFields.includes(field.key);
return (
<label key={field.key} className={cn(
"flex items-center gap-3 rounded-xl border px-3 py-2 text-sm transition-colors",
checked
? "border-violet-200 bg-violet-50 text-violet-700 dark:border-violet-500/30 dark:bg-violet-500/10 dark:text-violet-300"
: "border-slate-200 bg-white text-slate-600 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-300",
)}>
<input
type="checkbox"
checked={checked}
onChange={() => toggleField(field.key)}
className="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500"
/>
<span>{field.label}</span>
</label>
);
})}
</div>
{!hasSelectedFields ? <p className="text-xs text-rose-500"></p> : null}
</div>
</div>
{exportError ? <div className="crm-alert crm-alert-error mt-4">{exportError}</div> : null}
</ModalShell>
@ -1462,7 +1668,7 @@ export default function Opportunities() {
const [pushingOms, setPushingOms] = useState(false);
const [exporting, setExporting] = useState(false);
const [exportFilterOpen, setExportFilterOpen] = useState(false);
const [exportFilters, setExportFilters] = useState<OpportunityExportFilters>({});
const [exportFilters, setExportFilters] = useState<OpportunityExportFilters>(() => loadOpportunityExportPreferences());
const [error, setError] = useState("");
const [exportError, setExportError] = useState("");
const [items, setItems] = useState<OpportunityItem[]>([]);
@ -1591,7 +1797,7 @@ export default function Opportunities() {
return () => {
cancelled = true;
};
}, [selectedItem?.id]);
}, [selectedItem?.id, selectedItem?.updatedAt]);
useEffect(() => {
let cancelled = false;
@ -1960,48 +2166,26 @@ export default function Opportunities() {
setExporting(true);
setExportError("");
setExportFilters(filters);
persistOpportunityExportPreferences(filters);
try {
const overview = await getOpportunityOverview("", "全部");
const exportItems = (overview.items ?? [])
.filter((item) => (archiveTab === "active" ? !item.archived : Boolean(item.archived)))
.filter((item) => matchesOpportunityExportFilters(item, filters, effectiveConfidenceOptions));
const selectedFieldKeys = resolveSelectedOpportunityFields(filters.selectedFields);
if (exportItems.length <= 0) {
throw new Error(`当前筛选条件下暂无可导出的${archiveTab === "active" ? "未签单" : "已签单"}商机`);
}
if (selectedFieldKeys.length <= 0) {
throw new Error("请至少选择一个导出字段");
}
const ExcelJS = await import("exceljs");
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet("商机储备");
const headers = [
"项目编号",
"项目名称",
"项目地",
"最终用户",
"建设类型",
"运作方",
"项目阶段",
"项目把握度",
"预计金额(元)",
"预计下单时间",
"销售拓展人员姓名",
"销售拓展人员合作意向",
"销售拓展人员是否在职",
"拓展渠道名称",
"拓展渠道合作意向",
"拓展渠道建立联系时间",
"新华三负责人",
"售前",
"竞争对手",
"项目最新进展",
"后续规划",
"备注说明",
"创建人",
"更新修改时间",
"是否签单",
"是否推送OMS",
"跟进记录",
];
const columns = opportunityExportColumns.filter((column) => selectedFieldKeys.includes(column.key));
const headers = columns.map((column) => column.label);
worksheet.addRow(headers);
@ -2012,67 +2196,40 @@ export default function Opportunities() {
const relatedChannel = item.channelExpansionId
? channelExpansionOptions.find((option) => option.id === item.channelExpansionId) ?? null
: null;
const relatedSalesActive = item.salesExpansionActive ?? relatedSales?.active;
const followUpText = (item.followUps ?? [])
.map((record) => {
const summary = getOpportunityFollowUpSummary(record);
const lines = [
[normalizeOpportunityExportText(record.date), normalizeOpportunityExportText(record.user)].filter(Boolean).join(" / "),
normalizeOpportunityExportText(summary.communicationContent),
].filter(Boolean);
return lines.join("\n");
})
.filter(Boolean)
.join("\n\n");
worksheet.addRow([
normalizeOpportunityExportText(item.code),
normalizeOpportunityExportText(item.name),
normalizeOpportunityExportText(item.projectLocation),
normalizeOpportunityExportText(item.client),
normalizeOpportunityExportText(item.type || "新建"),
normalizeOpportunityExportText(item.operatorName),
normalizeOpportunityExportText(item.stage),
getConfidenceLabel(item.confidence, effectiveConfidenceOptions),
item.amount === null || item.amount === undefined ? "" : `¥${formatAmount(item.amount)}`,
normalizeOpportunityExportText(item.date),
normalizeOpportunityExportText(item.salesExpansionName || relatedSales?.name),
normalizeOpportunityExportText(item.salesExpansionIntent || relatedSales?.intent),
relatedSalesActive === undefined ? "" : relatedSalesActive ? "是" : "否",
normalizeOpportunityExportText(item.channelExpansionName || relatedChannel?.name),
normalizeOpportunityExportText(item.channelExpansionIntent || relatedChannel?.intent),
normalizeOpportunityExportText(item.channelExpansionEstablishedDate || relatedChannel?.establishedDate),
normalizeOpportunityExportText(item.salesExpansionName || relatedSales?.name),
normalizeOpportunityExportText(item.preSalesName),
normalizeOpportunityExportText(item.competitorName),
normalizeOpportunityExportText(item.latestProgress),
normalizeOpportunityExportText(item.nextPlan),
normalizeOpportunityExportText(item.notes),
normalizeOpportunityExportText(item.owner),
normalizeOpportunityExportText(item.updatedAt),
formatOpportunityBoolean(item.archived, "已签单", "未签单"),
formatOpportunityBoolean(item.pushedToOms, "已推送", "未推送"),
followUpText,
]);
worksheet.addRow(columns.map((column) => {
if (column.key === "confidence") {
return getConfidenceLabel(item.confidence, effectiveConfidenceOptions);
}
return column.value(item, relatedSales, relatedChannel);
}));
});
worksheet.views = [{ state: "frozen", ySplit: 1 }];
const followUpColumnIndex = headers.indexOf("跟进记录") + 1;
worksheet.getRow(1).height = 24;
worksheet.getRow(1).font = { bold: true };
worksheet.getRow(1).alignment = { vertical: "middle", horizontal: "center" };
headers.forEach((header, index) => {
const column = worksheet.getColumn(index + 1);
if (header === "跟进记录") {
column.width = 42;
} else if (header.includes("项目最新进展") || header.includes("后续规划") || header.includes("备注")) {
column.width = 24;
} else if (header.includes("项目名称") || header.includes("最终客户") || header.includes("最终用户")) {
column.width = 20;
} else {
column.width = 16;
const columnWidths = columns.map((column) => {
if (column.kind === "followup") {
return 42;
}
if (column.kind === "longText") {
return 24;
}
if (column.label.includes("项目名称") || column.label.includes("最终用户")) {
return 20;
}
return 16;
});
columns.forEach((columnConfig, index) => {
const column = worksheet.getColumn(index + 1);
column.width = columnWidths[index];
column.alignment = {
vertical: "top",
horizontal: "left",
wrapText: columnConfig.kind === "followup",
};
});
worksheet.eachRow((row, rowNumber) => {
@ -2086,9 +2243,10 @@ export default function Opportunities() {
cell.alignment = {
vertical: "top",
horizontal: rowNumber === 1 ? "center" : "left",
wrapText: headers[columnNumber - 1] === "跟进记录",
wrapText: rowNumber > 1 && columns[columnNumber - 1]?.kind === "followup",
};
});
const followUpColumnIndex = columns.findIndex((column) => column.kind === "followup") + 1;
if (rowNumber > 1 && followUpColumnIndex > 0) {
const followUpText = normalizeOpportunityExportText(row.getCell(followUpColumnIndex).value as string | null | undefined);
const lineCount = followUpText ? followUpText.split("\n").length : 1;
@ -2634,11 +2792,11 @@ export default function Opportunities() {
</div>
<div className="flex min-w-0 items-center gap-2 text-slate-600 dark:text-slate-300">
<span className="shrink-0 text-slate-400 dark:text-slate-500">:</span>
<span className="min-w-0 flex-1 truncate font-medium text-slate-900 dark:text-white">{opp.latestProgress || "暂无回写进展"}</span>
<span className="min-w-0 flex-1 truncate font-medium text-slate-900 dark:text-white">{opp.latestProgress || "暂无进展"}</span>
</div>
<div className="flex min-w-0 items-center gap-2 text-slate-600 dark:text-slate-300">
<span className="shrink-0 text-slate-400 dark:text-slate-500">:</span>
<span className="min-w-0 flex-1 truncate font-medium text-slate-900 dark:text-white">{opp.nextPlan || "暂无回写规划"}</span>
<span className="shrink-0 text-slate-400 dark:text-slate-500">{OPPORTUNITY_NEXT_PLAN_LABEL}:</span>
<span className="min-w-0 flex-1 truncate font-medium text-slate-900 dark:text-white">{opp.nextPlan || "暂无销售计划"}</span>
</div>
<div className="flex min-w-0 items-center gap-2 text-slate-600 dark:text-slate-300">
<span className="shrink-0 text-slate-400 dark:text-slate-500">:</span>
@ -2928,30 +3086,28 @@ export default function Opportunities() {
/>
{fieldErrors.opportunityType ? <p className="text-xs text-rose-500">{fieldErrors.opportunityType}</p> : null}
</label>
{detailItem ? (
<>
<label className="space-y-2 sm:col-span-2">
<span className="text-sm font-medium text-slate-700 dark:text-slate-300"></span>
<textarea
rows={3}
readOnly
value={detailItem.latestProgress || "暂无日报回写进展"}
className="w-full cursor-not-allowed rounded-xl border border-slate-200 bg-slate-100 px-4 py-3 text-sm text-slate-500 outline-none dark:border-slate-800 dark:bg-slate-800/70 dark:text-slate-400"
value={form.latestProgress || ""}
onChange={(e) => handleChange("latestProgress", e.target.value)}
placeholder="可直接填写项目当前推进情况;日报回写时也会同步更新这里。"
className="crm-input-box crm-input-text w-full border border-slate-200 bg-white outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500 dark:border-slate-800 dark:bg-slate-900/50"
/>
<p className="text-xs text-slate-400 dark:text-slate-500"></p>
<p className="text-xs text-slate-400 dark:text-slate-500"></p>
</label>
<label className="space-y-2 sm:col-span-2">
<span className="text-sm font-medium text-slate-700 dark:text-slate-300"></span>
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">{OPPORTUNITY_NEXT_PLAN_LABEL}</span>
<textarea
rows={3}
readOnly
value={detailItem.nextPlan || "暂无日报回写规划"}
className="w-full cursor-not-allowed rounded-xl border border-slate-200 bg-slate-100 px-4 py-3 text-sm text-slate-500 outline-none dark:border-slate-800 dark:bg-slate-800/70 dark:text-slate-400"
value={form.nextPlan || ""}
onChange={(e) => handleChange("nextPlan", e.target.value)}
placeholder={`可直接填写${OPPORTUNITY_NEXT_PLAN_LABEL};日报回写时也会同步更新这里。`}
className="crm-input-box crm-input-text w-full border border-slate-200 bg-white outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500 dark:border-slate-800 dark:bg-slate-900/50"
/>
<p className="text-xs text-slate-400 dark:text-slate-500"></p>
<p className="text-xs text-slate-400 dark:text-slate-500"></p>
</label>
</>
) : null}
<label className="space-y-2 sm:col-span-2">
<span className="text-sm font-medium text-slate-700 dark:text-slate-300"></span>
<textarea rows={4} value={form.description || ""} onChange={(e) => handleChange("description", e.target.value)} className="crm-input-box crm-input-text w-full border border-slate-200 bg-white outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500 dark:border-slate-800 dark:bg-slate-900/50" />
@ -3176,8 +3332,8 @@ export default function Opportunities() {
/>
<DetailItem label="竞争对手" value={detailItem.competitorName || "无"} />
<DetailItem label="建设类型" value={detailItem.type || "新建"} icon={<Tag className="h-3 w-3" />} />
<DetailItem label="项目最新进展" value={detailItem.latestProgress || "暂无日报回写进展"} className="md:col-span-2" />
<DetailItem label="后续规划" value={detailItem.nextPlan || "暂无日报回写规划"} className="md:col-span-2" />
<DetailItem label="项目最新进展" value={detailItem.latestProgress || "暂无进展"} className="md:col-span-2" />
<DetailItem label={OPPORTUNITY_NEXT_PLAN_LABEL} value={detailItem.nextPlan || "暂无销售计划"} className="md:col-span-2" />
<DetailItem label="备注说明" value={detailItem.notes || "无"} className="md:col-span-2" />
</div>
</div>
@ -3353,7 +3509,7 @@ function getOpportunityFollowUpSummary(record: {
}) {
const content = record.content || "";
const parsedLatestProgress = extractOpportunityFollowUpField(content, "项目最新进展");
const parsedNextPlan = extractOpportunityFollowUpField(content, "后续规划");
const parsedNextPlan = extractOpportunityNextPlan(content);
const communicationContent = buildOpportunityCommunicationContent(
pickOpportunityFollowUpValue(record.latestProgress, parsedLatestProgress),
pickOpportunityFollowUpValue(record.nextAction, parsedNextPlan),
@ -3375,6 +3531,11 @@ function extractOpportunityFollowUpField(content: string, label: string) {
return match?.[1]?.trim();
}
function extractOpportunityNextPlan(content: string) {
return extractOpportunityFollowUpField(content, OPPORTUNITY_NEXT_PLAN_LABEL)
|| extractOpportunityFollowUpField(content, LEGACY_OPPORTUNITY_NEXT_PLAN_LABEL);
}
function extractLegacyOpportunityCommunicationContent(rawValue?: string) {
const normalized = rawValue?.trim();
if (!normalized || normalized === "无") {
@ -3403,7 +3564,7 @@ function buildOpportunityCommunicationContent(
) {
const parts = [
latestProgress?.trim() ? `项目最新进展:${latestProgress.trim()}` : "",
nextPlan?.trim() ? `后续规划${nextPlan.trim()}` : "",
nextPlan?.trim() ? `${OPPORTUNITY_NEXT_PLAN_LABEL}${nextPlan.trim()}` : "",
].filter(Boolean);
if (parts.length > 0) {
return parts.join("\n");

View File

@ -70,10 +70,14 @@ import { loadTencentMapGlApi } from "@/lib/tencentMapGl";
import { cn } from "@/lib/utils";
import { isWecomBrowser, isWecomJsSdkLocationEnabled, resolveWecomLocation } from "@/lib/wecom";
const LEGACY_NEXT_PLAN_LABEL = "后续规划";
const OPPORTUNITY_NEXT_PLAN_LABEL = "下一步销售计划";
const WORK_DETAIL_NEXT_PLAN_HEADER = "后续规划 / 下一步销售计划";
const reportFieldLabels = {
sales: ["沟通内容", "后续规划"],
channel: ["沟通内容", "后续规划"],
opportunity: ["项目最新进展", "后续规划"],
sales: ["沟通内容", LEGACY_NEXT_PLAN_LABEL],
channel: ["沟通内容", LEGACY_NEXT_PLAN_LABEL],
opportunity: ["项目最新进展", OPPORTUNITY_NEXT_PLAN_LABEL],
} as const;
const COMPETITOR_OPTIONS = [
"深信服",
@ -446,7 +450,7 @@ async function exportDailyReportRowsToExcel(rows: WorkDailyReportExportRow[]) {
"跟进对象类型",
"跟进对象名称",
"工作内容/项目进展",
"后续规划",
WORK_DETAIL_NEXT_PLAN_HEADER,
];
summaryWorksheet.addRow(summaryHeaders);
@ -476,13 +480,13 @@ async function exportDailyReportRowsToExcel(rows: WorkDailyReportExportRow[]) {
formatWorkBizType(lineItem?.bizType),
normalizeWorkExportText(lineItem?.bizName),
normalizeWorkExportDetailContent(lineItem),
normalizeWorkExportText(lineItem?.nextPlan || extractContentByLabel(lineItem?.content, "后续规划")),
getLineItemNextPlan(lineItem),
]);
});
});
styleWorkExportWorksheet(summaryWorksheet, summaryHeaders, ["今日工作内容", "明日工作计划"]);
styleWorkExportWorksheet(detailWorksheet, detailHeaders, ["工作内容/项目进展", "后续规划"]);
styleWorkExportWorksheet(detailWorksheet, detailHeaders, ["工作内容/项目进展", WORK_DETAIL_NEXT_PLAN_HEADER]);
const buffer = await workbook.xlsx.writeBuffer();
await downloadWorkExcelFile(`销售日报_${formatWorkExportFilenameTime()}.xlsx`, buffer);
}
@ -654,6 +658,8 @@ export default function Work() {
const historyLoadMoreRef = useRef<HTMLDivElement | null>(null);
const historyScrollContainerRef = useRef<HTMLDivElement | null>(null);
const historyScrollIdleTimerRef = useRef<number | null>(null);
const historyLoadingRequestRef = useRef<string | null>(null);
const historyLoadedPagesRef = useRef(new Map<WorkSection, Set<number>>());
const [loading, setLoading] = useState(true);
const [refreshingLocation, setRefreshingLocation] = useState(false);
const [submittingCheckIn, setSubmittingCheckIn] = useState(false);
@ -1017,6 +1023,18 @@ export default function Work() {
}
async function loadHistory(section: WorkSection, page = 1, replace = false) {
const requestKey = `${section}:${page}`;
if (historyLoadingRequestRef.current === requestKey) {
return;
}
if (replace) {
historyLoadedPagesRef.current.set(section, new Set());
} else if (historyLoadedPagesRef.current.get(section)?.has(page)) {
return;
}
historyLoadingRequestRef.current = requestKey;
if (replace) {
setPageError("");
setHistoryData([]);
@ -1029,7 +1047,10 @@ export default function Work() {
const data = await getWorkHistory(section, page, 8);
const nextItems = data.items ?? [];
setPageError("");
setHistoryData((current) => replace ? nextItems : [...current, ...nextItems]);
setHistoryData((current) => replace ? dedupeWorkHistoryItems(nextItems) : mergeWorkHistoryItems(current, nextItems));
const loadedPages = historyLoadedPagesRef.current.get(section) ?? new Set<number>();
loadedPages.add(page);
historyLoadedPagesRef.current.set(section, loadedPages);
setHistoryHasMore(Boolean(data.hasMore));
setHistoryPage(data.page ?? page);
} catch (error) {
@ -1039,6 +1060,9 @@ export default function Work() {
}
setHistoryHasMore(false);
} finally {
if (historyLoadingRequestRef.current === requestKey) {
historyLoadingRequestRef.current = null;
}
setHistoryLoading(false);
setHistoryLoadingMore(false);
}
@ -1882,7 +1906,7 @@ export default function Work() {
/>
{groupExpanded
? group.items.map((item, index) => (
<div key={`${group.date}-${item.type}-${item.id}-${index}`}>
<div key={buildWorkHistoryItemKey(item)}>
<HistoryCard
item={item}
index={index}
@ -4022,6 +4046,29 @@ function formatHistoryGroupDate(date: string) {
return format(parsed, "M月d日 EEEE", { locale: zhCN });
}
function buildWorkHistoryItemKey(item: WorkHistoryItem) {
return [
item.type || "unknown",
item.id ?? "no-id",
item.date || "no-date",
item.time || "no-time",
item.status || "no-status",
item.content || "",
].join("::");
}
function dedupeWorkHistoryItems(items: WorkHistoryItem[]) {
const uniqueItems = new Map<string, WorkHistoryItem>();
for (const item of items) {
uniqueItems.set(buildWorkHistoryItemKey(item), item);
}
return Array.from(uniqueItems.values());
}
function mergeWorkHistoryItems(current: WorkHistoryItem[], next: WorkHistoryItem[]) {
return dedupeWorkHistoryItems([...current, ...next]);
}
function SummaryCell({ label, value }: { label: string; value: string }) {
return (
<div className="bg-white px-4 py-3 dark:bg-slate-900/50">
@ -4297,6 +4344,13 @@ function getTemplateFields(bizType: BizType) {
return [...reportFieldLabels[bizType]];
}
function getTemplateFieldAliases(bizType: BizType, field: string) {
if (bizType === "opportunity" && field === OPPORTUNITY_NEXT_PLAN_LABEL) {
return [OPPORTUNITY_NEXT_PLAN_LABEL, LEGACY_NEXT_PLAN_LABEL];
}
return [field];
}
function buildEditorMentionLine(bizType: BizType, bizName: string) {
return `@${getBizTypeLabel(bizType)} ${bizName}`;
}
@ -4328,7 +4382,7 @@ function parseTemplateValues(bizType: BizType, editorText: string) {
currentField = null;
continue;
}
const parsedFieldLine = parseTemplateFieldLine(rawLine, fieldSet);
const parsedFieldLine = parseTemplateFieldLine(rawLine, bizType, fieldSet);
if (parsedFieldLine) {
currentField = parsedFieldLine.field;
if (parsedFieldLine.field) {
@ -4348,7 +4402,7 @@ function parseTemplateValues(bizType: BizType, editorText: string) {
return values;
}
function parseTemplateFieldLine(rawLine: string, fieldSet: string[]) {
function parseTemplateFieldLine(rawLine: string, bizType: BizType, fieldSet: string[]) {
const match = rawLine.match(/^#\s*(.*)$/);
if (!match) {
return null;
@ -4356,11 +4410,12 @@ function parseTemplateFieldLine(rawLine: string, fieldSet: string[]) {
const content = match[1].trim();
for (const field of fieldSet) {
if (!content.startsWith(field)) {
for (const alias of getTemplateFieldAliases(bizType, field)) {
if (!content.startsWith(alias)) {
continue;
}
const suffix = content.slice(field.length).trimStart();
const suffix = content.slice(alias.length).trimStart();
if (!suffix) {
return { field, value: "" };
}
@ -4369,6 +4424,7 @@ function parseTemplateFieldLine(rawLine: string, fieldSet: string[]) {
}
return { field: null, value: "" };
}
}
return { field: null, value: "" };
}
@ -4382,10 +4438,10 @@ function normalizeLoadedLineItem(item: WorkReportLineItem): WorkReportLineItem {
const fieldValues: Record<string, string> = {};
if (normalized.bizType === "sales" || normalized.bizType === "channel") {
fieldValues["沟通内容"] = normalized.evaluationContent || extractContentByLabel(normalized.content, "沟通内容") || "";
fieldValues["后续规划"] = normalized.nextPlan || extractContentByLabel(normalized.content, "后续规划") || "";
fieldValues[LEGACY_NEXT_PLAN_LABEL] = normalized.nextPlan || extractContentByLabel(normalized.content, LEGACY_NEXT_PLAN_LABEL) || "";
} else {
fieldValues["项目最新进展"] = normalized.latestProgress || extractContentByLabel(normalized.content, "项目最新进展") || "";
fieldValues["后续规划"] = normalized.nextPlan || extractContentByLabel(normalized.content, "后续规划") || "";
fieldValues[OPPORTUNITY_NEXT_PLAN_LABEL] = normalized.nextPlan || extractContentByLabels(normalized.content, [OPPORTUNITY_NEXT_PLAN_LABEL, LEGACY_NEXT_PLAN_LABEL]) || "";
}
const bizName = normalized.bizName || "未命名对象";
normalized.editorText = sanitizeEditorText(
@ -4430,12 +4486,12 @@ function buildLinePreview(bizType: BizType, values: Record<string, string>) {
if (bizType === "opportunity") {
return [
values["项目最新进展"] ? `项目最新进展:${values["项目最新进展"]}` : "",
values["后续规划"] ? `后续规划:${values["后续规划"]}` : "",
values[OPPORTUNITY_NEXT_PLAN_LABEL] ? `${OPPORTUNITY_NEXT_PLAN_LABEL}${values[OPPORTUNITY_NEXT_PLAN_LABEL]}` : "",
].filter(Boolean).join("\n");
}
return [
values["沟通内容"] ? `沟通内容:${values["沟通内容"]}` : "",
values["后续规划"] ? `后续规划:${values["后续规划"]}` : "",
values[LEGACY_NEXT_PLAN_LABEL] ? `${LEGACY_NEXT_PLAN_LABEL}${values[LEGACY_NEXT_PLAN_LABEL]}` : "",
].filter(Boolean).join("\n");
}
@ -4459,6 +4515,26 @@ function extractContentByLabel(content: string | undefined, label: string) {
return match?.[1]?.trim() || "";
}
function extractContentByLabels(content: string | undefined, labels: string[]) {
for (const label of labels) {
const value = extractContentByLabel(content, label);
if (value) {
return value;
}
}
return "";
}
function getLineItemNextPlan(lineItem?: WorkReportLineItem) {
if (!lineItem) {
return "";
}
if (lineItem.bizType === "opportunity") {
return normalizeWorkExportText(lineItem.nextPlan || extractContentByLabels(lineItem.content, [OPPORTUNITY_NEXT_PLAN_LABEL, LEGACY_NEXT_PLAN_LABEL]));
}
return normalizeWorkExportText(lineItem.nextPlan || extractContentByLabel(lineItem.content, LEGACY_NEXT_PLAN_LABEL));
}
async function stampPhotoWithTimeAndLocation(file: File, locationText: string) {
const imageUrl = URL.createObjectURL(file);
try {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
import{u as P,r as t,f as b,s as y,j as e,T as E,R,a as B,b as L,B as w,c as z,g as U,d as q,l as F,e as M}from"./index-DIy3NosD.js";import{F as r}from"./index-C8zAgrXu.js";import{I as u}from"./index-ChEzJj4c.js";import{C as O}from"./index-D0LR2CrS.js";import"./useForm-Ee9FUUVh.js";import"./row-BFbQ_b2k.js";import"./index-CUzdQnnk.js";const{Text:$}=E;function N(a){return a instanceof Error&&a.message.toLowerCase().includes("captcha disabled")}function Q(){const{t:a}=P(),[l,o]=t.useState(null),[d,m]=t.useState(!0),[h,p]=t.useState(!1),[n,I]=t.useState(null),[v]=r.useForm(),C="汇智CRM管理后台",f=t.useCallback(async()=>{const s=await b();o(s)},[]),x=t.useCallback(async()=>{try{await f()}catch(s){o(null),N(s)&&m(!1)}},[f]);t.useEffect(()=>{let s=!1;return(async()=>{const[c,T]=await Promise.all([U("security.captcha.enabled","true").catch(()=>"true"),q().catch(()=>null)]);if(s)return;I(T);const j=c!=="false";if(m(j),!j){o(null);return}try{const g=await b();if(s)return;o(g)}catch(g){if(s)return;o(null),N(g)&&m(!1)}})(),()=>{s=!0}},[]),t.useEffect(()=>{new URLSearchParams(window.location.search).get("timeout")==="1"&&(y.warning(a("login.loginTimeout")),window.history.replaceState({},document.title,window.location.pathname))},[a]);const S=async s=>{p(!0);try{const i=await F({username:s.username,password:s.password,tenantCode:s.tenantCode,captchaId:d?l==null?void 0:l.captchaId:void 0,captchaCode:d?s.captchaCode:void 0});if(localStorage.setItem("accessToken",i.accessToken),localStorage.setItem("refreshToken",i.refreshToken),localStorage.setItem("username",s.username),i.availableTenants){localStorage.setItem("availableTenants",JSON.stringify(i.availableTenants));const c=JSON.parse(atob(i.accessToken.split(".")[1]));localStorage.setItem("activeTenantId",String(c.tenantId))}try{const c=await M();sessionStorage.setItem("userProfile",JSON.stringify(c))}catch{sessionStorage.removeItem("userProfile")}y.success(a("common.success")),window.location.href="/"}catch{d&&await x()}finally{p(!1)}},k=n!=null&&n.loginBgUrl?{backgroundImage:`url(${n.loginBgUrl})`,backgroundSize:"cover",backgroundPosition:"center",position:"relative"}:{};return e.jsxs("div",{className:"login-page",style:k,children:[e.jsx("div",{className:"login-page-backdrop"}),e.jsx("div",{className:"login-page-grid",children:e.jsx("section",{className:"login-panel",children:e.jsx("div",{className:"login-panel-card",children:e.jsxs("div",{className:"login-panel-layout",children:[e.jsx("div",{className:"login-left",children:e.jsxs("div",{className:"login-brand",children:[e.jsx("div",{className:"brand-logo-wrap",children:e.jsx("img",{src:(n==null?void 0:n.logoUrl)||"/logo.svg",alt:"Logo",className:"brand-logo-img"})}),e.jsxs("div",{className:"brand-copy",children:[e.jsx("p",{className:"brand-kicker",children:"智慧销售协同平台"}),e.jsx("span",{className:"brand-name",children:C})]})]})}),e.jsx("div",{className:"login-right",children:e.jsxs("div",{className:"login-container",children:[e.jsxs("div",{className:"login-header",children:[e.jsx("p",{className:"login-panel-eyebrow",children:a("login.welcome")}),e.jsx($,{type:"secondary",children:a("login.subtitle")})]}),e.jsxs(r,{form:v,layout:"vertical",onFinish:S,className:"login-form",requiredMark:!1,autoComplete:"off",children:[e.jsx(r.Item,{name:"username",rules:[{required:!0,message:a("login.username")}],children:e.jsx(u,{size:"large",prefix:e.jsx(R,{className:"text-gray-400","aria-hidden":"true"}),placeholder:a("login.username"),autoComplete:"username",spellCheck:!1,"aria-label":a("login.username")})}),e.jsx(r.Item,{name:"password",rules:[{required:!0,message:a("login.password")}],children:e.jsx(u.Password,{size:"large",prefix:e.jsx(B,{className:"text-gray-400","aria-hidden":"true"}),placeholder:a("login.password"),autoComplete:"current-password","aria-label":a("login.password")})}),d?e.jsx(r.Item,{name:"captchaCode",rules:[{required:!0,message:a("login.captcha")}],children:e.jsxs("div",{className:"captcha-wrapper",children:[e.jsx(u,{size:"large",prefix:e.jsx(L,{className:"text-gray-400","aria-hidden":"true"}),placeholder:a("login.captcha"),maxLength:6,"aria-label":a("login.captcha")}),e.jsx(w,{className:"captcha-image-btn",onClick:()=>void x(),icon:l?null:e.jsx(z,{spin:!0}),"aria-label":"刷新验证码",children:l?e.jsx("img",{src:l.imageBase64,alt:"验证码"}):null})]})}):null,e.jsx("div",{className:"login-extra",children:e.jsx(r.Item,{name:"remember",valuePropName:"checked",noStyle:!0,children:e.jsx(O,{children:a("login.rememberMe")})})}),e.jsx(r.Item,{children:e.jsx(w,{type:"primary",htmlType:"submit",loading:h,block:!0,size:"large",className:"login-submit-btn",children:a(h?"login.loggingIn":"login.submit")})})]})]})})]})})})})]})}export{Q as default};

View File

@ -1 +0,0 @@
import{cs as F,r as i,an as I,ce as S,ct as j,cu as v,cv as E,cw as $,ao as M,cx as N,cy as _,cz as A,cA as c,cB as V,cC as W,cD as z,cE as B,cF as x,cG as D,cH as G}from"./index-DIy3NosD.js";var H=function(e,n){var l={};for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&n.indexOf(r)<0&&(l[r]=e[r]);if(e!=null&&typeof Object.getOwnPropertySymbols=="function")for(var o=0,r=Object.getOwnPropertySymbols(e);o<r.length;o++)n.indexOf(r[o])<0&&Object.prototype.propertyIsEnumerable.call(e,r[o])&&(l[r[o]]=e[r[o]]);return l};const R=e=>{const{prefixCls:n,className:l,closeIcon:r,closable:o,type:a,title:b,children:d,footer:h}=e,y=H(e,["prefixCls","className","closeIcon","closable","type","title","children","footer"]),{getPrefixCls:m}=i.useContext(I),C=m(),s=n||m("modal"),p=S(C),[w,O,g]=j(s,p),f=`${s}-confirm`;let u={};return a?u={closable:o??!1,title:"",footer:"",children:i.createElement(v,Object.assign({},e,{prefixCls:s,confirmPrefixCls:f,rootPrefixCls:C,content:d}))}:u={closable:o??!0,title:b,footer:h!==null&&i.createElement(E,Object.assign({},e)),children:d},w(i.createElement($,Object.assign({prefixCls:s,className:M(O,`${s}-pure-panel`,a&&f,a&&`${f}-${a}`,l,g,p)},y,{closeIcon:N(s,r),closable:o},u)))},T=F(R);function P(e){return c(G(e))}const t=_;t.useModal=A;t.info=function(n){return c(V(n))};t.success=function(n){return c(W(n))};t.error=function(n){return c(z(n))};t.warning=P;t.warn=P;t.confirm=function(n){return c(B(n))};t.destroyAll=function(){for(;x.length;){const n=x.pop();n&&n()}};t.config=D;t._InternalPanelDoNotUseOrYouWillBeFired=T;export{t as M};

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
import{j as e,m as N,n as I,u as R,T as $,o as c,p as k,B as o,q as w,t as T,v as C,w as M,R as S,S as _}from"./index-DIy3NosD.js";import{P as D}from"./index-x0_qMNAh.js";import{T as l}from"./index-Do3ElwMr.js";import{R as h,C as r}from"./row-BFbQ_b2k.js";import{C as p}from"./index-JMVQVIid.js";import{F as E}from"./Table-C0wBOmw3.js";import"./iconUtil-DNX53dK0.js";import"./index-CUzdQnnk.js";import"./useForm-Ee9FUUVh.js";import"./index-D0LR2CrS.js";import"./Pagination-C_3XJ-9Y.js";function n({title:a,value:d,icon:i,color:s="blue",trend:t,suffix:u="",layout:b="column",gridColumn:m,className:f="",onClick:g,style:v={}}){const x={blue:"#1677ff",green:"#52c41a",orange:"#faad14",red:"#ff4d4f",purple:"#722ed1",gray:"#8c8c8c"}[s]||s,y={...m?{gridColumn:m}:{},...v};return e.jsxs("div",{className:`stat-card stat-card-${b} ${f}`,style:y,onClick:g,children:[e.jsxs("div",{className:"stat-card-header",children:[e.jsx("span",{className:"stat-card-title",children:a}),i&&e.jsx("span",{className:"stat-card-icon",style:{color:x},"aria-hidden":"true",children:i})]}),e.jsxs("div",{className:"stat-card-body",children:[e.jsxs("div",{className:"stat-card-value tabular-nums",style:{color:x},children:[d,u&&e.jsx("span",{className:"stat-card-suffix",children:u})]}),t&&e.jsxs("div",{className:`stat-card-trend ${t.direction==="up"?"trend-up":"trend-down"} tabular-nums`,"aria-label":`${t.direction==="up"?"Increase":"Decrease"} of ${t.value}%`,children:[t.direction==="up"?e.jsx(N,{"aria-hidden":"true"}):e.jsx(I,{"aria-hidden":"true"}),e.jsxs("span",{children:[Math.abs(t.value),"%"]})]})]})]})}const{Text:j}=$;function O(){const{t:a}=R(),d=[{key:"1",name:"Product Sync",time:"2024-02-10 14:00",duration:"45min",status:"processing"},{key:"2",name:"Tech Review",time:"2024-02-10 10:00",duration:"60min",status:"success"},{key:"3",name:"Daily Standup",time:"2024-02-10 09:00",duration:"15min",status:"success"},{key:"4",name:"Client Call",time:"2024-02-10 16:30",duration:"30min",status:"default"}],i=[{title:a("dashboard.meetingName"),dataIndex:"name",key:"name",render:s=>e.jsx(j,{strong:!0,children:s})},{title:a("dashboard.startTime"),dataIndex:"time",key:"time",className:"tabular-nums",render:s=>e.jsx(j,{type:"secondary",children:s})},{title:a("dashboard.duration"),dataIndex:"duration",key:"duration",width:100,className:"tabular-nums"},{title:a("common.status"),dataIndex:"status",key:"status",width:120,render:s=>s==="processing"?e.jsx(l,{icon:e.jsx(c,{spin:!0,"aria-hidden":"true"}),color:"processing",children:a("dashboardExt.processing")}):s==="success"?e.jsx(l,{icon:e.jsx(k,{"aria-hidden":"true"}),color:"success",children:a("dashboardExt.completed")}):e.jsx(l,{color:"default",children:a("dashboardExt.pending")})},{title:a("common.action"),key:"action",width:80,render:()=>e.jsx(o,{type:"link",size:"small",icon:e.jsx(w,{"aria-hidden":"true"}),"aria-label":a("dashboard.viewAll")})}];return e.jsxs("div",{className:"app-page dashboard-page",children:[e.jsx(D,{title:a("dashboard.title"),subtitle:a("dashboard.subtitle")}),e.jsx("div",{className:"app-page__page-actions",children:e.jsx(o,{icon:e.jsx(c,{"aria-hidden":"true"}),size:"small",children:a("common.refresh")})}),e.jsxs(h,{gutter:[24,24],children:[e.jsx(r,{xs:24,sm:12,lg:6,children:e.jsx(n,{title:a("dashboard.todayMeetings"),value:12,icon:e.jsx(T,{"aria-hidden":"true"}),color:"blue",trend:{value:8,direction:"up"}})}),e.jsx(r,{xs:24,sm:12,lg:6,children:e.jsx(n,{title:a("dashboard.activeDevices"),value:45,icon:e.jsx(C,{"aria-hidden":"true"}),color:"green",trend:{value:2,direction:"up"}})}),e.jsx(r,{xs:24,sm:12,lg:6,children:e.jsx(n,{title:a("dashboard.transcriptionDuration"),value:1280,suffix:"min",icon:e.jsx(M,{"aria-hidden":"true"}),color:"orange",trend:{value:5,direction:"down"}})}),e.jsx(r,{xs:24,sm:12,lg:6,children:e.jsx(n,{title:a("dashboard.totalUsers"),value:320,icon:e.jsx(S,{"aria-hidden":"true"}),color:"purple",trend:{value:12,direction:"up"}})})]}),e.jsxs(h,{gutter:[24,24],className:"mt-6",children:[e.jsx(r,{xs:24,xl:16,children:e.jsx(p,{title:a("dashboard.recentMeetings"),variant:"borderless",className:"app-page__content-card",extra:e.jsx(o,{type:"link",size:"small",children:a("dashboard.viewAll")}),styles:{body:{padding:0}},children:e.jsx(E,{dataSource:d,columns:i,pagination:!1,size:"middle",className:"roles-table"})})}),e.jsx(r,{xs:24,xl:8,children:e.jsx(p,{title:a("dashboard.deviceLoad"),variant:"borderless",className:"app-page__content-card",children:e.jsxs("div",{className:"flex flex-col items-center justify-center py-12",children:[e.jsx(_,{active:!0,paragraph:{rows:4}}),e.jsxs("div",{className:"mt-4 text-gray-400 flex items-center gap-2",children:[e.jsx(c,{spin:!0,"aria-hidden":"true"}),e.jsx("span",{children:a("dashboardExt.chartLoading")})]})]})})})]})]})}export{O as default};

View File

@ -1 +0,0 @@
import{r as u,h as p,j as e,L as f,a as o,T as j,B as l,i as w,k as y,s as g}from"./index-DIy3NosD.js";import{F as s}from"./index-C8zAgrXu.js";import{C as h}from"./index-JMVQVIid.js";import{I as a}from"./index-ChEzJj4c.js";import"./useForm-Ee9FUUVh.js";import"./row-BFbQ_b2k.js";const{Title:P,Text:b}=j;function F(){const[d,t]=u.useState(!1),c=p(),[m]=s.useForm(),n=()=>{localStorage.clear(),sessionStorage.clear(),c("/login")},x=async r=>{t(!0);try{await y({oldPassword:r.oldPassword,newPassword:r.newPassword}),g.success("密码已更新,请重新登录"),n()}finally{t(!1)}};return e.jsx(f,{style:{minHeight:"100vh",background:"#f0f2f5",display:"flex",alignItems:"center",justifyContent:"center"},children:e.jsxs(h,{style:{width:420,borderRadius:8,boxShadow:"0 4px 12px rgba(0,0,0,0.1)"},children:[e.jsxs("div",{className:"text-center mb-6",children:[e.jsx(o,{style:{fontSize:40,color:"#1890ff"}}),e.jsx(P,{level:3,style:{marginTop:16},children:"首次登录请修改密码"}),e.jsx(b,{type:"secondary",children:"当前账号被要求更新初始密码,提交成功后会跳转到登录页。"})]}),e.jsxs(s,{form:m,layout:"vertical",onFinish:x,children:[e.jsx(s.Item,{label:"当前密码",name:"oldPassword",rules:[{required:!0,message:"请输入当前密码"}],children:e.jsx(a.Password,{prefix:e.jsx(o,{})})}),e.jsx(s.Item,{label:"新密码",name:"newPassword",rules:[{required:!0,min:6,message:"新密码至少 6 位"}],children:e.jsx(a.Password,{prefix:e.jsx(o,{})})}),e.jsx(s.Item,{label:"确认新密码",name:"confirmPassword",dependencies:["newPassword"],rules:[{required:!0,message:"请再次输入新密码"},({getFieldValue:r})=>({validator(T,i){return!i||r("newPassword")===i?Promise.resolve():Promise.reject(new Error("两次输入的新密码不一致"))}})],children:e.jsx(a.Password,{prefix:e.jsx(o,{})})}),e.jsx(l,{type:"primary",htmlType:"submit",block:!0,size:"large",loading:d,style:{marginTop:8},children:"提交并重新登录"}),e.jsx(l,{type:"link",block:!0,icon:e.jsx(w,{}),onClick:n,style:{marginTop:8},children:"退出登录"})]})]})})}export{F as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,7 +0,0 @@
import{r as t,aW as I,bv as j,aS as L,ao as M,bc as v,aX as N,aV as y,am as T,bM as x,ar as H,aq as O,at as u,as as S,e2 as W}from"./index-DIy3NosD.js";var q=["prefixCls","className","style","checked","disabled","defaultChecked","type","title","onChange"],G=t.forwardRef(function(e,r){var a=e.prefixCls,i=a===void 0?"rc-checkbox":a,b=e.className,l=e.style,w=e.checked,s=e.disabled,p=e.defaultChecked,R=p===void 0?!1:p,h=e.type,f=h===void 0?"checkbox":h,P=e.title,d=e.onChange,_=I(e,q),c=t.useRef(null),g=t.useRef(null),D=j(R,{value:w}),$=L(D,2),C=$[0],z=$[1];t.useImperativeHandle(r,function(){return{focus:function(o){var n;(n=c.current)===null||n===void 0||n.focus(o)},blur:function(){var o;(o=c.current)===null||o===void 0||o.blur()},input:c.current,nativeElement:g.current}});var B=M(i,b,v(v({},"".concat(i,"-checked"),C),"".concat(i,"-disabled"),s)),E=function(o){s||("checked"in e||z(o.target.checked),d==null||d({target:y(y({},e),{},{type:f,checked:o.target.checked}),stopPropagation:function(){o.stopPropagation()},preventDefault:function(){o.preventDefault()},nativeEvent:o.nativeEvent}))};return t.createElement("span",{className:B,title:P,style:l,ref:g},t.createElement("input",N({},_,{className:"".concat(i,"-input"),ref:c,onChange:E,disabled:s,checked:!!C,type:f})),t.createElement("span",{className:"".concat(i,"-inner")}))});function V(e){const r=T.useRef(null),a=()=>{x.cancel(r.current),r.current=null};return[()=>{a(),r.current=x(()=>{r.current=null})},l=>{r.current&&(l.stopPropagation(),a()),e==null||e(l)}]}const F=e=>{const{checkboxCls:r}=e,a=`${r}-wrapper`;return[{[`${r}-group`]:Object.assign(Object.assign({},u(e)),{display:"inline-flex",flexWrap:"wrap",columnGap:e.marginXS,[`> ${e.antCls}-row`]:{flex:1}}),[a]:Object.assign(Object.assign({},u(e)),{display:"inline-flex",alignItems:"baseline",cursor:"pointer","&:after":{display:"inline-block",width:0,overflow:"hidden",content:"'\\a0'"},[`& + ${a}`]:{marginInlineStart:0},[`&${a}-in-form-item`]:{'input[type="checkbox"]':{width:14,height:14}}}),[r]:Object.assign(Object.assign({},u(e)),{position:"relative",whiteSpace:"nowrap",lineHeight:1,cursor:"pointer",borderRadius:e.borderRadiusSM,alignSelf:"center",[`${r}-input`]:{position:"absolute",inset:0,zIndex:1,cursor:"pointer",opacity:0,margin:0,[`&:focus-visible + ${r}-inner`]:W(e)},[`${r}-inner`]:{boxSizing:"border-box",display:"block",width:e.checkboxSize,height:e.checkboxSize,direction:"ltr",backgroundColor:e.colorBgContainer,border:`${S(e.lineWidth)} ${e.lineType} ${e.colorBorder}`,borderRadius:e.borderRadiusSM,borderCollapse:"separate",transition:`all ${e.motionDurationSlow}`,"&:after":{boxSizing:"border-box",position:"absolute",top:"50%",insetInlineStart:"25%",display:"table",width:e.calc(e.checkboxSize).div(14).mul(5).equal(),height:e.calc(e.checkboxSize).div(14).mul(8).equal(),border:`${S(e.lineWidthBold)} solid ${e.colorWhite}`,borderTop:0,borderInlineStart:0,transform:"rotate(45deg) scale(0) translate(-50%,-50%)",opacity:0,content:'""',transition:`all ${e.motionDurationFast} ${e.motionEaseInBack}, opacity ${e.motionDurationFast}`}},"& + span":{paddingInlineStart:e.paddingXS,paddingInlineEnd:e.paddingXS}})},{[`
${a}:not(${a}-disabled),
${r}:not(${r}-disabled)
`]:{[`&:hover ${r}-inner`]:{borderColor:e.colorPrimary}},[`${a}:not(${a}-disabled)`]:{[`&:hover ${r}-checked:not(${r}-disabled) ${r}-inner`]:{backgroundColor:e.colorPrimaryHover,borderColor:"transparent"},[`&:hover ${r}-checked:not(${r}-disabled):after`]:{borderColor:e.colorPrimaryHover}}},{[`${r}-checked`]:{[`${r}-inner`]:{backgroundColor:e.colorPrimary,borderColor:e.colorPrimary,"&:after":{opacity:1,transform:"rotate(45deg) scale(1) translate(-50%,-50%)",transition:`all ${e.motionDurationMid} ${e.motionEaseOutBack} ${e.motionDurationFast}`}}},[`
${a}-checked:not(${a}-disabled),
${r}-checked:not(${r}-disabled)
`]:{[`&:hover ${r}-inner`]:{backgroundColor:e.colorPrimaryHover,borderColor:"transparent"}}},{[r]:{"&-indeterminate":{"&":{[`${r}-inner`]:{backgroundColor:`${e.colorBgContainer}`,borderColor:`${e.colorBorder}`,"&:after":{top:"50%",insetInlineStart:"50%",width:e.calc(e.fontSizeLG).div(2).equal(),height:e.calc(e.fontSizeLG).div(2).equal(),backgroundColor:e.colorPrimary,border:0,transform:"translate(-50%, -50%) scale(1)",opacity:1,content:'""'}},[`&:hover ${r}-inner`]:{backgroundColor:`${e.colorBgContainer}`,borderColor:`${e.colorPrimary}`}}}}},{[`${a}-disabled`]:{cursor:"not-allowed"},[`${r}-disabled`]:{[`&, ${r}-input`]:{cursor:"not-allowed",pointerEvents:"none"},[`${r}-inner`]:{background:e.colorBgContainerDisabled,borderColor:e.colorBorder,"&:after":{borderColor:e.colorTextDisabled}},"&:after":{display:"none"},"& + span":{color:e.colorTextDisabled},[`&${r}-indeterminate ${r}-inner::after`]:{background:e.colorTextDisabled}}}]};function X(e,r){const a=H(r,{checkboxCls:`.${e}`,checkboxSize:r.controlInteractiveSize});return F(a)}const A=O("Checkbox",(e,{prefixCls:r})=>[X(r,e)]);export{G as C,V as a,X as g,A as u};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
import{am as X,r as a,an as A,cf as Z,bu as J,d0 as Q,ce as B,ao as w,d$ as U,cZ as Y,a$ as ee,ax as z}from"./index-DIy3NosD.js";import{u as D,a as te,C as se}from"./index-CUzdQnnk.js";const T=X.createContext(null);var ae=function(l,u){var o={};for(var t in l)Object.prototype.hasOwnProperty.call(l,t)&&u.indexOf(t)<0&&(o[t]=l[t]);if(l!=null&&typeof Object.getOwnPropertySymbols=="function")for(var n=0,t=Object.getOwnPropertySymbols(l);n<t.length;n++)u.indexOf(t[n])<0&&Object.prototype.propertyIsEnumerable.call(l,t[n])&&(o[t[n]]=l[t[n]]);return o};const le=(l,u)=>{var o;const{prefixCls:t,className:n,rootClassName:$,children:x,indeterminate:h=!1,style:j,onMouseEnter:y,onMouseLeave:c,skipGroup:O=!1,disabled:I}=l,r=ae(l,["prefixCls","className","rootClassName","children","indeterminate","style","onMouseEnter","onMouseLeave","skipGroup","disabled"]),{getPrefixCls:k,direction:_,checkbox:d}=a.useContext(A),s=a.useContext(T),{isFormItemInput:E}=a.useContext(Z),N=a.useContext(J),g=(o=(s==null?void 0:s.disabled)||I)!==null&&o!==void 0?o:N,m=a.useRef(r.value),p=a.useRef(null),V=Q(u,p);a.useEffect(()=>{s==null||s.registerValue(r.value)},[]),a.useEffect(()=>{if(!O)return r.value!==m.current&&(s==null||s.cancelValue(m.current),s==null||s.registerValue(r.value),m.current=r.value),()=>s==null?void 0:s.cancelValue(r.value)},[r.value]),a.useEffect(()=>{var f;!((f=p.current)===null||f===void 0)&&f.input&&(p.current.input.indeterminate=h)},[h]);const i=k("checkbox",t),S=B(i),[R,P,G]=D(i,S),v=Object.assign({},r);s&&!O&&(v.onChange=(...f)=>{r.onChange&&r.onChange.apply(r,f),s.toggleOption&&s.toggleOption({label:x,value:r.value})},v.name=s.name,v.checked=s.value.includes(r.value));const M=w(`${i}-wrapper`,{[`${i}-rtl`]:_==="rtl",[`${i}-wrapper-checked`]:v.checked,[`${i}-wrapper-disabled`]:g,[`${i}-wrapper-in-form-item`]:E},d==null?void 0:d.className,n,$,G,S,P),e=w({[`${i}-indeterminate`]:h},U,P),[C,b]=te(v.onClick);return R(a.createElement(Y,{component:"Checkbox",disabled:g},a.createElement("label",{className:M,style:Object.assign(Object.assign({},d==null?void 0:d.style),j),onMouseEnter:y,onMouseLeave:c,onClick:C},a.createElement(se,Object.assign({},v,{onClick:b,prefixCls:i,className:e,disabled:g,ref:V})),x!=null&&a.createElement("span",{className:`${i}-label`},x))))},q=a.forwardRef(le);var ne=function(l,u){var o={};for(var t in l)Object.prototype.hasOwnProperty.call(l,t)&&u.indexOf(t)<0&&(o[t]=l[t]);if(l!=null&&typeof Object.getOwnPropertySymbols=="function")for(var n=0,t=Object.getOwnPropertySymbols(l);n<t.length;n++)u.indexOf(t[n])<0&&Object.prototype.propertyIsEnumerable.call(l,t[n])&&(o[t[n]]=l[t[n]]);return o};const re=a.forwardRef((l,u)=>{const{defaultValue:o,children:t,options:n=[],prefixCls:$,className:x,rootClassName:h,style:j,onChange:y}=l,c=ne(l,["defaultValue","children","options","prefixCls","className","rootClassName","style","onChange"]),{getPrefixCls:O,direction:I}=a.useContext(A),[r,k]=a.useState(c.value||o||[]),[_,d]=a.useState([]);a.useEffect(()=>{"value"in c&&k(c.value||[])},[c.value]);const s=a.useMemo(()=>n.map(e=>typeof e=="string"||typeof e=="number"?{label:e,value:e}:e),[n]),E=e=>{d(C=>C.filter(b=>b!==e))},N=e=>{d(C=>[].concat(z(C),[e]))},g=e=>{const C=r.indexOf(e.value),b=z(r);C===-1?b.push(e.value):b.splice(C,1),"value"in c||k(b),y==null||y(b.filter(f=>_.includes(f)).sort((f,H)=>{const K=s.findIndex(L=>L.value===f),W=s.findIndex(L=>L.value===H);return K-W}))},m=O("checkbox",$),p=`${m}-group`,V=B(m),[i,S,R]=D(m,V),P=ee(c,["value","disabled"]),G=n.length?s.map(e=>a.createElement(q,{prefixCls:m,key:e.value.toString(),disabled:"disabled"in e?e.disabled:c.disabled,value:e.value,checked:r.includes(e.value),onChange:e.onChange,className:w(`${p}-item`,e.className),style:e.style,title:e.title,id:e.id,required:e.required},e.label)):t,v=a.useMemo(()=>({toggleOption:g,value:r,disabled:c.disabled,name:c.name,registerValue:N,cancelValue:E}),[g,r,c.disabled,c.name,N,E]),M=w(p,{[`${p}-rtl`]:I==="rtl"},x,h,R,V,S);return i(a.createElement("div",Object.assign({className:M,style:j},P,{ref:u}),a.createElement(T.Provider,{value:v},G)))}),F=q;F.Group=re;F.__ANT_CHECKBOX=!0;export{F as C};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
import{u as M,r as t,j as e,B as $,x as F,H,z as c,R as v,T as K,J as P,a9 as W,_ as Y,Z as D,a0 as G,s as w,a4 as J}from"./index-DIy3NosD.js";import{P as Z}from"./index-x0_qMNAh.js";import{R as N,C as d}from"./row-BFbQ_b2k.js";import{C as b}from"./index-JMVQVIid.js";import{I as q}from"./index-ChEzJj4c.js";import{F as A}from"./Table-C0wBOmw3.js";import{T as u}from"./index-Do3ElwMr.js";import{C as S}from"./index-D0LR2CrS.js";import"./iconUtil-DNX53dK0.js";import"./index-CUzdQnnk.js";import"./useForm-Ee9FUUVh.js";import"./Pagination-C_3XJ-9Y.js";const{Text:C}=K;function ne(){const{t:a}=M(),[o,I]=t.useState([]),[h,U]=t.useState([]),[T,m]=t.useState(!1),[p,x]=t.useState(!1),[f,g]=t.useState(!1),[l,y]=t.useState(null),[j,i]=t.useState([]),[n,_]=t.useState(""),R=t.useMemo(()=>o.find(s=>s.userId===l)||null,[o,l]),z=async()=>{m(!0);try{const s=await Y();I(s||[])}finally{m(!1)}},k=async()=>{x(!0);try{const s=await D();U(s||[])}finally{x(!1)}},E=async s=>{try{const r=await G(s);i(r||[])}catch{i([])}};t.useEffect(()=>{z(),k()},[]),t.useEffect(()=>{l?E(l):i([])},[l]);const L=t.useMemo(()=>{if(!n)return o;const s=n.toLowerCase();return o.filter(r=>r.username.toLowerCase().includes(s)||r.displayName.toLowerCase().includes(s))},[o,n]),B=async()=>{if(!l){w.warning(a("userRole.selectUser"));return}g(!0);try{await J(l,j),w.success(a("common.success"))}finally{g(!1)}};return e.jsxs("div",{className:"app-page",children:[e.jsx(Z,{title:a("userRole.title"),subtitle:a("userRole.subtitle")}),e.jsx("div",{className:"app-page__page-actions",children:e.jsx($,{type:"primary",icon:e.jsx(F,{"aria-hidden":"true"}),onClick:B,loading:f,disabled:!l,children:a(f?"common.loading":"common.save")})}),e.jsxs(N,{gutter:24,className:"app-page__split",style:{height:"calc(100vh - 180px)"},children:[e.jsx(d,{xs:24,lg:12,style:{height:"100%"},children:e.jsxs(b,{title:e.jsxs(c,{children:[e.jsx(v,{"aria-hidden":"true"}),e.jsx("span",{children:a("userRole.userList")})]}),className:"app-page__panel-card full-height-card",children:[e.jsx("div",{className:"mb-4",children:e.jsx(q,{placeholder:a("userRole.searchUser"),prefix:e.jsx(H,{"aria-hidden":"true",className:"text-gray-400"}),value:n,onChange:s=>_(s.target.value),allowClear:!0,"aria-label":a("userRole.searchUser")})}),e.jsx("div",{style:{height:"calc(100% - 60px)",overflowY:"auto"},children:e.jsx(A,{rowKey:"userId",size:"middle",loading:T,dataSource:L,rowSelection:{type:"radio",selectedRowKeys:l?[l]:[],onChange:s=>y(s[0])},onRow:s=>({onClick:()=>y(s.userId),className:"cursor-pointer"}),pagination:{pageSize:10,showTotal:s=>a("common.total",{total:s})},columns:[{title:a("users.userInfo"),key:"user",render:(s,r)=>e.jsxs("div",{className:"min-w-0",children:[e.jsx("div",{style:{fontWeight:500},className:"truncate",children:r.displayName}),e.jsxs("div",{style:{fontSize:12,color:"#8c8c8c"},className:"truncate",children:["@",r.username]})]})},{title:a("common.status"),dataIndex:"status",width:80,render:s=>s===1?e.jsx(u,{color:"green",className:"m-0",children:"Enabled"}):e.jsx(u,{className:"m-0",children:"Disabled"})}]})})]})}),e.jsx(d,{xs:24,lg:12,style:{height:"100%"},children:e.jsx(b,{title:e.jsxs(c,{children:[e.jsx(W,{"aria-hidden":"true"}),e.jsx("span",{children:a("userRole.grantRoles")})]}),className:"app-page__panel-card full-height-card",extra:R?e.jsxs(u,{color:"blue",children:[a("userRole.editing"),": ",R.displayName]}):null,children:l?e.jsxs("div",{style:{padding:"8px 0",height:"100%",overflowY:"auto"},children:[e.jsx(S.Group,{style:{width:"100%"},value:j,onChange:s=>i(s),disabled:p,children:e.jsx(N,{gutter:[16,16],children:h.map(s=>e.jsx(d,{span:12,children:e.jsx(S,{value:s.roleId,className:"w-full",children:e.jsxs(c,{direction:"vertical",size:0,children:[e.jsx("span",{style:{fontWeight:500},children:s.roleName}),e.jsx(C,{type:"secondary",style:{fontSize:12},className:"tabular-nums",children:s.roleCode})]})})},s.roleId))})}),!h.length&&!p&&e.jsx(P,{description:"No roles available"})]}):e.jsxs("div",{className:"flex flex-col items-center justify-center py-20 bg-gray-50 rounded-lg border border-dashed border-gray-200",children:[e.jsx(v,{style:{fontSize:40,color:"#bfbfbf",marginBottom:16},"aria-hidden":"true"}),e.jsx(C,{type:"secondary",children:a("userRole.selectUser")})]})})})]})]})}export{ne as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
import{j as e,T as d}from"./index-DIy3NosD.js";const{Title:i,Text:n}=d,c=({title:r,subtitle:s,extra:a,className:l=""})=>e.jsxs("div",{className:`page-header flex justify-between items-end mb-6 ${l}`,children:[e.jsxs("div",{children:[e.jsx(i,{level:4,className:"mb-1",style:{margin:0},children:r}),s&&e.jsx(n,{type:"secondary",style:{display:"block"},children:s})]}),a&&e.jsx("div",{className:"page-header-extra",children:a})]});export{c as P};

View File

@ -1 +0,0 @@
import{aq as A,r as s,d2 as $,an as w,bw as F,bx as D,db as E,B as M,dc as U,dd as Y,de as G,ao as S,au as J,bv as K,df as Q,a$ as Z}from"./index-DIy3NosD.js";const ee=e=>{const{componentCls:n,iconCls:a,antCls:t,zIndexPopup:o,colorText:u,colorWarning:f,marginXXS:c,marginXS:i,fontSize:g,fontWeightStrong:v,colorTextHeading:y}=e;return{[n]:{zIndex:o,[`&${t}-popover`]:{fontSize:g},[`${n}-message`]:{marginBottom:i,display:"flex",flexWrap:"nowrap",alignItems:"start",[`> ${n}-message-icon ${a}`]:{color:f,fontSize:g,lineHeight:1,marginInlineEnd:i},[`${n}-title`]:{fontWeight:v,color:y,"&:only-child":{fontWeight:"normal"}},[`${n}-description`]:{marginTop:c,color:u}},[`${n}-buttons`]:{textAlign:"end",whiteSpace:"nowrap",button:{marginInlineStart:i}}}}},te=e=>{const{zIndexPopupBase:n}=e;return{zIndexPopup:n+60}},I=A("Popconfirm",e=>ee(e),te,{resetStyle:!1});var ne=function(e,n){var a={};for(var t in e)Object.prototype.hasOwnProperty.call(e,t)&&n.indexOf(t)<0&&(a[t]=e[t]);if(e!=null&&typeof Object.getOwnPropertySymbols=="function")for(var o=0,t=Object.getOwnPropertySymbols(e);o<t.length;o++)n.indexOf(t[o])<0&&Object.prototype.propertyIsEnumerable.call(e,t[o])&&(a[t[o]]=e[t[o]]);return a};const k=e=>{const{prefixCls:n,okButtonProps:a,cancelButtonProps:t,title:o,description:u,cancelText:f,okText:c,okType:i="primary",icon:g=s.createElement($,null),showCancel:v=!0,close:y,onConfirm:C,onCancel:O,onPopupClick:m}=e,{getPrefixCls:d}=s.useContext(w),[p]=F("Popconfirm",D.Popconfirm),b=E(o),x=E(u);return s.createElement("div",{className:`${n}-inner-content`,onClick:m},s.createElement("div",{className:`${n}-message`},g&&s.createElement("span",{className:`${n}-message-icon`},g),s.createElement("div",{className:`${n}-message-text`},b&&s.createElement("div",{className:`${n}-title`},b),x&&s.createElement("div",{className:`${n}-description`},x))),s.createElement("div",{className:`${n}-buttons`},v&&s.createElement(M,Object.assign({onClick:O,size:"small"},t),f||(p==null?void 0:p.cancelText)),s.createElement(U,{buttonProps:Object.assign(Object.assign({size:"small"},Y(i)),a),actionFn:C,close:y,prefixCls:d("btn"),quitOnNullishReturnValue:!0,emitEvent:!0},c||(p==null?void 0:p.okText))))},oe=e=>{const{prefixCls:n,placement:a,className:t,style:o}=e,u=ne(e,["prefixCls","placement","className","style"]),{getPrefixCls:f}=s.useContext(w),c=f("popconfirm",n),[i]=I(c);return i(s.createElement(G,{placement:a,className:S(c,t),style:o,content:s.createElement(k,Object.assign({prefixCls:c},u))}))};var se=function(e,n){var a={};for(var t in e)Object.prototype.hasOwnProperty.call(e,t)&&n.indexOf(t)<0&&(a[t]=e[t]);if(e!=null&&typeof Object.getOwnPropertySymbols=="function")for(var o=0,t=Object.getOwnPropertySymbols(e);o<t.length;o++)n.indexOf(t[o])<0&&Object.prototype.propertyIsEnumerable.call(e,t[o])&&(a[t[o]]=e[t[o]]);return a};const ae=s.forwardRef((e,n)=>{var a,t;const{prefixCls:o,placement:u="top",trigger:f="click",okType:c="primary",icon:i=s.createElement($,null),children:g,overlayClassName:v,onOpenChange:y,onVisibleChange:C,overlayStyle:O,styles:m,classNames:d}=e,p=se(e,["prefixCls","placement","trigger","okType","icon","children","overlayClassName","onOpenChange","onVisibleChange","overlayStyle","styles","classNames"]),{getPrefixCls:b,className:x,style:T,classNames:j,styles:h}=J("popconfirm"),[_,z]=K(!1,{value:(a=e.open)!==null&&a!==void 0?a:e.visible,defaultValue:(t=e.defaultOpen)!==null&&t!==void 0?t:e.defaultVisible}),P=(l,r)=>{z(l,!0),C==null||C(l),y==null||y(l,r)},B=l=>{P(!1,l)},V=l=>{var r;return(r=e.onConfirm)===null||r===void 0?void 0:r.call(void 0,l)},W=l=>{var r;P(!1,l),(r=e.onCancel)===null||r===void 0||r.call(void 0,l)},R=(l,r)=>{const{disabled:q=!1}=e;q||P(l,r)},N=b("popconfirm",o),H=S(N,x,v,j.root,d==null?void 0:d.root),L=S(j.body,d==null?void 0:d.body),[X]=I(N);return X(s.createElement(Q,Object.assign({},Z(p,["title"]),{trigger:f,placement:u,onOpenChange:R,open:_,ref:n,classNames:{root:H,body:L},styles:{root:Object.assign(Object.assign(Object.assign(Object.assign({},h.root),T),O),m==null?void 0:m.root),body:Object.assign(Object.assign({},h.body),m==null?void 0:m.body)},content:s.createElement(k,Object.assign({okType:c,icon:i},e,{prefixCls:N,close:B,onConfirm:V,onCancel:W})),"data-popover-inject":!0}),g))}),le=ae;le._InternalPanelDoNotUseOrYouWillBeFired=oe;export{le as P};

View File

@ -1 +0,0 @@
import{aC as t}from"./index-DIy3NosD.js";async function n(a){return(await t.get("/sys/api/orgs",{params:{tenantId:a}})).data.data}async function p(a){return(await t.post("/sys/api/orgs",a)).data.data}async function o(a,s){return(await t.put(`/sys/api/orgs/${a}`,s)).data.data}async function c(a){return(await t.delete(`/sys/api/orgs/${a}`)).data.data}export{p as c,c as d,n as l,o as u};

View File

@ -1 +0,0 @@
import{cm as r}from"./index-DIy3NosD.js";const s=(t,o,a,e)=>({total:t,current:o,pageSize:a,onChange:e,showSizeChanger:!0,showQuickJumper:!0,showTotal:n=>r.t("common.total",{total:n}),pageSizeOptions:["10","20","50","100"]});export{s as g};

View File

@ -1 +0,0 @@
import{r as f,an as A,ea as I,ao as G,az as S,ay as _,eb as z}from"./index-DIy3NosD.js";const k=f.createContext({});var J=function(e,l){var n={};for(var t in e)Object.prototype.hasOwnProperty.call(e,t)&&l.indexOf(t)<0&&(n[t]=e[t]);if(e!=null&&typeof Object.getOwnPropertySymbols=="function")for(var r=0,t=Object.getOwnPropertySymbols(e);r<t.length;r++)l.indexOf(t[r])<0&&Object.prototype.propertyIsEnumerable.call(e,t[r])&&(n[t[r]]=e[t[r]]);return n};function R(e){return e==="auto"?"1 1 auto":typeof e=="number"?`${e} ${e} auto`:/^\d+(\.\d+)?(px|em|rem|%)$/.test(e)?`0 0 ${e}`:e}const M=["xs","sm","md","lg","xl","xxl"],F=f.forwardRef((e,l)=>{const{getPrefixCls:n,direction:t}=f.useContext(A),{gutter:r,wrap:c}=f.useContext(k),{prefixCls:p,span:i,order:g,offset:m,push:h,pull:O,className:E,children:b,flex:x,style:C}=e,d=J(e,["prefixCls","span","order","offset","push","pull","className","children","flex","style"]),o=n("col",p),[N,P,y]=I(o),j={};let $={};M.forEach(a=>{let s={};const v=e[a];typeof v=="number"?s.span=v:typeof v=="object"&&(s=v||{}),delete d[a],$=Object.assign(Object.assign({},$),{[`${o}-${a}-${s.span}`]:s.span!==void 0,[`${o}-${a}-order-${s.order}`]:s.order||s.order===0,[`${o}-${a}-offset-${s.offset}`]:s.offset||s.offset===0,[`${o}-${a}-push-${s.push}`]:s.push||s.push===0,[`${o}-${a}-pull-${s.pull}`]:s.pull||s.pull===0,[`${o}-rtl`]:t==="rtl"}),s.flex&&($[`${o}-${a}-flex`]=!0,j[`--${o}-${a}-flex`]=R(s.flex))});const w=G(o,{[`${o}-${i}`]:i!==void 0,[`${o}-order-${g}`]:g,[`${o}-offset-${m}`]:m,[`${o}-push-${h}`]:h,[`${o}-pull-${O}`]:O},E,$,P,y),u={};if(r!=null&&r[0]){const a=typeof r[0]=="number"?`${r[0]/2}px`:`calc(${r[0]} / 2)`;u.paddingLeft=a,u.paddingRight=a}return x&&(u.flex=R(x),c===!1&&!u.minWidth&&(u.minWidth=0)),N(f.createElement("div",Object.assign({},d,{style:Object.assign(Object.assign(Object.assign({},u),C),j),className:w,ref:l}),b))});function B(e,l){const n=[void 0,void 0],t=Array.isArray(e)?e:[e,void 0],r=l||{xs:!0,sm:!0,md:!0,lg:!0,xl:!0,xxl:!0};return t.forEach((c,p)=>{if(typeof c=="object"&&c!==null)for(let i=0;i<S.length;i++){const g=S[i];if(r[g]&&c[g]!==void 0){n[p]=c[g];break}}else n[p]=c}),n}var L=function(e,l){var n={};for(var t in e)Object.prototype.hasOwnProperty.call(e,t)&&l.indexOf(t)<0&&(n[t]=e[t]);if(e!=null&&typeof Object.getOwnPropertySymbols=="function")for(var r=0,t=Object.getOwnPropertySymbols(e);r<t.length;r++)l.indexOf(t[r])<0&&Object.prototype.propertyIsEnumerable.call(e,t[r])&&(n[t[r]]=e[t[r]]);return n};function V(e,l){const[n,t]=f.useState(typeof e=="string"?e:""),r=()=>{if(typeof e=="string"&&t(e),typeof e=="object")for(let c=0;c<S.length;c++){const p=S[c];if(!l||!l[p])continue;const i=e[p];if(i!==void 0){t(i);return}}};return f.useEffect(()=>{r()},[JSON.stringify(e),l]),n}const H=f.forwardRef((e,l)=>{const{prefixCls:n,justify:t,align:r,className:c,style:p,children:i,gutter:g=0,wrap:m}=e,h=L(e,["prefixCls","justify","align","className","style","children","gutter","wrap"]),{getPrefixCls:O,direction:E}=f.useContext(A),b=_(!0,null),x=V(r,b),C=V(t,b),d=O("row",n),[o,N,P]=z(d),y=B(g,b),j=G(d,{[`${d}-no-wrap`]:m===!1,[`${d}-${C}`]:C,[`${d}-${x}`]:x,[`${d}-rtl`]:E==="rtl"},c,N,P),$={};if(y!=null&&y[0]){const s=typeof y[0]=="number"?`${y[0]/-2}px`:`calc(${y[0]} / -2)`;$.marginLeft=s,$.marginRight=s}const[w,u]=y;$.rowGap=u;const a=f.useMemo(()=>({gutter:[w,u],wrap:m}),[w,u,m]);return o(f.createElement(k.Provider,{value:a},f.createElement("div",Object.assign({},h,{className:j,style:Object.assign(Object.assign({},$),p),ref:l}),i)))});export{F as C,H as R};

View File

@ -1 +0,0 @@
import{aC as n}from"./index-DIy3NosD.js";async function r(a){return(await n.get("/sys/api/tenants",{params:a})).data.data}async function p(a){return(await n.post("/sys/api/tenants",a)).data.data}async function c(a,t){return(await n.put(`/sys/api/tenants/${a}`,t)).data.data}async function i(a){return(await n.delete(`/sys/api/tenants/${a}`)).data.data}export{p as c,i as d,r as l,c as u};

View File

@ -1 +0,0 @@
import{aC as s,r}from"./index-DIy3NosD.js";async function f(t){return(await s.get("/sys/api/dict-types",{params:t})).data.data}async function l(t){return(await s.post("/sys/api/dict-types",t)).data.data}async function m(t,a){return(await s.put(`/sys/api/dict-types/${t}`,a)).data.data}async function w(t){return(await s.delete(`/sys/api/dict-types/${t}`)).data.data}async function D(t){return(await s.get("/sys/api/dict-items",{params:{typeCode:t}})).data.data}async function h(t){return(await s.post("/sys/api/dict-items",t)).data.data}async function g(t,a){return(await s.put(`/sys/api/dict-items/${t}`,a)).data.data}async function I(t){return(await s.delete(`/sys/api/dict-items/${t}`)).data.data}async function o(t){return(await s.get(`/sys/api/dict-items/type/${t}`)).data.data}const e={};function $(t){const[a,c]=r.useState(e[t]||[]),[d,p]=r.useState(!e[t]);return r.useEffect(()=>{if(e[t]){c(e[t]),p(!1);return}let n=!0;return(async()=>{try{const i=await o(t);n&&(e[t]=i,c(i))}catch(i){console.error(`Failed to fetch dictionary ${t}:`,i)}finally{n&&p(!1)}})(),()=>{n=!1}},[t]),{items:a,loading:d}}export{D as a,m as b,l as c,w as d,I as e,f,g,h,$ as u};

File diff suppressed because one or more lines are too long

View File

@ -5,7 +5,7 @@
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>UnisBase - 智能会议系统</title>
<script type="module" crossorigin src="/assets/index-DIy3NosD.js"></script>
<script type="module" crossorigin src="/assets/index-DdTU1GmP.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CaWPk49l.css">
</head>
<body>

View File

@ -2293,6 +2293,15 @@
"yallist": "^3.0.2"
}
},
"node_modules/lucide-react": {
"version": "0.546.0",
"resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.546.0.tgz",
"integrity": "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/make-dir": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/make-dir/-/make-dir-2.1.0.tgz",

View File

@ -51,7 +51,7 @@ import {
ZoomInOutlined_default,
ZoomOutOutlined_default,
require_react_is
} from "./chunk-JVSQUNE5.js";
} from "./chunk-NDVMXJDK.js";
import {
require_react
} from "./chunk-RLJ2RCJQ.js";

View File

@ -1,97 +1,106 @@
{
"hash": "f92a4912",
"hash": "c73f5e08",
"configHash": "dbaa87de",
"lockfileHash": "6f14f382",
"browserHash": "f45e5295",
"lockfileHash": "cd02bc2e",
"browserHash": "052213b8",
"optimized": {
"react": {
"src": "../../react/index.js",
"file": "react.js",
"fileHash": "5bb3ee7a",
"fileHash": "2387bc28",
"needsInterop": true
},
"react-dom": {
"src": "../../react-dom/index.js",
"file": "react-dom.js",
"fileHash": "7c2afce6",
"fileHash": "3d2f5ddf",
"needsInterop": true
},
"react/jsx-dev-runtime": {
"src": "../../react/jsx-dev-runtime.js",
"file": "react_jsx-dev-runtime.js",
"fileHash": "84bff04d",
"fileHash": "64ec3b5f",
"needsInterop": true
},
"react/jsx-runtime": {
"src": "../../react/jsx-runtime.js",
"file": "react_jsx-runtime.js",
"fileHash": "da47a32d",
"fileHash": "1969f2a4",
"needsInterop": true
},
"@ant-design/icons": {
"src": "../../@ant-design/icons/es/index.js",
"file": "@ant-design_icons.js",
"fileHash": "09d7af82",
"fileHash": "4e7c5123",
"needsInterop": false
},
"antd": {
"src": "../../antd/es/index.js",
"file": "antd.js",
"fileHash": "d449f085",
"fileHash": "f83c9b2b",
"needsInterop": false
},
"axios": {
"src": "../../axios/index.js",
"file": "axios.js",
"fileHash": "9badde3c",
"fileHash": "33f1c643",
"needsInterop": false
},
"dayjs": {
"src": "../../dayjs/dayjs.min.js",
"file": "dayjs.js",
"fileHash": "8adcd232",
"fileHash": "f75c44a0",
"needsInterop": true
},
"i18next": {
"src": "../../i18next/dist/esm/i18next.js",
"file": "i18next.js",
"fileHash": "8a363fcc",
"fileHash": "771d450b",
"needsInterop": false
},
"i18next-browser-languagedetector": {
"src": "../../i18next-browser-languagedetector/dist/esm/i18nextBrowserLanguageDetector.js",
"file": "i18next-browser-languagedetector.js",
"fileHash": "862136e7",
"fileHash": "861910aa",
"needsInterop": false
},
"react-dom/client": {
"src": "../../react-dom/client.js",
"file": "react-dom_client.js",
"fileHash": "c6f6876d",
"fileHash": "af92414c",
"needsInterop": true
},
"react-i18next": {
"src": "../../react-i18next/dist/es/index.js",
"file": "react-i18next.js",
"fileHash": "ae9fa8c9",
"fileHash": "e4a57ecd",
"needsInterop": false
},
"react-router-dom": {
"src": "../../react-router-dom/dist/index.js",
"file": "react-router-dom.js",
"fileHash": "dd38c78e",
"fileHash": "346068ef",
"needsInterop": false
},
"zustand": {
"src": "../../zustand/esm/index.mjs",
"file": "zustand.js",
"fileHash": "0b8234b0",
"fileHash": "2dc24c20",
"needsInterop": false
},
"lucide-react": {
"src": "../../lucide-react/dist/esm/lucide-react.js",
"file": "lucide-react.js",
"fileHash": "2001bf42",
"needsInterop": false
}
},
"chunks": {
"chunk-JVSQUNE5": {
"file": "chunk-JVSQUNE5.js"
"chunk-IDVUNHDH": {
"file": "chunk-IDVUNHDH.js"
},
"chunk-GL7YRBYQ": {
"file": "chunk-GL7YRBYQ.js"
},
"chunk-NUMECXU6": {
"file": "chunk-NUMECXU6.js"
@ -99,11 +108,8 @@
"chunk-CM2AK5IQ": {
"file": "chunk-CM2AK5IQ.js"
},
"chunk-GL7YRBYQ": {
"file": "chunk-GL7YRBYQ.js"
},
"chunk-IDVUNHDH": {
"file": "chunk-IDVUNHDH.js"
"chunk-NDVMXJDK": {
"file": "chunk-NDVMXJDK.js"
},
"chunk-RLJ2RCJQ": {
"file": "chunk-RLJ2RCJQ.js"

View File

@ -1,4 +1,10 @@
"use client";
import {
require_react_dom
} from "./chunk-NUMECXU6.js";
import {
require_dayjs_min
} from "./chunk-CM2AK5IQ.js";
import {
BarsOutlined_default,
CalendarOutlined_default,
@ -52,13 +58,7 @@ import {
ZoomInOutlined_default,
ZoomOutOutlined_default,
require_react_is
} from "./chunk-JVSQUNE5.js";
import {
require_react_dom
} from "./chunk-NUMECXU6.js";
import {
require_dayjs_min
} from "./chunk-CM2AK5IQ.js";
} from "./chunk-NDVMXJDK.js";
import {
require_react
} from "./chunk-RLJ2RCJQ.js";

View File

@ -1,474 +0,0 @@
import {
__commonJS
} from "./chunk-DC5AMYBS.js";
// node_modules/react-is/cjs/react-is.development.js
var require_react_is_development = __commonJS({
"node_modules/react-is/cjs/react-is.development.js"(exports) {
"use strict";
if (true) {
(function() {
"use strict";
var REACT_ELEMENT_TYPE = Symbol.for("react.element");
var REACT_PORTAL_TYPE = Symbol.for("react.portal");
var REACT_FRAGMENT_TYPE = Symbol.for("react.fragment");
var REACT_STRICT_MODE_TYPE = Symbol.for("react.strict_mode");
var REACT_PROFILER_TYPE = Symbol.for("react.profiler");
var REACT_PROVIDER_TYPE = Symbol.for("react.provider");
var REACT_CONTEXT_TYPE = Symbol.for("react.context");
var REACT_SERVER_CONTEXT_TYPE = Symbol.for("react.server_context");
var REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref");
var REACT_SUSPENSE_TYPE = Symbol.for("react.suspense");
var REACT_SUSPENSE_LIST_TYPE = Symbol.for("react.suspense_list");
var REACT_MEMO_TYPE = Symbol.for("react.memo");
var REACT_LAZY_TYPE = Symbol.for("react.lazy");
var REACT_OFFSCREEN_TYPE = Symbol.for("react.offscreen");
var enableScopeAPI = false;
var enableCacheElement = false;
var enableTransitionTracing = false;
var enableLegacyHidden = false;
var enableDebugTracing = false;
var REACT_MODULE_REFERENCE;
{
REACT_MODULE_REFERENCE = Symbol.for("react.module.reference");
}
function isValidElementType(type) {
if (typeof type === "string" || typeof type === "function") {
return true;
}
if (type === REACT_FRAGMENT_TYPE || type === REACT_PROFILER_TYPE || enableDebugTracing || type === REACT_STRICT_MODE_TYPE || type === REACT_SUSPENSE_TYPE || type === REACT_SUSPENSE_LIST_TYPE || enableLegacyHidden || type === REACT_OFFSCREEN_TYPE || enableScopeAPI || enableCacheElement || enableTransitionTracing) {
return true;
}
if (typeof type === "object" && type !== null) {
if (type.$$typeof === REACT_LAZY_TYPE || type.$$typeof === REACT_MEMO_TYPE || type.$$typeof === REACT_PROVIDER_TYPE || type.$$typeof === REACT_CONTEXT_TYPE || type.$$typeof === REACT_FORWARD_REF_TYPE || // This needs to include all possible module reference object
// types supported by any Flight configuration anywhere since
// we don't know which Flight build this will end up being used
// with.
type.$$typeof === REACT_MODULE_REFERENCE || type.getModuleId !== void 0) {
return true;
}
}
return false;
}
function typeOf(object) {
if (typeof object === "object" && object !== null) {
var $$typeof = object.$$typeof;
switch ($$typeof) {
case REACT_ELEMENT_TYPE:
var type = object.type;
switch (type) {
case REACT_FRAGMENT_TYPE:
case REACT_PROFILER_TYPE:
case REACT_STRICT_MODE_TYPE:
case REACT_SUSPENSE_TYPE:
case REACT_SUSPENSE_LIST_TYPE:
return type;
default:
var $$typeofType = type && type.$$typeof;
switch ($$typeofType) {
case REACT_SERVER_CONTEXT_TYPE:
case REACT_CONTEXT_TYPE:
case REACT_FORWARD_REF_TYPE:
case REACT_LAZY_TYPE:
case REACT_MEMO_TYPE:
case REACT_PROVIDER_TYPE:
return $$typeofType;
default:
return $$typeof;
}
}
case REACT_PORTAL_TYPE:
return $$typeof;
}
}
return void 0;
}
var ContextConsumer = REACT_CONTEXT_TYPE;
var ContextProvider = REACT_PROVIDER_TYPE;
var Element = REACT_ELEMENT_TYPE;
var ForwardRef = REACT_FORWARD_REF_TYPE;
var Fragment = REACT_FRAGMENT_TYPE;
var Lazy = REACT_LAZY_TYPE;
var Memo = REACT_MEMO_TYPE;
var Portal = REACT_PORTAL_TYPE;
var Profiler = REACT_PROFILER_TYPE;
var StrictMode = REACT_STRICT_MODE_TYPE;
var Suspense = REACT_SUSPENSE_TYPE;
var SuspenseList = REACT_SUSPENSE_LIST_TYPE;
var hasWarnedAboutDeprecatedIsAsyncMode = false;
var hasWarnedAboutDeprecatedIsConcurrentMode = false;
function isAsyncMode(object) {
{
if (!hasWarnedAboutDeprecatedIsAsyncMode) {
hasWarnedAboutDeprecatedIsAsyncMode = true;
console["warn"]("The ReactIs.isAsyncMode() alias has been deprecated, and will be removed in React 18+.");
}
}
return false;
}
function isConcurrentMode(object) {
{
if (!hasWarnedAboutDeprecatedIsConcurrentMode) {
hasWarnedAboutDeprecatedIsConcurrentMode = true;
console["warn"]("The ReactIs.isConcurrentMode() alias has been deprecated, and will be removed in React 18+.");
}
}
return false;
}
function isContextConsumer(object) {
return typeOf(object) === REACT_CONTEXT_TYPE;
}
function isContextProvider(object) {
return typeOf(object) === REACT_PROVIDER_TYPE;
}
function isElement(object) {
return typeof object === "object" && object !== null && object.$$typeof === REACT_ELEMENT_TYPE;
}
function isForwardRef(object) {
return typeOf(object) === REACT_FORWARD_REF_TYPE;
}
function isFragment(object) {
return typeOf(object) === REACT_FRAGMENT_TYPE;
}
function isLazy(object) {
return typeOf(object) === REACT_LAZY_TYPE;
}
function isMemo(object) {
return typeOf(object) === REACT_MEMO_TYPE;
}
function isPortal(object) {
return typeOf(object) === REACT_PORTAL_TYPE;
}
function isProfiler(object) {
return typeOf(object) === REACT_PROFILER_TYPE;
}
function isStrictMode(object) {
return typeOf(object) === REACT_STRICT_MODE_TYPE;
}
function isSuspense(object) {
return typeOf(object) === REACT_SUSPENSE_TYPE;
}
function isSuspenseList(object) {
return typeOf(object) === REACT_SUSPENSE_LIST_TYPE;
}
exports.ContextConsumer = ContextConsumer;
exports.ContextProvider = ContextProvider;
exports.Element = Element;
exports.ForwardRef = ForwardRef;
exports.Fragment = Fragment;
exports.Lazy = Lazy;
exports.Memo = Memo;
exports.Portal = Portal;
exports.Profiler = Profiler;
exports.StrictMode = StrictMode;
exports.Suspense = Suspense;
exports.SuspenseList = SuspenseList;
exports.isAsyncMode = isAsyncMode;
exports.isConcurrentMode = isConcurrentMode;
exports.isContextConsumer = isContextConsumer;
exports.isContextProvider = isContextProvider;
exports.isElement = isElement;
exports.isForwardRef = isForwardRef;
exports.isFragment = isFragment;
exports.isLazy = isLazy;
exports.isMemo = isMemo;
exports.isPortal = isPortal;
exports.isProfiler = isProfiler;
exports.isStrictMode = isStrictMode;
exports.isSuspense = isSuspense;
exports.isSuspenseList = isSuspenseList;
exports.isValidElementType = isValidElementType;
exports.typeOf = typeOf;
})();
}
}
});
// node_modules/react-is/index.js
var require_react_is = __commonJS({
"node_modules/react-is/index.js"(exports, module) {
"use strict";
if (false) {
module.exports = null;
} else {
module.exports = require_react_is_development();
}
}
});
// node_modules/@ant-design/icons-svg/es/asn/CheckCircleFilled.js
var CheckCircleFilled = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z" } }] }, "name": "check-circle", "theme": "filled" };
var CheckCircleFilled_default = CheckCircleFilled;
// node_modules/@ant-design/icons-svg/es/asn/CloseCircleFilled.js
var CloseCircleFilled = { "icon": { "tag": "svg", "attrs": { "fill-rule": "evenodd", "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm127.98 274.82h-.04l-.08.06L512 466.75 384.14 338.88c-.04-.05-.06-.06-.08-.06a.12.12 0 00-.07 0c-.03 0-.05.01-.09.05l-45.02 45.02a.2.2 0 00-.05.09.12.12 0 000 .07v.02a.27.27 0 00.06.06L466.75 512 338.88 639.86c-.05.04-.06.06-.06.08a.12.12 0 000 .07c0 .03.01.05.05.09l45.02 45.02a.2.2 0 00.09.05.12.12 0 00.07 0c.02 0 .04-.01.08-.05L512 557.25l127.86 127.87c.04.04.06.05.08.05a.12.12 0 00.07 0c.03 0 .05-.01.09-.05l45.02-45.02a.2.2 0 00.05-.09.12.12 0 000-.07v-.02a.27.27 0 00-.05-.06L557.25 512l127.87-127.86c.04-.04.05-.06.05-.08a.12.12 0 000-.07c0-.03-.01-.05-.05-.09l-45.02-45.02a.2.2 0 00-.09-.05.12.12 0 00-.07 0z" } }] }, "name": "close-circle", "theme": "filled" };
var CloseCircleFilled_default = CloseCircleFilled;
// node_modules/@ant-design/icons-svg/es/asn/CloseOutlined.js
var CloseOutlined = { "icon": { "tag": "svg", "attrs": { "fill-rule": "evenodd", "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M799.86 166.31c.02 0 .04.02.08.06l57.69 57.7c.04.03.05.05.06.08a.12.12 0 010 .06c0 .03-.02.05-.06.09L569.93 512l287.7 287.7c.04.04.05.06.06.09a.12.12 0 010 .07c0 .02-.02.04-.06.08l-57.7 57.69c-.03.04-.05.05-.07.06a.12.12 0 01-.07 0c-.03 0-.05-.02-.09-.06L512 569.93l-287.7 287.7c-.04.04-.06.05-.09.06a.12.12 0 01-.07 0c-.02 0-.04-.02-.08-.06l-57.69-57.7c-.04-.03-.05-.05-.06-.07a.12.12 0 010-.07c0-.03.02-.05.06-.09L454.07 512l-287.7-287.7c-.04-.04-.05-.06-.06-.09a.12.12 0 010-.07c0-.02.02-.04.06-.08l57.7-57.69c.03-.04.05-.05.07-.06a.12.12 0 01.07 0c.03 0 .05.02.09.06L512 454.07l287.7-287.7c.04-.04.06-.05.09-.06a.12.12 0 01.07 0z" } }] }, "name": "close", "theme": "outlined" };
var CloseOutlined_default = CloseOutlined;
// node_modules/@ant-design/icons-svg/es/asn/ExclamationCircleFilled.js
var ExclamationCircleFilled = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm-32 232c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V296zm32 440a48.01 48.01 0 010-96 48.01 48.01 0 010 96z" } }] }, "name": "exclamation-circle", "theme": "filled" };
var ExclamationCircleFilled_default = ExclamationCircleFilled;
// node_modules/@ant-design/icons-svg/es/asn/InfoCircleFilled.js
var InfoCircleFilled = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm32 664c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V456c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272zm-32-344a48.01 48.01 0 010-96 48.01 48.01 0 010 96z" } }] }, "name": "info-circle", "theme": "filled" };
var InfoCircleFilled_default = InfoCircleFilled;
// node_modules/@ant-design/icons-svg/es/asn/LoadingOutlined.js
var LoadingOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "0 0 1024 1024", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 00-94.3-139.9 437.71 437.71 0 00-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3.1 19.9-16 36-35.9 36z" } }] }, "name": "loading", "theme": "outlined" };
var LoadingOutlined_default = LoadingOutlined;
// node_modules/@ant-design/icons-svg/es/asn/RightOutlined.js
var RightOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z" } }] }, "name": "right", "theme": "outlined" };
var RightOutlined_default = RightOutlined;
// node_modules/@ant-design/icons-svg/es/asn/CheckOutlined.js
var CheckOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 00-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z" } }] }, "name": "check", "theme": "outlined" };
var CheckOutlined_default = CheckOutlined;
// node_modules/@ant-design/icons-svg/es/asn/DownOutlined.js
var DownOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z" } }] }, "name": "down", "theme": "outlined" };
var DownOutlined_default = DownOutlined;
// node_modules/@ant-design/icons-svg/es/asn/SearchOutlined.js
var SearchOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z" } }] }, "name": "search", "theme": "outlined" };
var SearchOutlined_default = SearchOutlined;
// node_modules/@ant-design/icons-svg/es/asn/VerticalAlignTopOutlined.js
var VerticalAlignTopOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M859.9 168H164.1c-4.5 0-8.1 3.6-8.1 8v60c0 4.4 3.6 8 8.1 8h695.8c4.5 0 8.1-3.6 8.1-8v-60c0-4.4-3.6-8-8.1-8zM518.3 355a8 8 0 00-12.6 0l-112 141.7a7.98 7.98 0 006.3 12.9h73.9V848c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V509.7H624c6.7 0 10.4-7.7 6.3-12.9L518.3 355z" } }] }, "name": "vertical-align-top", "theme": "outlined" };
var VerticalAlignTopOutlined_default = VerticalAlignTopOutlined;
// node_modules/@ant-design/icons-svg/es/asn/LeftOutlined.js
var LeftOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z" } }] }, "name": "left", "theme": "outlined" };
var LeftOutlined_default = LeftOutlined;
// node_modules/@ant-design/icons-svg/es/asn/BarsOutlined.js
var BarsOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "0 0 1024 1024", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M912 192H328c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h584c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 284H328c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h584c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 284H328c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h584c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM104 228a56 56 0 10112 0 56 56 0 10-112 0zm0 284a56 56 0 10112 0 56 56 0 10-112 0zm0 284a56 56 0 10112 0 56 56 0 10-112 0z" } }] }, "name": "bars", "theme": "outlined" };
var BarsOutlined_default = BarsOutlined;
// node_modules/@ant-design/icons-svg/es/asn/EllipsisOutlined.js
var EllipsisOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M176 511a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0z" } }] }, "name": "ellipsis", "theme": "outlined" };
var EllipsisOutlined_default = EllipsisOutlined;
// node_modules/@ant-design/icons-svg/es/asn/PlusOutlined.js
var PlusOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M482 152h60q8 0 8 8v704q0 8-8 8h-60q-8 0-8-8V160q0-8 8-8z" } }, { "tag": "path", "attrs": { "d": "M192 474h672q8 0 8 8v60q0 8-8 8H160q-8 0-8-8v-60q0-8 8-8z" } }] }, "name": "plus", "theme": "outlined" };
var PlusOutlined_default = PlusOutlined;
// node_modules/@ant-design/icons-svg/es/asn/UpOutlined.js
var UpOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M890.5 755.3L537.9 269.2c-12.8-17.6-39-17.6-51.7 0L133.5 755.3A8 8 0 00140 768h75c5.1 0 9.9-2.5 12.9-6.6L512 369.8l284.1 391.6c3 4.1 7.8 6.6 12.9 6.6h75c6.5 0 10.3-7.4 6.5-12.7z" } }] }, "name": "up", "theme": "outlined" };
var UpOutlined_default = UpOutlined;
// node_modules/@ant-design/icons-svg/es/asn/SwapRightOutlined.js
var SwapRightOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "0 0 1024 1024", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M873.1 596.2l-164-208A32 32 0 00684 376h-64.8c-6.7 0-10.4 7.7-6.3 13l144.3 183H152c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h695.9c26.8 0 41.7-30.8 25.2-51.8z" } }] }, "name": "swap-right", "theme": "outlined" };
var SwapRightOutlined_default = SwapRightOutlined;
// node_modules/@ant-design/icons-svg/es/asn/CalendarOutlined.js
var CalendarOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M880 184H712v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H384v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H144c-17.7 0-32 14.3-32 32v664c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V216c0-17.7-14.3-32-32-32zm-40 656H184V460h656v380zM184 392V256h128v48c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-48h256v48c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-48h128v136H184z" } }] }, "name": "calendar", "theme": "outlined" };
var CalendarOutlined_default = CalendarOutlined;
// node_modules/@ant-design/icons-svg/es/asn/ClockCircleOutlined.js
var ClockCircleOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z" } }, { "tag": "path", "attrs": { "d": "M686.7 638.6L544.1 535.5V288c0-4.4-3.6-8-8-8H488c-4.4 0-8 3.6-8 8v275.4c0 2.6 1.2 5 3.3 6.5l165.4 120.6c3.6 2.6 8.6 1.8 11.2-1.7l28.6-39c2.6-3.7 1.8-8.7-1.8-11.2z" } }] }, "name": "clock-circle", "theme": "outlined" };
var ClockCircleOutlined_default = ClockCircleOutlined;
// node_modules/@ant-design/icons-svg/es/asn/FileTextOutlined.js
var FileTextOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM790.2 326H602V137.8L790.2 326zm1.8 562H232V136h302v216a42 42 0 0042 42h216v494zM504 618H320c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8zM312 490v48c0 4.4 3.6 8 8 8h384c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H320c-4.4 0-8 3.6-8 8z" } }] }, "name": "file-text", "theme": "outlined" };
var FileTextOutlined_default = FileTextOutlined;
// node_modules/@ant-design/icons-svg/es/asn/QuestionCircleOutlined.js
var QuestionCircleOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z" } }, { "tag": "path", "attrs": { "d": "M623.6 316.7C593.6 290.4 554 276 512 276s-81.6 14.5-111.6 40.7C369.2 344 352 380.7 352 420v7.6c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V420c0-44.1 43.1-80 96-80s96 35.9 96 80c0 31.1-22 59.6-56.1 72.7-21.2 8.1-39.2 22.3-52.1 40.9-13.1 19-19.9 41.8-19.9 64.9V620c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8v-22.7a48.3 48.3 0 0130.9-44.8c59-22.7 97.1-74.7 97.1-132.5.1-39.3-17.1-76-48.3-103.3zM472 732a40 40 0 1080 0 40 40 0 10-80 0z" } }] }, "name": "question-circle", "theme": "outlined" };
var QuestionCircleOutlined_default = QuestionCircleOutlined;
// node_modules/@ant-design/icons-svg/es/asn/EyeOutlined.js
var EyeOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z" } }] }, "name": "eye", "theme": "outlined" };
var EyeOutlined_default = EyeOutlined;
// node_modules/@ant-design/icons-svg/es/asn/RotateLeftOutlined.js
var RotateLeftOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "defs", "attrs": {}, "children": [{ "tag": "style", "attrs": {} }] }, { "tag": "path", "attrs": { "d": "M672 418H144c-17.7 0-32 14.3-32 32v414c0 17.7 14.3 32 32 32h528c17.7 0 32-14.3 32-32V450c0-17.7-14.3-32-32-32zm-44 402H188V494h440v326z" } }, { "tag": "path", "attrs": { "d": "M819.3 328.5c-78.8-100.7-196-153.6-314.6-154.2l-.2-64c0-6.5-7.6-10.1-12.6-6.1l-128 101c-4 3.1-3.9 9.1 0 12.3L492 318.6c5.1 4 12.7.4 12.6-6.1v-63.9c12.9.1 25.9.9 38.8 2.5 42.1 5.2 82.1 18.2 119 38.7 38.1 21.2 71.2 49.7 98.4 84.3 27.1 34.7 46.7 73.7 58.1 115.8a325.95 325.95 0 016.5 140.9h74.9c14.8-103.6-11.3-213-81-302.3z" } }] }, "name": "rotate-left", "theme": "outlined" };
var RotateLeftOutlined_default = RotateLeftOutlined;
// node_modules/@ant-design/icons-svg/es/asn/RotateRightOutlined.js
var RotateRightOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "defs", "attrs": {}, "children": [{ "tag": "style", "attrs": {} }] }, { "tag": "path", "attrs": { "d": "M480.5 251.2c13-1.6 25.9-2.4 38.8-2.5v63.9c0 6.5 7.5 10.1 12.6 6.1L660 217.6c4-3.2 4-9.2 0-12.3l-128-101c-5.1-4-12.6-.4-12.6 6.1l-.2 64c-118.6.5-235.8 53.4-314.6 154.2A399.75 399.75 0 00123.5 631h74.9c-.9-5.3-1.7-10.7-2.4-16.1-5.1-42.1-2.1-84.1 8.9-124.8 11.4-42.2 31-81.1 58.1-115.8 27.2-34.7 60.3-63.2 98.4-84.3 37-20.6 76.9-33.6 119.1-38.8z" } }, { "tag": "path", "attrs": { "d": "M880 418H352c-17.7 0-32 14.3-32 32v414c0 17.7 14.3 32 32 32h528c17.7 0 32-14.3 32-32V450c0-17.7-14.3-32-32-32zm-44 402H396V494h440v326z" } }] }, "name": "rotate-right", "theme": "outlined" };
var RotateRightOutlined_default = RotateRightOutlined;
// node_modules/@ant-design/icons-svg/es/asn/SwapOutlined.js
var SwapOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M847.9 592H152c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h605.2L612.9 851c-4.1 5.2-.4 13 6.3 13h72.5c4.9 0 9.5-2.2 12.6-6.1l168.8-214.1c16.5-21 1.6-51.8-25.2-51.8zM872 356H266.8l144.3-183c4.1-5.2.4-13-6.3-13h-72.5c-4.9 0-9.5 2.2-12.6 6.1L150.9 380.2c-16.5 21-1.6 51.8 25.1 51.8h696c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8z" } }] }, "name": "swap", "theme": "outlined" };
var SwapOutlined_default = SwapOutlined;
// node_modules/@ant-design/icons-svg/es/asn/ZoomInOutlined.js
var ZoomInOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M637 443H519V309c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v134H325c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h118v134c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V519h118c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8zm284 424L775 721c122.1-148.9 113.6-369.5-26-509-148-148.1-388.4-148.1-537 0-148.1 148.6-148.1 389 0 537 139.5 139.6 360.1 148.1 509 26l146 146c3.2 2.8 8.3 2.8 11 0l43-43c2.8-2.7 2.8-7.8 0-11zM696 696c-118.8 118.7-311.2 118.7-430 0-118.7-118.8-118.7-311.2 0-430 118.8-118.7 311.2-118.7 430 0 118.7 118.8 118.7 311.2 0 430z" } }] }, "name": "zoom-in", "theme": "outlined" };
var ZoomInOutlined_default = ZoomInOutlined;
// node_modules/@ant-design/icons-svg/es/asn/ZoomOutOutlined.js
var ZoomOutOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M637 443H325c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h312c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8zm284 424L775 721c122.1-148.9 113.6-369.5-26-509-148-148.1-388.4-148.1-537 0-148.1 148.6-148.1 389 0 537 139.5 139.6 360.1 148.1 509 26l146 146c3.2 2.8 8.3 2.8 11 0l43-43c2.8-2.7 2.8-7.8 0-11zM696 696c-118.8 118.7-311.2 118.7-430 0-118.7-118.8-118.7-311.2 0-430 118.8-118.7 311.2-118.7 430 0 118.7 118.8 118.7 311.2 0 430z" } }] }, "name": "zoom-out", "theme": "outlined" };
var ZoomOutOutlined_default = ZoomOutOutlined;
// node_modules/@ant-design/icons-svg/es/asn/EyeInvisibleOutlined.js
var EyeInvisibleOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M942.2 486.2Q889.47 375.11 816.7 305l-50.88 50.88C807.31 395.53 843.45 447.4 874.7 512 791.5 684.2 673.4 766 512 766q-72.67 0-133.87-22.38L323 798.75Q408 838 512 838q288.3 0 430.2-300.3a60.29 60.29 0 000-51.5zm-63.57-320.64L836 122.88a8 8 0 00-11.32 0L715.31 232.2Q624.86 186 512 186q-288.3 0-430.2 300.3a60.3 60.3 0 000 51.5q56.69 119.4 136.5 191.41L112.48 835a8 8 0 000 11.31L155.17 889a8 8 0 0011.31 0l712.15-712.12a8 8 0 000-11.32zM149.3 512C232.6 339.8 350.7 258 512 258c54.54 0 104.13 9.36 149.12 28.39l-70.3 70.3a176 176 0 00-238.13 238.13l-83.42 83.42C223.1 637.49 183.3 582.28 149.3 512zm246.7 0a112.11 112.11 0 01146.2-106.69L401.31 546.2A112 112 0 01396 512z" } }, { "tag": "path", "attrs": { "d": "M508 624c-3.46 0-6.87-.16-10.25-.47l-52.82 52.82a176.09 176.09 0 00227.42-227.42l-52.82 52.82c.31 3.38.47 6.79.47 10.25a111.94 111.94 0 01-112 112z" } }] }, "name": "eye-invisible", "theme": "outlined" };
var EyeInvisibleOutlined_default = EyeInvisibleOutlined;
// node_modules/@ant-design/icons-svg/es/asn/DoubleLeftOutlined.js
var DoubleLeftOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M272.9 512l265.4-339.1c4.1-5.2.4-12.9-6.3-12.9h-77.3c-4.9 0-9.6 2.3-12.6 6.1L186.8 492.3a31.99 31.99 0 000 39.5l255.3 326.1c3 3.9 7.7 6.1 12.6 6.1H532c6.7 0 10.4-7.7 6.3-12.9L272.9 512zm304 0l265.4-339.1c4.1-5.2.4-12.9-6.3-12.9h-77.3c-4.9 0-9.6 2.3-12.6 6.1L490.8 492.3a31.99 31.99 0 000 39.5l255.3 326.1c3 3.9 7.7 6.1 12.6 6.1H836c6.7 0 10.4-7.7 6.3-12.9L576.9 512z" } }] }, "name": "double-left", "theme": "outlined" };
var DoubleLeftOutlined_default = DoubleLeftOutlined;
// node_modules/@ant-design/icons-svg/es/asn/DoubleRightOutlined.js
var DoubleRightOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M533.2 492.3L277.9 166.1c-3-3.9-7.7-6.1-12.6-6.1H188c-6.7 0-10.4 7.7-6.3 12.9L447.1 512 181.7 851.1A7.98 7.98 0 00188 864h77.3c4.9 0 9.6-2.3 12.6-6.1l255.3-326.1c9.1-11.7 9.1-27.9 0-39.5zm304 0L581.9 166.1c-3-3.9-7.7-6.1-12.6-6.1H492c-6.7 0-10.4 7.7-6.3 12.9L751.1 512 485.7 851.1A7.98 7.98 0 00492 864h77.3c4.9 0 9.6-2.3 12.6-6.1l255.3-326.1c9.1-11.7 9.1-27.9 0-39.5z" } }] }, "name": "double-right", "theme": "outlined" };
var DoubleRightOutlined_default = DoubleRightOutlined;
// node_modules/@ant-design/icons-svg/es/asn/ReloadOutlined.js
var ReloadOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M909.1 209.3l-56.4 44.1C775.8 155.1 656.2 92 521.9 92 290 92 102.3 279.5 102 511.5 101.7 743.7 289.8 932 521.9 932c181.3 0 335.8-115 394.6-276.1 1.5-4.2-.7-8.9-4.9-10.3l-56.7-19.5a8 8 0 00-10.1 4.8c-1.8 5-3.8 10-5.9 14.9-17.3 41-42.1 77.8-73.7 109.4A344.77 344.77 0 01655.9 829c-42.3 17.9-87.4 27-133.8 27-46.5 0-91.5-9.1-133.8-27A341.5 341.5 0 01279 755.2a342.16 342.16 0 01-73.7-109.4c-17.9-42.4-27-87.4-27-133.9s9.1-91.5 27-133.9c17.3-41 42.1-77.8 73.7-109.4 31.6-31.6 68.4-56.4 109.3-73.8 42.3-17.9 87.4-27 133.8-27 46.5 0 91.5 9.1 133.8 27a341.5 341.5 0 01109.3 73.8c9.9 9.9 19.2 20.4 27.8 31.4l-60.2 47a8 8 0 003 14.1l175.6 43c5 1.2 9.9-2.6 9.9-7.7l.8-180.9c-.1-6.6-7.8-10.3-13-6.2z" } }] }, "name": "reload", "theme": "outlined" };
var ReloadOutlined_default = ReloadOutlined;
// node_modules/@ant-design/icons-svg/es/asn/StarFilled.js
var StarFilled = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M908.1 353.1l-253.9-36.9L540.7 86.1c-3.1-6.3-8.2-11.4-14.5-14.5-15.8-7.8-35-1.3-42.9 14.5L369.8 316.2l-253.9 36.9c-7 1-13.4 4.3-18.3 9.3a32.05 32.05 0 00.6 45.3l183.7 179.1-43.4 252.9a31.95 31.95 0 0046.4 33.7L512 754l227.1 119.4c6.2 3.3 13.4 4.4 20.3 3.2 17.4-3 29.1-19.5 26.1-36.9l-43.4-252.9 183.7-179.1c5-4.9 8.3-11.3 9.3-18.3 2.7-17.5-9.5-33.7-27-36.3z" } }] }, "name": "star", "theme": "filled" };
var StarFilled_default = StarFilled;
// node_modules/@ant-design/icons-svg/es/asn/WarningFilled.js
var WarningFilled = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M955.7 856l-416-720c-6.2-10.7-16.9-16-27.7-16s-21.6 5.3-27.7 16l-416 720C56 877.4 71.4 904 96 904h832c24.6 0 40-26.6 27.7-48zM480 416c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v184c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V416zm32 352a48.01 48.01 0 010-96 48.01 48.01 0 010 96z" } }] }, "name": "warning", "theme": "filled" };
var WarningFilled_default = WarningFilled;
// node_modules/@ant-design/icons-svg/es/asn/FilterFilled.js
var FilterFilled = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M349 838c0 17.7 14.2 32 31.8 32h262.4c17.6 0 31.8-14.3 31.8-32V642H349v196zm531.1-684H143.9c-24.5 0-39.8 26.7-27.5 48l221.3 376h348.8l221.3-376c12.1-21.3-3.2-48-27.7-48z" } }] }, "name": "filter", "theme": "filled" };
var FilterFilled_default = FilterFilled;
// node_modules/@ant-design/icons-svg/es/asn/FileOutlined.js
var FileOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM790.2 326H602V137.8L790.2 326zm1.8 562H232V136h302v216a42 42 0 0042 42h216v494z" } }] }, "name": "file", "theme": "outlined" };
var FileOutlined_default = FileOutlined;
// node_modules/@ant-design/icons-svg/es/asn/FolderOpenOutlined.js
var FolderOpenOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M928 444H820V330.4c0-17.7-14.3-32-32-32H473L355.7 186.2a8.15 8.15 0 00-5.5-2.2H96c-17.7 0-32 14.3-32 32v592c0 17.7 14.3 32 32 32h698c13 0 24.8-7.9 29.7-20l134-332c1.5-3.8 2.3-7.9 2.3-12 0-17.7-14.3-32-32-32zM136 256h188.5l119.6 114.4H748V444H238c-13 0-24.8 7.9-29.7 20L136 643.2V256zm635.3 512H159l103.3-256h612.4L771.3 768z" } }] }, "name": "folder-open", "theme": "outlined" };
var FolderOpenOutlined_default = FolderOpenOutlined;
// node_modules/@ant-design/icons-svg/es/asn/FolderOutlined.js
var FolderOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M880 298.4H521L403.7 186.2a8.15 8.15 0 00-5.5-2.2H144c-17.7 0-32 14.3-32 32v592c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V330.4c0-17.7-14.3-32-32-32zM840 768H184V256h188.5l119.6 114.4H840V768z" } }] }, "name": "folder", "theme": "outlined" };
var FolderOutlined_default = FolderOutlined;
// node_modules/@ant-design/icons-svg/es/asn/HolderOutlined.js
var HolderOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M300 276.5a56 56 0 1056-97 56 56 0 00-56 97zm0 284a56 56 0 1056-97 56 56 0 00-56 97zM640 228a56 56 0 10112 0 56 56 0 00-112 0zm0 284a56 56 0 10112 0 56 56 0 00-112 0zM300 844.5a56 56 0 1056-97 56 56 0 00-56 97zM640 796a56 56 0 10112 0 56 56 0 00-112 0z" } }] }, "name": "holder", "theme": "outlined" };
var HolderOutlined_default = HolderOutlined;
// node_modules/@ant-design/icons-svg/es/asn/CaretDownFilled.js
var CaretDownFilled = { "icon": { "tag": "svg", "attrs": { "viewBox": "0 0 1024 1024", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M840.4 300H183.6c-19.7 0-30.7 20.8-18.5 35l328.4 380.8c9.4 10.9 27.5 10.9 37 0L858.9 335c12.2-14.2 1.2-35-18.5-35z" } }] }, "name": "caret-down", "theme": "filled" };
var CaretDownFilled_default = CaretDownFilled;
// node_modules/@ant-design/icons-svg/es/asn/MinusSquareOutlined.js
var MinusSquareOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M328 544h368c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H328c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8z" } }, { "tag": "path", "attrs": { "d": "M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zm-40 728H184V184h656v656z" } }] }, "name": "minus-square", "theme": "outlined" };
var MinusSquareOutlined_default = MinusSquareOutlined;
// node_modules/@ant-design/icons-svg/es/asn/PlusSquareOutlined.js
var PlusSquareOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M328 544h152v152c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V544h152c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H544V328c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v152H328c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8z" } }, { "tag": "path", "attrs": { "d": "M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zm-40 728H184V184h656v656z" } }] }, "name": "plus-square", "theme": "outlined" };
var PlusSquareOutlined_default = PlusSquareOutlined;
// node_modules/@ant-design/icons-svg/es/asn/CaretDownOutlined.js
var CaretDownOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "0 0 1024 1024", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M840.4 300H183.6c-19.7 0-30.7 20.8-18.5 35l328.4 380.8c9.4 10.9 27.5 10.9 37 0L858.9 335c12.2-14.2 1.2-35-18.5-35z" } }] }, "name": "caret-down", "theme": "outlined" };
var CaretDownOutlined_default = CaretDownOutlined;
// node_modules/@ant-design/icons-svg/es/asn/CaretUpOutlined.js
var CaretUpOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "0 0 1024 1024", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M858.9 689L530.5 308.2c-9.4-10.9-27.5-10.9-37 0L165.1 689c-12.2 14.2-1.2 35 18.5 35h656.8c19.7 0 30.7-20.8 18.5-35z" } }] }, "name": "caret-up", "theme": "outlined" };
var CaretUpOutlined_default = CaretUpOutlined;
// node_modules/@ant-design/icons-svg/es/asn/DeleteOutlined.js
var DeleteOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M360 184h-8c4.4 0 8-3.6 8-8v8h304v-8c0 4.4 3.6 8 8 8h-8v72h72v-80c0-35.3-28.7-64-64-64H352c-35.3 0-64 28.7-64 64v80h72v-72zm504 72H160c-17.7 0-32 14.3-32 32v32c0 4.4 3.6 8 8 8h60.4l24.7 523c1.6 34.1 29.8 61 63.9 61h454c34.2 0 62.3-26.8 63.9-61l24.7-523H888c4.4 0 8-3.6 8-8v-32c0-17.7-14.3-32-32-32zM731.3 840H292.7l-24.2-512h487l-24.2 512z" } }] }, "name": "delete", "theme": "outlined" };
var DeleteOutlined_default = DeleteOutlined;
// node_modules/@ant-design/icons-svg/es/asn/EditOutlined.js
var EditOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M257.7 752c2 0 4-.2 6-.5L431.9 722c2-.4 3.9-1.3 5.3-2.8l423.9-423.9a9.96 9.96 0 000-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2a33.5 33.5 0 009.4 29.8c6.6 6.4 14.9 9.9 23.8 9.9zm67.4-174.4L687.8 215l73.3 73.3-362.7 362.6-88.9 15.7 15.6-89zM880 836H144c-17.7 0-32 14.3-32 32v36c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-36c0-17.7-14.3-32-32-32z" } }] }, "name": "edit", "theme": "outlined" };
var EditOutlined_default = EditOutlined;
// node_modules/@ant-design/icons-svg/es/asn/EnterOutlined.js
var EnterOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M864 170h-60c-4.4 0-8 3.6-8 8v518H310v-73c0-6.7-7.8-10.5-13-6.3l-141.9 112a8 8 0 000 12.6l141.9 112c5.3 4.2 13 .4 13-6.3v-75h498c35.3 0 64-28.7 64-64V178c0-4.4-3.6-8-8-8z" } }] }, "name": "enter", "theme": "outlined" };
var EnterOutlined_default = EnterOutlined;
// node_modules/@ant-design/icons-svg/es/asn/CopyOutlined.js
var CopyOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M832 64H296c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h496v688c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V96c0-17.7-14.3-32-32-32zM704 192H192c-17.7 0-32 14.3-32 32v530.7c0 8.5 3.4 16.6 9.4 22.6l173.3 173.3c2.2 2.2 4.7 4 7.4 5.5v1.9h4.2c3.5 1.3 7.2 2 11 2H704c17.7 0 32-14.3 32-32V224c0-17.7-14.3-32-32-32zM350 856.2L263.9 770H350v86.2zM664 888H414V746c0-22.1-17.9-40-40-40H232V264h432v624z" } }] }, "name": "copy", "theme": "outlined" };
var CopyOutlined_default = CopyOutlined;
// node_modules/@ant-design/icons-svg/es/asn/FileTwoTone.js
var FileTwoTone = { "icon": function render(primaryColor, secondaryColor) {
return { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M534 352V136H232v752h560V394H576a42 42 0 01-42-42z", "fill": secondaryColor } }, { "tag": "path", "attrs": { "d": "M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM602 137.8L790.2 326H602V137.8zM792 888H232V136h302v216a42 42 0 0042 42h216v494z", "fill": primaryColor } }] };
}, "name": "file", "theme": "twotone" };
var FileTwoTone_default = FileTwoTone;
// node_modules/@ant-design/icons-svg/es/asn/PaperClipOutlined.js
var PaperClipOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M779.3 196.6c-94.2-94.2-247.6-94.2-341.7 0l-261 260.8c-1.7 1.7-2.6 4-2.6 6.4s.9 4.7 2.6 6.4l36.9 36.9a9 9 0 0012.7 0l261-260.8c32.4-32.4 75.5-50.2 121.3-50.2s88.9 17.8 121.2 50.2c32.4 32.4 50.2 75.5 50.2 121.2 0 45.8-17.8 88.8-50.2 121.2l-266 265.9-43.1 43.1c-40.3 40.3-105.8 40.3-146.1 0-19.5-19.5-30.2-45.4-30.2-73s10.7-53.5 30.2-73l263.9-263.8c6.7-6.6 15.5-10.3 24.9-10.3h.1c9.4 0 18.1 3.7 24.7 10.3 6.7 6.7 10.3 15.5 10.3 24.9 0 9.3-3.7 18.1-10.3 24.7L372.4 653c-1.7 1.7-2.6 4-2.6 6.4s.9 4.7 2.6 6.4l36.9 36.9a9 9 0 0012.7 0l215.6-215.6c19.9-19.9 30.8-46.3 30.8-74.4s-11-54.6-30.8-74.4c-41.1-41.1-107.9-41-149 0L463 364 224.8 602.1A172.22 172.22 0 00174 724.8c0 46.3 18.1 89.8 50.8 122.5 33.9 33.8 78.3 50.7 122.7 50.7 44.4 0 88.8-16.9 122.6-50.7l309.2-309C824.8 492.7 850 432 850 367.5c.1-64.6-25.1-125.3-70.7-170.9z" } }] }, "name": "paper-clip", "theme": "outlined" };
var PaperClipOutlined_default = PaperClipOutlined;
// node_modules/@ant-design/icons-svg/es/asn/PictureTwoTone.js
var PictureTwoTone = { "icon": function render2(primaryColor, secondaryColor) {
return { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32zm-40 632H136v-39.9l138.5-164.3 150.1 178L658.1 489 888 761.6V792zm0-129.8L664.2 396.8c-3.2-3.8-9-3.8-12.2 0L424.6 666.4l-144-170.7c-3.2-3.8-9-3.8-12.2 0L136 652.7V232h752v430.2z", "fill": primaryColor } }, { "tag": "path", "attrs": { "d": "M424.6 765.8l-150.1-178L136 752.1V792h752v-30.4L658.1 489z", "fill": secondaryColor } }, { "tag": "path", "attrs": { "d": "M136 652.7l132.4-157c3.2-3.8 9-3.8 12.2 0l144 170.7L652 396.8c3.2-3.8 9-3.8 12.2 0L888 662.2V232H136v420.7zM304 280a88 88 0 110 176 88 88 0 010-176z", "fill": secondaryColor } }, { "tag": "path", "attrs": { "d": "M276 368a28 28 0 1056 0 28 28 0 10-56 0z", "fill": secondaryColor } }, { "tag": "path", "attrs": { "d": "M304 456a88 88 0 100-176 88 88 0 000 176zm0-116c15.5 0 28 12.5 28 28s-12.5 28-28 28-28-12.5-28-28 12.5-28 28-28z", "fill": primaryColor } }] };
}, "name": "picture", "theme": "twotone" };
var PictureTwoTone_default = PictureTwoTone;
// node_modules/@ant-design/icons-svg/es/asn/DownloadOutlined.js
var DownloadOutlined = { "icon": { "tag": "svg", "attrs": { "viewBox": "64 64 896 896", "focusable": "false" }, "children": [{ "tag": "path", "attrs": { "d": "M505.7 661a8 8 0 0012.6 0l112-141.7c4.1-5.2.4-12.9-6.3-12.9h-74.1V168c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v338.3H400c-6.7 0-10.4 7.7-6.3 12.9l112 141.8zM878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8z" } }] }, "name": "download", "theme": "outlined" };
var DownloadOutlined_default = DownloadOutlined;
export {
require_react_is,
CheckCircleFilled_default,
CloseCircleFilled_default,
CloseOutlined_default,
ExclamationCircleFilled_default,
InfoCircleFilled_default,
LoadingOutlined_default,
RightOutlined_default,
CheckOutlined_default,
DownOutlined_default,
SearchOutlined_default,
VerticalAlignTopOutlined_default,
LeftOutlined_default,
BarsOutlined_default,
EllipsisOutlined_default,
PlusOutlined_default,
UpOutlined_default,
SwapRightOutlined_default,
CalendarOutlined_default,
ClockCircleOutlined_default,
FileTextOutlined_default,
QuestionCircleOutlined_default,
EyeOutlined_default,
RotateLeftOutlined_default,
RotateRightOutlined_default,
SwapOutlined_default,
ZoomInOutlined_default,
ZoomOutOutlined_default,
EyeInvisibleOutlined_default,
DoubleLeftOutlined_default,
DoubleRightOutlined_default,
ReloadOutlined_default,
StarFilled_default,
WarningFilled_default,
FilterFilled_default,
FileOutlined_default,
FolderOpenOutlined_default,
FolderOutlined_default,
HolderOutlined_default,
CaretDownFilled_default,
MinusSquareOutlined_default,
PlusSquareOutlined_default,
CaretDownOutlined_default,
CaretUpOutlined_default,
DeleteOutlined_default,
EditOutlined_default,
EnterOutlined_default,
CopyOutlined_default,
FileTwoTone_default,
PaperClipOutlined_default,
PictureTwoTone_default,
DownloadOutlined_default
};
/*! Bundled license information:
react-is/cjs/react-is.development.js:
(**
* @license React
* react-is.development.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*)
*/
//# sourceMappingURL=chunk-JVSQUNE5.js.map

File diff suppressed because one or more lines are too long

View File

@ -1,9 +1,9 @@
import {
require_shim
} from "./chunk-GL7YRBYQ.js";
import {
keysFromSelector
} from "./chunk-IDVUNHDH.js";
import {
require_shim
} from "./chunk-GL7YRBYQ.js";
import {
require_react
} from "./chunk-RLJ2RCJQ.js";

View File

@ -14,6 +14,7 @@
"classnames": "^2.5.1",
"i18next": "^25.8.6",
"i18next-browser-languagedetector": "^8.2.1",
"lucide-react": "^0.546.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^16.5.4",
@ -2272,6 +2273,15 @@
"yallist": "^3.0.2"
}
},
"node_modules/lucide-react": {
"version": "0.546.0",
"resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.546.0.tgz",
"integrity": "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/make-dir": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/make-dir/-/make-dir-2.1.0.tgz",

View File

@ -15,6 +15,7 @@
"classnames": "^2.5.1",
"i18next": "^25.8.6",
"i18next-browser-languagedetector": "^8.2.1",
"lucide-react": "^0.546.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^16.5.4",

View File

@ -18,7 +18,7 @@ import {
ReadOutlined,
BookOutlined,
} from '@ant-design/icons'
import { message } from 'antd'
import { App } from 'antd'
import { getUserMenus } from '@/api/menu'
import useUserStore from '@/stores/userStore'
import ModernSidebar from '../ModernSidebar/ModernSidebar'
@ -43,6 +43,7 @@ const iconMap = {
}
function AppSider({ collapsed, onToggle }) {
const { message } = App.useApp()
const navigate = useNavigate()
const location = useLocation()
const { user, logout } = useUserStore()

View File

@ -1,6 +1,6 @@
import { useState, useMemo } from 'react'
import { Document, Page, pdfjs } from 'react-pdf'
import { Button, Space, InputNumber, message, Spin } from 'antd'
import { App, Button, Space, InputNumber, Spin } from 'antd'
import {
LeftOutlined,
RightOutlined,
@ -15,6 +15,7 @@ import './PDFViewer.css'
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf-worker/pdf.worker.min.mjs'
function PDFViewer({ url, filename }) {
const { message } = App.useApp()
const [numPages, setNumPages] = useState(null)
const [pageNumber, setPageNumber] = useState(1)
const [scale, setScale] = useState(1.0)

View File

@ -1,6 +1,6 @@
import { useState, useMemo, useRef, useEffect, useCallback } from 'react'
import { Document, Page, pdfjs } from 'react-pdf'
import { Button, Space, InputNumber, message, Spin } from 'antd'
import { App, Button, Space, InputNumber, Spin } from 'antd'
import {
ZoomInOutlined,
ZoomOutOutlined,
@ -16,6 +16,7 @@ import './VirtualPDFViewer.css'
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf-worker/pdf.worker.min.mjs'
function VirtualPDFViewer({ url, filename }) {
const { message } = App.useApp()
const [numPages, setNumPages] = useState(null)
const [scale, setScale] = useState(1.0)
const [pdfOriginalSize, setPdfOriginalSize] = useState({ width: 595, height: 842 }) // A4

View File

@ -0,0 +1,22 @@
import http from "@/api/http";
import type { DashboardAnalyticsConfig, DashboardAnalyticsPanelPreview, DashboardAnalyticsPreviewCard } from "./types";
export async function getDashboardAnalyticsConfig(tenantId?: number) {
const resp = await http.get("/sys/api/admin/dashboard-analytics-config", { params: { tenantId } });
return resp.data.data as DashboardAnalyticsConfig;
}
export async function updateDashboardAnalyticsConfig(payload: DashboardAnalyticsConfig) {
const resp = await http.put("/sys/api/admin/dashboard-analytics-config", payload);
return resp.data.data as boolean;
}
export async function previewDashboardAnalyticsConfig(tenantId?: number) {
const resp = await http.get("/sys/api/admin/dashboard-analytics-config/preview", { params: { tenantId } });
return resp.data.data as DashboardAnalyticsPanelPreview;
}
export async function previewDashboardAnalyticsCardDetail(cardKey: string, tenantId?: number) {
const resp = await http.get("/sys/api/admin/dashboard-analytics-config/preview/card-detail", { params: { tenantId, cardKey } });
return resp.data.data as DashboardAnalyticsPreviewCard;
}

Some files were not shown because too many files have changed in this diff Show More