修改项目整体风格
This commit is contained in:
12
app.go
12
app.go
@@ -6,6 +6,7 @@ import (
|
||||
"go-site-clone/services"
|
||||
"go-site-clone/utils"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
@@ -124,3 +125,14 @@ func (a *App) OpenSiteFileDir(pathDir string) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *App) DeleteSiteFileDir(pathDir string) bool {
|
||||
appConfig, _ := config.LoadConfig()
|
||||
newPath := filepath.Join(appConfig.SiteFileDir, pathDir)
|
||||
err := os.RemoveAll(newPath)
|
||||
if err != nil {
|
||||
fmt.Println("删除文件夹失败:", err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
14
config.yaml
14
config.yaml
@@ -1,16 +1,10 @@
|
||||
appName: "Go Desk"
|
||||
appName: "Go Site Clone"
|
||||
|
||||
# 当前监听的端口
|
||||
port: 6997
|
||||
|
||||
# ws 连接地址
|
||||
wsUrl: "106.12.33.188:6996"
|
||||
|
||||
# STUN 服务器地址
|
||||
stunUrl: "stun:106.12.33.188:3478"
|
||||
|
||||
# api地址
|
||||
apiUrl: "http://106.12.33.188:6996"
|
||||
|
||||
# 网页文件保存目录
|
||||
siteFileDir: "www"
|
||||
|
||||
# 打包的软件保存目录
|
||||
packSiteFileDir: "site-dist"
|
||||
|
||||
@@ -7,12 +7,10 @@ import (
|
||||
)
|
||||
|
||||
type AppConfig struct {
|
||||
Port int `yaml:"port"`
|
||||
WsUrl string `yaml:"wsUrl"`
|
||||
StunUrl string `yaml:"stunUrl"`
|
||||
ApiUrl string `yaml:"apiUrl"`
|
||||
AppName string `yaml:"appName"`
|
||||
SiteFileDir string `yaml:"siteFileDir"`
|
||||
Port int `yaml:"port"`
|
||||
AppName string `yaml:"appName"`
|
||||
SiteFileDir string `yaml:"siteFileDir"`
|
||||
PackSiteFileDir string `yaml:"packSiteFileDir"`
|
||||
}
|
||||
|
||||
func LoadConfig() (*AppConfig, error) {
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
<template>
|
||||
|
||||
<!-- Active Users Card -->
|
||||
<a-card :bordered="false" class="dashboard-bar-chart" style="margin-bottom: 20px;">
|
||||
<chart-bar :height="220" :data="barChartData"></chart-bar>
|
||||
<div class="card-title">
|
||||
<h2>Active Users</h2>
|
||||
<p>than last week <span class="text-success">+23%</span></p>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<p>We have created multiple options for you to put together and customise into pixel perfect pages.</p>
|
||||
</div>
|
||||
<a-row class="card-footer" type="flex" justify="center" align="top">
|
||||
<a-col :span="6">
|
||||
<h4>3,6K</h4>
|
||||
<span>Users</span>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<h4>2m</h4>
|
||||
<span>Clicks</span>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<h4>$772</h4>
|
||||
<span>Sales</span>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<h4>82</h4>
|
||||
<span>Items</span>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
<!-- Active Users Card -->
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
const barChartData = {
|
||||
labels: ["01", "02", "03", "04", "05", "06", "07", "08", "09"],
|
||||
datasets: [
|
||||
{
|
||||
label: "Sales",
|
||||
backgroundColor: '#fff',
|
||||
borderWidth: 0,
|
||||
borderSkipped: false,
|
||||
borderRadius: 6,
|
||||
data: [850, 600, 500, 620, 900, 500, 900, 630, 900],
|
||||
maxBarThickness: 20
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -1,44 +0,0 @@
|
||||
<template>
|
||||
<a-card :bordered="false" class="dashboard-bar-line header-solid">
|
||||
<template #title>
|
||||
<h2>Sales Overview</h2>
|
||||
<p>than last year <span class="text-success">+20%</span></p>
|
||||
</template>
|
||||
|
||||
<template #extra>
|
||||
<a-badge color="primary" class="badge-dot-primary" text="Traffic" />
|
||||
<a-badge color="primary" class="badge-dot-secondary" text="Sales" />
|
||||
</template>
|
||||
|
||||
<chart-line :height="310" :data="lineChartData" />
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 定义折线图数据
|
||||
const lineChartData = {
|
||||
labels: ["Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
|
||||
datasets: [
|
||||
{
|
||||
label: "Mobile apps",
|
||||
tension: 0.4,
|
||||
borderWidth: 0,
|
||||
pointRadius: 0,
|
||||
borderColor: "#1890FF",
|
||||
borderWidth: 3,
|
||||
data: [50, 40, 300, 220, 500, 250, 400, 230, 500],
|
||||
maxBarThickness: 6
|
||||
},
|
||||
{
|
||||
label: "Websites",
|
||||
tension: 0.4,
|
||||
borderWidth: 0,
|
||||
pointRadius: 0,
|
||||
borderColor: "#B37FEB",
|
||||
borderWidth: 3,
|
||||
data: [30, 90, 40, 140, 290, 290, 340, 230, 400],
|
||||
maxBarThickness: 6
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
@@ -1,101 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<canvas ref="chartRef" :style="{ height: height + 'px' }"></canvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Chart, registerables } from 'chart.js'
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
|
||||
// 注册 Chart.js 所有组件
|
||||
Chart.register(...registerables)
|
||||
|
||||
// 定义 props
|
||||
const props = defineProps<{
|
||||
data: any
|
||||
height: number
|
||||
}>()
|
||||
|
||||
// chart 实例引用
|
||||
const chartRef = ref<HTMLCanvasElement | null>(null)
|
||||
let chartInstance: Chart | null = null
|
||||
|
||||
onMounted(() => {
|
||||
if (chartRef.value) {
|
||||
const ctx = chartRef.value.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
chartInstance = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: props.data,
|
||||
options: {
|
||||
layout: {
|
||||
padding: { top: 30, right: 15, left: 10, bottom: 5 }
|
||||
},
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false }
|
||||
},
|
||||
// Chart.js v3+ 的 tooltip 配置在 plugins.tooltip
|
||||
// 如果你用的是 v4,tooltips 配置需要改为 plugins.tooltip
|
||||
// 这里保留原写法
|
||||
tooltips: {
|
||||
enabled: true,
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
grid: {
|
||||
display: true,
|
||||
color: 'rgba(255, 255, 255, .2)',
|
||||
borderDash: [6],
|
||||
borderDashOffset: 6
|
||||
},
|
||||
ticks: {
|
||||
suggestedMin: 0,
|
||||
suggestedMax: 1000,
|
||||
display: true,
|
||||
color: '#fff',
|
||||
font: {
|
||||
size: 14,
|
||||
lineHeight: 1.5,
|
||||
weight: '600',
|
||||
family: 'Open Sans'
|
||||
}
|
||||
}
|
||||
},
|
||||
x: {
|
||||
grid: { display: false },
|
||||
ticks: {
|
||||
display: true,
|
||||
color: '#fff',
|
||||
font: {
|
||||
size: 14,
|
||||
lineHeight: 1.5,
|
||||
weight: '600',
|
||||
family: 'Open Sans'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.destroy()
|
||||
chartInstance = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
canvas {
|
||||
background-image: linear-gradient(to right, #00369E, #005CFD, #A18DFF);
|
||||
}
|
||||
</style>
|
||||
@@ -1,98 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<canvas ref="chartRef" :style="{ height: height + 'px' }"></canvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Chart, registerables } from 'chart.js'
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
|
||||
// 注册 Chart.js 所有组件
|
||||
Chart.register(...registerables)
|
||||
|
||||
// 定义 props
|
||||
const props = defineProps<{
|
||||
data: any
|
||||
height: number
|
||||
}>()
|
||||
|
||||
// DOM 引用
|
||||
const chartRef = ref<HTMLCanvasElement | null>(null)
|
||||
// Chart 实例
|
||||
let chartInstance: Chart | null = null
|
||||
|
||||
onMounted(() => {
|
||||
if (!chartRef.value) return
|
||||
const ctx = chartRef.value.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
chartInstance = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: props.data,
|
||||
options: {
|
||||
layout: {
|
||||
padding: { top: 30, right: 15, left: 10, bottom: 5 }
|
||||
},
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false }
|
||||
},
|
||||
// Chart.js v3+ 的 tooltip 配置在 plugins.tooltip
|
||||
// 如果你用的是 v4,需要改成 plugins.tooltip
|
||||
tooltips: {
|
||||
enabled: true,
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
grid: {
|
||||
display: true,
|
||||
color: 'rgba(0, 0, 0, .2)',
|
||||
borderDash: [6],
|
||||
borderDashOffset: 6
|
||||
},
|
||||
ticks: {
|
||||
suggestedMin: 0,
|
||||
suggestedMax: 1000,
|
||||
display: true,
|
||||
color: '#8C8C8C',
|
||||
font: {
|
||||
size: 14,
|
||||
lineHeight: 1.8,
|
||||
weight: '600',
|
||||
family: 'Open Sans'
|
||||
}
|
||||
}
|
||||
},
|
||||
x: {
|
||||
grid: { display: false },
|
||||
ticks: {
|
||||
display: true,
|
||||
color: '#8C8C8C',
|
||||
font: {
|
||||
size: 14,
|
||||
lineHeight: 1.5,
|
||||
weight: '600',
|
||||
family: 'Open Sans'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.destroy()
|
||||
chartInstance = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 你可以在这里加样式 */
|
||||
</style>
|
||||
@@ -1,34 +1,313 @@
|
||||
<template>
|
||||
<div class="header-outside">
|
||||
<a-layout-header class="header">
|
||||
<div class="header-outside">
|
||||
<a-layout-header class="modern-header">
|
||||
<div class="header-content">
|
||||
<!-- 左侧面包屑导航 -->
|
||||
<div class="header-left">
|
||||
<a-breadcrumb class="breadcrumb">
|
||||
<a-breadcrumb-item>
|
||||
<home-outlined />
|
||||
<span>{{ breadcrumbTitle }}</span>
|
||||
</a-breadcrumb-item>
|
||||
</a-breadcrumb>
|
||||
</div>
|
||||
|
||||
<div class="header-menu">
|
||||
<div></div>
|
||||
<div>
|
||||
|
||||
<!-- 右侧操作区 -->
|
||||
<div class="header-right">
|
||||
<!-- 搜索框 -->
|
||||
<div class="search-box">
|
||||
<a-input
|
||||
v-model:value="searchValue"
|
||||
placeholder="搜索功能..."
|
||||
class="search-input"
|
||||
allow-clear
|
||||
>
|
||||
<template #prefix>
|
||||
<search-outlined class="search-icon" />
|
||||
</template>
|
||||
</a-input>
|
||||
</div>
|
||||
|
||||
<!-- 快捷操作 -->
|
||||
<div class="header-actions">
|
||||
<a-tooltip title="通知">
|
||||
<a-badge :count="3" :offset="[-4, 4]">
|
||||
<div class="action-btn">
|
||||
<bell-outlined class="action-icon" />
|
||||
</div>
|
||||
</a-badge>
|
||||
</a-tooltip>
|
||||
|
||||
<a-tooltip title="设置">
|
||||
<div class="action-btn">
|
||||
<setting-outlined class="action-icon" />
|
||||
</div>
|
||||
</a-tooltip>
|
||||
|
||||
<a-divider type="vertical" class="action-divider" />
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<a-dropdown placement="bottomRight">
|
||||
<div class="user-info">
|
||||
<a-avatar
|
||||
class="user-avatar"
|
||||
:style="{ backgroundColor: '#667eea' }"
|
||||
>
|
||||
<template #icon>
|
||||
<user-outlined />
|
||||
</template>
|
||||
</a-avatar>
|
||||
<span class="user-name">管理员</span>
|
||||
<down-outlined class="dropdown-icon" />
|
||||
</div>
|
||||
<template #overlay>
|
||||
<a-menu class="user-menu">
|
||||
<a-menu-item key="profile">
|
||||
<user-outlined />
|
||||
<span>个人中心</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="settings">
|
||||
<setting-outlined />
|
||||
<span>系统设置</span>
|
||||
</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item key="logout">
|
||||
<logout-outlined />
|
||||
<span>退出登录</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-layout-header>
|
||||
</div>
|
||||
|
||||
|
||||
</a-layout-header>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { DownOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
HomeOutlined,
|
||||
SearchOutlined,
|
||||
BellOutlined,
|
||||
SettingOutlined,
|
||||
UserOutlined,
|
||||
DownOutlined,
|
||||
LogoutOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
const searchValue = ref('');
|
||||
|
||||
// 根据路由动态生成面包屑标题
|
||||
const breadcrumbTitle = computed(() => {
|
||||
const path = route.path;
|
||||
if (path === '/' || path === '') return '控制台';
|
||||
if (path.includes('download')) return '整站下载';
|
||||
if (path.includes('record')) return '下载记录';
|
||||
if (path.includes('webpage')) return 'Nginx网站';
|
||||
if (path.includes('pack')) return '打包应用';
|
||||
return '控制台';
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header-menu{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.header-outside {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
/* z-index: 9999; */
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.modern-header {
|
||||
background: white;
|
||||
padding: 0 24px;
|
||||
height: 64px;
|
||||
line-height: 64px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 左侧区域 */
|
||||
.header-left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
line-height: 64px;
|
||||
}
|
||||
|
||||
:deep(.breadcrumb .ant-breadcrumb-link) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.breadcrumb .anticon) {
|
||||
font-size: 18px;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
/* 右侧区域 */
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* 搜索框 */
|
||||
.search-box {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 240px;
|
||||
border-radius: 20px;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.search-input:hover,
|
||||
.search-input:focus {
|
||||
background: white;
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15);
|
||||
}
|
||||
|
||||
:deep(.search-input .ant-input) {
|
||||
background: transparent;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: #e6f7ff;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.action-btn:hover .action-icon {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.action-divider {
|
||||
height: 24px;
|
||||
margin: 0 4px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 用户信息 */
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.user-info:hover {
|
||||
background: #e6f7ff;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.user-info:hover .dropdown-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* 用户菜单 */
|
||||
:deep(.user-menu) {
|
||||
min-width: 160px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
:deep(.user-menu .ant-dropdown-menu-item) {
|
||||
padding: 10px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.user-menu .ant-dropdown-menu-item:hover) {
|
||||
background: #e6f7ff;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.modern-header {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import {GreetService} from "../../bindings/go-site-clone";
|
||||
import {Events} from "@wailsio/runtime";
|
||||
|
||||
const name = ref('')
|
||||
const result = ref('Please enter your name below 👇')
|
||||
const time = ref('Listening for Time event...')
|
||||
|
||||
const doGreet = () => {
|
||||
let localName = name.value;
|
||||
if (!localName) {
|
||||
localName = 'anonymous';
|
||||
}
|
||||
GreetService.Greet(localName).then((resultValue) => {
|
||||
result.value = resultValue;
|
||||
}).catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
Events.On('time', (timeValue) => {
|
||||
time.value = timeValue.data;
|
||||
});
|
||||
})
|
||||
|
||||
defineProps({
|
||||
msg: String,
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<div class="result">{{ result }}</div>
|
||||
<div class="card">
|
||||
<div class="input-box">
|
||||
<input class="input" v-model="name" type="text" autocomplete="off"/>
|
||||
<button class="btn" @click="doGreet">Greet</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<div><p>Click on the Wails logo to learn more</p></div>
|
||||
<div><p>{{ time }}</p></div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,84 +1,385 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="menu-out">
|
||||
<a-layout-sider v-model:collapsed="collapsed" collapsible style="height: 100%;">
|
||||
<div class="logo" />
|
||||
<a-menu v-model:selectedKeys="selectedKeys" theme="dark" mode="inline" @click="menuClick">
|
||||
<a-menu-item key="">
|
||||
<pie-chart-outlined />
|
||||
<!-- 折叠按钮 - 移到外层以避免被遮挡 -->
|
||||
<div class="collapse-trigger" @click="collapsed = !collapsed">
|
||||
<menu-unfold-outlined v-if="collapsed" class="trigger-icon" />
|
||||
<menu-fold-outlined v-else class="trigger-icon" />
|
||||
</div>
|
||||
|
||||
<a-layout-sider
|
||||
v-model:collapsed="collapsed"
|
||||
:trigger="null"
|
||||
:width="240"
|
||||
class="modern-sider"
|
||||
>
|
||||
<!-- Logo区域 -->
|
||||
<div class="logo-container">
|
||||
<div class="logo-icon">
|
||||
<cloud-server-outlined class="icon" />
|
||||
</div>
|
||||
<transition name="fade">
|
||||
<div v-if="!collapsed" class="logo-text">
|
||||
<div class="app-title">整站克隆</div>
|
||||
<div class="app-subtitle">Site Clone</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<!-- 菜单 -->
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
mode="inline"
|
||||
class="modern-menu"
|
||||
@click="menuClick"
|
||||
>
|
||||
<a-menu-item key="" class="menu-item">
|
||||
<template #icon>
|
||||
<home-outlined class="menu-icon" />
|
||||
</template>
|
||||
<span>控制台</span>
|
||||
</a-menu-item>
|
||||
<a-sub-menu key="site">
|
||||
<template #title>
|
||||
<span>
|
||||
<user-outlined />
|
||||
<span>仿站</span>
|
||||
</span>
|
||||
</template>
|
||||
<a-menu-item key="download">整站下载</a-menu-item>
|
||||
<a-menu-item key="record">仿站记录</a-menu-item>
|
||||
</a-sub-menu>
|
||||
<a-sub-menu key="run">
|
||||
<template #title>
|
||||
<span>
|
||||
<IeOutlined />
|
||||
<span>运行</span>
|
||||
</span>
|
||||
</template>
|
||||
<a-menu-item key="webpage">Nginx网站</a-menu-item>
|
||||
<a-menu-item key="pack">打包应用</a-menu-item>
|
||||
</a-sub-menu>
|
||||
|
||||
<a-menu-divider class="menu-divider" />
|
||||
|
||||
<a-menu-item-group title="站点管理" class="menu-group">
|
||||
<a-menu-item key="site/download" class="menu-item">
|
||||
<template #icon>
|
||||
<cloud-download-outlined class="menu-icon" />
|
||||
</template>
|
||||
<span>整站下载</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="site/record" class="menu-item">
|
||||
<template #icon>
|
||||
<history-outlined class="menu-icon" />
|
||||
</template>
|
||||
<span>下载记录</span>
|
||||
</a-menu-item>
|
||||
</a-menu-item-group>
|
||||
|
||||
<a-menu-divider class="menu-divider" />
|
||||
|
||||
<a-menu-item-group title="部署运行" class="menu-group">
|
||||
<a-menu-item key="run/webpage" class="menu-item">
|
||||
<template #icon>
|
||||
<global-outlined class="menu-icon" />
|
||||
</template>
|
||||
<span>Nginx网站</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="run/pack" class="menu-item">
|
||||
<template #icon>
|
||||
<appstore-outlined class="menu-icon" />
|
||||
</template>
|
||||
<span>打包应用</span>
|
||||
</a-menu-item>
|
||||
</a-menu-item-group>
|
||||
</a-menu>
|
||||
|
||||
<!-- 底部信息 -->
|
||||
<div class="sider-footer">
|
||||
<transition name="fade">
|
||||
<div v-if="!collapsed" class="footer-content">
|
||||
<div class="version-info">
|
||||
<api-outlined class="footer-icon" />
|
||||
<span>v1.0.0</span>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</a-layout-sider>
|
||||
</div>
|
||||
<div>
|
||||
<a-layout-sider v-model:collapsed="collapsed" collapsible style="height: 100%;"></a-layout-sider>
|
||||
<a-layout-sider
|
||||
v-model:collapsed="collapsed"
|
||||
:trigger="null"
|
||||
:width="240"
|
||||
style="height: 100%; visibility: hidden;"
|
||||
></a-layout-sider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
PieChartOutlined,
|
||||
UserOutlined,
|
||||
IeOutlined,
|
||||
HomeOutlined,
|
||||
CloudDownloadOutlined,
|
||||
HistoryOutlined,
|
||||
GlobalOutlined,
|
||||
AppstoreOutlined,
|
||||
CloudServerOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
ApiOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import { ref } from 'vue';
|
||||
import { ref, watch, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const collapsed = ref<boolean>(false);
|
||||
const selectedKeys = ref<string[]>(['']);
|
||||
const route = useRoute();
|
||||
|
||||
// 监听路由变化更新选中状态
|
||||
watch(() => route.path, (newPath) => {
|
||||
updateSelectedKeys(newPath);
|
||||
}, { immediate: true });
|
||||
|
||||
onMounted(() => {
|
||||
updateSelectedKeys(route.path);
|
||||
});
|
||||
|
||||
function updateSelectedKeys(path: string) {
|
||||
// 移除开头的斜杠
|
||||
const cleanPath = path.startsWith('/') ? path.slice(1) : path;
|
||||
selectedKeys.value = cleanPath ? [cleanPath] : [''];
|
||||
}
|
||||
|
||||
function menuClick(e: any) {
|
||||
console.log(e);
|
||||
const keyList = e.keyPath
|
||||
let path = ''
|
||||
for (let i = 0; i < keyList.length; i++) {
|
||||
const element = keyList[i];
|
||||
path += "/"+element
|
||||
const key = e.key;
|
||||
if (key) {
|
||||
navigateTo('/' + key);
|
||||
} else {
|
||||
navigateTo('/');
|
||||
}
|
||||
navigateTo(path)
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.logo {
|
||||
height: 32px;
|
||||
margin: 16px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.site-layout .site-layout-background {
|
||||
background: #fff;
|
||||
}
|
||||
[data-theme='dark'] .site-layout .site-layout-background {
|
||||
background: #141414;
|
||||
}
|
||||
|
||||
.menu-out{
|
||||
.menu-out {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 9999;
|
||||
z-index: 1000;
|
||||
min-height: 100vh;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.modern-sider {
|
||||
background: linear-gradient(180deg, #1a1f35 0%, #0f1419 100%) !important;
|
||||
box-shadow: 4px 0 24px rgba(0, 0, 0, 0.15);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow: visible;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-layout-sider-children) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Logo区域 */
|
||||
.logo-container {
|
||||
padding: 24px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
margin-bottom: 8px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.logo-icon .icon {
|
||||
font-size: 24px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 2px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.app-subtitle {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 折叠按钮 */
|
||||
.collapse-trigger {
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
right: -14px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
z-index: 1001;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.collapse-trigger:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.trigger-icon {
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 菜单样式 */
|
||||
.modern-menu {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
/* 隐藏滚动条但保持功能 */
|
||||
.modern-menu::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.modern-menu::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.modern-menu::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.modern-menu::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
:deep(.ant-menu-item-group-title) {
|
||||
color: rgba(255, 255, 255, 0.45) !important;
|
||||
font-size: 12px !important;
|
||||
font-weight: 600 !important;
|
||||
padding: 16px 16px 8px !important;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
:deep(.menu-item) {
|
||||
margin: 4px 0 !important;
|
||||
border-radius: 10px !important;
|
||||
height: 44px !important;
|
||||
line-height: 44px !important;
|
||||
color: rgba(255, 255, 255, 0.75) !important;
|
||||
transition: all 0.3s ease !important;
|
||||
font-size: 14px !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
:deep(.menu-item:hover) {
|
||||
background: rgba(255, 255, 255, 0.08) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
:deep(.menu-item.ant-menu-item-selected) {
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.2) 0%, rgba(118, 75, 162, 0.2) 100%) !important;
|
||||
color: white !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:deep(.menu-item.ant-menu-item-selected::before) {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 3px;
|
||||
height: 60%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
|
||||
:deep(.menu-icon) {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
:deep(.menu-divider) {
|
||||
background: rgba(255, 255, 255, 0.08) !important;
|
||||
margin: 16px 16px !important;
|
||||
}
|
||||
|
||||
/* 底部信息 */
|
||||
.sider-footer {
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.version-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.footer-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 过渡动画 */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 折叠状态调整 */
|
||||
:deep(.ant-layout-sider-collapsed) .logo-container {
|
||||
padding: 24px 16px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
:deep(.ant-layout-sider-collapsed) .modern-menu {
|
||||
padding: 8px 8px;
|
||||
}
|
||||
|
||||
:deep(.ant-layout-sider-collapsed .ant-menu-item-group-title) {
|
||||
padding: 8px 0 !important;
|
||||
text-align: center;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
:deep(.ant-layout-sider-collapsed .menu-item) {
|
||||
padding: 0 calc(50% - 18px / 2) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,35 +0,0 @@
|
||||
<template>
|
||||
<a-card :bordered="false" class="widget-1" style="margin-bottom: 20px;">
|
||||
<a-statistic
|
||||
:title="title"
|
||||
:value="value"
|
||||
:prefix="prefix"
|
||||
:suffix="suffix"
|
||||
:precision="0"
|
||||
class="text-success"
|
||||
:class="'text-' + status"
|
||||
/>
|
||||
<div class="icon" v-html="icon"></div>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
title?: string
|
||||
value?: number
|
||||
prefix?: string
|
||||
suffix?: string
|
||||
icon?: string
|
||||
status?: string
|
||||
}>()
|
||||
|
||||
// 默认值处理(可选)
|
||||
const {
|
||||
title = '',
|
||||
value = 0,
|
||||
prefix = '',
|
||||
suffix = '',
|
||||
icon = '',
|
||||
status = 'success'
|
||||
} = props
|
||||
</script>
|
||||
@@ -1,18 +1,31 @@
|
||||
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<a-layout style="min-height: 100vh">
|
||||
<div class="app-layout">
|
||||
<a-layout class="main-layout">
|
||||
<SiderMenu />
|
||||
<a-layout>
|
||||
<Header></Header>
|
||||
<a-layout-content style="margin: 0 16px">
|
||||
<div style="min-height: calc(100vh - 155px)">
|
||||
<a-layout class="content-layout">
|
||||
<Header />
|
||||
<a-layout-content class="page-content">
|
||||
<div class="content-wrapper">
|
||||
<slot />
|
||||
</div>
|
||||
</a-layout-content>
|
||||
<a-layout-footer style="text-align: center">
|
||||
<!-- Ant Design ©2018 Created by Ant UED -->
|
||||
<a-layout-footer class="app-footer">
|
||||
<div class="footer-content">
|
||||
<div class="footer-text">
|
||||
<span class="copyright">© 2024 整站克隆工具</span>
|
||||
<span class="separator">•</span>
|
||||
<span class="version">Version 1.0.0</span>
|
||||
</div>
|
||||
<div class="footer-links">
|
||||
<a href="#" class="footer-link">使用文档</a>
|
||||
<span class="separator">•</span>
|
||||
<a href="#" class="footer-link">意见反馈</a>
|
||||
<span class="separator">•</span>
|
||||
<a href="#" class="footer-link">关于我们</a>
|
||||
</div>
|
||||
</div>
|
||||
</a-layout-footer>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
@@ -20,7 +33,107 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
// 初始化逻辑
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-layout {
|
||||
min-height: 100vh;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
.main-layout {
|
||||
min-height: 100vh;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.content-layout {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: calc(100vh - 128px);
|
||||
background: transparent;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
min-height: calc(100vh - 128px);
|
||||
}
|
||||
|
||||
/* 页面底部 */
|
||||
.app-footer {
|
||||
background: white;
|
||||
padding: 20px 24px;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.footer-text,
|
||||
.footer-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.copyright {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.version {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.separator {
|
||||
color: #d9d9d9;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.footer-link:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.app-footer {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.footer-text,
|
||||
.footer-links {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,79 +1,775 @@
|
||||
<template>
|
||||
<div>
|
||||
<a-breadcrumb style="margin: 10px 0">
|
||||
<a-breadcrumb-item>控制台</a-breadcrumb-item>
|
||||
</a-breadcrumb>
|
||||
<div>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="24" :lg="12" :xl="6" class="mb-24" v-for="(stat, index) in stats" :key="index">
|
||||
<WidgetCounter :title="stat.title" :value="stat.value" :prefix="stat.prefix" :suffix="stat.suffix"
|
||||
:icon="stat.icon" :status="stat.status"></WidgetCounter>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="24" type="flex" align="stretch">
|
||||
<a-col :span="24" :lg="10" class="mb-24">
|
||||
<CardBarChart></CardBarChart>
|
||||
</a-col>
|
||||
<a-col :span="24" :lg="14" class="mb-24">
|
||||
<CardLineChart></CardLineChart>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-page">
|
||||
<!-- 面包屑导航 -->
|
||||
<a-breadcrumb class="page-breadcrumb">
|
||||
<a-breadcrumb-item>
|
||||
<home-outlined />
|
||||
<span>控制台</span>
|
||||
</a-breadcrumb-item>
|
||||
</a-breadcrumb>
|
||||
|
||||
<!-- 背景装饰 -->
|
||||
<div class="background-decoration">
|
||||
<div class="gradient-blob blob-1"></div>
|
||||
<div class="gradient-blob blob-2"></div>
|
||||
<div class="gradient-blob blob-3"></div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="main-container">
|
||||
<!-- 欢迎区域 -->
|
||||
<div class="welcome-section">
|
||||
<div class="welcome-content">
|
||||
<div class="welcome-text">
|
||||
<h1 class="welcome-title">
|
||||
<rocket-outlined class="welcome-icon" />
|
||||
欢迎使用整站下载工具
|
||||
</h1>
|
||||
<p class="welcome-subtitle">快速克隆任何网站,一键下载所有资源文件</p>
|
||||
</div>
|
||||
<div class="welcome-actions">
|
||||
<a-button type="primary" size="large" @click="goToDownload">
|
||||
<template #icon><cloud-download-outlined /></template>
|
||||
开始下载
|
||||
</a-button>
|
||||
<a-button size="large" @click="goToRecords">
|
||||
<template #icon><history-outlined /></template>
|
||||
查看记录
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="welcome-illustration">
|
||||
<cloud-server-outlined class="illustration-icon" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-grid">
|
||||
<a-card class="stat-card" :bordered="false" hoverable>
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon sites-icon">
|
||||
<global-outlined />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">已下载站点</div>
|
||||
<div class="stat-value">{{ stats.totalSites }}</div>
|
||||
<div class="stat-trend positive">
|
||||
<arrow-up-outlined />
|
||||
<span>较上周 +{{ stats.sitesTrend }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<a-card class="stat-card" :bordered="false" hoverable>
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon files-icon">
|
||||
<file-outlined />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">总文件数</div>
|
||||
<div class="stat-value">{{ formatNumber(stats.totalFiles) }}</div>
|
||||
<div class="stat-trend positive">
|
||||
<arrow-up-outlined />
|
||||
<span>较上周 +{{ stats.filesTrend }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<a-card class="stat-card" :bordered="false" hoverable>
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon storage-icon">
|
||||
<database-outlined />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">占用空间</div>
|
||||
<div class="stat-value">{{ stats.totalStorage }}</div>
|
||||
<div class="stat-trend positive">
|
||||
<arrow-up-outlined />
|
||||
<span>较上周 +{{ stats.storageTrend }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<a-card class="stat-card" :bordered="false" hoverable>
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon success-icon">
|
||||
<check-circle-outlined />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">成功率</div>
|
||||
<div class="stat-value">{{ stats.successRate }}%</div>
|
||||
<div class="stat-trend positive">
|
||||
<arrow-up-outlined />
|
||||
<span>较上周 +{{ stats.rateTrend }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<!-- 功能卡片 -->
|
||||
<a-row :gutter="24" class="feature-cards">
|
||||
<a-col :xs="24" :sm="12" :lg="8">
|
||||
<a-card class="feature-card" :bordered="false" hoverable @click="goToDownload">
|
||||
<div class="feature-icon-wrapper download-feature">
|
||||
<cloud-download-outlined class="feature-icon" />
|
||||
</div>
|
||||
<h3 class="feature-title">整站下载</h3>
|
||||
<p class="feature-desc">输入网址,自动分析并下载网站的所有资源文件,包括HTML、CSS、JS、图片等</p>
|
||||
<a-button type="link" class="feature-link">
|
||||
开始使用
|
||||
<right-outlined />
|
||||
</a-button>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :sm="12" :lg="8">
|
||||
<a-card class="feature-card" :bordered="false" hoverable @click="goToRecords">
|
||||
<div class="feature-icon-wrapper records-feature">
|
||||
<history-outlined class="feature-icon" />
|
||||
</div>
|
||||
<h3 class="feature-title">下载记录</h3>
|
||||
<p class="feature-desc">查看所有已下载的网站记录,管理文件,支持快速打开本地文件夹查看内容</p>
|
||||
<a-button type="link" class="feature-link">
|
||||
查看记录
|
||||
<right-outlined />
|
||||
</a-button>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :sm="12" :lg="8">
|
||||
<a-card class="feature-card" :bordered="false" hoverable>
|
||||
<div class="feature-icon-wrapper settings-feature">
|
||||
<setting-outlined class="feature-icon" />
|
||||
</div>
|
||||
<h3 class="feature-title">高级设置</h3>
|
||||
<p class="feature-desc">自定义下载配置,设置保存路径、并发数、超时时间等参数,优化下载体验</p>
|
||||
<a-button type="link" class="feature-link">
|
||||
配置选项
|
||||
<right-outlined />
|
||||
</a-button>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 使用指南 -->
|
||||
<a-card class="guide-card" :bordered="false">
|
||||
<template #title>
|
||||
<div class="card-title-wrapper">
|
||||
<book-outlined class="title-icon" />
|
||||
<span>快速开始</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<a-row :gutter="[24, 24]">
|
||||
<a-col :xs="24" :md="8">
|
||||
<div class="guide-step">
|
||||
<div class="step-number">1</div>
|
||||
<h4 class="step-title">输入网址</h4>
|
||||
<p class="step-desc">在整站下载页面输入你想要下载的网站完整地址</p>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="8">
|
||||
<div class="guide-step">
|
||||
<div class="step-number">2</div>
|
||||
<h4 class="step-title">分析资源</h4>
|
||||
<p class="step-desc">系统自动分析网站结构,统计所有可下载的资源文件</p>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="8">
|
||||
<div class="guide-step">
|
||||
<div class="step-number">3</div>
|
||||
<h4 class="step-title">开始下载</h4>
|
||||
<p class="step-desc">点击下载按钮,等待下载完成,即可在本地查看文件</p>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
HomeOutlined,
|
||||
RocketOutlined,
|
||||
CloudDownloadOutlined,
|
||||
CloudServerOutlined,
|
||||
HistoryOutlined,
|
||||
GlobalOutlined,
|
||||
FileOutlined,
|
||||
DatabaseOutlined,
|
||||
CheckCircleOutlined,
|
||||
ArrowUpOutlined,
|
||||
RightOutlined,
|
||||
SettingOutlined,
|
||||
BookOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
// Counter Widgets stats
|
||||
const stats = [
|
||||
{
|
||||
title: "Today’s Sales",
|
||||
value: 53000,
|
||||
prefix: "$",
|
||||
suffix: "+30%",
|
||||
icon: `
|
||||
<svg width="22" height="22" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.43338 7.41784C8.58818 7.31464 8.77939 7.2224 9 7.15101L9.00001 8.84899C8.77939 8.7776 8.58818 8.68536 8.43338 8.58216C8.06927 8.33942 8 8.1139 8 8C8 7.8861 8.06927 7.66058 8.43338 7.41784Z" fill="#111827"/>
|
||||
<path d="M11 12.849L11 11.151C11.2206 11.2224 11.4118 11.3146 11.5666 11.4178C11.9308 11.6606 12 11.8861 12 12C12 12.1139 11.9308 12.3394 11.5666 12.5822C11.4118 12.6854 11.2206 12.7776 11 12.849Z" fill="#111827"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 18C14.4183 18 18 14.4183 18 10C18 5.58172 14.4183 2 10 2C5.58172 2 2 5.58172 2 10C2 14.4183 5.58172 18 10 18ZM11 5C11 4.44772 10.5523 4 10 4C9.44772 4 9 4.44772 9 5V5.09199C8.3784 5.20873 7.80348 5.43407 7.32398 5.75374C6.6023 6.23485 6 7.00933 6 8C6 8.99067 6.6023 9.76515 7.32398 10.2463C7.80348 10.5659 8.37841 10.7913 9.00001 10.908L9.00002 12.8492C8.60902 12.7223 8.31917 12.5319 8.15667 12.3446C7.79471 11.9275 7.16313 11.8827 6.74599 12.2447C6.32885 12.6067 6.28411 13.2382 6.64607 13.6554C7.20855 14.3036 8.05956 14.7308 9 14.9076L9 15C8.99999 15.5523 9.44769 16 9.99998 16C10.5523 16 11 15.5523 11 15L11 14.908C11.6216 14.7913 12.1965 14.5659 12.676 14.2463C13.3977 13.7651 14 12.9907 14 12C14 11.0093 13.3977 10.2348 12.676 9.75373C12.1965 9.43407 11.6216 9.20873 11 9.09199L11 7.15075C11.391 7.27771 11.6808 7.4681 11.8434 7.65538C12.2053 8.07252 12.8369 8.11726 13.254 7.7553C13.6712 7.39335 13.7159 6.76176 13.354 6.34462C12.7915 5.69637 11.9405 5.26915 11 5.09236V5Z" fill="#111827"/>
|
||||
</svg>`,
|
||||
},
|
||||
{
|
||||
title: "Today’s Users",
|
||||
value: 3200,
|
||||
suffix: "+20%",
|
||||
icon: `
|
||||
<svg width="22" height="22" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 6C9 7.65685 7.65685 9 6 9C4.34315 9 3 7.65685 3 6C3 4.34315 4.34315 3 6 3C7.65685 3 9 4.34315 9 6Z" fill="#111827"/>
|
||||
<path d="M17 6C17 7.65685 15.6569 9 14 9C12.3431 9 11 7.65685 11 6C11 4.34315 12.3431 3 14 3C15.6569 3 17 4.34315 17 6Z" fill="#111827"/>
|
||||
<path d="M12.9291 17C12.9758 16.6734 13 16.3395 13 16C13 14.3648 12.4393 12.8606 11.4998 11.6691C12.2352 11.2435 13.0892 11 14 11C16.7614 11 19 13.2386 19 16V17H12.9291Z" fill="#111827"/>
|
||||
<path d="M6 11C8.76142 11 11 13.2386 11 16V17H1V16C1 13.2386 3.23858 11 6 11Z" fill="#111827"/>
|
||||
</svg>`,
|
||||
},
|
||||
{
|
||||
title: "New Clients",
|
||||
value: 1200,
|
||||
prefix: "+",
|
||||
status: "danger",
|
||||
suffix: "-20%",
|
||||
icon: `
|
||||
<svg width="22" height="22" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.17157 5.17157C4.73367 3.60948 7.26633 3.60948 8.82843 5.17157L10 6.34315L11.1716 5.17157C12.7337 3.60948 15.2663 3.60948 16.8284 5.17157C18.3905 6.73367 18.3905 9.26633 16.8284 10.8284L10 17.6569L3.17157 10.8284C1.60948 9.26633 1.60948 6.73367 3.17157 5.17157Z" fill="#111827"/>
|
||||
</svg>`,
|
||||
},
|
||||
{
|
||||
title: "New Orders",
|
||||
value: 13200,
|
||||
prefix: "$",
|
||||
suffix: "+10%",
|
||||
icon: `
|
||||
<svg width="22" height="22" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 2C7.79086 2 6 3.79086 6 6V7H5C4.49046 7 4.06239 7.38314 4.00612 7.88957L3.00612 16.8896C2.97471 17.1723 3.06518 17.455 3.25488 17.6669C3.44458 17.8789 3.71556 18 4 18H16C16.2844 18 16.5554 17.8789 16.7451 17.6669C16.9348 17.455 17.0253 17.1723 16.9939 16.8896L15.9939 7.88957C15.9376 7.38314 15.5096 7 15 7H14V6C14 3.79086 12.2091 2 10 2ZM12 7V6C12 4.89543 11.1046 4 10 4C8.89543 4 8 4.89543 8 6V7H12ZM6 10C6 9.44772 6.44772 9 7 9C7.55228 9 8 9.44772 8 10C8 10.5523 7.55228 11 7 11C6.44772 11 6 10.5523 6 10ZM13 9C12.4477 9 12 9.44772 12 10C12 10.5523 12.4477 11 13 11C13.5523 11 14 10.5523 14 10C14 9.44772 13.5523 9 13 9Z" fill="#111827"/>
|
||||
</svg>`,
|
||||
},
|
||||
];
|
||||
const router = useRouter()
|
||||
|
||||
// 统计数据
|
||||
const stats = reactive({
|
||||
totalSites: 24,
|
||||
sitesTrend: 15,
|
||||
totalFiles: 15680,
|
||||
filesTrend: 23,
|
||||
totalStorage: '2.8 GB',
|
||||
storageTrend: 18,
|
||||
successRate: 98.5,
|
||||
rateTrend: 2.5
|
||||
})
|
||||
|
||||
// 格式化数字
|
||||
const formatNumber = (num: number) => {
|
||||
return num.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 跳转到下载页面
|
||||
const goToDownload = () => {
|
||||
router.push('/site/download')
|
||||
}
|
||||
|
||||
// 跳转到记录页面
|
||||
const goToRecords = () => {
|
||||
router.push('/site/record')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
/* 页面布局 */
|
||||
.dashboard-page {
|
||||
min-height: calc(100vh - 64px);
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page-breadcrumb {
|
||||
padding: 16px 24px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.page-breadcrumb span {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
/* 背景装饰 */
|
||||
.background-decoration {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gradient-blob {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(100px);
|
||||
opacity: 0.25;
|
||||
animation: float 25s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.blob-1 {
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
top: -15%;
|
||||
right: -15%;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.blob-2 {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
bottom: -15%;
|
||||
left: -15%;
|
||||
animation-delay: 12s;
|
||||
}
|
||||
|
||||
.blob-3 {
|
||||
width: 450px;
|
||||
height: 450px;
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
animation-delay: 8s;
|
||||
}
|
||||
|
||||
/* 主容器 */
|
||||
.main-container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 32px 24px;
|
||||
}
|
||||
|
||||
/* 欢迎区域 */
|
||||
.welcome-section {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 48px;
|
||||
margin-bottom: 32px;
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 48px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
animation: fadeInDown 0.6s ease-out;
|
||||
}
|
||||
|
||||
.welcome-content {
|
||||
flex: 1;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 16px 0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.welcome-icon {
|
||||
font-size: 42px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.welcome-subtitle {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin: 0 0 32px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.welcome-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.welcome-actions .ant-btn {
|
||||
height: 48px;
|
||||
padding: 0 32px;
|
||||
border-radius: 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 4px 16px rgba(24, 144, 255, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.welcome-actions .ant-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(24, 144, 255, 0.3);
|
||||
}
|
||||
|
||||
.welcome-illustration {
|
||||
flex-shrink: 0;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%);
|
||||
border-radius: 50%;
|
||||
animation: pulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.illustration-icon {
|
||||
font-size: 100px;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 32px;
|
||||
animation: fadeInUp 0.6s ease-out 0.1s both;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
:deep(.stat-card .ant-card-body) {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sites-icon {
|
||||
background: linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%);
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.files-icon {
|
||||
background: linear-gradient(135deg, #f3e5f5 0%, #e1bee7 100%);
|
||||
color: #722ed1;
|
||||
}
|
||||
|
||||
.storage-icon {
|
||||
background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%);
|
||||
color: #fa8c16;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-trend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stat-trend.positive {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
/* 功能卡片 */
|
||||
.feature-cards {
|
||||
margin-bottom: 32px;
|
||||
animation: fadeInUp 0.6s ease-out 0.2s both;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-6px);
|
||||
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
:deep(.feature-card .ant-card-body) {
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.feature-icon-wrapper {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 24px;
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.download-feature {
|
||||
background: linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%);
|
||||
}
|
||||
|
||||
.records-feature {
|
||||
background: linear-gradient(135deg, #f3e5f5 0%, #e1bee7 100%);
|
||||
}
|
||||
|
||||
.settings-feature {
|
||||
background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%);
|
||||
}
|
||||
|
||||
.feature-card:hover .feature-icon-wrapper {
|
||||
transform: scale(1.1) rotate(5deg);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.download-feature .feature-icon {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.records-feature .feature-icon {
|
||||
color: #722ed1;
|
||||
}
|
||||
|
||||
.settings-feature .feature-icon {
|
||||
color: #fa8c16;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin: 0 0 20px 0;
|
||||
min-height: 64px;
|
||||
}
|
||||
|
||||
.feature-link {
|
||||
padding: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 使用指南 */
|
||||
.guide-card {
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.06);
|
||||
animation: fadeInUp 0.6s ease-out 0.3s both;
|
||||
}
|
||||
|
||||
:deep(.guide-card .ant-card-head) {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
.card-title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
font-size: 20px;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.guide-step {
|
||||
text-align: center;
|
||||
padding: 24px;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.guide-step:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.step-number {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin: 0 auto 16px;
|
||||
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.step-desc {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 动画 */
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translate(0, 0) rotate(0deg);
|
||||
}
|
||||
33% {
|
||||
transform: translate(40px, -40px) rotate(120deg);
|
||||
}
|
||||
66% {
|
||||
transform: translate(-30px, 30px) rotate(240deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1200px) {
|
||||
.main-container {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.welcome-section {
|
||||
flex-direction: column;
|
||||
padding: 32px 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 28px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.welcome-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.welcome-illustration {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.illustration-icon {
|
||||
font-size: 80px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.page-breadcrumb {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.welcome-section {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 24px;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.welcome-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.welcome-subtitle {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.welcome-actions .ant-btn {
|
||||
height: 40px;
|
||||
padding: 0 24px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.feature-card .ant-card-body) {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
min-height: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,876 @@
|
||||
<template>
|
||||
<div>1</div>
|
||||
<div class="pack-page">
|
||||
<!-- 背景装饰 -->
|
||||
<div class="background-decoration">
|
||||
<div class="gradient-blob blob-1"></div>
|
||||
<div class="gradient-blob blob-2"></div>
|
||||
<div class="gradient-blob blob-3"></div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="main-container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<div class="title-section">
|
||||
<appstore-outlined class="page-icon" />
|
||||
<div>
|
||||
<h1 class="page-title">应用打包</h1>
|
||||
<p class="page-subtitle">将下载的网站打包成独立应用程序</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 打包向导 -->
|
||||
<a-card class="wizard-card" :bordered="false">
|
||||
<a-steps :current="currentStep" class="pack-steps">
|
||||
<a-step title="选择网站" />
|
||||
<a-step title="配置应用" />
|
||||
<a-step title="打包设置" />
|
||||
<a-step title="开始打包" />
|
||||
</a-steps>
|
||||
|
||||
<!-- 步骤内容 -->
|
||||
<div class="step-content">
|
||||
<!-- 步骤1: 选择网站 -->
|
||||
<div v-if="currentStep === 0" class="step-panel">
|
||||
<div class="step-header">
|
||||
<h3 class="step-title">选择要打包的网站</h3>
|
||||
<a-button @click="getDownloadList" :loading="loading" size="small">
|
||||
<template #icon><reload-outlined /></template>
|
||||
刷新列表
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading-state">
|
||||
<a-spin size="large" tip="加载中..." />
|
||||
</div>
|
||||
|
||||
<div v-else-if="availableSites.length === 0" class="empty-state">
|
||||
<inbox-outlined class="empty-icon" />
|
||||
<p class="empty-text">暂无已下载的网站</p>
|
||||
<a-button type="primary" @click="goToDownload">
|
||||
<template #icon><cloud-download-outlined /></template>
|
||||
去下载网站
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div v-else class="sites-list">
|
||||
<a-radio-group v-model:value="selectedSite" class="site-radio-group">
|
||||
<a-card
|
||||
v-for="site in availableSites"
|
||||
:key="site.id"
|
||||
class="site-card"
|
||||
:class="{ 'selected': selectedSite === site.id }"
|
||||
:bordered="false"
|
||||
@click="selectedSite = site.id"
|
||||
>
|
||||
<a-radio :value="site.id" class="site-radio">
|
||||
<div class="site-content">
|
||||
<a-avatar :style="{ backgroundColor: getAvatarColor(site.name) }">
|
||||
{{ site.name.charAt(0).toUpperCase() }}
|
||||
</a-avatar>
|
||||
<div class="site-details">
|
||||
<div class="site-name">{{ site.name }}</div>
|
||||
<div class="site-info">
|
||||
<span>{{ site.size }}</span>
|
||||
<span>•</span>
|
||||
<span>{{ formatTime(site.modTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-radio>
|
||||
</a-card>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤2: 配置应用 -->
|
||||
<div v-if="currentStep === 1" class="step-panel">
|
||||
<h3 class="step-title">配置应用信息</h3>
|
||||
<a-form layout="vertical" class="config-form">
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="24">
|
||||
<a-form-item label="应用名称">
|
||||
<a-input v-model:value="appConfig.name" placeholder="请输入应用名称" size="large" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12">
|
||||
<a-form-item label="应用版本">
|
||||
<a-input v-model:value="appConfig.version" placeholder="1.0.0" size="large" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12">
|
||||
<a-form-item label="作者">
|
||||
<a-input v-model:value="appConfig.author" placeholder="请输入作者名称" size="large" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item label="应用描述">
|
||||
<a-textarea
|
||||
v-model:value="appConfig.description"
|
||||
placeholder="请输入应用描述"
|
||||
:rows="4"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 步骤3: 打包设置 -->
|
||||
<div v-if="currentStep === 2" class="step-panel">
|
||||
<h3 class="step-title">打包设置</h3>
|
||||
<a-form layout="vertical" class="config-form">
|
||||
<a-form-item label="目标平台">
|
||||
<a-checkbox-group v-model:value="packConfig.platforms" class="platform-group">
|
||||
<a-card class="platform-card" :bordered="false">
|
||||
<a-checkbox value="windows">
|
||||
<windows-outlined class="platform-icon" />
|
||||
<span>Windows</span>
|
||||
</a-checkbox>
|
||||
</a-card>
|
||||
<a-card class="platform-card" :bordered="false">
|
||||
<a-checkbox value="macos">
|
||||
<apple-outlined class="platform-icon" />
|
||||
<span>macOS</span>
|
||||
</a-checkbox>
|
||||
</a-card>
|
||||
<a-card class="platform-card" :bordered="false">
|
||||
<a-checkbox value="linux">
|
||||
<linux-outlined class="platform-icon" />
|
||||
<span>Linux</span>
|
||||
</a-checkbox>
|
||||
</a-card>
|
||||
</a-checkbox-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-row :gutter="24">
|
||||
<a-col :xs="24" :sm="12">
|
||||
<a-form-item label="窗口宽度">
|
||||
<a-input-number
|
||||
v-model:value="packConfig.width"
|
||||
:min="800"
|
||||
class="full-width"
|
||||
size="large"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12">
|
||||
<a-form-item label="窗口高度">
|
||||
<a-input-number
|
||||
v-model:value="packConfig.height"
|
||||
:min="600"
|
||||
class="full-width"
|
||||
size="large"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="输出目录">
|
||||
<a-input
|
||||
v-model:value="packConfig.outputDir"
|
||||
placeholder="选择输出目录"
|
||||
size="large"
|
||||
>
|
||||
<template #suffix>
|
||||
<folder-outlined class="input-icon" />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 步骤4: 开始打包 -->
|
||||
<div v-if="currentStep === 3" class="step-panel">
|
||||
<h3 class="step-title">准备打包</h3>
|
||||
<div class="pack-summary">
|
||||
<a-descriptions :column="1" bordered>
|
||||
<a-descriptions-item label="网站名称">{{ selectedSiteName }}</a-descriptions-item>
|
||||
<a-descriptions-item label="应用名称">{{ appConfig.name }}</a-descriptions-item>
|
||||
<a-descriptions-item label="版本号">{{ appConfig.version }}</a-descriptions-item>
|
||||
<a-descriptions-item label="目标平台">
|
||||
<a-tag v-for="platform in packConfig.platforms" :key="platform" color="blue">
|
||||
{{ platform }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="窗口尺寸">
|
||||
{{ packConfig.width }} x {{ packConfig.height }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<div class="pack-action">
|
||||
<a-button
|
||||
type="primary"
|
||||
size="large"
|
||||
:loading="packing"
|
||||
@click="startPacking"
|
||||
block
|
||||
>
|
||||
<template #icon><rocket-outlined /></template>
|
||||
{{ packing ? '打包中...' : '开始打包' }}
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 打包进度 -->
|
||||
<div v-if="packing" class="pack-progress">
|
||||
<a-progress :percent="packProgress" status="active" />
|
||||
<p class="progress-text">{{ packProgressText }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤操作 -->
|
||||
<div class="step-actions">
|
||||
<a-button v-if="currentStep > 0" @click="prevStep" size="large">
|
||||
<template #icon><left-outlined /></template>
|
||||
上一步
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="currentStep < 3"
|
||||
type="primary"
|
||||
@click="nextStep"
|
||||
:disabled="!canProceed"
|
||||
size="large"
|
||||
>
|
||||
下一步
|
||||
<template #icon><right-outlined /></template>
|
||||
</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<!-- 帮助提示 -->
|
||||
<a-card class="help-card" :bordered="false">
|
||||
<template #title>
|
||||
<div class="card-title-wrapper">
|
||||
<question-circle-outlined class="title-icon" />
|
||||
<span>帮助说明</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="help-content">
|
||||
<p><strong>打包流程:</strong></p>
|
||||
<ol>
|
||||
<li>选择要打包的已下载网站</li>
|
||||
<li>填写应用的基本信息(名称、版本、作者等)</li>
|
||||
<li>配置打包参数(目标平台、窗口大小等)</li>
|
||||
<li>确认信息无误后开始打包</li>
|
||||
</ol>
|
||||
<p><strong>注意事项:</strong></p>
|
||||
<ul>
|
||||
<li>打包过程可能需要几分钟,请耐心等待</li>
|
||||
<li>确保有足够的磁盘空间用于存储打包文件</li>
|
||||
<li>不同平台的打包文件格式不同(Windows: .exe, macOS: .app, Linux: .AppImage)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
WindowsOutlined,
|
||||
AppleOutlined,
|
||||
FolderOutlined,
|
||||
RocketOutlined,
|
||||
LeftOutlined,
|
||||
RightOutlined,
|
||||
QuestionCircleOutlined,
|
||||
ReloadOutlined,
|
||||
InboxOutlined,
|
||||
CloudDownloadOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import { App } from "../../../bindings/go-site-clone";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 步骤控制
|
||||
const currentStep = ref(0);
|
||||
const selectedSite = ref<string>('');
|
||||
const packing = ref(false);
|
||||
const packProgress = ref(0);
|
||||
const packProgressText = ref('');
|
||||
const loading = ref(false);
|
||||
|
||||
// 从后端获取已下载的网站列表
|
||||
const availableSites = ref<any[]>([]);
|
||||
|
||||
// 获取网站列表
|
||||
const getDownloadList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await App.GetDownloadList();
|
||||
availableSites.value = res.map((site: any, index: number) => ({
|
||||
id: index.toString(),
|
||||
name: site.name,
|
||||
size: formatSize(site.size),
|
||||
files: '未知', // 如果后端提供文件数量可以使用
|
||||
modTime: site.modTime,
|
||||
rawData: site
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('获取列表失败:', error);
|
||||
message.error('获取站点列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化文件大小
|
||||
const formatSize = (bytes: number) => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
// 页面挂载时加载数据
|
||||
onMounted(() => {
|
||||
getDownloadList();
|
||||
});
|
||||
|
||||
// 应用配置
|
||||
const appConfig = ref({
|
||||
name: '',
|
||||
version: '1.0.0',
|
||||
author: '',
|
||||
description: ''
|
||||
});
|
||||
|
||||
// 打包配置
|
||||
const packConfig = ref({
|
||||
platforms: ['windows'],
|
||||
width: 1280,
|
||||
height: 800,
|
||||
outputDir: ''
|
||||
});
|
||||
|
||||
// 计算属性
|
||||
const selectedSiteName = computed(() => {
|
||||
const site = availableSites.value.find(s => s.id === selectedSite.value);
|
||||
return site?.name || '';
|
||||
});
|
||||
|
||||
const canProceed = computed(() => {
|
||||
if (currentStep.value === 0) return selectedSite.value !== '';
|
||||
if (currentStep.value === 1) return appConfig.value.name !== '';
|
||||
if (currentStep.value === 2) return packConfig.value.platforms.length > 0;
|
||||
return true;
|
||||
});
|
||||
|
||||
// 步骤操作
|
||||
const nextStep = () => {
|
||||
if (currentStep.value < 3) {
|
||||
currentStep.value++;
|
||||
}
|
||||
};
|
||||
|
||||
const prevStep = () => {
|
||||
if (currentStep.value > 0) {
|
||||
currentStep.value--;
|
||||
}
|
||||
};
|
||||
|
||||
// 开始打包
|
||||
const startPacking = () => {
|
||||
packing.value = true;
|
||||
packProgress.value = 0;
|
||||
|
||||
// 模拟打包进度
|
||||
const interval = setInterval(() => {
|
||||
packProgress.value += 10;
|
||||
|
||||
if (packProgress.value <= 30) {
|
||||
packProgressText.value = '正在准备打包环境...';
|
||||
} else if (packProgress.value <= 60) {
|
||||
packProgressText.value = '正在编译应用...';
|
||||
} else if (packProgress.value <= 90) {
|
||||
packProgressText.value = '正在生成安装包...';
|
||||
} else {
|
||||
packProgressText.value = '打包完成!';
|
||||
}
|
||||
|
||||
if (packProgress.value >= 100) {
|
||||
clearInterval(interval);
|
||||
packing.value = false;
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
|
||||
// 获取头像颜色
|
||||
const getAvatarColor = (name: string) => {
|
||||
const colors = ['#1890ff', '#52c41a', '#722ed1', '#fa8c16', '#eb2f96'];
|
||||
const index = name.charCodeAt(0) % colors.length;
|
||||
return colors[index];
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (dateStr: string) => {
|
||||
if (!dateStr) return '--';
|
||||
const date = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
|
||||
const minute = 60 * 1000;
|
||||
const hour = 60 * minute;
|
||||
const day = 24 * hour;
|
||||
|
||||
if (diff < hour) {
|
||||
return Math.floor(diff / minute) + ' 分钟前';
|
||||
} else if (diff < day) {
|
||||
return Math.floor(diff / hour) + ' 小时前';
|
||||
} else if (diff < 7 * day) {
|
||||
return Math.floor(diff / day) + ' 天前';
|
||||
} else {
|
||||
return date.toLocaleDateString('zh-CN');
|
||||
}
|
||||
};
|
||||
|
||||
// 跳转到下载页面
|
||||
const goToDownload = () => {
|
||||
router.push('/site/download');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 页面布局 */
|
||||
.pack-page {
|
||||
min-height: calc(100vh - 64px);
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 背景装饰 - 复用之前的样式 */
|
||||
.background-decoration {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gradient-blob {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(100px);
|
||||
opacity: 0.25;
|
||||
animation: float 25s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.blob-1 {
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
top: -15%;
|
||||
right: -15%;
|
||||
}
|
||||
|
||||
.blob-2 {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
bottom: -15%;
|
||||
left: -15%;
|
||||
animation-delay: 12s;
|
||||
}
|
||||
|
||||
.blob-3 {
|
||||
width: 450px;
|
||||
height: 450px;
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
animation-delay: 8s;
|
||||
}
|
||||
|
||||
/* 主容器 */
|
||||
.main-container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 32px 24px;
|
||||
}
|
||||
|
||||
/* 页面标题 */
|
||||
.page-header {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 32px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
|
||||
animation: fadeInDown 0.6s ease-out;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.title-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.page-icon {
|
||||
font-size: 48px;
|
||||
color: #722ed1;
|
||||
padding: 16px;
|
||||
background: linear-gradient(135deg, #f3e5f5 0%, #e1bee7 100%);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin: 4px 0 0 0;
|
||||
}
|
||||
|
||||
/* 向导卡片 */
|
||||
.wizard-card {
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.06);
|
||||
margin-bottom: 24px;
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
:deep(.wizard-card .ant-card-body) {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.pack-steps {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
/* 步骤内容 */
|
||||
.step-content {
|
||||
min-height: 400px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.step-panel {
|
||||
animation: fadeIn 0.4s ease-out;
|
||||
}
|
||||
|
||||
.step-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-header .step-title {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
/* 加载和空状态 */
|
||||
.loading-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80px;
|
||||
color: #d9d9d9;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
/* 网站列表 */
|
||||
.sites-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.site-radio-group {
|
||||
width: 100%;
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.site-card {
|
||||
border: 2px solid #f0f0f0;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.site-card.selected {
|
||||
border-color: #1890ff;
|
||||
background: #e6f7ff;
|
||||
}
|
||||
|
||||
.site-card:hover {
|
||||
border-color: #1890ff;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.15);
|
||||
}
|
||||
|
||||
.site-radio {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.site-radio .ant-radio) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.site-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.site-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.site-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.site-info {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 配置表单 */
|
||||
.config-form {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
/* 平台选择 */
|
||||
.platform-group {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.platform-card {
|
||||
border: 2px solid #f0f0f0;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:deep(.platform-card .ant-card-body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.platform-card:has(:checked) {
|
||||
border-color: #1890ff;
|
||||
background: #e6f7ff;
|
||||
}
|
||||
|
||||
.platform-icon {
|
||||
font-size: 32px;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* 打包摘要 */
|
||||
.pack-summary {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.pack-action {
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.pack-progress {
|
||||
margin-top: 24px;
|
||||
padding: 24px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
text-align: center;
|
||||
margin: 12px 0 0 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 步骤操作 */
|
||||
.step-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
/* 帮助卡片 */
|
||||
.help-card {
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.06);
|
||||
animation: fadeInUp 0.6s ease-out 0.1s both;
|
||||
}
|
||||
|
||||
.card-title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
font-size: 20px;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.help-content {
|
||||
color: #666;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.help-content strong {
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.help-content ol,
|
||||
.help-content ul {
|
||||
margin: 12px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.help-content li {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
/* 通用样式 */
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.input-icon:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
/* 动画 */
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translate(0, 0) rotate(0deg);
|
||||
}
|
||||
33% {
|
||||
transform: translate(40px, -40px) rotate(120deg);
|
||||
}
|
||||
66% {
|
||||
transform: translate(-30px, 30px) rotate(240deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.main-container {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
:deep(.wizard-card .ant-card-body) {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.sites-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.platform-group {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,936 @@
|
||||
<template>
|
||||
<div>1</div>
|
||||
<div class="webpage-page">
|
||||
<!-- 背景装饰 -->
|
||||
<div class="background-decoration">
|
||||
<div class="gradient-blob blob-1"></div>
|
||||
<div class="gradient-blob blob-2"></div>
|
||||
<div class="gradient-blob blob-3"></div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="main-container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<div class="title-section">
|
||||
<global-outlined class="page-icon" />
|
||||
<div>
|
||||
<h1 class="page-title">网站托管</h1>
|
||||
<p class="page-subtitle">使用 Nginx 托管和预览已下载的网站</p>
|
||||
</div>
|
||||
</div>
|
||||
<a-space>
|
||||
<a-button size="large" @click="handleRefresh" :loading="loading">
|
||||
<template #icon><reload-outlined /></template>
|
||||
刷新
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
size="large"
|
||||
:danger="nginxRunning"
|
||||
@click="toggleNginx"
|
||||
:loading="nginxLoading"
|
||||
>
|
||||
<template #icon>
|
||||
<poweroff-outlined v-if="nginxRunning" />
|
||||
<play-circle-outlined v-else />
|
||||
</template>
|
||||
{{ nginxRunning ? '停止 Nginx' : '启动 Nginx' }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nginx 状态卡片 -->
|
||||
<a-card class="status-card" :bordered="false">
|
||||
<div class="status-header">
|
||||
<div class="status-title">
|
||||
<api-outlined class="title-icon" />
|
||||
<span>Nginx 服务状态</span>
|
||||
</div>
|
||||
<a-badge
|
||||
:status="nginxRunning ? 'processing' : 'default'"
|
||||
:text="nginxRunning ? '运行中' : '未启动'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<a-row :gutter="24" class="status-info">
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<div class="info-item">
|
||||
<div class="info-label">
|
||||
<thunderbolt-outlined />
|
||||
服务状态
|
||||
</div>
|
||||
<div class="info-value" :style="{ color: nginxRunning ? '#52c41a' : '#999' }">
|
||||
{{ nginxRunning ? '正常运行' : '已停止' }}
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<div class="info-item">
|
||||
<div class="info-label">
|
||||
<cluster-outlined />
|
||||
托管站点
|
||||
</div>
|
||||
<div class="info-value">{{ hostingSites.length }} 个</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<div class="info-item">
|
||||
<div class="info-label">
|
||||
<clock-circle-outlined />
|
||||
运行时长
|
||||
</div>
|
||||
<div class="info-value">{{ uptime }}</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<div class="info-item">
|
||||
<div class="info-label">
|
||||
<fund-outlined />
|
||||
总访问量
|
||||
</div>
|
||||
<div class="info-value">{{ totalVisits }}</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<!-- 网站托管列表 -->
|
||||
<a-card class="sites-card" :bordered="false">
|
||||
<template #title>
|
||||
<div class="card-title-wrapper">
|
||||
<folder-open-outlined class="title-icon" />
|
||||
<span>网站托管管理</span>
|
||||
<a-badge :count="hostingSites.length" :number-style="{ backgroundColor: '#1890ff' }" />
|
||||
</div>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="showAddModal">
|
||||
<template #icon><plus-outlined /></template>
|
||||
添加站点
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<div v-if="loading" class="loading-state">
|
||||
<a-spin size="large" tip="加载中..." />
|
||||
</div>
|
||||
|
||||
<div v-else-if="hostingSites.length === 0" class="empty-state">
|
||||
<inbox-outlined class="empty-icon" />
|
||||
<p class="empty-text">暂无托管的网站</p>
|
||||
<a-button type="primary" @click="showAddModal">
|
||||
<template #icon><plus-outlined /></template>
|
||||
添加第一个站点
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
v-else
|
||||
:columns="columns"
|
||||
:data-source="hostingSites"
|
||||
:pagination="{ pageSize: 10, showSizeChanger: true, showTotal: (total) => `共 ${total} 个站点` }"
|
||||
:scroll="{ x: 800 }"
|
||||
class="hosting-table"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'site'">
|
||||
<div class="site-cell">
|
||||
<a-avatar :style="{ backgroundColor: getAvatarColor(record.name) }" size="small">
|
||||
{{ record.name.charAt(0).toUpperCase() }}
|
||||
</a-avatar>
|
||||
<div class="site-details">
|
||||
<div class="site-name">{{ record.name }}</div>
|
||||
<div class="site-path">{{ record.path }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'domain'">
|
||||
<div class="domain-cell">
|
||||
<a-tag color="blue" v-for="domain in record.domains" :key="domain">
|
||||
{{ domain }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'port'">
|
||||
<a-tag color="cyan">{{ record.port }}</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-badge
|
||||
:status="record.enabled ? 'success' : 'default'"
|
||||
:text="record.enabled ? '已启用' : '已禁用'"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space size="small">
|
||||
<a-button type="link" size="small" @click="openSite(record)" :disabled="!nginxRunning || !record.enabled">
|
||||
<template #icon><eye-outlined /></template>
|
||||
访问
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="openFolder(record)">
|
||||
<template #icon><folder-outlined /></template>
|
||||
文件夹
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="editSite(record)">
|
||||
<template #icon><edit-outlined /></template>
|
||||
编辑
|
||||
</a-button>
|
||||
<a-switch
|
||||
v-model:checked="record.enabled"
|
||||
size="small"
|
||||
@change="toggleSiteStatus(record)"
|
||||
/>
|
||||
<a-popconfirm
|
||||
title="确定要删除此站点吗?"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="deleteSite(record)"
|
||||
>
|
||||
<a-button type="link" size="small" danger>
|
||||
<template #icon><delete-outlined /></template>
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑站点对话框 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="editingRecord ? '编辑站点' : '添加站点'"
|
||||
width="700px"
|
||||
@ok="handleModalOk"
|
||||
@cancel="handleModalCancel"
|
||||
>
|
||||
<a-form :model="formData" layout="vertical" ref="formRef">
|
||||
<a-form-item label="选择网站" name="name" :rules="[{ required: true, message: '请选择网站' }]">
|
||||
<a-select v-model:value="formData.name" placeholder="请选择已下载的网站" @change="onSiteSelect">
|
||||
<a-select-option v-for="site in availableSites" :key="site.name" :value="site.name">
|
||||
{{ site.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="域名配置" name="domains" :rules="[{ required: true, message: '请至少添加一个域名' }]">
|
||||
<a-select
|
||||
v-model:value="formData.domains"
|
||||
mode="tags"
|
||||
placeholder="输入域名后按回车添加"
|
||||
:token-separators="[',', ' ']"
|
||||
>
|
||||
</a-select>
|
||||
<div class="form-tip">
|
||||
<info-circle-outlined />
|
||||
<span>可以添加多个域名,如 example.com 或 127.0.0.1</span>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="监听端口" name="port" :rules="[{ required: true, message: '请输入端口' }]">
|
||||
<a-input-number
|
||||
v-model:value="formData.port"
|
||||
:min="80"
|
||||
:max="65535"
|
||||
placeholder="80"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="默认首页" name="index">
|
||||
<a-input v-model:value="formData.index" placeholder="index.html" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="网站路径" name="path">
|
||||
<a-input v-model:value="formData.path" disabled />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="启用状态">
|
||||
<a-switch v-model:checked="formData.enabled" />
|
||||
<span style="margin-left: 12px; color: #666;">{{ formData.enabled ? '启用后立即生效' : '暂不启用' }}</span>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import {
|
||||
GlobalOutlined,
|
||||
PlayCircleOutlined,
|
||||
PoweroffOutlined,
|
||||
ApiOutlined,
|
||||
FolderOpenOutlined,
|
||||
ReloadOutlined,
|
||||
InboxOutlined,
|
||||
PlusOutlined,
|
||||
EyeOutlined,
|
||||
FolderOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
ThunderboltOutlined,
|
||||
ClusterOutlined,
|
||||
ClockCircleOutlined,
|
||||
FundOutlined,
|
||||
InfoCircleOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import { App } from "../../../bindings/go-site-clone";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false);
|
||||
const nginxRunning = ref(false);
|
||||
const nginxLoading = ref(false);
|
||||
const uptime = ref('--');
|
||||
const totalVisits = ref(0);
|
||||
|
||||
// 托管站点列表
|
||||
const hostingSites = ref<any[]>([]);
|
||||
const availableSites = ref<any[]>([]);
|
||||
|
||||
// 对话框
|
||||
const modalVisible = ref(false);
|
||||
const editingRecord = ref<any>(null);
|
||||
const formRef = ref();
|
||||
const formData = ref({
|
||||
name: '',
|
||||
domains: ['127.0.0.1'],
|
||||
port: 80,
|
||||
path: '',
|
||||
index: 'index.html',
|
||||
enabled: true
|
||||
});
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{
|
||||
title: '网站',
|
||||
key: 'site',
|
||||
width: '25%',
|
||||
},
|
||||
{
|
||||
title: '域名',
|
||||
key: 'domain',
|
||||
width: '25%',
|
||||
},
|
||||
{
|
||||
title: '端口',
|
||||
key: 'port',
|
||||
width: '10%',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: '10%',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: '30%',
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
},
|
||||
];
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
});
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
await Promise.all([
|
||||
getAvailableSites(),
|
||||
getHostingSites()
|
||||
]);
|
||||
};
|
||||
|
||||
// 获取可用网站列表(已下载的)
|
||||
const getAvailableSites = async () => {
|
||||
try {
|
||||
const res = await App.GetDownloadList();
|
||||
availableSites.value = res || [];
|
||||
} catch (error) {
|
||||
console.error('获取网站列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取托管列表
|
||||
const getHostingSites = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
// TODO: 调用后端接口获取托管列表
|
||||
// 暂时使用模拟数据
|
||||
hostingSites.value = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'example.com',
|
||||
path: '/www/example.com',
|
||||
domains: ['example.com', '127.0.0.1'],
|
||||
port: 80,
|
||||
index: 'index.html',
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
} catch (error) {
|
||||
console.error('获取托管列表失败:', error);
|
||||
message.error('获取托管列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新数据
|
||||
const handleRefresh = () => {
|
||||
loadData();
|
||||
message.success('刷新成功');
|
||||
};
|
||||
|
||||
// 启停 Nginx
|
||||
const toggleNginx = async () => {
|
||||
nginxLoading.value = true;
|
||||
try {
|
||||
if (nginxRunning.value) {
|
||||
// TODO: 调用后端接口停止 Nginx
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
nginxRunning.value = false;
|
||||
uptime.value = '--';
|
||||
message.success('Nginx 已停止');
|
||||
} else {
|
||||
// TODO: 调用后端接口启动 Nginx
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
nginxRunning.value = true;
|
||||
uptime.value = '00:00:00';
|
||||
message.success('Nginx 已启动');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(nginxRunning.value ? '停止失败' : '启动失败');
|
||||
} finally {
|
||||
nginxLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 显示添加对话框
|
||||
const showAddModal = () => {
|
||||
editingRecord.value = null;
|
||||
formData.value = {
|
||||
name: '',
|
||||
domains: ['127.0.0.1'],
|
||||
port: 80,
|
||||
path: '',
|
||||
index: 'index.html',
|
||||
enabled: true
|
||||
};
|
||||
modalVisible.value = true;
|
||||
};
|
||||
|
||||
// 编辑站点
|
||||
const editSite = (record: any) => {
|
||||
editingRecord.value = record;
|
||||
formData.value = { ...record };
|
||||
modalVisible.value = true;
|
||||
};
|
||||
|
||||
// 网站选择改变
|
||||
const onSiteSelect = (value: string) => {
|
||||
const site = availableSites.value.find(s => s.name === value);
|
||||
if (site) {
|
||||
formData.value.path = `/www/${value}`;
|
||||
formData.value.domains = [value];
|
||||
}
|
||||
};
|
||||
|
||||
// 确定对话框
|
||||
const handleModalOk = async () => {
|
||||
try {
|
||||
await formRef.value?.validate();
|
||||
|
||||
if (editingRecord.value) {
|
||||
// 更新
|
||||
const index = hostingSites.value.findIndex(s => s.id === editingRecord.value.id);
|
||||
if (index !== -1) {
|
||||
hostingSites.value[index] = { ...formData.value, id: editingRecord.value.id };
|
||||
}
|
||||
message.success('更新成功');
|
||||
} else {
|
||||
// 添加
|
||||
hostingSites.value.push({
|
||||
...formData.value,
|
||||
id: Date.now().toString()
|
||||
});
|
||||
message.success('添加成功');
|
||||
}
|
||||
|
||||
modalVisible.value = false;
|
||||
} catch (error) {
|
||||
console.error('验证失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 取消对话框
|
||||
const handleModalCancel = () => {
|
||||
modalVisible.value = false;
|
||||
};
|
||||
|
||||
// 切换站点状态
|
||||
const toggleSiteStatus = (record: any) => {
|
||||
message.success(record.enabled ? '已启用' : '已禁用');
|
||||
};
|
||||
|
||||
// 删除站点
|
||||
const deleteSite = (record: any) => {
|
||||
const index = hostingSites.value.findIndex(s => s.id === record.id);
|
||||
if (index !== -1) {
|
||||
hostingSites.value.splice(index, 1);
|
||||
message.success('删除成功');
|
||||
}
|
||||
};
|
||||
|
||||
// 访问站点
|
||||
const openSite = (record: any) => {
|
||||
const url = `http://${record.domains[0]}:${record.port}`;
|
||||
window.open(url, '_blank');
|
||||
};
|
||||
|
||||
// 打开文件夹
|
||||
const openFolder = async (record: any) => {
|
||||
try {
|
||||
await App.OpenSiteFileDir(record.name);
|
||||
message.success('已打开文件夹');
|
||||
} catch (error) {
|
||||
console.error('打开文件夹失败:', error);
|
||||
message.error('打开文件夹失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 获取头像颜色
|
||||
const getAvatarColor = (name: string) => {
|
||||
const colors = ['#1890ff', '#52c41a', '#722ed1', '#fa8c16', '#eb2f96'];
|
||||
const index = name.charCodeAt(0) % colors.length;
|
||||
return colors[index];
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 页面布局 */
|
||||
.webpage-page {
|
||||
min-height: calc(100vh - 64px);
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 背景装饰 */
|
||||
.background-decoration {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gradient-blob {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(100px);
|
||||
opacity: 0.25;
|
||||
animation: float 25s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.blob-1 {
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
top: -15%;
|
||||
right: -15%;
|
||||
}
|
||||
|
||||
.blob-2 {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
bottom: -15%;
|
||||
left: -15%;
|
||||
animation-delay: 12s;
|
||||
}
|
||||
|
||||
.blob-3 {
|
||||
width: 450px;
|
||||
height: 450px;
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
animation-delay: 8s;
|
||||
}
|
||||
|
||||
/* 主容器 */
|
||||
.main-container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 32px 24px;
|
||||
}
|
||||
|
||||
/* 页面标题 */
|
||||
.page-header {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 32px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
|
||||
animation: fadeInDown 0.6s ease-out;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.title-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.page-icon {
|
||||
font-size: 48px;
|
||||
color: #1890ff;
|
||||
padding: 16px;
|
||||
background: linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin: 4px 0 0 0;
|
||||
}
|
||||
|
||||
/* 卡片样式 */
|
||||
.status-card,
|
||||
.sites-card,
|
||||
.config-card {
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.06);
|
||||
margin-bottom: 24px;
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
.card-title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
font-size: 20px;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
/* 状态卡片 */
|
||||
.status-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.status-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-info {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
||||
border-radius: 12px;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid #f0f0f0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.info-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 托管表格 */
|
||||
.hosting-table {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
:deep(.hosting-table .ant-table) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(.hosting-table .ant-table-thead > tr > th) {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #1890ff;
|
||||
}
|
||||
|
||||
:deep(.hosting-table .ant-table-tbody > tr) {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:deep(.hosting-table .ant-table-tbody > tr:hover) {
|
||||
background: #e6f7ff !important;
|
||||
}
|
||||
|
||||
.site-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.site-details {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.site-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.site-path {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.domain-cell {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* 加载和空状态 */
|
||||
.loading-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 80px 20px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 100px;
|
||||
color: #d9d9d9;
|
||||
margin-bottom: 24px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
/* 表单提示 */
|
||||
.form-tip {
|
||||
margin-top: 8px;
|
||||
padding: 8px 12px;
|
||||
background: #e6f7ff;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #1890ff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* 对话框样式优化 */
|
||||
:deep(.ant-modal-header) {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border-bottom: 2px solid #1890ff;
|
||||
}
|
||||
|
||||
:deep(.ant-modal-title) {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 按钮样式优化 */
|
||||
:deep(.ant-btn-primary) {
|
||||
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
|
||||
:deep(.ant-btn-primary:hover) {
|
||||
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
|
||||
}
|
||||
|
||||
/* 网站网格 */
|
||||
.sites-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.site-item {
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.site-item:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.site-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.site-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.site-url {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.site-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 表单 */
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 动画 */
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translate(0, 0) rotate(0deg);
|
||||
}
|
||||
33% {
|
||||
transform: translate(40px, -40px) rotate(120deg);
|
||||
}
|
||||
66% {
|
||||
transform: translate(-30px, 30px) rotate(240deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.main-container {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.title-section {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.sites-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,64 +1,683 @@
|
||||
<template>
|
||||
<a-breadcrumb style="margin: 10px 0">
|
||||
<a-breadcrumb-item>仿站</a-breadcrumb-item>
|
||||
<a-breadcrumb-item>仿站记录</a-breadcrumb-item>
|
||||
</a-breadcrumb>
|
||||
<div>
|
||||
<a-card :bordered="false" class="table-list">
|
||||
<a-table :dataSource="dataSource" :columns="columns" >
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'action'">
|
||||
<span>
|
||||
<a @click="openFileDir(record.name)">查看文件</a>
|
||||
</span>
|
||||
</template>
|
||||
<div class="record-page">
|
||||
<!-- 面包屑导航 -->
|
||||
<a-breadcrumb class="page-breadcrumb">
|
||||
<a-breadcrumb-item>
|
||||
<home-outlined />
|
||||
<span>仿站</span>
|
||||
</a-breadcrumb-item>
|
||||
<a-breadcrumb-item>
|
||||
<history-outlined />
|
||||
<span>仿站记录</span>
|
||||
</a-breadcrumb-item>
|
||||
</a-breadcrumb>
|
||||
|
||||
<!-- 背景装饰 -->
|
||||
<div class="background-decoration">
|
||||
<div class="gradient-blob blob-1"></div>
|
||||
<div class="gradient-blob blob-2"></div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="main-container">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<div class="header-left">
|
||||
<folder-open-outlined class="header-icon" />
|
||||
<div class="header-info">
|
||||
<h1 class="page-title">仿站记录</h1>
|
||||
<p class="page-subtitle">查看和管理已下载的网站资源</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<a-space size="middle">
|
||||
<a-button @click="handleRefresh" :loading="loading">
|
||||
<template #icon><reload-outlined /></template>
|
||||
刷新列表
|
||||
</a-button>
|
||||
<a-badge :count="dataSource.length" :number-style="{ backgroundColor: '#52c41a' }">
|
||||
<a-button type="primary">
|
||||
<template #icon><database-outlined /></template>
|
||||
总计站点
|
||||
</a-button>
|
||||
</a-badge>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格卡片 -->
|
||||
<a-card :bordered="false" class="table-card">
|
||||
<template #title>
|
||||
<div class="card-title-wrapper">
|
||||
<appstore-outlined class="title-icon" />
|
||||
<span>站点列表</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<a-table
|
||||
:dataSource="dataSource"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="paginationConfig"
|
||||
:scroll="{ x: 800 }"
|
||||
class="custom-table"
|
||||
>
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<!-- 名称列 -->
|
||||
<template v-if="column.key === 'name'">
|
||||
<div class="name-cell">
|
||||
<a-avatar
|
||||
:style="{ backgroundColor: getAvatarColor(index) }"
|
||||
:size="40"
|
||||
shape="square"
|
||||
>
|
||||
<template #icon><global-outlined /></template>
|
||||
</a-avatar>
|
||||
<div class="name-info">
|
||||
<div class="site-name">{{ record.name }}</div>
|
||||
<div class="site-path">{{ record.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 大小列 -->
|
||||
<template v-if="column.key === 'size'">
|
||||
<a-tag color="blue" class="size-tag">
|
||||
<file-outlined />
|
||||
{{ formatSize(record.size) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 权限列 -->
|
||||
<template v-if="column.key === 'mode'">
|
||||
<a-tag color="purple">
|
||||
<lock-outlined />
|
||||
{{ record.mode }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 修改时间列 -->
|
||||
<template v-if="column.key === 'modTime'">
|
||||
<div class="time-cell">
|
||||
<clock-circle-outlined class="time-icon" />
|
||||
<span>{{ formatTime(record.modTime) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button
|
||||
type="link"
|
||||
@click="openFileDir(record.name)"
|
||||
class="action-btn"
|
||||
>
|
||||
<template #icon><folder-open-outlined /></template>
|
||||
打开文件夹
|
||||
</a-button>
|
||||
<a-button
|
||||
type="link"
|
||||
danger
|
||||
@click="handleDelete(record)"
|
||||
class="action-btn"
|
||||
>
|
||||
<template #icon><delete-outlined /></template>
|
||||
删除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<template #emptyText>
|
||||
<a-empty description="暂无仿站记录">
|
||||
<template #image>
|
||||
<inbox-outlined style="font-size: 64px; color: #d9d9d9;" />
|
||||
</template>
|
||||
</a-empty>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { App } from "../../../bindings/go-site-clone";
|
||||
const columns = [
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: '大小',
|
||||
dataIndex: 'size',
|
||||
key: 'size',
|
||||
},
|
||||
{
|
||||
title: '权限',
|
||||
dataIndex: 'mode',
|
||||
key: 'mode',
|
||||
},
|
||||
{
|
||||
title: '修改时间',
|
||||
key: 'modTime',
|
||||
dataIndex: 'modTime',
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
key: 'action',
|
||||
},
|
||||
];
|
||||
import {
|
||||
HomeOutlined,
|
||||
HistoryOutlined,
|
||||
FolderOpenOutlined,
|
||||
ReloadOutlined,
|
||||
DatabaseOutlined,
|
||||
AppstoreOutlined,
|
||||
GlobalOutlined,
|
||||
FileOutlined,
|
||||
LockOutlined,
|
||||
ClockCircleOutlined,
|
||||
DeleteOutlined,
|
||||
InboxOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { App } from "../../../bindings/go-site-clone"
|
||||
import { Modal, message } from 'ant-design-vue'
|
||||
|
||||
const dataSource = ref([]);
|
||||
const siteList = ref([])
|
||||
// 获取网站列表
|
||||
const getList = async ()=> {
|
||||
App.GetDownloadList().then((res)=>{
|
||||
dataSource.value = JSON.parse(JSON.stringify(res))
|
||||
})
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{
|
||||
title: '站点名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: '35%',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '文件大小',
|
||||
dataIndex: 'size',
|
||||
key: 'size',
|
||||
width: '15%',
|
||||
sorter: (a, b) => a.size - b.size,
|
||||
},
|
||||
{
|
||||
title: '权限',
|
||||
dataIndex: 'mode',
|
||||
key: 'mode',
|
||||
width: '15%',
|
||||
},
|
||||
{
|
||||
title: '最后修改时间',
|
||||
key: 'modTime',
|
||||
dataIndex: 'modTime',
|
||||
width: '20%',
|
||||
sorter: (a, b) => new Date(a.modTime) - new Date(b.modTime),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: '15%',
|
||||
fixed: 'right',
|
||||
},
|
||||
]
|
||||
|
||||
// 分页配置
|
||||
const paginationConfig = {
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条,共 ${total} 条记录`,
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
}
|
||||
|
||||
const dataSource = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 获取网站列表
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await App.GetDownloadList()
|
||||
dataSource.value = JSON.parse(JSON.stringify(res))
|
||||
} catch (error) {
|
||||
console.error('获取列表失败:', error)
|
||||
message.error('获取站点列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
// 打开文件夹
|
||||
const openFileDir = async (name) => {
|
||||
App.OpenSiteFileDir(name)
|
||||
}
|
||||
|
||||
// 刷新列表
|
||||
const handleRefresh = () => {
|
||||
getList()
|
||||
message.success('刷新成功')
|
||||
}
|
||||
|
||||
// 打开文件夹
|
||||
const openFileDir = async (name) => {
|
||||
try {
|
||||
await App.OpenSiteFileDir(name)
|
||||
message.success('已打开文件夹')
|
||||
} catch (error) {
|
||||
console.error('打开文件夹失败:', error)
|
||||
message.error('打开文件夹失败')
|
||||
}
|
||||
onMounted(()=> {
|
||||
getList()
|
||||
}
|
||||
|
||||
// 删除记录
|
||||
const handleDelete = (record) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除站点 "${record.name}" 吗?此操作不可恢复。`,
|
||||
okText: '确定',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await App.DeleteSiteFileDir(record.name)
|
||||
message.success('删除成功')
|
||||
getList()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
message.error('删除失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
const formatSize = (bytes) => {
|
||||
if (!bytes || bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time) => {
|
||||
if (!time) return '-'
|
||||
const date = new Date(time)
|
||||
const now = new Date()
|
||||
const diff = now - date
|
||||
|
||||
// 小于1分钟
|
||||
if (diff < 60000) {
|
||||
return '刚刚'
|
||||
}
|
||||
// 小于1小时
|
||||
if (diff < 3600000) {
|
||||
return Math.floor(diff / 60000) + ' 分钟前'
|
||||
}
|
||||
// 小于24小时
|
||||
if (diff < 86400000) {
|
||||
return Math.floor(diff / 3600000) + ' 小时前'
|
||||
}
|
||||
// 小于7天
|
||||
if (diff < 604800000) {
|
||||
return Math.floor(diff / 86400000) + ' 天前'
|
||||
}
|
||||
|
||||
// 格式化为完整日期
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取头像颜色
|
||||
const getAvatarColor = (index) => {
|
||||
const colors = [
|
||||
'#1890ff', '#52c41a', '#722ed1', '#fa8c16',
|
||||
'#eb2f96', '#13c2c2', '#faad14', '#f5222d'
|
||||
]
|
||||
return colors[index % colors.length]
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 页面布局 */
|
||||
.record-page {
|
||||
min-height: calc(100vh - 64px);
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page-breadcrumb {
|
||||
padding: 16px 24px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.page-breadcrumb span {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
/* 背景装饰 */
|
||||
.background-decoration {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gradient-blob {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(100px);
|
||||
opacity: 0.25;
|
||||
animation: float 25s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.blob-1 {
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
top: -15%;
|
||||
right: -15%;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.blob-2 {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
bottom: -15%;
|
||||
left: -15%;
|
||||
animation-delay: 12s;
|
||||
}
|
||||
|
||||
/* 主容器 */
|
||||
.main-container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 32px 24px;
|
||||
}
|
||||
|
||||
/* 页面头部 */
|
||||
.page-header {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 28px 32px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.06);
|
||||
animation: fadeInDown 0.6s ease-out;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
font-size: 48px;
|
||||
color: #1890ff;
|
||||
padding: 16px;
|
||||
background: linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%);
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 8px 0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 表格卡片 */
|
||||
.table-card {
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.06);
|
||||
overflow: hidden;
|
||||
animation: fadeInUp 0.6s ease-out 0.2s both;
|
||||
}
|
||||
|
||||
:deep(.table-card .ant-card-head) {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
.card-title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
font-size: 20px;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
/* 自定义表格样式 */
|
||||
.custom-table {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(.custom-table .ant-table) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(.custom-table .ant-table-thead > tr > th) {
|
||||
background: #fafafa;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #e8e8e8;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
:deep(.custom-table .ant-table-tbody > tr) {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:deep(.custom-table .ant-table-tbody > tr:hover) {
|
||||
background: rgba(24, 144, 255, 0.04);
|
||||
transform: translateX(4px);
|
||||
box-shadow: -4px 0 0 0 #1890ff;
|
||||
}
|
||||
|
||||
:deep(.custom-table .ant-table-tbody > tr > td) {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
/* 名称单元格 */
|
||||
.name-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.name-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.site-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.site-path {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 大小标签 */
|
||||
.size-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 12px;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* 时间单元格 */
|
||||
.time-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.time-icon {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.action-btn {
|
||||
padding: 4px 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* 动画 */
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translate(0, 0) rotate(0deg);
|
||||
}
|
||||
33% {
|
||||
transform: translate(40px, -40px) rotate(120deg);
|
||||
}
|
||||
66% {
|
||||
transform: translate(-30px, 30px) rotate(240deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1200px) {
|
||||
.main-container {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-right :deep(.ant-space) {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
:deep(.table-card .ant-card-body) {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
:deep(.custom-table .ant-table-thead > tr > th),
|
||||
:deep(.custom-table .ant-table-tbody > tr > td) {
|
||||
padding: 12px 8px;
|
||||
}
|
||||
|
||||
.name-cell {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.page-breadcrumb {
|
||||
padding: 12px 16px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
font-size: 36px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.custom-table .ant-table) {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -13,6 +13,14 @@ import * as services$0 from "./services/models.js";
|
||||
// @ts-ignore: Unused imports
|
||||
import * as utils$0 from "./utils/models.js";
|
||||
|
||||
/**
|
||||
* @param {string} pathDir
|
||||
* @returns {$CancellablePromise<boolean>}
|
||||
*/
|
||||
export function DeleteSiteFileDir(pathDir) {
|
||||
return $Call.ByID(2647643782, pathDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} uri
|
||||
* @param {services$0.ResourcesList} obj
|
||||
|
||||
Reference in New Issue
Block a user