🆕 基础功能完善
This commit is contained in:
parent
0037c1ea44
commit
b66a3e4ad8
250
internal/docker/stats.go
Normal file
250
internal/docker/stats.go
Normal 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)
|
||||
}
|
92
internal/handler/api_container.go
Normal file
92
internal/handler/api_container.go
Normal 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
|
||||
}
|
81
internal/handler/api_login.go
Normal file
81
internal/handler/api_login.go
Normal 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,
|
||||
})
|
||||
}
|
38
internal/handler/health.go
Normal file
38
internal/handler/health.go
Normal 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,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
43
internal/middleware/debug.go
Normal file
43
internal/middleware/debug.go
Normal 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
|
||||
}
|
||||
}
|
187
internal/view/api/index.html
Normal file
187
internal/view/api/index.html
Normal 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>
|
40
internal/view/components/card.html
Normal file
40
internal/view/components/card.html
Normal 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>
|
140
internal/view/components/data_table.html
Normal file
140
internal/view/components/data_table.html
Normal 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>
|
41
internal/view/components/loading.html
Normal file
41
internal/view/components/loading.html
Normal 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>
|
62
internal/view/components/modal.html
Normal file
62
internal/view/components/modal.html
Normal 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>
|
55
internal/view/components/theme_toggle.html
Normal file
55
internal/view/components/theme_toggle.html
Normal 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>
|
30
internal/view/error/404.html
Normal file
30
internal/view/error/404.html
Normal 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>
|
115
internal/view/home/index.html
Normal file
115
internal/view/home/index.html
Normal 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>
|
451
internal/view/home/landing.html
Normal file
451
internal/view/home/landing.html
Normal 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">© 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>
|
7
internal/view/layouts/responsive_nav.html
Normal file
7
internal/view/layouts/responsive_nav.html
Normal file
@ -0,0 +1,7 @@
|
||||
<!-- 简化后的移动端导航 -->
|
||||
<script>
|
||||
// 移除了复杂的导航功能,采用简化设计
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('简化版界面已加载 - 移除了侧边栏功能');
|
||||
});
|
||||
</script>
|
134
internal/view/robot/send_message.html
Normal file
134
internal/view/robot/send_message.html
Normal 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
10
internal/view/test.html
Normal 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
112
public/css/dark-mode.css
Normal 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
178
public/css/styles.css
Normal 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
79
public/css/table.css
Normal 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
50
public/css/theme.css
Normal 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
243
public/js/app.js
Normal 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
219
public/js/components.js
Normal 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
86
public/js/nav.js
Normal 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();
|
||||
}
|
||||
});
|
||||
});
|
161
public/js/robot-dashboard.js
Normal file
161
public/js/robot-dashboard.js
Normal 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
164
public/js/utils.js
Normal 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, ' '));
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user