🆕 基础功能完善

This commit is contained in:
李寻欢 2025-04-02 14:29:57 +08:00
parent 0037c1ea44
commit b66a3e4ad8
26 changed files with 3068 additions and 0 deletions

250
internal/docker/stats.go Normal file
View File

@ -0,0 +1,250 @@
package docker
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/docker/docker/api/types"
)
// ContainerStats 容器统计数据结构
type ContainerStats struct {
CPUPercentage float64 `json:"cpu_percent"` // CPU 使用百分比
MemoryUsage int64 `json:"memory_usage"` // 内存使用量(字节)
MemoryLimit int64 `json:"memory_limit"` // 内存限制(字节)
MemoryPercentage float64 `json:"memory_percent"` // 内存使用百分比
NetworkRx int64 `json:"network_rx"` // 网络接收(字节)
NetworkTx int64 `json:"network_tx"` // 网络发送(字节)
BlockRead int64 `json:"block_read"` // 块设备读取(字节)
BlockWrite int64 `json:"block_write"` // 块设备写入(字节)
PID int `json:"pid"` // 进程ID
Status string `json:"status"` // 容器状态
Uptime string `json:"uptime"` // 运行时间
}
// GetStats 获取容器的实时统计数据
func GetStats(ctx context.Context, containerID string) (*ContainerStats, error) {
if containerID == "" {
return nil, fmt.Errorf("容器ID不能为空")
}
cli := GetClient()
if cli == nil {
return nil, fmt.Errorf("Docker客户端未初始化")
}
// 获取容器详细信息
inspect, err := cli.ContainerInspect(ctx, containerID)
if err != nil {
return nil, fmt.Errorf("无法获取容器信息: %w", err)
}
// 获取容器状态
status := "stopped"
if inspect.State.Running {
status = "running"
} else if inspect.State.Paused {
status = "paused"
} else if inspect.State.Restarting {
status = "restarting"
}
// 计算运行时间
uptime := "0s"
if inspect.State.Running && inspect.State.StartedAt != "" {
startTime, err := time.Parse(time.RFC3339, inspect.State.StartedAt)
if err == nil {
duration := time.Since(startTime)
uptime = formatDuration(duration)
}
}
// 如果容器未运行,返回基本信息
if !inspect.State.Running {
return &ContainerStats{
Status: status,
Uptime: uptime,
PID: inspect.State.Pid,
}, nil
}
// 获取容器实时统计数据
stats, err := cli.ContainerStats(ctx, containerID, false) // 不需要持续流式数据
if err != nil {
return nil, fmt.Errorf("获取容器统计数据失败: %w", err)
}
defer stats.Body.Close()
// 解析统计数据JSON
var statsJSON types.StatsJSON
if err := json.NewDecoder(stats.Body).Decode(&statsJSON); err != nil {
return nil, fmt.Errorf("解析容器统计数据失败: %w", err)
}
result := &ContainerStats{
Status: status,
Uptime: uptime,
PID: inspect.State.Pid,
}
// 计算CPU使用百分比
result.CPUPercentage = calculateCPUPercentage(&statsJSON)
// 设置内存使用情况
result.MemoryUsage = getMemoryUsage(&statsJSON)
result.MemoryLimit = getMemoryLimit(&statsJSON)
result.MemoryPercentage = calculateMemoryPercentage(&statsJSON)
// 获取网络数据
networkStats := getNetworkStats(&statsJSON)
result.NetworkRx = networkStats.rx
result.NetworkTx = networkStats.tx
// 获取I/O数据
ioStats := getIOStats(&statsJSON)
result.BlockRead = ioStats.read
result.BlockWrite = ioStats.write
return result, nil
}
// 计算CPU使用百分比
func calculateCPUPercentage(stats *types.StatsJSON) float64 {
if stats == nil {
return 0.0
}
// CPU使用率计算公式 = (CPU使用时间 / CPU总时间) * 核心数 * 100%
cpuDelta := float64(stats.CPUStats.CPUUsage.TotalUsage) - float64(stats.PreCPUStats.CPUUsage.TotalUsage)
systemDelta := float64(stats.CPUStats.SystemUsage) - float64(stats.PreCPUStats.SystemUsage)
numCPUs := uint32(1) // 默认为1个CPU
if len(stats.CPUStats.CPUUsage.PercpuUsage) > 0 {
numCPUs = uint32(len(stats.CPUStats.CPUUsage.PercpuUsage))
}
if systemDelta > 0 && cpuDelta > 0 {
cpuPercent := (cpuDelta / systemDelta) * float64(numCPUs) * 100.0
return roundToTwo(cpuPercent)
}
return 0.0
}
// 获取内存使用量
func getMemoryUsage(stats *types.StatsJSON) int64 {
if stats == nil || stats.MemoryStats.Usage == 0 {
return 0
}
// 某些环境下,需要减去缓存
if stats.MemoryStats.Stats != nil {
if cache, ok := stats.MemoryStats.Stats["cache"]; ok {
return int64(stats.MemoryStats.Usage - cache) // 转换为int64
}
}
return int64(stats.MemoryStats.Usage) // 转换为int64
}
// 获取内存限制
func getMemoryLimit(stats *types.StatsJSON) int64 {
if stats == nil || stats.MemoryStats.Limit == 0 {
return 0
}
return int64(stats.MemoryStats.Limit) // 转换为int64
}
// 计算内存使用百分比
func calculateMemoryPercentage(stats *types.StatsJSON) float64 {
if stats == nil {
return 0.0
}
usage := getMemoryUsage(stats)
limit := getMemoryLimit(stats)
if limit > 0 && usage > 0 {
memPercent := float64(usage) / float64(limit) * 100.0
return roundToTwo(memPercent)
}
return 0.0
}
// 网络统计结构
type networkStats struct {
rx int64
tx int64
}
// 获取网络统计数据
func getNetworkStats(stats *types.StatsJSON) networkStats {
if stats == nil || len(stats.Networks) == 0 {
return networkStats{}
}
var rx, tx int64
for _, network := range stats.Networks {
rx += int64(network.RxBytes) // 转换为int64
tx += int64(network.TxBytes) // 转换为int64
}
return networkStats{rx: rx, tx: tx}
}
// IO统计结构
type ioStats struct {
read int64
write int64
}
// 获取IO统计数据
func getIOStats(stats *types.StatsJSON) ioStats {
if stats == nil {
return ioStats{}
}
var read, write int64
if len(stats.BlkioStats.IoServiceBytesRecursive) > 0 {
for _, blkio := range stats.BlkioStats.IoServiceBytesRecursive {
switch blkio.Op {
case "Read":
read += int64(blkio.Value) // 转换为int64
case "Write":
write += int64(blkio.Value) // 转换为int64
}
}
}
return ioStats{read: read, write: write}
}
// 四舍五入到两位小数
func roundToTwo(num float64) float64 {
if num < 0 {
return 0.0 // 避免负值
}
return float64(int(num*100)) / 100
}
// 格式化时间持续
func formatDuration(d time.Duration) string {
if d < 0 {
d = 0 // 避免负值
}
days := int(d.Hours() / 24)
hours := int(d.Hours()) % 24
minutes := int(d.Minutes()) % 60
seconds := int(d.Seconds()) % 60
if days > 0 {
return fmt.Sprintf("%dd %dh %dm", days, hours, minutes)
}
if hours > 0 {
return fmt.Sprintf("%dh %dm", hours, minutes)
}
if minutes > 0 {
return fmt.Sprintf("%dm %ds", minutes, seconds)
}
return fmt.Sprintf("%ds", seconds)
}

View File

@ -0,0 +1,92 @@
package handler
import (
"context"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"gitee.ltd/lxh/wechat-robot/internal/docker"
)
// GetContainerLogs 获取容器日志
func GetContainerLogs(c *fiber.Ctx) error {
containerID := c.Params("id")
if containerID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"message": "容器ID不能为空",
})
}
// 获取日志行数
lines := c.Query("lines", "100")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// 获取容器日志
logs, err := docker.GetContainerLogs(ctx, containerID, lines)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"message": "获取容器日志失败: " + err.Error(),
})
}
// 处理日志输出,移除控制字符
cleanLogs := cleanDockerLogs(logs)
return c.JSON(fiber.Map{
"success": true,
"logs": cleanLogs,
})
}
// GetContainerStats 获取容器状态
func GetContainerStats(c *fiber.Ctx) error {
containerID := c.Params("id")
if containerID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"message": "容器ID不能为空",
})
}
// 创建上下文,添加超时控制
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 获取真实的容器统计数据
stats, err := docker.GetStats(ctx, containerID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"message": "获取容器统计数据失败: " + err.Error(),
})
}
return c.JSON(fiber.Map{
"success": true,
"stats": stats,
})
}
// cleanDockerLogs 移除Docker日志中的控制字符
func cleanDockerLogs(logs string) string {
// Docker输出日志中可能包含控制字符需要清理
result := logs
// 移除8字节头部
lines := strings.Split(result, "\n")
for i, line := range lines {
if len(line) > 8 {
// 跳过每行前8个字节的头部信息
lines[i] = line[8:]
}
}
result = strings.Join(lines, "\n")
return result
}

View File

@ -0,0 +1,81 @@
package handler
import (
"context"
"strconv"
"time"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
"gitee.ltd/lxh/wechat-robot/internal/docker"
"gitee.ltd/lxh/wechat-robot/internal/model"
)
// CheckQRCodeStatus 检查二维码状态
func CheckQRCodeStatus(c *fiber.Ctx) error {
id, err := strconv.Atoi(c.Params("id"))
if err != nil {
return c.JSON(fiber.Map{
"success": false,
"message": "无效的机器人ID",
})
}
uuid := c.Query("uuid")
if uuid == "" {
return c.JSON(fiber.Map{
"success": false,
"message": "UUID参数缺失",
})
}
// 获取机器人实例
db := model.GetDB()
var robot model.Robot
if err := db.First(&robot, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return c.JSON(fiber.Map{
"success": false,
"message": "机器人不存在",
})
}
return c.JSON(fiber.Map{
"success": false,
"message": "查询数据库失败",
})
}
// 调用CheckUuid API检查二维码状态
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// 调用CheckUuid API检查二维码状态传递容器访问地址
response, err := docker.CheckUuid(ctx, robot.ContainerID, uuid, robot.ContainerHost)
if err != nil {
return c.JSON(fiber.Map{
"success": false,
"message": "检查二维码状态失败: " + err.Error(),
})
}
// 如果检测到已登录,更新机器人状态
if response.Success && response.Data.Status == 2 {
// 开启自动心跳,传递容器访问地址
if robot.WechatID != "" {
_, _ = docker.AutoHeartbeatStart(ctx, robot.ContainerID, robot.WechatID, robot.ContainerHost)
}
// 更新机器人状态
robot.Status = model.RobotStatusOnline
now := time.Now()
robot.LastLoginAt = &now
db.Save(&robot)
}
return c.JSON(fiber.Map{
"success": response.Success,
"status": response.Data.Status,
"message": response.Message,
})
}

View File

