diff --git a/frontend/src/components/DataGrid.tsx b/frontend/src/components/DataGrid.tsx
index 74a0094..7a1a604 100644
--- a/frontend/src/components/DataGrid.tsx
+++ b/frontend/src/components/DataGrid.tsx
@@ -73,15 +73,18 @@ const splitCellKey = (cellKey: string): { rowKey: string; colName: string } | nu
};
};
-// Normalize RFC3339-like datetime strings to `YYYY-MM-DD HH:mm:ss` for display/editing.
-// Also handle invalid datetime values like '0000-00-00 00:00:00'
+// Normalize common datetime strings to `YYYY-MM-DD HH:mm:ss` for display/editing.
+// Handles RFC3339 and Go-style datetime text like `2024-05-13 08:32:47 +0800 CST`.
+// Also keep invalid datetime values like `0000-00-00 00:00:00` unchanged.
const normalizeDateTimeString = (val: string) => {
// 检查是否为无效日期时间(0000-00-00 或类似格式)
if (/^0{4}-0{2}-0{2}/.test(val)) {
return val; // 保持原样显示,不尝试转换
}
- const match = val.match(/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})/);
+ const match = val.match(
+ /^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2}:\d{2})(?:\.\d+)?(?:\s*(?:Z|[+-]\d{2}:?\d{2})(?:\s+[A-Za-z_\/+-]+)?)?$/
+ );
if (!match) return val;
return `${match[1]} ${match[2]}`;
};
@@ -179,11 +182,12 @@ const normalizeValueForJsonView = (value: any): any => {
if (value === null || value === undefined) return value;
if (typeof value === 'string') {
- if (!looksLikeJsonText(value)) return value;
+ const normalizedText = normalizeDateTimeString(value);
+ if (!looksLikeJsonText(normalizedText)) return normalizedText;
try {
- return normalizeValueForJsonView(JSON.parse(value));
+ return normalizeValueForJsonView(JSON.parse(normalizedText));
} catch {
- return value;
+ return normalizedText;
}
}
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx
index 59b461f..fc07757 100644
--- a/frontend/src/components/Sidebar.tsx
+++ b/frontend/src/components/Sidebar.tsx
@@ -268,6 +268,19 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
return `${schema}.${name}`;
};
+ const splitQualifiedName = (qualifiedName: string): { schemaName: string; objectName: string } => {
+ const raw = String(qualifiedName || '').trim();
+ if (!raw) return { schemaName: '', objectName: '' };
+ const idx = raw.lastIndexOf('.');
+ if (idx <= 0 || idx >= raw.length - 1) {
+ return { schemaName: '', objectName: raw };
+ }
+ return {
+ schemaName: raw.substring(0, idx),
+ objectName: raw.substring(idx + 1),
+ };
+ };
+
const buildViewsMetadataQuery = (dialect: string, dbName: string): string => {
const safeDbName = escapeSQLLiteral(dbName);
switch (dialect) {
@@ -539,105 +552,214 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
const res = await DBGetTables(config as any, conn.dbName);
if (res.success) {
setConnectionStates(prev => ({ ...prev, [key as string]: 'success' }));
- const tables = (res.data as any[]).map((row: any) => {
- const tableName = Object.values(row)[0] as string;
- const tableDisplayName = getSidebarTableDisplayName(conn, tableName);
- return {
- title: tableDisplayName,
- key: `${conn.id}-${conn.dbName}-${tableName}`,
- icon: ,
- type: 'table' as const,
- dataRef: { ...conn, tableName },
- isLeaf: false,
- };
- });
- const [views, triggers, routines] = await Promise.all([
- loadViews(conn, conn.dbName),
- loadDatabaseTriggers(conn, conn.dbName),
- loadFunctions(conn, conn.dbName),
- ]);
+ const tableEntries = (res.data as any[]).map((row: any) => {
+ const tableName = Object.values(row)[0] as string;
+ const parsed = splitQualifiedName(tableName);
+ return {
+ tableName,
+ schemaName: parsed.schemaName,
+ displayName: getSidebarTableDisplayName(conn, tableName),
+ };
+ });
- // 获取当前数据库的排序偏好
- const sortPreferenceKey = `${conn.id}-${conn.dbName}`;
- const sortBy = tableSortPreference[sortPreferenceKey] || 'name';
+ const [views, triggers, routines] = await Promise.all([
+ loadViews(conn, conn.dbName),
+ loadDatabaseTriggers(conn, conn.dbName),
+ loadFunctions(conn, conn.dbName),
+ ]);
- // 根据排序偏好排序表
- if (sortBy === 'frequency') {
- // 按使用频率排序(降序)
- tables.sort((a, b) => {
- const keyA = `${conn.id}-${conn.dbName}-${a.dataRef.tableName}`;
- const keyB = `${conn.id}-${conn.dbName}-${b.dataRef.tableName}`;
- const countA = tableAccessCount[keyA] || 0;
- const countB = tableAccessCount[keyB] || 0;
- if (countA !== countB) {
- return countB - countA; // 降序
- }
- // 频率相同时按名称排序
- return a.title.toLowerCase().localeCompare(b.title.toLowerCase());
- });
- } else {
- // 按名称排序(字母顺序)
- tables.sort((a, b) => a.title.toLowerCase().localeCompare(b.title.toLowerCase()));
- }
+ const viewEntries = views.map((viewName) => {
+ const parsed = splitQualifiedName(viewName);
+ return {
+ viewName,
+ schemaName: parsed.schemaName,
+ displayName: getSidebarTableDisplayName(conn, viewName),
+ };
+ });
- // Sort views by name (case-insensitive)
- views.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
+ const triggerEntries = triggers.map((trigger) => {
+ const triggerParsed = splitQualifiedName(trigger.triggerName);
+ const tableParsed = splitQualifiedName(trigger.tableName);
+ const schemaName = tableParsed.schemaName || triggerParsed.schemaName;
+ const triggerObjectName = triggerParsed.objectName || trigger.triggerName;
+ const tableObjectName = tableParsed.objectName || trigger.tableName;
+ const displayName = tableObjectName ? `${triggerObjectName} (${tableObjectName})` : triggerObjectName;
+ return {
+ ...trigger,
+ schemaName,
+ displayName,
+ };
+ });
- // Sort triggers by display name (case-insensitive)
- triggers.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()));
+ const routineEntries = routines.map((routine) => {
+ const parsed = splitQualifiedName(routine.routineName);
+ const typeLabel = routine.routineType === 'PROCEDURE' ? 'P' : 'F';
+ return {
+ ...routine,
+ schemaName: parsed.schemaName,
+ displayName: `${parsed.objectName || routine.routineName} [${typeLabel}]`,
+ };
+ });
- // Sort routines by display name (case-insensitive)
- routines.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()));
+ // 获取当前数据库的排序偏好
+ const sortPreferenceKey = `${conn.id}-${conn.dbName}`;
+ const sortBy = tableSortPreference[sortPreferenceKey] || 'name';
- const viewNodes: TreeNode[] = views.map((viewName) => ({
- title: getSidebarTableDisplayName(conn, viewName),
- key: `${conn.id}-${conn.dbName}-view-${viewName}`,
- icon: ,
- type: 'view',
- dataRef: { ...conn, viewName, tableName: viewName },
- isLeaf: true,
- }));
+ // 根据排序偏好排序表
+ if (sortBy === 'frequency') {
+ // 按使用频率排序(降序)
+ tableEntries.sort((a, b) => {
+ const keyA = `${conn.id}-${conn.dbName}-${a.tableName}`;
+ const keyB = `${conn.id}-${conn.dbName}-${b.tableName}`;
+ const countA = tableAccessCount[keyA] || 0;
+ const countB = tableAccessCount[keyB] || 0;
+ if (countA !== countB) {
+ return countB - countA;
+ }
+ return a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase());
+ });
+ } else {
+ tableEntries.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()));
+ }
- const triggerNodes: TreeNode[] = triggers.map((trigger) => ({
- title: trigger.displayName,
- key: `${conn.id}-${conn.dbName}-trigger-${trigger.triggerName}-${trigger.tableName}`,
- icon: ,
- type: 'db-trigger',
- dataRef: { ...conn, triggerName: trigger.triggerName, triggerTableName: trigger.tableName },
- isLeaf: true,
- }));
+ // Sort views by name (case-insensitive)
+ viewEntries.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()));
- const routineNodes: TreeNode[] = routines.map((r) => ({
- title: r.displayName,
- key: `${conn.id}-${conn.dbName}-routine-${r.routineName}`,
- icon: ,
- type: 'routine',
- dataRef: { ...conn, routineName: r.routineName, routineType: r.routineType },
- isLeaf: true,
- }));
+ // Sort triggers by display name (case-insensitive)
+ triggerEntries.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()));
- const buildObjectGroup = (groupKey: string, groupTitle: string, groupIcon: React.ReactNode, children: TreeNode[]): TreeNode => ({
- title: `${groupTitle} (${children.length})`,
- key: `${key}-${groupKey}`,
- icon: groupIcon,
- type: 'object-group',
- isLeaf: children.length === 0,
- children: children.length > 0 ? children : undefined,
- dataRef: { ...conn, dbName: conn.dbName, groupKey }
- });
+ // Sort routines by display name (case-insensitive)
+ routineEntries.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()));
- const groupedNodes: TreeNode[] = [
- buildObjectGroup('tables', '表', , tables),
- buildObjectGroup('views', '视图', , viewNodes),
- buildObjectGroup('routines', '函数', , routineNodes),
- buildObjectGroup('triggers', '触发器', , triggerNodes),
- ];
+ const buildTableNode = (entry: { tableName: string; schemaName: string; displayName: string }): TreeNode => ({
+ title: entry.displayName,
+ key: `${conn.id}-${conn.dbName}-${entry.tableName}`,
+ icon: ,
+ type: 'table',
+ dataRef: { ...conn, tableName: entry.tableName, schemaName: entry.schemaName },
+ isLeaf: false,
+ });
- setTreeData(origin => updateTreeData(origin, key, [queriesNode, ...groupedNodes]));
- } else {
- setConnectionStates(prev => ({ ...prev, [key as string]: 'error' }));
- message.error({ content: res.message, key: `db-${key}-tables` });
+ const buildViewNode = (entry: { viewName: string; schemaName: string; displayName: string }): TreeNode => ({
+ title: entry.displayName,
+ key: `${conn.id}-${conn.dbName}-view-${entry.viewName}`,
+ icon: ,
+ type: 'view',
+ dataRef: { ...conn, viewName: entry.viewName, tableName: entry.viewName, schemaName: entry.schemaName },
+ isLeaf: true,
+ });
+
+ const buildTriggerNode = (entry: { triggerName: string; tableName: string; schemaName: string; displayName: string }): TreeNode => ({
+ title: entry.displayName,
+ key: `${conn.id}-${conn.dbName}-trigger-${entry.triggerName}-${entry.tableName}`,
+ icon: ,
+ type: 'db-trigger',
+ dataRef: { ...conn, triggerName: entry.triggerName, triggerTableName: entry.tableName, schemaName: entry.schemaName },
+ isLeaf: true,
+ });
+
+ const buildRoutineNode = (entry: { routineName: string; routineType: string; schemaName: string; displayName: string }): TreeNode => ({
+ title: entry.displayName,
+ key: `${conn.id}-${conn.dbName}-routine-${entry.routineName}`,
+ icon: ,
+ type: 'routine',
+ dataRef: { ...conn, routineName: entry.routineName, routineType: entry.routineType, schemaName: entry.schemaName },
+ isLeaf: true,
+ });
+
+ const buildObjectGroup = (
+ parentKey: string,
+ groupKey: string,
+ groupTitle: string,
+ groupIcon: React.ReactNode,
+ children: TreeNode[],
+ extraData: Record = {}
+ ): TreeNode => ({
+ title: `${groupTitle} (${children.length})`,
+ key: `${parentKey}-${groupKey}`,
+ icon: groupIcon,
+ type: 'object-group',
+ isLeaf: children.length === 0,
+ children: children.length > 0 ? children : undefined,
+ dataRef: { ...conn, dbName: conn.dbName, groupKey, ...extraData }
+ });
+
+ const shouldGroupBySchema = shouldHideSchemaPrefix(conn as SavedConnection);
+ if (shouldGroupBySchema) {
+ type SchemaBucket = {
+ schemaName: string;
+ tables: TreeNode[];
+ views: TreeNode[];
+ routines: TreeNode[];
+ triggers: TreeNode[];
+ };
+
+ const schemaMap = new Map();
+ const getSchemaBucket = (rawSchemaName: string): SchemaBucket => {
+ const schemaName = String(rawSchemaName || '').trim();
+ const schemaKey = schemaName || '__default__';
+ let bucket = schemaMap.get(schemaKey);
+ if (!bucket) {
+ bucket = {
+ schemaName,
+ tables: [],
+ views: [],
+ routines: [],
+ triggers: [],
+ };
+ schemaMap.set(schemaKey, bucket);
+ }
+ return bucket;
+ };
+
+ tableEntries.forEach((entry) => getSchemaBucket(entry.schemaName).tables.push(buildTableNode(entry)));
+ viewEntries.forEach((entry) => getSchemaBucket(entry.schemaName).views.push(buildViewNode(entry)));
+ routineEntries.forEach((entry) => getSchemaBucket(entry.schemaName).routines.push(buildRoutineNode(entry)));
+ triggerEntries.forEach((entry) => getSchemaBucket(entry.schemaName).triggers.push(buildTriggerNode(entry)));
+
+ const schemaNodes: TreeNode[] = Array.from(schemaMap.values())
+ .sort((a, b) => {
+ if (!a.schemaName && !b.schemaName) return 0;
+ if (!a.schemaName) return -1;
+ if (!b.schemaName) return 1;
+ return a.schemaName.toLowerCase().localeCompare(b.schemaName.toLowerCase());
+ })
+ .map((bucket) => {
+ const schemaNodeKey = `${key}-schema-${bucket.schemaName || 'default'}`;
+ const schemaTitle = bucket.schemaName || '默认模式';
+ const groupedNodes: TreeNode[] = [
+ buildObjectGroup(schemaNodeKey, 'tables', '表', , bucket.tables, { schemaName: bucket.schemaName }),
+ buildObjectGroup(schemaNodeKey, 'views', '视图', , bucket.views, { schemaName: bucket.schemaName }),
+ buildObjectGroup(schemaNodeKey, 'routines', '函数', , bucket.routines, { schemaName: bucket.schemaName }),
+ buildObjectGroup(schemaNodeKey, 'triggers', '触发器', , bucket.triggers, { schemaName: bucket.schemaName }),
+ ];
+
+ return {
+ title: schemaTitle,
+ key: schemaNodeKey,
+ icon: ,
+ type: 'object-group' as const,
+ isLeaf: groupedNodes.length === 0,
+ children: groupedNodes,
+ dataRef: { ...conn, dbName: conn.dbName, groupKey: 'schema', schemaName: bucket.schemaName }
+ };
+ });
+
+ setTreeData(origin => updateTreeData(origin, key, [queriesNode, ...schemaNodes]));
+ } else {
+ const groupedNodes: TreeNode[] = [
+ buildObjectGroup(key as string, 'tables', '表', , tableEntries.map(buildTableNode)),
+ buildObjectGroup(key as string, 'views', '视图', , viewEntries.map(buildViewNode)),
+ buildObjectGroup(key as string, 'routines', '函数', , routineEntries.map(buildRoutineNode)),
+ buildObjectGroup(key as string, 'triggers', '触发器', , triggerEntries.map(buildTriggerNode)),
+ ];
+
+ setTreeData(origin => updateTreeData(origin, key, [queriesNode, ...groupedNodes]));
+ }
+ } else {
+ setConnectionStates(prev => ({ ...prev, [key as string]: 'error' }));
+ message.error({ content: res.message, key: `db-${key}-tables` });
}
} finally {
loadingNodesRef.current.delete(loadKey);
diff --git a/internal/app/methods_file.go b/internal/app/methods_file.go
index 8e87e96..711a094 100644
--- a/internal/app/methods_file.go
+++ b/internal/app/methods_file.go
@@ -77,7 +77,6 @@ func (a *App) ImportConfigFile() connection.QueryResult {
return connection.QueryResult{Success: true, Data: string(content)}
}
-
// PreviewImportFile 解析导入文件,返回字段列表、总行数、前 5 行预览数据
func (a *App) PreviewImportFile(filePath string) connection.QueryResult {
if filePath == "" {
@@ -220,6 +219,148 @@ func parseImportFile(filePath string) ([]map[string]interface{}, []string, error
return rows, columns, nil
}
+func normalizeColumnName(name string) string {
+ return strings.ToLower(strings.TrimSpace(name))
+}
+
+func buildImportColumnTypeMap(defs []connection.ColumnDefinition) map[string]string {
+ result := make(map[string]string, len(defs))
+ for _, def := range defs {
+ key := normalizeColumnName(def.Name)
+ if key == "" {
+ continue
+ }
+ result[key] = strings.TrimSpace(def.Type)
+ }
+ return result
+}
+
+func isTimezoneAwareColumnType(columnType string) bool {
+ typ := strings.ToLower(strings.TrimSpace(columnType))
+ if typ == "" {
+ return false
+ }
+ return strings.Contains(typ, "with time zone") ||
+ strings.Contains(typ, "with timezone") ||
+ strings.Contains(typ, "datetimeoffset") ||
+ strings.Contains(typ, "timestamptz")
+}
+
+func isDateTimeColumnType(columnType string) bool {
+ typ := strings.ToLower(strings.TrimSpace(columnType))
+ if typ == "" {
+ return false
+ }
+ return strings.Contains(typ, "datetime") || strings.Contains(typ, "timestamp")
+}
+
+func isTimeOnlyColumnType(columnType string) bool {
+ typ := strings.ToLower(strings.TrimSpace(columnType))
+ if typ == "" {
+ return false
+ }
+ if strings.Contains(typ, "datetime") || strings.Contains(typ, "timestamp") {
+ return false
+ }
+ return strings.Contains(typ, "time")
+}
+
+func isDateOnlyColumnType(dbType, columnType string) bool {
+ typ := strings.ToLower(strings.TrimSpace(columnType))
+ if typ == "" {
+ return false
+ }
+ if strings.Contains(typ, "datetime") || strings.Contains(typ, "timestamp") || strings.Contains(typ, "time") {
+ return false
+ }
+ if !strings.Contains(typ, "date") {
+ return false
+ }
+ db := strings.ToLower(strings.TrimSpace(dbType))
+ // Oracle/Dameng 的 DATE 带时间语义,不能按纯日期裁剪。
+ return db != "oracle" && db != "dameng"
+}
+
+func isTemporalColumnType(dbType, columnType string) bool {
+ return isDateTimeColumnType(columnType) || isTimeOnlyColumnType(columnType) || isDateOnlyColumnType(dbType, columnType)
+}
+
+func parseTemporalString(raw string) (time.Time, bool) {
+ text := strings.TrimSpace(raw)
+ if text == "" {
+ return time.Time{}, false
+ }
+
+ layouts := []string{
+ "2006-01-02 15:04:05.999999999 -0700 MST",
+ "2006-01-02 15:04:05 -0700 MST",
+ "2006-01-02 15:04:05.999999999 -0700",
+ "2006-01-02 15:04:05 -0700",
+ time.RFC3339Nano,
+ time.RFC3339,
+ "2006-01-02 15:04:05.999999999",
+ "2006-01-02 15:04:05",
+ "2006-01-02",
+ "15:04:05.999999999",
+ "15:04:05",
+ }
+
+ for _, layout := range layouts {
+ parsed, err := time.Parse(layout, text)
+ if err == nil {
+ return parsed, true
+ }
+ }
+
+ return time.Time{}, false
+}
+
+func normalizeImportTemporalValue(dbType, columnType, raw string) string {
+ text := strings.TrimSpace(raw)
+ if text == "" {
+ return text
+ }
+
+ parsed, ok := parseTemporalString(text)
+ if !ok {
+ if isDateTimeColumnType(columnType) {
+ candidate := strings.ReplaceAll(text, "T", " ")
+ if len(candidate) >= 19 {
+ prefix := candidate[:19]
+ if _, err := time.Parse("2006-01-02 15:04:05", prefix); err == nil {
+ return prefix
+ }
+ }
+ }
+ return text
+ }
+
+ if isTimeOnlyColumnType(columnType) {
+ return parsed.Format("15:04:05")
+ }
+ if isDateOnlyColumnType(dbType, columnType) {
+ return parsed.Format("2006-01-02")
+ }
+ if isTimezoneAwareColumnType(columnType) {
+ return parsed.Format("2006-01-02 15:04:05-07:00")
+ }
+ return parsed.Format("2006-01-02 15:04:05")
+}
+
+func formatImportSQLValue(dbType, columnType string, value interface{}) string {
+ if value == nil {
+ return "NULL"
+ }
+
+ if isTemporalColumnType(dbType, columnType) {
+ normalized := normalizeImportTemporalValue(dbType, columnType, fmt.Sprintf("%v", value))
+ escaped := strings.ReplaceAll(normalized, "'", "''")
+ return "'" + escaped + "'"
+ }
+
+ return formatSQLValue(dbType, value)
+}
+
// ImportDataWithProgress 执行导入并发送进度事件
func (a *App) ImportDataWithProgress(config connection.ConnectionConfig, dbName, tableName, filePath string) connection.QueryResult {
rows, columns, err := parseImportFile(filePath)
@@ -237,6 +378,12 @@ func (a *App) ImportDataWithProgress(config connection.ConnectionConfig, dbName,
return connection.QueryResult{Success: false, Message: err.Error()}
}
+ schemaName, pureTableName := normalizeSchemaAndTable(config, dbName, tableName)
+ columnTypeMap := map[string]string{}
+ if defs, colErr := dbInst.GetColumns(schemaName, pureTableName); colErr == nil {
+ columnTypeMap = buildImportColumnTypeMap(defs)
+ }
+
totalRows := len(rows)
successCount := 0
var errorLogs []string
@@ -250,13 +397,8 @@ func (a *App) ImportDataWithProgress(config connection.ConnectionConfig, dbName,
var values []string
for _, col := range columns {
val := row[col]
- if val == nil {
- values = append(values, "NULL")
- } else {
- vStr := fmt.Sprintf("%v", val)
- vStr = strings.ReplaceAll(vStr, "'", "''")
- values = append(values, fmt.Sprintf("'%s'", vStr))
- }
+ colType := columnTypeMap[normalizeColumnName(col)]
+ values = append(values, formatImportSQLValue(runConfig.Type, colType, val))
}
query := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)",
@@ -848,7 +990,7 @@ func writeRowsToFile(f *os.File, data []map[string]interface{}, columns []string
continue
}
- s := fmt.Sprintf("%v", val)
+ s := formatExportCellText(val)
if format == "md" {
s = strings.ReplaceAll(s, "|", "\\|")
s = strings.ReplaceAll(s, "\n", "
")
@@ -894,6 +1036,24 @@ func writeRowsToFile(f *os.File, data []map[string]interface{}, columns []string
return nil
}
+func formatExportCellText(val interface{}) string {
+ if val == nil {
+ return "NULL"
+ }
+
+ switch v := val.(type) {
+ case time.Time:
+ return v.Format("2006-01-02 15:04:05")
+ case *time.Time:
+ if v == nil {
+ return "NULL"
+ }
+ return v.Format("2006-01-02 15:04:05")
+ default:
+ return fmt.Sprintf("%v", val)
+ }
+}
+
// writeRowsToXlsx 使用 excelize 写入真正的 xlsx 格式文件
func writeRowsToXlsx(filename string, data []map[string]interface{}, columns []string) error {
xlsx := excelize.NewFile()
@@ -915,7 +1075,7 @@ func writeRowsToXlsx(filename string, data []map[string]interface{}, columns []s
if val == nil {
xlsx.SetCellValue(sheet, cell, "NULL")
} else {
- xlsx.SetCellValue(sheet, cell, fmt.Sprintf("%v", val))
+ xlsx.SetCellValue(sheet, cell, formatExportCellText(val))
}
}
}