@ -0,0 +1,38 @@
package handler
import (
"runtime"
"time"
"gitee.ltd/lxh/wechat-robot/internal/model"
"github.com/gofiber/fiber/v2"
)
// HealthCheck 提供容器健康检查接口
func HealthCheck(c *fiber.Ctx) error {
// 检查数据库连接
var count int64
dbErr := model.GetDB().Raw("SELECT 1").Count(&count).Error
// 获取运行时信息
var m runtime.MemStats
runtime.ReadMemStats(&m)
return c.JSON(fiber.Map{
"status": "ok",
"message": "service is healthy",
"timestamp": time.Now().Format(time.RFC3339),
"database": map[string]interface{}{
"connected": dbErr == nil,
},
"system": map[string]interface{}{
"goroutines": runtime.NumGoroutine(),
"memory": map[string]interface{}{
"alloc": m.Alloc / 1024 / 1024,
"total_alloc": m.TotalAlloc / 1024 / 1024,
"sys": m.Sys / 1024 / 1024,
"gc_cycles": m.NumGC,
},
},
})
}

View File

@ -0,0 +1,43 @@
package middleware
import (
"log"
"time"
"github.com/gofiber/fiber/v2"
)
// DebugMiddleware 用于记录更详细的请求信息以便排查问题
func DebugMiddleware() fiber.Handler {
return func(c *fiber.Ctx) error {
// 记录开始时间
start := time.Now()
// 输出请求详情
log.Printf("[DEBUG] 收到请求: %s %s", c.Method(), c.Path())
log.Printf("[DEBUG] 查询参数: %s", c.Request().URI().QueryArgs().String())
log.Printf("[DEBUG] 请求头: %v", c.GetReqHeaders())
// 处理请求
err := c.Next()
// 请求结束,计算耗时
duration := time.Since(start)
status := c.Response().StatusCode()
if err != nil {
log.Printf("[DEBUG] 请求错误: %s %s - %d - %v - %s",
c.Method(), c.Path(), status, err, duration)
} else {
log.Printf("[DEBUG] 请求完成: %s %s - %d - %s",
c.Method(), c.Path(), status, duration)
}
if status == 500 {
log.Printf("[ERROR] 内部服务器错误: %s %s - %v",
c.Method(), c.Path(), err)
}
return err
}
}

View File

@ -0,0 +1,187 @@
<div class="bg-white shadow rounded-lg p-6">
<h1 class="text-2xl font-bold text-gray-800 mb-6">API 文档</h1>
<div class="mb-10">
<p class="text-gray-600 mb-4">
微信机器人管理系统提供了一系列API接口方便您进行系统集成或自动化操作。
所有API都需要认证请在请求头中添加<code class="bg-gray-100 px-2 py-1 rounded">Authorization</code>头部。
</p>
<div class="bg-gray-100 p-4 rounded-md">
<h3 class="text-lg font-medium text-gray-800 mb-2">认证方式</h3>
<p class="mb-2">在HTTP请求头中添加</p>
<pre class="bg-gray-800 text-white p-3 rounded-md overflow-x-auto">
Authorization: Bearer YOUR_API_TOKEN</pre>
<p class="mt-2 text-sm text-gray-600">API Token可在系统设置中生成和管理。</p>
</div>
</div>
<h2 class="text-xl font-bold text-gray-800 mb-4 mt-10">可用API端点</h2>
<div class="space-y-8">
<!-- 机器人相关API -->
<div class="border-b pb-6">
<h3 class="text-lg font-medium text-gray-800 mb-4 flex items-center">
<i class="fas fa-robot mr-2 text-indigo-500"></i>机器人管理
</h3>
<!-- GET /api/robots -->
<div class="api-endpoint mb-6">
<div class="flex items-center mb-2">
<span class="inline-block px-2 py-1 text-xs font-medium bg-green-100 text-green-800 rounded-md mr-3">GET</span>
<span class="font-mono text-sm">/api/v1/robots</span>
</div>
<p class="text-gray-600 mb-2">获取所有机器人列表</p>
<div class="mt-2">
<button class="text-xs text-indigo-600 hover:text-indigo-800" onclick="toggleExample('get-robots-example')">
查看示例 <i class="fas fa-chevron-down ml-1"></i>
</button>
<div id="get-robots-example" class="hidden mt-2">
<pre class="bg-gray-800 text-white text-xs p-3 rounded-md overflow-x-auto">
{
"success": true,
"data": [
{
"id": 1,
"nickname": "客服机器人",
"container_id": "abcdef123456",
"status": "online",
"wechat_id": "wxid_12345",
"created_at": "2025-01-01T12:00:00Z",
"last_login_at": "2025-01-02T12:00:00Z"
},
{
"id": 2,
"nickname": "营销机器人",
"container_id": "ghijkl789012",
"status": "offline",
"wechat_id": null,
"created_at": "2025-01-01T14:00:00Z",
"last_login_at": null
}
]
}</pre>
</div>
</div>
</div>
<!-- GET /api/robots/:id -->
<div class="api-endpoint mb-6">
<div class="flex items-center mb-2">
<span class="inline-block px-2 py-1 text-xs font-medium bg-green-100 text-green-800 rounded-md mr-3">GET</span>
<span class="font-mono text-sm">/api/v1/robots/:id</span>
</div>
<p class="text-gray-600 mb-2">获取指定机器人的详细信息</p>
<div class="mt-2">
<button class="text-xs text-indigo-600 hover:text-indigo-800" onclick="toggleExample('get-robot-example')">
查看示例 <i class="fas fa-chevron-down ml-1"></i>
</button>
<div id="get-robot-example" class="hidden mt-2">
<pre class="bg-gray-800 text-white text-xs p-3 rounded-md overflow-x-auto">
{
"success": true,
"data": {
"id": 1,
"nickname": "客服机器人",
"container_id": "abcdef123456",
"container_host": "localhost:9001",
"status": "online",
"wechat_id": "wxid_12345",
"avatar": "base64-encoded-avatar",
"created_at": "2025-01-01T12:00:00Z",
"last_login_at": "2025-01-02T12:00:00Z"
}
}</pre>
</div>
</div>
</div>
<!-- POST /api/robots -->
<div class="api-endpoint">
<div class="flex items-center mb-2">
<span class="inline-block px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 rounded-md mr-3">POST</span>
<span class="font-mono text-sm">/api/v1/robots</span>
</div>
<p class="text-gray-600 mb-2">创建新机器人</p>
<div class="mt-2">
<button class="text-xs text-indigo-600 hover:text-indigo-800" onclick="toggleExample('post-robot-example')">
查看示例 <i class="fas fa-chevron-down ml-1"></i>
</button>
<div id="post-robot-example" class="hidden mt-2">
<p class="text-sm font-medium mb-1">请求体:</p>
<pre class="bg-gray-800 text-white text-xs p-3 rounded-md overflow-x-auto">
{
"name": "新机器人",
"port": 9002 // 可选,容器端口映射
}</pre>
<p class="text-sm font-medium mt-3 mb-1">响应:</p>
<pre class="bg-gray-800 text-white text-xs p-3 rounded-md overflow-x-auto">
{
"success": true,
"data": {
"id": 3,
"nickname": "新机器人",
"container_id": "mnopqr345678",
"container_host": "localhost:9002",
"status": "offline",
"created_at": "2025-01-03T10:00:00Z"
}
}</pre>
</div>
</div>
</div>
</div>
<!-- 联系人相关API -->
<div class="border-b pb-6">
<h3 class="text-lg font-medium text-gray-800 mb-4 flex items-center">
<i class="fas fa-address-book mr-2 text-indigo-500"></i>联系人管理
</h3>
<!-- GET /api/robots/:id/contacts -->
<div class="api-endpoint mb-6">
<div class="flex items-center mb-2">
<span class="inline-block px-2 py-1 text-xs font-medium bg-green-100 text-green-800 rounded-md mr-3">GET</span>
<span class="font-mono text-sm">/api/v1/robots/:id/contacts</span>
</div>
<p class="text-gray-600">获取机器人的联系人列表</p>
</div>
<!-- GET /api/contacts/:id/messages -->
<div class="api-endpoint">
<div class="flex items-center mb-2">
<span class="inline-block px-2 py-1 text-xs font-medium bg-green-100 text-green-800 rounded-md mr-3">GET</span>
<span class="font-mono text-sm">/api/v1/contacts/:id/messages</span>
</div>
<p class="text-gray-600">获取与联系人的聊天记录</p>
</div>
</div>
<!-- 消息相关API -->
<div class="border-b pb-6">
<h3 class="text-lg font-medium text-gray-800 mb-4 flex items-center">
<i class="fas fa-comment mr-2 text-indigo-500"></i>消息管理
</h3>
<!-- POST /api/contacts/:id/messages -->
<div class="api-endpoint">
<div class="flex items-center mb-2">
<span class="inline-block px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 rounded-md mr-3">POST</span>
<span class="font-mono text-sm">/api/v1/contacts/:id/messages</span>
</div>
<p class="text-gray-600">发送消息给联系人</p>
</div>
</div>
</div>
</div>
<script>
function toggleExample(elementId) {
const element = document.getElementById(elementId);
if (element.classList.contains('hidden')) {
element.classList.remove('hidden');
} else {
element.classList.add('hidden');
}
}
</script>

View File

@ -0,0 +1,40 @@
{{/*
使用方法:
{{ embed "components/card" }}
内容放这里
{{ end }}
*/}}
<div class="bg-white rounded-lg shadow-sm overflow-hidden transition-all duration-200 hover:shadow-md mb-6">
{{if or .Title .Icon}}
<div class="p-4 border-b border-gray-100 flex justify-between items-center">
<div class="flex items-center">
{{if .Icon}}
<div class="rounded-full p-2 {{if .IconBg}}{{.IconBg}}{{else}}bg-indigo-100{{end}} mr-3">
<i class="{{.Icon}} {{if .IconColor}}{{.IconColor}}{{else}}text-indigo-600{{end}}"></i>
</div>
{{end}}
{{if .Title}}
<h3 class="font-medium text-gray-800">{{.Title}}</h3>
{{end}}
</div>
{{if .ActionButton}}
<div>
{{.ActionButton}}
</div>
{{end}}
</div>
{{end}}
<div class="p-4">
{{.Content}}
</div>
{{if .Footer}}
<div class="bg-gray-50 px-4 py-3 text-right text-sm text-gray-500 border-t border-gray-100">
{{.Footer}}
</div>
{{end}}
</div>

View File

@ -0,0 +1,140 @@
<!--
使用方法:
{{template "components/data_table.html" (map
"Headers" (slice
(map "Name" "ID" "Key" "id" "Sortable" true "Type" "number")
(map "Name" "名称" "Key" "name" "Sortable" true)
(map "Name" "创建日期" "Key" "created_at" "Sortable" true "Type" "date")
(map "Name" "状态" "Key" "status")
(map "Name" "操作" "Actions" true)
)
"Data" .Items
"EmptyMessage" "没有找到数据"
"ID" "my-table"
)}}
-->
<div class="bg-white overflow-hidden rounded-lg shadow-sm">
<div class="overflow-x-auto">
<table id="{{if .ID}}{{.ID}}{{else}}data-table{{end}}" class="data-table w-full">
<thead>
<tr class="bg-gray-50 text-left">
{{range .Headers}}
<th class="px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider{{if .Sortable}} sortable{{if .Type}} sort-{{.Type}}{{end}}{{end}}">
{{.Name}}
</th>
{{end}}
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
{{if .Data}}
{{range $item := .Data}}
<tr class="hover:bg-gray-50 transition-colors">
{{range $.Headers}}
{{if .Actions}}
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<div class="flex items-center space-x-2">
{{if $item.ViewLink}}
<a href="{{$item.ViewLink}}" class="text-indigo-600 hover:text-indigo-900">
<i class="fas fa-eye"></i>
</a>
{{end}}
{{if $item.EditLink}}
<a href="{{$item.EditLink}}" class="text-blue-600 hover:text-blue-900">
<i class="fas fa-edit"></i>
</a>
{{end}}
{{if $item.DeleteLink}}
<a href="{{$item.DeleteLink}}" class="text-red-600 hover:text-red-900" data-confirm="确定要删除这项吗?" data-confirm-type="danger">
<i class="fas fa-trash-alt"></i>
</a>
{{end}}
{{if $item.CustomActions}}
{{$item.CustomActions}}
{{end}}
</div>
</td>
{{else}}
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{if eq .Key "status"}}
{{if eq (index $item .Key) "online"}}
<span class="px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
在线
</span>
{{else if eq (index $item .Key) "offline"}}
<span class="px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800">
离线
</span>
{{else if eq (index $item .Key) "error"}}
<span class="px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">
错误
</span>
{{else}}
{{index $item .Key}}
{{end}}
{{else}}
{{index $item .Key}}
{{end}}
</td>
{{end}}
{{end}}
</tr>
{{end}}
{{else}}
<tr>
<td colspan="{{len .Headers}}" class="px-6 py-10 text-center text-gray-500">
<div class="flex flex-col items-center justify-center">
<svg class="w-12 h-12 text-gray-300 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>{{if .EmptyMessage}}{{.EmptyMessage}}{{else}}没有找到数据{{end}}</span>
</div>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{if and .Pagination .Pagination.TotalPages}}
<div class="bg-white px-4 py-3 border-t border-gray-200 sm:px-6">
<nav class="flex items-center justify-between">
<div class="hidden sm:block">
<p class="text-sm text-gray-700">
显示第
<span class="font-medium">{{.Pagination.FirstItem}}</span>
<span class="font-medium">{{.Pagination.LastItem}}</span>
条,共
<span class="font-medium">{{.Pagination.Total}}</span>
条记录
</p>
</div>
<div class="flex-1 flex justify-between sm:justify-end space-x-1">
{{if .Pagination.HasPrev}}
<a href="{{.Pagination.PrevURL}}" class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
上一页
</a>
{{else}}
<span class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-300 bg-gray-50 cursor-not-allowed">
上一页
</span>
{{end}}
{{if .Pagination.HasNext}}
<a href="{{.Pagination.NextURL}}" class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
下一页
</a>
{{else}}
<span class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-300 bg-gray-50 cursor-not-allowed">
下一页
</span>
{{end}}
</div>
</nav>
</div>
{{end}}
</div>

View File

@ -0,0 +1,41 @@
<div class="loading-overlay fixed inset-0 z-50 flex items-center justify-center bg-white bg-opacity-75 transition-opacity duration-300" style="opacity: 0; visibility: hidden;">
<div class="text-center">
<div class="inline-block">
<svg class="animate-spin h-10 w-10 text-indigo-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
<div class="mt-4 text-indigo-600 font-medium text-lg">加载中...</div>
</div>
</div>
<script>
// 创建全局加载器
window.Loader = {
show: function(message) {
const loader = document.querySelector('.loading-overlay');
if (loader) {
// 设置自定义消息
if (message) {
const messageEl = loader.querySelector('.text-lg');
if (messageEl) messageEl.textContent = message;
}
// 显示加载器
loader.style.opacity = '1';
loader.style.visibility = 'visible';
}
},
hide: function() {
const loader = document.querySelector('.loading-overlay');
if (loader) {
loader.style.opacity = '0';
setTimeout(() => {
loader.style.visibility = 'hidden';
}, 300);
}
}
};
</script>

View File

@ -0,0 +1,62 @@
<!--
用法示例:
<button onclick="App.openModal('example-modal')">打开弹窗</button>
<div id="example-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>示例标题</h3>
<button class="modal-close" onclick="App.closeModal('example-modal')">×</button>
</div>
<div class="modal-body">
<p>这是一个示例弹窗内容</p>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="App.closeModal('example-modal')">取消</button>
<button class="btn-primary">确认</button>
</div>
</div>
</div>
-->
<style>
.modal {
@apply fixed inset-0 z-50 overflow-y-auto hidden;
}
.modal.is-active {
@apply block;
}
.modal-backdrop {
@apply fixed inset-0 bg-black bg-opacity-50 transition-opacity;
}
.modal-container {
@apply flex min-h-screen items-end justify-center py-4 px-4 text-center sm:block sm:p-0;
}
.modal-content {
@apply relative inline-block w-full max-w-lg bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full;
}
.modal-header {
@apply px-6 py-4 border-b border-gray-200 flex justify-between items-center;
}
.modal-header h3 {
@apply text-lg font-medium text-gray-800;
}
.modal-close {
@apply text-gray-400 hover:text-gray-500 focus:outline-none text-xl font-bold;
}
.modal-body {
@apply p-6;
}
.modal-footer {
@apply px-6 py-4 bg-gray-50 flex justify-end space-x-3;
}
</style>

View File

@ -0,0 +1,55 @@
<button id="theme-toggle" class="p-2 rounded-full hover:bg-white/10 transition-colors" aria-label="切换深色模式" title="切换深色模式">
<span id="theme-toggle-light-icon" class="hidden">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" fill-rule="evenodd" clip-rule="evenodd"></path>
</svg>
</span>
<span id="theme-toggle-dark-icon" class="hidden">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
</svg>
</span>
</button>
<script>
// 主题切换功能
document.addEventListener('DOMContentLoaded', function() {
const themeToggleBtn = document.getElementById('theme-toggle');
const lightIcon = document.getElementById('theme-toggle-light-icon');
const darkIcon = document.getElementById('theme-toggle-dark-icon');
// 根据当前主题显示对应图标
function updateThemeIcon() {
const isDarkMode = document.documentElement.getAttribute('data-theme') === 'dark';
lightIcon.classList.toggle('hidden', !isDarkMode);
darkIcon.classList.toggle('hidden', isDarkMode);
}
// 初始化主题
if (!localStorage.theme) {
// 如果用户没有设置主题,则使用系统首选项
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.setAttribute('data-theme', 'dark');
} else {
document.documentElement.setAttribute('data-theme', 'light');
}
} else {
document.documentElement.setAttribute('data-theme', localStorage.theme);
}
// 更新图标
updateThemeIcon();
// 监听按钮点击
themeToggleBtn.addEventListener('click', function() {
const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.theme = newTheme;
// 更新图标
updateThemeIcon();
});
});
</script>

View File

@ -0,0 +1,30 @@
<div class="min-h-[60vh] flex items-center justify-center px-4">
<div class="max-w-md w-full text-center">
<div class="mb-6">
<div class="inline-flex items-center justify-center w-24 h-24 rounded-full bg-indigo-100 text-indigo-500">
<i class="fas fa-map-signs text-5xl"></i>
</div>
</div>
<h1 class="text-4xl font-bold text-gray-800 mb-4">页面未找到</h1>
<p class="text-lg text-gray-600 mb-8">
很抱歉,您要访问的页面不存在或已被移除。
</p>
<div class="flex flex-col sm:flex-row justify-center space-y-3 sm:space-y-0 sm:space-x-3">
<a href="/" class="btn-primary">
<i class="fas fa-home mr-2"></i> 返回首页
</a>
<button onclick="window.history.back()" class="btn-secondary">
<i class="fas fa-arrow-left mr-2"></i> 返回上一页
</button>
</div>
</div>
</div>
<style>
.btn-primary {
@apply inline-flex items-center justify-center px-5 py-3 border border-transparent rounded-md shadow-sm text-base font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors;
}
.btn-secondary {
@apply inline-flex items-center justify-center px-5 py-3 border border-gray-300 rounded-md shadow-sm text-base font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors;
}
</style>

View File

@ -0,0 +1,115 @@
<div class="space-y-6">
<!-- 欢迎区块 -->
<div class="bg-gradient-to-r from-indigo-600 to-indigo-400 rounded-lg shadow-md p-6 text-white">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold">欢迎使用微信机器人管理系统</h1>
<p class="mt-2 text-indigo-100">管理您的微信自动化实例,提升社交效率</p>
</div>
<div class="hidden md:block">
<i class="fas fa-robot text-6xl text-white/50"></i>
</div>
</div>
</div>
<!-- 状态概览 -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="bg-white rounded-lg shadow-sm p-5 transform transition-all duration-200 hover:shadow-md">
<div class="flex items-center">
<div class="p-3 rounded-full bg-indigo-100 text-indigo-600 mr-4">
<i class="fas fa-robot text-xl"></i>
</div>
<div>
<div class="text-gray-500">总机器人</div>
<div class="text-2xl font-semibold">{{.Stats.TotalRobots}}</div>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow-sm p-5 transform transition-all duration-200 hover:shadow-md">
<div class="flex items-center">
<div class="p-3 rounded-full bg-green-100 text-green-600 mr-4">
<i class="fas fa-check-circle text-xl"></i>
</div>
<div>
<div class="text-gray-500">在线机器人</div>
<div class="text-2xl font-semibold">{{.Stats.OnlineRobots}}</div>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow-sm p-5 transform transition-all duration-200 hover:shadow-md">
<div class="flex items-center">
<div class="p-3 rounded-full bg-blue-100 text-blue-600 mr-4">
<i class="fas fa-comments text-xl"></i>
</div>
<div>
<div class="text-gray-500">总消息</div>
<div class="text-2xl font-semibold">{{.Stats.TotalMessages}}</div>
</div>
</div>
</div>
</div>
<!-- 快捷操作 -->
<div class="bg-white rounded-lg shadow-sm p-6">
<h2 class="text-lg font-medium text-gray-800 mb-4">快捷操作</h2>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<a href="/admin/robots/new" class="flex flex-col items-center justify-center p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors">
<div class="p-3 rounded-full bg-indigo-100 mb-3">
<i class="fas fa-plus text-indigo-600"></i>
</div>
<span class="text-sm text-gray-700">新建机器人</span>
</a>
<a href="/admin/robots" class="flex flex-col items-center justify-center p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors">
<div class="p-3 rounded-full bg-blue-100 mb-3">
<i class="fas fa-list text-blue-600"></i>
</div>
<span class="text-sm text-gray-700">机器人列表</span>
</a>
<a href="#" class="flex flex-col items-center justify-center p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors" onclick="App.notify('此功能正在开发中', 'info'); return false;">
<div class="p-3 rounded-full bg-green-100 mb-3">
<i class="fas fa-chart-line text-green-600"></i>
</div>
<span class="text-sm text-gray-700">数据统计</span>
</a>
<a href="#" class="flex flex-col items-center justify-center p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors" onclick="App.notify('此功能正在开发中', 'info'); return false;">
<div class="p-3 rounded-full bg-purple-100 mb-3">
<i class="fas fa-cog text-purple-600"></i>
</div>
<span class="text-sm text-gray-700">系统设置</span>
</a>
</div>
</div>
<!-- 最近活动 -->
<div class="bg-white rounded-lg shadow-sm p-6">
<h2 class="text-lg font-medium text-gray-800 mb-4">最近活动</h2>
{{if .RecentActivities}}
<div class="space-y-4">
{{range .RecentActivities}}
<div class="flex items-start">
<div class="flex-shrink-0 h-10 w-10 rounded-full bg-indigo-100 flex items-center justify-center text-indigo-500">
<i class="fas fa-{{.Icon}}"></i>
</div>
<div class="ml-4 flex-1">
<div class="flex items-center justify-between">
<p class="text-sm font-medium text-gray-900">{{.Title}}</p>
<p class="text-xs text-gray-500">{{.Time}}</p>
</div>
<p class="text-sm text-gray-500">{{.Description}}</p>
</div>
</div>
{{end}}
</div>
{{else}}
<div class="text-center py-8 text-gray-500">
<i class="fas fa-inbox text-5xl mb-3 text-gray-300"></i>
<p>暂无活动记录</p>
</div>
{{end}}
</div>
</div>

View File

@ -0,0 +1,451 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Title}} - 微信自动化管理平台</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background-color: #f8fafc;
}
/* 更精致的背景效果 */
.hero-section {
position: relative;
overflow: hidden;
background-color: white;
}
.hero-dots {
background-image: radial-gradient(#e2e8f0 1px, transparent 1px);
background-size: 20px 20px;
opacity: 0.4;
}
.feature-card {
transition: all 0.3s ease;
}
.feature-card:hover {
transform: translateY(-8px);
}
.animate-float {
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0% { transform: translateY(0px); }
50% { transform: translateY(-15px); }
100% { transform: translateY(0px); }
}
.glass-card {
background: white;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(255, 255, 255, 0.2);
}
</style>
</head>
<body>
<!-- 导航栏 -->
<header class="fixed w-full z-50 bg-white shadow-sm border-b border-gray-100">
<nav class="container mx-auto px-4 py-4 flex items-center justify-between">
<div class="flex items-center">
<div class="text-2xl font-bold text-slate-700 flex items-center">
<i class="fas fa-robot mr-2"></i> WeChat<span class="text-gray-800">Bot</span>
</div>
</div>
<div class="flex items-center space-x-8">
<a href="#features" class="text-gray-700 hover:text-slate-900 transition-colors text-sm font-medium">功能特性</a>
<a href="#use-cases" class="text-gray-700 hover:text-slate-900 transition-colors text-sm font-medium">应用场景</a>
<a href="/login" class="bg-white text-slate-700 border border-slate-300 hover:border-slate-400 py-2.5 px-5 rounded-lg shadow-sm hover:shadow transition-all text-sm font-medium">
登录系统
</a>
</div>
</nav>
</header>
<!-- 英雄区域 - 更简洁精致的设计 -->
<section class="hero-section pt-32 pb-24">
<!-- 装饰背景 -->
<div class="absolute inset-0 hero-dots"></div>
<div class="absolute top-20 right-10 w-64 h-64 rounded-full bg-blue-50 blur-3xl opacity-50"></div>
<div class="absolute bottom-10 left-10 w-64 h-64 rounded-full bg-indigo-50 blur-3xl opacity-50"></div>
<div class="container mx-auto px-4 relative z-10">
<div class="flex flex-col lg:flex-row items-center justify-between">
<div class="lg:w-1/2 mb-10 lg:mb-0 text-center lg:text-left">
<div class="inline-block px-4 py-1 bg-blue-50 text-blue-600 rounded-full text-sm font-medium mb-6">
新一代管理工具
</div>
<h1 class="text-4xl sm:text-5xl lg:text-6xl font-bold mb-6 text-gray-900 leading-tight">
智能微信<br />自动化管理系统
</h1>
<p class="text-xl text-gray-600 mb-10 max-w-lg mx-auto lg:mx-0 leading-relaxed">
提升您的社交效率和业务拓展能力,智能化管理多个微信账号的一站式平台
</p>
<div class="flex flex-col sm:flex-row justify-center lg:justify-start space-y-4 sm:space-y-0 sm:space-x-4">
<a href="/login" class="bg-slate-800 hover:bg-slate-700 text-white py-4 px-8 rounded-lg shadow-md hover:shadow-lg transition-all text-center">
立即体验
</a>
<a href="#features" class="bg-white hover:bg-gray-50 text-slate-800 border border-gray-200 py-4 px-8 rounded-lg shadow-sm hover:shadow transition-all text-center">
了解更多
</a>
</div>
</div>
<div class="lg:w-1/2 flex justify-center lg:justify-end">
<div class="relative">
<div class="absolute -top-10 -right-10 w-64 h-64 rounded-full bg-indigo-50 opacity-60"></div>
<div class="animate-float">
<img src="/public/images/dashboard-preview.png" alt="仪表盘预览" class="rounded-xl shadow-xl max-w-full lg:max-w-md z-10 relative glass-card">
</div>
</div>
</div>
</div>
</div>
<!-- 优雅的分隔线 -->
<div class="absolute bottom-0 left-0 right-0 h-16 bg-gradient-to-b from-transparent to-gray-50"></div>
</section>
<!-- 特性区域 -->
<section id="features" class="py-20 bg-gray-50">
<div class="container mx-auto px-4">
<div class="text-center mb-16">
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 mb-4">强大功能特性</h2>
<p class="text-xl text-gray-600 max-w-3xl mx-auto">我们提供完整的微信自动化管理解决方案,满足多样化的业务需求</p>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
<div class="feature-card bg-white p-8 rounded-2xl shadow-lg hover:shadow-xl">
<div class="w-14 h-14 bg-indigo-100 rounded-xl flex items-center justify-center text-indigo-600 mb-6">
<i class="fas fa-robot text-2xl"></i>
</div>
<h3 class="text-xl font-bold mb-3 text-gray-900">多机器人管理</h3>
<p class="text-gray-600 leading-relaxed">集中管理多个微信账号,实现业务统一调度和集中监控,提高运营效率</p>
</div>
<div class="feature-card bg-white p-8 rounded-2xl shadow-lg hover:shadow-xl">
<div class="w-14 h-14 bg-green-100 rounded-xl flex items-center justify-center text-green-600 mb-6">
<i class="fas fa-comments text-2xl"></i>
</div>
<h3 class="text-xl font-bold mb-3 text-gray-900">智能自动回复</h3>
<p class="text-gray-600 leading-relaxed">设置自动回复规则,让您的社交沟通更高效,提升客户响应速度</p>
</div>
<div class="feature-card bg-white p-8 rounded-2xl shadow-lg hover:shadow-xl">
<div class="w-14 h-14 bg-blue-100 rounded-xl flex items-center justify-center text-blue-600 mb-6">
<i class="fas fa-chart-line text-2xl"></i>
</div>
<h3 class="text-xl font-bold mb-3 text-gray-900">数据分析报表</h3>
<p class="text-gray-600 leading-relaxed">全面分析互动数据,挖掘有价值信息,辅助决策,提升社交效率</p>
</div>
<div class="feature-card bg-white p-8 rounded-2xl shadow-lg hover:shadow-xl">
<div class="w-14 h-14 bg-purple-100 rounded-xl flex items-center justify-center text-purple-600 mb-6">
<i class="fas fa-users text-2xl"></i>
</div>
<h3 class="text-xl font-bold mb-3 text-gray-900">高效群组管理</h3>
<p class="text-gray-600 leading-relaxed">一键管理多个群组,自动化欢迎和群组通知,轻松维护群关系</p>
</div>
<div class="feature-card bg-white p-8 rounded-2xl shadow-lg hover:shadow-xl">
<div class="w-14 h-14 bg-amber-100 rounded-xl flex items-center justify-center text-amber-600 mb-6">
<i class="fas fa-clock text-2xl"></i>
</div>
<h3 class="text-xl font-bold mb-3 text-gray-900">定时任务系统</h3>
<p class="text-gray-600 leading-relaxed">设置消息定时发送,构建自动化工作流程,解放人力,提高效率</p>
</div>
<div class="feature-card bg-white p-8 rounded-2xl shadow-lg hover:shadow-xl">
<div class="w-14 h-14 bg-rose-100 rounded-xl flex items-center justify-center text-rose-600 mb-6">
<i class="fas fa-shield-alt text-2xl"></i>
</div>
<h3 class="text-xl font-bold mb-3 text-gray-900">安全容器隔离</h3>
<p class="text-gray-600 leading-relaxed">采用容器化技术,保障账号安全,数据加密存储,保护隐私</p>
</div>
</div>
</div>
</section>
<!-- 使用场景 -->
<section id="use-cases" class="py-20 bg-white relative">
<!-- 使用更柔和的背景 -->
<div class="absolute left-0 right-0 h-64 bg-gray-50/50 top-0"></div>
<div class="container mx-auto px-4 relative z-10">
<div class="text-center mb-16">
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 mb-4">应用场景</h2>
<p class="text-xl text-gray-600 max-w-3xl mx-auto">适用于各行各业的微信自动化需求,提升管理效率</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div class="bg-white rounded-2xl shadow-lg p-8 transition-all hover:shadow-xl hover:-translate-y-1">
<div class="flex items-center mb-6">
<div class="w-12 h-12 bg-indigo-100 rounded-xl flex items-center justify-center text-indigo-600 mr-4">
<i class="fas fa-store text-xl"></i>
</div>
<h3 class="text-2xl font-bold text-gray-900">客户服务</h3>
</div>
<p class="text-gray-600 mb-6 leading-relaxed">自动化客户咨询回复,提供产品信息和服务支持,大幅提升客户满意度与服务效率</p>
<ul class="space-y-3">
<li class="flex items-center text-gray-700">
<span class="w-6 h-6 rounded-full bg-green-100 flex items-center justify-center mr-3 text-green-600">
<i class="fas fa-check text-sm"></i>
</span>
智能分类和处理客户需求
</li>
<li class="flex items-center text-gray-700">
<span class="w-6 h-6 rounded-full bg-green-100 flex items-center justify-center mr-3 text-green-600">
<i class="fas fa-check text-sm"></i>
</span>
24小时无间断自动响应
</li>
<li class="flex items-center text-gray-700">
<span class="w-6 h-6 rounded-full bg-green-100 flex items-center justify-center mr-3 text-green-600">
<i class="fas fa-check text-sm"></i>
</span>
定制化客户服务流程
</li>
</ul>
</div>
<div class="bg-white rounded-2xl shadow-lg p-8 transition-all hover:shadow-xl hover:-translate-y-1">
<div class="flex items-center mb-6">
<div class="w-12 h-12 bg-green-100 rounded-xl flex items-center justify-center text-green-600 mr-4">
<i class="fas fa-bullhorn text-xl"></i>
</div>
<h3 class="text-2xl font-bold text-gray-900">营销推广</h3>
</div>
<p class="text-gray-600 mb-6 leading-relaxed">精准投放营销内容管理营销活动统计用户反馈数据提升转化率和ROI</p>
<ul class="space-y-3">
<!-- 修正这里的语法错误将items中心改为items-center -->
<li class="flex items-center text-gray-700">
<span class="w-6 h-6 rounded-full bg-green-100 flex items-center justify-center mr-3 text-green-600">
<i class="fas fa-check text-sm"></i>
</span>
智能分群精准营销
</li>
<li class="flex items-center text-gray-700">
<span class="w-6 h-6 rounded-full bg-green-100 flex items-center justify-center mr-3 text-green-600">
<i class="fas fa-check text-sm"></i>
</span>
定时推送营销内容
</li>
<li class="flex items-center text-gray-700">
<span class="w-6 h-6 rounded-full bg-green-100 flex items-center justify-center mr-3 text-green-600">
<i class="fas fa-check text-sm"></i>
</span>
营销效果数据分析
</li>
</ul>
</div>
<div class="bg-white rounded-2xl shadow-lg p-8 transition-all hover:shadow-xl hover:-translate-y-1">
<div class="flex items-center mb-6">
<div class="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center text-blue-600 mr-4">
<i class="fas fa-chalkboard-teacher text-xl"></i>
</div>
<h3 class="text-2xl font-bold text-gray-900">教育培训</h3>
</div>
<p class="text-gray-600 mb-6 leading-relaxed">管理学习群组,自动发送学习资料,组织在线答疑和互动,提升学习体验</p>
<ul class="space-y-3">
<li class="flex items-center text-gray-700">
<span class="w-6 h-6 rounded-full bg-green-100 flex items-center justify-center mr-3 text-green-600">
<i class="fas fa-check text-sm"></i>
</span>
智能学习进度跟踪
</li>
<!-- 修正此处的错误:将"items中心"改为"items-center" -->
<li class="flex items-center text-gray-700">
<span class="w-6 h-6 rounded-full bg-green-100 flex items-center justify-center mr-3 text-green-600">
<i class="fas fa-check text-sm"></i>
</span>
自动推送学习资料
</li>
<li class="flex items-center text-gray-700">
<span class="w-6 h-6 rounded-full bg-green-100 flex items-center justify-center mr-3 text-green-600">
<i class="fas fa-check text-sm"></i>
</span>
智能答疑与互动
</li>
</ul>
</div>
<div class="bg-white rounded-2xl shadow-lg p-8 transition-all hover:shadow-xl hover:-translate-y-1">
<div class="flex items-center mb-6">
<div class="w-12 h-12 bg-purple-100 rounded-xl flex items-center justify-center text-purple-600 mr-4">
<i class="fas fa-users-cog text-xl"></i>
</div>
<h3 class="text-2xl font-bold text-gray-900">团队协作</h3>
</div>
<p class="text-gray-600 mb-6 leading-relaxed">自动化团队通知,日程提醒,简化内部沟通流程,提高协作效率</p>
<ul class="space-y-3">
<li class="flex items-center text-gray-700">
<span class="w-6 h-6 rounded-full bg-green-100 flex items-center justify-center mr-3 text-green-600">
<i class="fas fa-check text-sm"></i>
</span>
自动项目进度通知
</li>
<li class="flex items-center text-gray-700">
<span class="w-6 h-6 rounded-full bg-green-100 flex items-center justify-center mr-3 text-green-600">
<i class="fas fa-check text-sm"></i>
</span>
智能会议安排提醒
</li>
<li class="flex items-center text-gray-700">
<span class="w-6 h-6 rounded-full bg-green-100 flex items-center justify-center mr-3 text-green-600">
<i class="fas fa-check text-sm"></i>
</span>
团队任务自动分配
</li>
</ul>
</div>
</div>
</div>
</section>
<!-- 行动号召区域 - 更优雅 -->
<!-- 修复这里的错误:将"bg白"改为"bg-white" -->
<section class="py-20 relative bg-white overflow-hidden">
<!-- 装饰背景 -->
<div class="absolute inset-0 hero-dots opacity-30"></div>
<div class="absolute top-0 right-0 bottom-0 w-1/3 bg-gradient-to-l from-gray-50 to-transparent"></div>
<div class="absolute top-0 left-0 bottom-0 w-1/3 bg-gradient-to-r from-gray-50 to-transparent"></div>
<div class="container mx-auto px-4 relative z-10 text-center">
<div class="max-w-3xl mx-auto">
<h2 class="text-3xl sm:text-4xl font-bold text-gray-900 mb-6">准备好提升微信管理效率了吗?</h2>
<p class="text-xl text-gray-600 mb-8">立即体验我们的平台,享受自动化带来的便利与效率</p>
<a href="/login" class="inline-block bg-slate-800 text-white py-4 px-8 rounded-lg shadow-md font-medium text-lg transition-all hover:shadow-lg hover:-translate-y-1">
立即开始使用
</a>
</div>
</div>
</section>
<!-- 页脚 - 更轻盈的设计 -->
<footer class="bg-gray-100 text-gray-600 py-16 border-t border-gray-200">
<div class="container mx-auto px-4">
<div class="flex flex-col md:flex-row justify-between">
<div class="mb-10 md:mb-0">
<h3 class="text-xl font-bold mb-4 flex items-center text-slate-700">
<i class="fas fa-robot mr-2"></i> WeChat<span class="text-slate-500">Bot</span>
</h3>
<p class="text-gray-500 mb-6 max-w-md">
智能微信自动化管理平台,为企业和个人提供高效的微信账号管理解决方案
</p>
<div class="flex space-x-4">
<a href="#" class="w-10 h-10 rounded-full bg-white border border-gray-200 flex items-center justify-center text-gray-500 hover:bg-gray-50 hover:text-slate-700 transition-all">
<i class="fab fa-weixin"></i>
</a>
<a href="#" class="w-10 h-10 rounded-full bg-white border border-gray-200 flex items-center justify-center text-gray-500 hover:bg-gray-50 hover:text-slate-700 transition-all">
<i class="fab fa-weibo"></i>
</a>
<a href="#" class="w-10 h-10 rounded-full bg-white border border-gray-200 flex items-center justify-center text-gray-500 hover:bg-gray-50 hover:text-slate-700 transition-all">
<i class="fab fa-github"></i>
</a>
</div>
</div>
<div class="grid grid-cols-2 md:grid-cols-3 gap-8">
<div>
<h4 class="text-lg font-semibold mb-4 text-slate-600">产品</h4>
<ul class="space-y-2">
<!-- 修正链接颜色确保在浅色背景下可见hover状态修改为深色 -->
<li><a href="#features" class="text-gray-600 hover:text-gray-900 transition-colors">特性</a></li>
<li><a href="#use-cases" class="text-gray-600 hover:text-gray-900 transition-colors">应用场景</a></li>
<li><a href="/api" class="text-gray-600 hover:text-gray-900 transition-colors">API文档</a></li>
</ul>
</div>
<div>
<h4 class="text-lg font-semibold mb-4 text-slate-600">资源</h4>
<ul class="space-y-2">
<li><a href="#" class="text-gray-600 hover:text-gray-900 transition-colors">开发文档</a></li>
<li><a href="#" class="text-gray-600 hover:text-gray-900 transition-colors">常见问题</a></li>
<li><a href="#" class="text-gray-600 hover:text-gray-900 transition-colors">使用教程</a></li>
</ul>
</div>
<div>
<h4 class="text-lg font-semibold mb-4 text-slate-600">关于我们</h4>
<ul class="space-y-2">
<li><a href="#" class="text-gray-600 hover:text-gray-900 transition-colors">公司介绍</a></li>
<li><a href="#" class="text-gray-600 hover:text-gray-900 transition-colors">联系我们</a></li>
<li><a href="#" class="text-gray-600 hover:text-gray-900 transition-colors">隐私政策</a></li>
</ul>
</div>
</div>
</div>
<div class="border-t border-gray-200 mt-12 pt-8 flex flex-col md:flex-row justify-between items-center">
<p class="text-gray-500">&copy; 2025 WeChatBot 微信机器人管理系统</p>
<p class="text-gray-500 mt-4 md:mt-0"><a href="#" class="text-slate-600 hover:text-slate-800">AI团队</a> 精心打造</p>
</div>
</div>
</footer>
<!-- 回到顶部按钮 - 更新为深色调 -->
<button id="back-to-top" class="fixed bottom-6 right-6 bg-slate-800 text-white p-3 rounded-full shadow-lg opacity-0 invisible transition-all z-50 hover:-translate-y-1 hover:shadow-xl">
<i class="fas fa-arrow-up"></i>
</button>
<script>
// 回到顶部按钮功能
document.addEventListener('DOMContentLoaded', function() {
const backToTopButton = document.getElementById('back-to-top');
window.addEventListener('scroll', function() {
if (window.pageYOffset > 300) {
backToTopButton.classList.remove('opacity-0', 'invisible');
backToTopButton.classList.add('opacity-100', 'visible');
} else {
backToTopButton.classList.remove('opacity-100', 'visible');
backToTopButton.classList.add('opacity-0', 'invisible');
}
});
backToTopButton.addEventListener('click', function() {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
// 平滑滚动到锚点
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function(e) {
e.preventDefault();
const targetId = this.getAttribute('href');
if (targetId === '#') return;
const targetElement = document.querySelector(targetId);
if (targetElement) {
targetElement.scrollIntoView({
behavior: 'smooth'
});
}
});
});
// 导航栏滚动效果
const header = document.querySelector('header');
window.addEventListener('scroll', function() {
if (window.scrollY > 50) {
header.classList.add('shadow-md');
} else {
header.classList.remove('shadow-md');
}
});
});
</script>
</body>
</html>

View File

@ -0,0 +1,7 @@
<!-- 简化后的移动端导航 -->
<script>
// 移除了复杂的导航功能,采用简化设计
document.addEventListener('DOMContentLoaded', function() {
console.log('简化版界面已加载 - 移除了侧边栏功能');
});
</script>

View File

@ -0,0 +1,134 @@
<div id="send-message-modal" class="modal">
<div class="modal-backdrop"></div>
<div class="modal-container">
<div class="modal-content">
<div class="modal-header">
<h3>发送消息</h3>
<button class="modal-close" onclick="App.closeModal('send-message-modal')">×</button>
</div>
<div class="modal-body">
<form id="send-message-form" class="space-y-4">
<input type="hidden" id="contact-id" name="contact_id" value="">
<input type="hidden" id="robot-id" name="robot_id" value="{{.Robot.ID}}">
<div>
<label for="recipient" class="block text-sm font-medium text-gray-700 mb-1">发送给</label>
<div class="flex items-center">
<img id="contact-avatar" src="" alt="Contact" class="w-8 h-8 rounded-full mr-3">
<div>
<div id="contact-name" class="text-sm font-medium">联系人名称</div>
<div id="contact-type" class="text-xs text-gray-500">联系人类型</div>
</div>
</div>
</div>
<div>
<label for="message-type" class="block text-sm font-medium text-gray-700 mb-1">消息类型</label>
<select id="message-type" name="type" class="form-control">
<option value="text" selected>文本</option>
<option value="image">图片</option>
<option value="file">文件</option>
</select>
</div>
<div id="text-content-container">
<label for="message-content" class="block text-sm font-medium text-gray-700 mb-1">消息内容</label>
<textarea id="message-content" name="content" rows="5" class="form-control" placeholder="请输入消息内容..."></textarea>
</div>
<div id="file-content-container" class="hidden">
<label for="file-upload" class="block text-sm font-medium text-gray-700 mb-1">上传文件</label>
<div class="flex items-center justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md">
<div class="space-y-1 text-center">
<svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48" aria-hidden="true">
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<div class="flex justify-center">
<label for="file-upload" class="relative cursor-pointer bg-white rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500">
<span>上传文件</span>
<input id="file-upload" name="file" type="file" class="sr-only">
</label>
</div>
<p class="text-xs text-gray-500">PNG, JPG, PDF, DOC 最大 10MB</p>
</div>
</div>
</div>
<div id="selected-file" class="hidden mt-2">
<div class="flex items-center text-sm">
<i class="fas fa-file mr-2 text-gray-400"></i>
<span id="file-name" class="text-xs text-gray-600 truncate"></span>
<button type="button" id="remove-file" class="ml-2 text-xs text-red-500">
<i class="fas fa-times"></i>
</button>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn-secondary" onclick="App.closeModal('send-message-modal')">取消</button>
<button type="button" class="btn-primary" id="send-message-btn">发送</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 消息类型切换
const messageType = document.getElementById('message-type');
const textContainer = document.getElementById('text-content-container');
const fileContainer = document.getElementById('file-content-container');
messageType.addEventListener('change', function() {
if (this.value === 'text') {
textContainer.classList.remove('hidden');
fileContainer.classList.add('hidden');
} else {
textContainer.classList.add('hidden');
fileContainer.classList.remove('hidden');
}
});
// 文件上传处理
const fileUpload = document.getElementById('file-upload');
const selectedFile = document.getElementById('selected-file');
const fileName = document.getElementById('file-name');
const removeFile = document.getElementById('remove-file');
fileUpload.addEventListener('change', function() {
if (this.files.length > 0) {
fileName.textContent = this.files[0].name;
selectedFile.classList.remove('hidden');
}
});
removeFile.addEventListener('click', function() {
fileUpload.value = '';
selectedFile.classList.add('hidden');
});
// 发送消息按钮处理
document.getElementById('send-message-btn').addEventListener('click', async function() {
// 模拟消息发送
App.notify('消息发送功能开发中', 'info');
App.closeModal('send-message-modal');
});
// 设置联系人信息到模态框
window.setMessageRecipient = function(contactId, name, type, avatar) {
document.getElementById('contact-id').value = contactId;
document.getElementById('contact-name').textContent = name;
document.getElementById('contact-type').textContent = type === 'group' ? '群聊' : '好友';
const avatarImg = document.getElementById('contact-avatar');
if (avatar) {
avatarImg.src = avatar;
} else {
avatarImg.src = type === 'group'
? '/public/images/default-group-avatar.png'
: '/public/images/default-user-avatar.png';
}
};
});
</script>

10
internal/view/test.html Normal file
View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>测试页面</title>
</head>
<body>
<h1>测试页面</h1>
<p>如果您能看到这个页面,说明模板引擎工作正常。</p>
</body>
</html>

112
public/css/dark-mode.css Normal file
View File

@ -0,0 +1,112 @@
/* 深色模式样式 */
html[data-theme='dark'] {
/* 主题色 */
--color-bg-main: #121826;
--color-bg-card: #1e293b;
--color-text-primary: #f3f4f6;
--color-text-secondary: #cbd5e1;
--color-text-muted: #94a3b8;
--color-border: #334155;
/* 色彩重写 */
color-scheme: dark;
}
/* 深色模式主体样式 */
html[data-theme='dark'] body {
background-color: var(--color-bg-main);
color: var(--color-text-primary);
}
/* 深色模式卡片样式 */
html[data-theme='dark'] .bg-white,
html[data-theme='dark'] .card {
background-color: var(--color-bg-card);
border-color: var(--color-border);
}
/* 深色模式表格样式 */
html[data-theme='dark'] .data-table thead {
background-color: #1a2234;
}
html[data-theme='dark'] .data-table tbody tr {
border-color: var(--color-border);
}
html[data-theme='dark'] .data-table tbody tr:hover {
background-color: #273145;
}
/* 深色模式表单样式 */
html[data-theme='dark'] .form-control {
background-color: #1a2234;
border-color: var(--color-border);
color: var(--color-text-primary);
}
html[data-theme='dark'] .form-control:focus {
border-color: #6366f1;
}
html[data-theme='dark'] .form-label {
color: var(--color-text-secondary);
}
/* 深色模式文本样式 */
html[data-theme='dark'] .text-gray-800 {
color: var(--color-text-primary);
}
html[data-theme='dark'] .text-gray-600,
html[data-theme='dark'] .text-gray-700 {
color: var(--color-text-secondary);
}
html[data-theme='dark'] .text-gray-500 {
color: var(--color-text-muted);
}
/* 深色模式按钮样式 */
html[data-theme='dark'] .btn-secondary {
background-color: #334155;
border-color: #475569;
color: var(--color-text-primary);
}
html[data-theme='dark'] .btn-secondary:hover {
background-color: #475569;
}
/* 深色模式模态框 */
html[data-theme='dark'] .modal-content {
background-color: var(--color-bg-card);
}
html[data-theme='dark'] .modal-header,
html[data-theme='dark'] .modal-footer {
border-color: var(--color-border);
}
html[data-theme='dark'] .modal-footer {
background-color: #1a2234;
}
/* 深色模式导航栏 */
html[data-theme='dark'] #mobile-menu .bg-white {
background-color: var(--color-bg-card);
}
html[data-theme='dark'] #mobile-menu .border-gray-200 {
border-color: var(--color-border);
}
html[data-theme='dark'] #mobile-menu .hover\:bg-gray-100:hover {
background-color: #273145;
}
/* 深色模式高亮 */
html[data-theme='dark'] pre,
html[data-theme='dark'] code {
background-color: #0f172a;
}

178
public/css/styles.css Normal file
View File

@ -0,0 +1,178 @@
/* 公共样式 */
.btn {
@apply inline-flex items-center justify-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 transition duration-200;
}
.btn-primary {
@apply bg-indigo-600 hover:bg-indigo-700 text-white focus:ring-indigo-500;
}
.btn-secondary {
@apply bg-white border-gray-300 hover:bg-gray-50 text-gray-700 focus:ring-indigo-500;
}
.btn-success {
@apply bg-green-600 hover:bg-green-700 text-white focus:ring-green-500;
}
.btn-danger {
@apply bg-red-600 hover:bg-red-700 text-white focus:ring-red-500;
}
.btn-warning {
@apply bg-yellow-500 hover:bg-yellow-600 text-white focus:ring-yellow-500;
}
.btn-sm {
@apply px-3 py-1.5 text-xs;
}
.btn-lg {
@apply px-6 py-3 text-base;
}
.btn-icon {
@apply p-2;
}
/* 卡片组件 */
.card {
@apply bg-white rounded-lg shadow-sm transition-all duration-200;
}
.card:hover {
@apply shadow-md;
}
.card-header {
@apply p-4 border-b border-gray-100;
}
.card-body {
@apply p-4;
}
.card-footer {
@apply p-4 bg-gray-50 border-t border-gray-100;
}
/* 表格样式 */
.table {
@apply min-w-full divide-y divide-gray-200;
}
.table th {
@apply px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider;
}
.table td {
@apply px-6 py-4 whitespace-nowrap text-sm text-gray-500;
}
.table tr {
@apply border-b border-gray-100;
}
.table-hover tr:hover {
@apply bg-gray-50;
}
/* 表单样式 */
.form-control {
@apply block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500;
}
.form-label {
@apply block text-sm font-medium text-gray-700 mb-1;
}
.form-group {
@apply mb-4;
}
.form-help {
@apply mt-1 text-xs text-gray-500;
}
.form-error {
@apply mt-1 text-xs text-red-600;
}
/* 标签样式 */
.badge {
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
}
.badge-success {
@apply bg-green-100 text-green-800;
}
.badge-warning {
@apply bg-yellow-100 text-yellow-800;
}
.badge-danger {
@apply bg-red-100 text-red-800;
}
.badge-info {
@apply bg-blue-100 text-blue-800;
}
.badge-default {
@apply bg-gray-100 text-gray-800;
}
/* 动画 */
@keyframes pulse-ring {
0% {
transform: scale(0.8);
opacity: 0.8;
}
70%, 100% {
transform: scale(1.5);
opacity: 0;
}
}
.animate-pulse-ring:before {
content: '';
@apply absolute rounded-full bg-current w-full h-full;
z-index: -1;
animation: pulse-ring 2s cubic-bezier(0.455, 0.03, 0.515, 0.955) infinite;
}
/* 响应式实用工具 */
.responsive-grid {
@apply grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6;
}
/* 聊天气泡 */
.chat-bubble {
@apply max-w-[70%] p-3 rounded-lg;
}
.chat-bubble-outgoing {
@apply bg-indigo-100 rounded-tl-lg rounded-tr-lg rounded-bl-lg ml-auto;
}
.chat-bubble-incoming {
@apply bg-gray-100 rounded-tl-lg rounded-tr-lg rounded-br-lg;
}
/* 高级导航 */
.nav-tabs {
@apply flex border-b border-gray-200;
}
.nav-tab {
@apply py-2 px-4 text-center border-b-2 font-medium text-sm;
}
.nav-tab-active {
@apply border-indigo-500 text-indigo-600;
}
.nav-tab-inactive {
@apply border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300;
}

79
public/css/table.css Normal file
View File

@ -0,0 +1,79 @@
/* 现代化数据表格样式 */
.data-table {
@apply w-full bg-white shadow-sm rounded-lg overflow-hidden;
}
.data-table thead {
@apply bg-gray-50;
}
.data-table th {
@apply px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider text-left;
}
.data-table th.sortable {
@apply cursor-pointer hover:bg-gray-100 transition-colors duration-150;
}
.data-table th.sortable::after {
content: "\f0dc"; /* fa-sort */
font-family: "Font Awesome 6 Free";
font-weight: 900;
@apply ml-1 text-gray-400;
}
.data-table th.sortable.sort-asc::after {
content: "\f0de"; /* fa-sort-up */
@apply text-indigo-500;
}
.data-table th.sortable.sort-desc::after {
content: "\f0dd"; /* fa-sort-down */
@apply text-indigo-500;
}
.data-table td {
@apply px-6 py-4 whitespace-nowrap text-sm text-gray-500;
}
.data-table tbody tr {
@apply border-b border-gray-200 transition-colors duration-150;
}
.data-table tbody tr:hover {
@apply bg-gray-50;
}
.data-table tbody tr:last-child {
@apply border-b-0;
}
/* 空数据状态 */
.data-table-empty {
@apply py-12 text-center;
}
/* 表格分页控件 */
.table-pagination {
@apply flex items-center justify-between py-3 px-4 bg-white border-t border-gray-200 text-sm text-gray-700;
}
.table-pagination-info {
@apply text-sm text-gray-500;
}
.table-pagination-controls {
@apply flex space-x-1;
}
.pagination-button {
@apply px-3 py-1 rounded-md border border-gray-300 bg-white hover:bg-gray-50 focus:outline-none focus:ring-1 focus:ring-indigo-500 transition-colors;
}
.pagination-button.active {
@apply bg-indigo-50 text-indigo-700 border-indigo-300;
}
.pagination-button.disabled {
@apply opacity-50 cursor-not-allowed;
}

50
public/css/theme.css Normal file
View File

@ -0,0 +1,50 @@
:root {
/* 主题色 */
--color-primary: #4f46e5;
--color-primary-dark: #4338ca;
--color-primary-light: #6366f1;
--color-secondary: #64748b;
--color-success: #10b981;
--color-warning: #f59e0b;
--color-danger: #ef4444;
--color-info: #3b82f6;
/* 背景色 */
--color-bg-main: #f5f7fa;
--color-bg-card: #ffffff;
--color-bg-sidebar: #1e293b;
/* 文本色 */
--color-text-primary: #1e293b;
--color-text-secondary: #64748b;
--color-text-muted: #94a3b8;
--color-text-light: #ffffff;
/* 边框 */
--color-border: #e2e8f0;
--border-radius-sm: 0.25rem;
--border-radius: 0.375rem;
--border-radius-lg: 0.5rem;
/* 阴影 */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
/* 过渡 */
--transition-fast: 150ms;
--transition-normal: 300ms;
--transition-slow: 500ms;
/* 布局 */
--content-width: 1280px;
--header-height: 64px;
--sidebar-width: 240px;
}
@media (prefers-color-scheme: dark) {
:root {
/* 暗色主题变量可在此定义 */
}
}

243
public/js/app.js Normal file
View File

@ -0,0 +1,243 @@
/**
* 全局应用函数
*/
// 核心工具函数
const App = {
/**
* 初始化应用
*/
init() {
this.setupEventListeners();
this.setupTheme();
},
/**
* 设置事件监听器
*/
setupEventListeners() {
// 全局事件委托
document.addEventListener('click', (e) => {
// 处理移动导航菜单切换
if (e.target.matches('#mobile-menu-toggle') || e.target.closest('#mobile-menu-toggle')) {
this.toggleMobileMenu();
}
// 处理通知关闭按钮
if (e.target.matches('.close-notification') || e.target.closest('.close-notification')) {
const notification = e.target.closest('.notification');
if (notification) this.closeNotification(notification);
}
// 处理模态框关闭
if (e.target.matches('.modal-overlay')) {
const modalId = e.target.dataset.modalId;
if (modalId) this.closeModal(modalId);
}
});
// 响应键盘事件
document.addEventListener('keydown', (e) => {
// ESC键关闭模态框
if (e.key === 'Escape') {
const modal = document.querySelector('.modal.is-active');
if (modal) {
const modalId = modal.id;
this.closeModal(modalId);
}
}
});
},
/**
* 设置主题相关功能
*/
setupTheme() {
// 读取用户偏好
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
document.documentElement.setAttribute('data-theme', savedTheme);
}
// 监听主题切换按钮
document.querySelectorAll('[data-toggle-theme]').forEach(btn => {
btn.addEventListener('click', () => {
const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
});
});
},
/**
* 切换移动菜单
*/
toggleMobileMenu() {
const mobileMenu = document.getElementById('mobile-menu');
if (mobileMenu) {
const isActive = mobileMenu.classList.contains('active');
if (isActive) {
mobileMenu.classList.remove('active');
mobileMenu.classList.add('inactive');
} else {
mobileMenu.classList.remove('inactive');
mobileMenu.classList.add('active');
}
}
},
/**
* 显示通知
* @param {string} message - 通知消息
* @param {string} type - 通知类型 (success, error, warning, info)
* @param {number} duration - 显示时长(ms)0为不自动关闭
*/
notify(message, type = 'info', duration = 5000) {
// 如果已存在通知容器则使用,否则创建
let container = document.getElementById('notification-container');
if (!container) {
container = document.createElement('div');
container.id = 'notification-container';
container.className = 'fixed top-4 right-4 z-50 flex flex-col items-end space-y-2';
document.body.appendChild(container);
}
// 创建通知元素
const id = 'notification-' + Date.now();
const notification = document.createElement('div');
// 设置通知样式
notification.id = id;
notification.className = `notification bg-white shadow-md rounded-md p-4 transform translate-x-full transition-transform duration-300 max-w-sm flex items-start border-l-4 ${this._getNotificationColorClass(type)}`;
// 设置通知内容
notification.innerHTML = `
<div class="${this._getNotificationIconClass(type)} w-5 h-5 mr-3 flex-shrink-0"></div>
<div class="flex-grow mr-2">
<p class="text-sm text-gray-800">${message}</p>
</div>
<button class="close-notification text-gray-400 hover:text-gray-600 flex-shrink-0 focus:outline-none">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
</button>
`;
// 添加到容器
container.appendChild(notification);
// 激活动画
setTimeout(() => {
notification.classList.remove('translate-x-full');
notification.classList.add('translate-x-0');
}, 10);
// 设置自动关闭
if (duration > 0) {
setTimeout(() => {
this.closeNotification(notification);
}, duration);
}
return id;
},
/**
* 关闭通知
* @param {HTMLElement|string} notification - 通知元素或ID
*/
closeNotification(notification) {
// 如果传入字符串ID则获取对应元素
if (typeof notification === 'string') {
notification = document.getElementById(notification);
if (!notification) return;
}
// 动画关闭
notification.classList.remove('translate-x-0');
notification.classList.add('translate-x-full');
// 移除元素
setTimeout(() => {
if (notification && notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300);
},
/**
* 获取通知颜色类名
* @private
*/
_getNotificationColorClass(type) {
switch (type) {
case 'success': return 'border-green-500';
case 'error': return 'border-red-500';
case 'warning': return 'border-yellow-500';
default: return 'border-blue-500';
}
},
/**
* 获取通知图标类名
* @private
*/
_getNotificationIconClass(type) {
const baseClass = 'text-white rounded-full p-1 flex items-center justify-center';
switch (type) {
case 'success':
return `${baseClass} bg-green-500`;
case 'error':
return `${baseClass} bg-red-500`;
case 'warning':
return `${baseClass} bg-yellow-500`;
default:
return `${baseClass} bg-blue-500`;
}
},
/**
* 打开模态框
* @param {string} id - 模态框ID
*/
openModal(id) {
const modal = document.getElementById(id);
if (!modal) return;
modal.classList.add('is-active');
document.body.classList.add('modal-open');
// 创建动画效果
modal.querySelector('.modal-content').classList.add('scale-100', 'opacity-100');
modal.querySelector('.modal-content').classList.remove('scale-95', 'opacity-0');
},
/**
* 关闭模态框
* @param {string} id - 模态框ID
*/
closeModal(id) {
const modal = document.getElementById(id);
if (!modal) return;
const content = modal.querySelector('.modal-content');
content.classList.remove('scale-100', 'opacity-100');
content.classList.add('scale-95', 'opacity-0');
// 等待动画完成后隐藏
setTimeout(() => {
modal.classList.remove('is-active');
document.body.classList.remove('modal-open');
}, 200);
}
};
// 初始化
document.addEventListener('DOMContentLoaded', () => {
App.init();
});

219
public/js/components.js Normal file
View File

@ -0,0 +1,219 @@
/**
* 共享UI组件库
*/
// 显示通知消息
function showNotification(message, type = 'success', duration = 3000) {
const notificationContainer = document.getElementById('notification-container') || createNotificationContainer();
const notification = document.createElement('div');
notification.className = `notification notification-${type} flex items-center p-4 mb-3 rounded-md shadow-md transform transition-all duration-300 ease-in-out translate-x-full`;
// 设置图标
let icon = '';
switch(type) {
case 'success':
icon = '<svg class="w-5 h-5 mr-3 text-green-500" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path></svg>';
break;
case 'warning':
icon = '<svg class="w-5 h-5 mr-3 text-yellow-500" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v4a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>';
break;
case 'error':
icon = '<svg class="w-5 h-5 mr-3 text-red-500" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path></svg>';
break;
default:
icon = '<svg class="w-5 h-5 mr-3 text-blue-500" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path></svg>';
}
notification.innerHTML = `
${icon}
<span class="flex-grow">${message}</span>
<button class="ml-3 text-gray-400 hover:text-gray-600 focus:outline-none">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</button>
`;
notificationContainer.appendChild(notification);
// 关闭按钮功能
notification.querySelector('button').addEventListener('click', () => {
closeNotification(notification);
});
// 显示动画
setTimeout(() => {
notification.classList.remove('translate-x-full');
notification.classList.add('translate-x-0');
}, 10);
// 自动关闭
if (duration) {
setTimeout(() => {
closeNotification(notification);
}, duration);
}
return notification;
}
function closeNotification(notification) {
notification.classList.remove('translate-x-0');
notification.classList.add('translate-x-full');
setTimeout(() => {
notification.remove();
}, 300);
}
function createNotificationContainer() {
const container = document.createElement('div');
container.id = 'notification-container';
container.className = 'fixed top-4 right-4 z-50 w-80 flex flex-col items-end';
document.body.appendChild(container);
return container;
}
// 确认对话框
function confirmDialog(message, options = {}) {
return new Promise((resolve) => {
const defaults = {
title: '确认操作',
confirmText: '确认',
cancelText: '取消',
type: 'question' // question, warning, danger
};
const settings = {...defaults, ...options};
// 创建遮罩
const overlay = document.createElement('div');
overlay.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';
// 选择颜色
let colorClass = 'text-blue-600';
let buttonClass = 'bg-blue-600 hover:bg-blue-700';
if (settings.type === 'warning') {
colorClass = 'text-yellow-600';
buttonClass = 'bg-yellow-600 hover:bg-yellow-700';
} else if (settings.type === 'danger') {
colorClass = 'text-red-600';
buttonClass = 'bg-red-600 hover:bg-red-700';
}
// 创建对话框
overlay.innerHTML = `
<div class="bg-white rounded-lg shadow-xl overflow-hidden max-w-md w-full mx-4 transform transition-all scale-95 opacity-0">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium ${colorClass}">${settings.title}</h3>
</div>
<div class="p-6">
<p class="text-gray-700">${message}</p>
</div>
<div class="px-6 py-3 bg-gray-50 flex justify-end space-x-2">
<button id="cancel-btn" class="px-4 py-2 bg-gray-200 hover:bg-gray-300 text-gray-800 rounded-md">
${settings.cancelText}
</button>
<button id="confirm-btn" class="px-4 py-2 ${buttonClass} text-white rounded-md">
${settings.confirmText}
</button>
</div>
</div>
`;
document.body.appendChild(overlay);
// 显示动画
setTimeout(() => {
const dialog = overlay.querySelector('div.bg-white');
dialog.classList.remove('scale-95', 'opacity-0');
dialog.classList.add('scale-100', 'opacity-100');
}, 10);
// 事件处理
const closeDialog = (result) => {
const dialog = overlay.querySelector('div.bg-white');
dialog.classList.remove('scale-100', 'opacity-100');
dialog.classList.add('scale-95', 'opacity-0');
setTimeout(() => {
overlay.remove();
resolve(result);
}, 200);
};
overlay.querySelector('#confirm-btn').addEventListener('click', () => closeDialog(true));
overlay.querySelector('#cancel-btn').addEventListener('click', () => closeDialog(false));
});
}
// 表格排序
function initTableSort() {
document.querySelectorAll('table.sortable').forEach(table => {
table.querySelectorAll('th.sortable').forEach(header => {
header.addEventListener('click', () => {
const index = Array.from(header.parentNode.children).indexOf(header);
const isNumeric = header.classList.contains('sort-numeric');
const isDate = header.classList.contains('sort-date');
const isAsc = !header.classList.contains('sort-asc');
// 清除所有表头的排序状态
table.querySelectorAll('th.sortable').forEach(th => {
th.classList.remove('sort-asc', 'sort-desc');
});
// 设置当前表头的排序状态
header.classList.add(isAsc ? 'sort-asc' : 'sort-desc');
// 获取并排序行
const tbody = table.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
rows.sort((a, b) => {
let aValue = a.children[index].textContent.trim();
let bValue = b.children[index].textContent.trim();
if (isNumeric) {
aValue = parseFloat(aValue);
bValue = parseFloat(bValue);
return isAsc ? aValue - bValue : bValue - aValue;
} else if (isDate) {
aValue = new Date(aValue);
bValue = new Date(bValue);
return isAsc ? aValue - bValue : bValue - aValue;
} else {
return isAsc ?
aValue.localeCompare(bValue) :
bValue.localeCompare(aValue);
}
});
// 移除现有行并按排序顺序重新添加
rows.forEach(row => tbody.appendChild(row));
});
});
});
}
// 初始化时绑定组件行为
document.addEventListener('DOMContentLoaded', function() {
// 初始化排序表格
initTableSort();
// 绑定确认删除按钮
document.querySelectorAll('[data-confirm]').forEach(button => {
button.addEventListener('click', async (e) => {
e.preventDefault();
const message = button.dataset.confirm || '确定要执行此操作吗?';
const type = button.dataset.confirmType || 'warning';
const confirmed = await confirmDialog(message, { type });
if (confirmed) {
if (button.tagName === 'A') {
window.location.href = button.href;
} else if (button.form) {
button.form.submit();
}
}
});
});
});

86
public/js/nav.js Normal file
View File

@ -0,0 +1,86 @@
/**
* 移动端导航管理脚本
*/
document.addEventListener('DOMContentLoaded', function() {
// 导航元素
const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
const closeMobileMenu = document.getElementById('close-mobile-menu');
const mobileMenu = document.getElementById('mobile-menu');
// 如果移动菜单元素不存在,不执行后续代码
if (!mobileMenu) return;
// 获取遮罩层
const overlay = mobileMenu.querySelector('.opacity-50');
// 打开菜单函数
function openMobileMenu() {
mobileMenu.classList.remove('translate-x-full');
mobileMenu.classList.add('translate-x-0');
document.body.classList.add('overflow-hidden');
// 淡入遮罩
if (overlay) {
overlay.classList.remove('opacity-0');
overlay.classList.add('opacity-50');
}
}
// 关闭菜单函数
function closeMobileMenu() {
mobileMenu.classList.remove('translate-x-0');
mobileMenu.classList.add('translate-x-full');
document.body.classList.remove('overflow-hidden');
// 淡出遮罩
if (overlay) {
overlay.classList.remove('opacity-50');
overlay.classList.add('opacity-0');
}
}
// 绑定打开菜单事件
if (mobileMenuToggle) {
mobileMenuToggle.addEventListener('click', openMobileMenu);
}
// 绑定关闭菜单事件
if (closeMobileMenu) {
closeMobileMenu.addEventListener('click', closeMobileMenu);
}
// 点击遮罩关闭菜单
if (overlay) {
overlay.addEventListener('click', closeMobileMenu);
}
// 菜单项激活状态
const currentPath = window.location.pathname;
const menuItems = mobileMenu.querySelectorAll('a[href]');
menuItems.forEach(item => {
const itemPath = item.getAttribute('href');
// 路径完全匹配或者是子路径
if (currentPath === itemPath ||
(itemPath !== '/' && currentPath.startsWith(itemPath))) {
item.classList.add('bg-indigo-50', 'text-indigo-700', 'font-medium');
item.classList.remove('hover:bg-gray-100');
// 如果有图标,也更新图标颜色
const icon = item.querySelector('i');
if (icon) {
icon.classList.remove('text-gray-400');
icon.classList.add('text-indigo-600');
}
}
});
// 监听ESC键关闭菜单
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' &&
mobileMenu &&
!mobileMenu.classList.contains('translate-x-full')) {
closeMobileMenu();
}
});
});

View File

@ -0,0 +1,161 @@
/**
* 机器人管理仪表盘交互脚本
*/
document.addEventListener('DOMContentLoaded', function() {
// 初始化机器人卡片动效
initRobotCards();
// 初始化搜索和过滤功能
initSearchAndFilter();
// 初始化刷新动效
initRefreshAnimation();
// 初始化状态统计
updateStatusCounts();
});
/**
* 初始化机器人卡片交互效果
*/
function initRobotCards() {
const robotCards = document.querySelectorAll('.robot-card');
robotCards.forEach(card => {
// 添加悬停效果
card.addEventListener('mouseenter', function() {
this.classList.add('transform', 'scale-102');
});
card.addEventListener('mouseleave', function() {
this.classList.remove('transform', 'scale-102');
});
// 为删除按钮添加确认对话框
const deleteBtn = card.querySelector('.delete-robot');
if (deleteBtn) {
deleteBtn.addEventListener('click', async function(e) {
e.preventDefault();
const robotName = this.dataset.robotName || '此机器人';
const confirmed = await confirmDialog(
`确定要删除 ${robotName} 吗?此操作不可恢复!`,
{ type: 'danger', title: '确认删除' }
);
if (confirmed) {
window.location.href = this.getAttribute('href');
}
});
}
});
}
/**
* 初始化搜索和过滤功能
*/
function initSearchAndFilter() {
const searchInput = document.getElementById('robot-search');
const filterButtons = document.querySelectorAll('.filter-btn');
const robotCards = document.querySelectorAll('.robot-card');
if (!searchInput) return;
let currentFilter = 'all';
// 搜索功能
searchInput.addEventListener('input', function() {
filterRobots();
});
// 过滤功能
filterButtons.forEach(button => {
button.addEventListener('click', function() {
filterButtons.forEach(btn => {
btn.classList.remove('bg-indigo-600', 'text-white');
btn.classList.add('bg-white', 'text-gray-700', 'border', 'border-gray-300');
});
this.classList.remove('bg-white', 'text-gray-700', 'border', 'border-gray-300');
this.classList.add('bg-indigo-600', 'text-white');
currentFilter = this.dataset.filter;
filterRobots();
});
});
function filterRobots() {
const searchTerm = searchInput.value.toLowerCase();
robotCards.forEach(card => {
const robotName = card.querySelector('.robot-name').textContent.toLowerCase();
const robotStatus = card.dataset.status;
const matchesSearch = robotName.includes(searchTerm);
const matchesFilter = currentFilter === 'all' || robotStatus === currentFilter;
if (matchesSearch && matchesFilter) {
card.classList.remove('hidden');
} else {
card.classList.add('hidden');
}
});
// 更新过滤后的计数
updateFilteredCounts();
}
function updateFilteredCounts() {
const visibleCards = document.querySelectorAll('.robot-card:not(.hidden)');
document.getElementById('filtered-count').textContent = visibleCards.length;
}
}
/**
* 更新状态计数
*/
function updateStatusCounts() {
const robotCards = document.querySelectorAll('.robot-card');
let onlineCount = 0;
let offlineCount = 0;
let errorCount = 0;
robotCards.forEach(card => {
const status = card.dataset.status;
if (status === 'online') {
onlineCount++;
} else if (status === 'error') {
errorCount++;
} else {
offlineCount++;
}
});
// 更新计数显示
const onlineCounter = document.getElementById('online-count');
const offlineCounter = document.getElementById('offline-count');
const errorCounter = document.getElementById('error-count');
if (onlineCounter) onlineCounter.textContent = onlineCount;
if (offlineCounter) offlineCounter.textContent = offlineCount;
if (errorCounter) errorCounter.textContent = errorCount;
}
/**
* 初始化刷新动画
*/
function initRefreshAnimation() {
const refreshButton = document.getElementById('refresh-dashboard');
if (!refreshButton) return;
refreshButton.addEventListener('click', function() {
// 添加旋转动画
const icon = this.querySelector('i');
icon.classList.add('animate-spin');
// 模拟加载延迟
setTimeout(() => {
window.location.reload();
}, 500);
});
}

164
public/js/utils.js Normal file
View File

@ -0,0 +1,164 @@
/**
* 通用工具函数库
*/
// 格式化日期时间
function formatDateTime(dateString, format = 'YYYY-MM-DD HH:mm:ss') {
const date = new Date(dateString);
if (isNaN(date.getTime())) return '';
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return format
.replace('YYYY', year)
.replace('MM', month)
.replace('DD', day)
.replace('HH', hours)
.replace('mm', minutes)
.replace('ss', seconds);
}
// 时间距离现在
function timeAgo(dateString) {
const date = new Date(dateString);
if (isNaN(date.getTime())) return '';
const now = new Date();
const secondsAgo = Math.floor((now - date) / 1000);
if (secondsAgo < 60) {
return '刚刚';
} else if (secondsAgo < 3600) {
const minutes = Math.floor(secondsAgo / 60);
return `${minutes}分钟前`;
} else if (secondsAgo < 86400) {
const hours = Math.floor(secondsAgo / 3600);
return `${hours}小时前`;
} else if (secondsAgo < 2592000) {
const days = Math.floor(secondsAgo / 86400);
return `${days}天前`;
} else if (secondsAgo < 31536000) {
const months = Math.floor(secondsAgo / 2592000);
return `${months}个月前`;
} else {
const years = Math.floor(secondsAgo / 31536000);
return `${years}年前`;
}
}
// 格式化状态标签
function formatStatus(status, labels = {}) {
const defaultLabels = {
online: { text: '在线', class: 'bg-green-100 text-green-800' },
offline: { text: '离线', class: 'bg-gray-100 text-gray-800' },
error: { text: '错误', class: 'bg-red-100 text-red-800' },
pending: { text: '待处理', class: 'bg-yellow-100 text-yellow-800' },
success: { text: '成功', class: 'bg-green-100 text-green-800' },
failure: { text: '失败', class: 'bg-red-100 text-red-800' },
};
const statusLabel = { ...defaultLabels, ...labels }[status] || { text: status, class: 'bg-gray-100 text-gray-800' };
return `<span class="px-2 py-1 text-xs rounded-full ${statusLabel.class}">${statusLabel.text}</span>`;
}
// 格式化文件大小
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// 复制文本到剪贴板
function copyToClipboard(text) {
return new Promise((resolve, reject) => {
if (!navigator.clipboard) {
fallbackCopyToClipboard(text);
resolve(true);
return;
}
navigator.clipboard.writeText(text)
.then(() => resolve(true))
.catch(err => {
fallbackCopyToClipboard(text);
resolve(true);
});
});
}
// 复制文本到剪贴板的后备方案
function fallbackCopyToClipboard(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
// 设置样式使文本域不可见
textArea.style.position = 'fixed';
textArea.style.top = '-9999px';
textArea.style.left = '-9999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
} catch (err) {
console.error('无法复制文本: ', err);
}
document.body.removeChild(textArea);
}
// 防抖函数
function debounce(func, wait = 300) {
let timeout;
return function(...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(context, args);
}, wait);
};
}
// 节流函数
function throttle(func, limit = 300) {
let inThrottle;
return function(...args) {
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
// 从URL获取查询参数
function getQueryParam(name, url = window.location.href) {
name = name.replace(/[\[\]]/g, '\\$&');
const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)');
const results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, ' '));
}