ZKit管理端
This commit is contained in:
24
Zkit-admin/.gitignore
vendored
Normal file
24
Zkit-admin/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
3
Zkit-admin/.vscode/extensions.json
vendored
Normal file
3
Zkit-admin/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
5
Zkit-admin/README.md
Normal file
5
Zkit-admin/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Vue 3 + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
|
||||
15
Zkit-admin/index.html
Normal file
15
Zkit-admin/index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Zkit Admin</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1917
Zkit-admin/package-lock.json
generated
Normal file
1917
Zkit-admin/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
Zkit-admin/package.json
Normal file
21
Zkit-admin/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "zkit-admin",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"pinia": "^3.0.4",
|
||||
"sass": "^1.94.2",
|
||||
"vue": "^3.5.24",
|
||||
"vue-router": "^4.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"vite": "^7.2.4"
|
||||
}
|
||||
}
|
||||
1
Zkit-admin/public/vite.svg
Normal file
1
Zkit-admin/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
9
Zkit-admin/src/App.vue
Normal file
9
Zkit-admin/src/App.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
234
Zkit-admin/src/assets/base.css
Normal file
234
Zkit-admin/src/assets/base.css
Normal file
@@ -0,0 +1,234 @@
|
||||
:root {
|
||||
/* Colors */
|
||||
--primary: #07C160;
|
||||
--primary-hover: #06ad56;
|
||||
--primary-light: #e6f9f0;
|
||||
--text-dark: #181c32;
|
||||
--text-gray: #5e6278;
|
||||
--text-light: #b5b5c3;
|
||||
--text-white: #ffffff;
|
||||
--bg-body: #f5f8fa;
|
||||
--bg-card: #ffffff;
|
||||
--bg-hover: #f5f8fa;
|
||||
|
||||
/* Spacing */
|
||||
--spacing-xs: 4px;
|
||||
--spacing-sm: 8px;
|
||||
--spacing-md: 16px;
|
||||
--spacing-lg: 24px;
|
||||
--spacing-xl: 32px;
|
||||
|
||||
/* Border Radius */
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 12px;
|
||||
--radius-lg: 20px;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 0 20px 0 rgba(76, 87, 125, 0.02);
|
||||
--shadow-lg: 0 10px 30px 0 rgba(76, 87, 125, 0.05);
|
||||
|
||||
/* Fonts */
|
||||
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family);
|
||||
background-color: var(--bg-body);
|
||||
color: var(--text-gray);
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* Utility Classes */
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-md);
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary);
|
||||
color: var(--text-white);
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--primary-hover);
|
||||
box-shadow: 0 4px 12px rgba(7, 193, 96, 0.3);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #d9364a;
|
||||
box-shadow: 0 4px 12px rgba(246, 78, 96, 0.3);
|
||||
}
|
||||
|
||||
/* Pastel Colors for Avatars/Badges */
|
||||
.bg-light-success {
|
||||
background-color: #e6f9f0;
|
||||
color: #07C160;
|
||||
}
|
||||
|
||||
.bg-light-primary {
|
||||
background-color: #e1f0ff;
|
||||
color: #3699ff;
|
||||
}
|
||||
|
||||
.bg-light-warning {
|
||||
background-color: #fff4de;
|
||||
color: #ffa800;
|
||||
}
|
||||
|
||||
.bg-light-danger {
|
||||
background-color: #ffe2e5;
|
||||
color: #f64e60;
|
||||
}
|
||||
|
||||
.bg-light-info {
|
||||
background-color: #c9f7f5;
|
||||
color: #1bc5bd;
|
||||
}
|
||||
|
||||
.bg-light-dark {
|
||||
background-color: #f5f8fa;
|
||||
color: #5e6278;
|
||||
}
|
||||
|
||||
/* Table Styles */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: transparent;
|
||||
color: #b5b5c3;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
padding: 16px 24px;
|
||||
text-align: left;
|
||||
border-bottom: 1px dashed #eff2f5;
|
||||
white-space: nowrap;
|
||||
/* Fix for mobile text wrapping */
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 20px 24px;
|
||||
color: var(--text-gray);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border-bottom: 1px dashed #eff2f5;
|
||||
transition: background-color 0.2s;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
/* Fix for mobile text wrapping */
|
||||
}
|
||||
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
tr:hover td {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Badge Styles */
|
||||
.badge {
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Button Text Style (Ghost) */
|
||||
.btn-text {
|
||||
background: transparent;
|
||||
color: var(--text-gray);
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-text:hover {
|
||||
background-color: #f5f8fa;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.btn-text.primary {
|
||||
color: var(--primary);
|
||||
background-color: var(--primary-light);
|
||||
}
|
||||
|
||||
.btn-text.primary:hover {
|
||||
background-color: var(--primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Scrollbar - Hidden but scrollable */
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: transparent;
|
||||
}
|
||||
1
Zkit-admin/src/assets/vue.svg
Normal file
1
Zkit-admin/src/assets/vue.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
173
Zkit-admin/src/components/ConfirmModal.vue
Normal file
173
Zkit-admin/src/components/ConfirmModal.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<transition name="modal-fade">
|
||||
<div v-if="visible" class="modal-overlay" @click.self="handleCancel">
|
||||
<div class="modal-container confirm-modal">
|
||||
<div class="modal-header">
|
||||
<div class="icon-wrapper">
|
||||
<i class="fa-solid fa-triangle-exclamation"></i>
|
||||
</div>
|
||||
<h3>{{ title }}</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{ message }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-cancel" @click="handleCancel">取消</button>
|
||||
<button class="btn-danger" @click="handleConfirm">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
visible: Boolean,
|
||||
title: {
|
||||
type: String,
|
||||
default: '确认删除'
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
default: '确定要删除吗?此操作无法撤销。'
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible', 'confirm'])
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
emit('confirm')
|
||||
emit('update:visible', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.modal-container.confirm-modal {
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
width: 420px;
|
||||
max-width: 90%;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: slide-up 0.3s ease-out;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 24px 24px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background-color: #fff4de;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffa800;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
color: #1e1e2d;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 0 24px 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-body p {
|
||||
margin: 0;
|
||||
color: #5e6278;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 20px 24px;
|
||||
border-top: 1px solid #f0f2f5;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
padding: 10px 24px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e4e6ef;
|
||||
background-color: #fff;
|
||||
color: #5e6278;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-cancel:hover {
|
||||
background-color: #f5f8fa;
|
||||
color: #1e1e2d;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
padding: 10px 24px;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
background-color: #f64e60;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #d9364a;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
.modal-fade-enter-active,
|
||||
.modal-fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-fade-enter-from,
|
||||
.modal-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes slide-up {
|
||||
from {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
387
Zkit-admin/src/components/Header.vue
Normal file
387
Zkit-admin/src/components/Header.vue
Normal file
@@ -0,0 +1,387 @@
|
||||
<template>
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<button class="hamburger-btn" @click="appStore.toggleMobileSidebar">
|
||||
<i class="fa-solid fa-bars"></i>
|
||||
</button>
|
||||
<h2 class="page-title">{{ routeName }}</h2>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="search-bar">
|
||||
<span class="search-icon"><i class="fa-solid fa-magnifying-glass"></i></span>
|
||||
<input type="text" placeholder="搜索..." />
|
||||
</div>
|
||||
<div class="action-icons">
|
||||
<div class="icon-wrapper-relative">
|
||||
<button class="icon-btn" @click.stop="toggleNotification" :class="{ 'active': notificationOpen }">
|
||||
<i class="fa-regular fa-bell"></i>
|
||||
<span class="badge-dot" v-if="hasUnread"></span>
|
||||
</button>
|
||||
<NotificationDropdown v-if="notificationOpen" @update-unread="updateUnreadStatus" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-profile" @click="toggleDropdown">
|
||||
<div class="avatar">
|
||||
<img src="https://ui-avatars.com/api/?name=Admin&background=E6F9F0&color=07C160" alt="Admin" />
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<span class="username">管理员</span>
|
||||
</div>
|
||||
|
||||
<!-- Dropdown Menu -->
|
||||
<transition name="dropdown-fade">
|
||||
<div v-if="dropdownOpen" class="dropdown-menu" @click.stop>
|
||||
<div class="user-summary-mobile">
|
||||
<div class="info">
|
||||
<div class="name">管理员</div>
|
||||
<div class="email">admin@zkit.com</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown-item">
|
||||
<i class="fa-regular fa-user"></i>
|
||||
<span>个人资料</span>
|
||||
</div>
|
||||
<div class="dropdown-item">
|
||||
<i class="fa-solid fa-sliders"></i>
|
||||
<span>账号设置</span>
|
||||
</div>
|
||||
<div class="dropdown-divider"></div>
|
||||
<div class="dropdown-item">
|
||||
<i class="fa-regular fa-circle-question"></i>
|
||||
<span>帮助中心</span>
|
||||
</div>
|
||||
<router-link to="/login" class="dropdown-item logout">
|
||||
<i class="fa-solid fa-right-from-bracket"></i>
|
||||
<span>退出登录</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useAppStore } from '../stores/app'
|
||||
import NotificationDropdown from './NotificationDropdown.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const appStore = useAppStore()
|
||||
const dropdownOpen = ref(false)
|
||||
const notificationOpen = ref(false)
|
||||
const hasUnread = ref(true)
|
||||
|
||||
const routeName = computed(() => {
|
||||
const nameMap = {
|
||||
'dashboard': '仪表盘',
|
||||
'users': '用户管理',
|
||||
'tools': '工具管理',
|
||||
'announcements': '公告管理',
|
||||
'faqs': 'FAQ 管理',
|
||||
'ads': '广告管理',
|
||||
'footer': '页脚管理',
|
||||
'login': '登录'
|
||||
}
|
||||
return nameMap[route.name] || route.name
|
||||
})
|
||||
|
||||
const toggleDropdown = () => {
|
||||
dropdownOpen.value = !dropdownOpen.value
|
||||
notificationOpen.value = false
|
||||
}
|
||||
|
||||
const toggleNotification = () => {
|
||||
notificationOpen.value = !notificationOpen.value
|
||||
dropdownOpen.value = false
|
||||
}
|
||||
|
||||
const updateUnreadStatus = (status) => {
|
||||
hasUnread.value = status
|
||||
}
|
||||
|
||||
const closeDropdown = (e) => {
|
||||
if (!e.target.closest('.user-profile') && !e.target.closest('.action-icons')) {
|
||||
dropdownOpen.value = false
|
||||
notificationOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', closeDropdown)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', closeDropdown)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
height: 80px;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 32px;
|
||||
box-shadow: 0 0 20px rgba(0,0,0,0.03);
|
||||
z-index: 90;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #181c32;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
font-size: 14px;
|
||||
color: #a1a5b7;
|
||||
}
|
||||
|
||||
.search-bar input {
|
||||
padding: 10px 16px 10px 38px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
background-color: #f5f8fa;
|
||||
font-size: 13px;
|
||||
width: 240px;
|
||||
transition: all 0.3s;
|
||||
outline: none;
|
||||
color: #5e6278;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.search-bar input:focus {
|
||||
background-color: #fff;
|
||||
border-color: #e4e6ef;
|
||||
color: #181c32;
|
||||
}
|
||||
|
||||
.action-icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.icon-wrapper-relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #a1a5b7;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.icon-btn:hover,
|
||||
.icon-btn.active {
|
||||
background-color: #f5f8fa;
|
||||
color: #07C160;
|
||||
}
|
||||
|
||||
.badge-dot {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: #f64e60;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.user-profile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
padding: 6px 12px;
|
||||
border-radius: 10px;
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #181c32;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(7, 193, 96, 0.2);
|
||||
border: 2px solid #e6f9f0;
|
||||
}
|
||||
|
||||
.avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 12px);
|
||||
right: 0;
|
||||
left: auto;
|
||||
width: 200px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
overflow: hidden;
|
||||
z-index: 1000;
|
||||
border: 1px solid #e4e6ef;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.user-summary-mobile {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f5f8fa;
|
||||
margin-bottom: 8px;
|
||||
background-color: #fcfcfc;
|
||||
}
|
||||
|
||||
.user-summary-mobile .name {
|
||||
font-weight: 600;
|
||||
color: #181c32;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.user-summary-mobile .email {
|
||||
font-size: 12px;
|
||||
color: #a1a5b7;
|
||||
}
|
||||
|
||||
.dropdown-divider {
|
||||
height: 1px;
|
||||
background: #e4e6ef;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 16px;
|
||||
color: #5e6278;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background-color: #f5f8fa;
|
||||
color: #181c32;
|
||||
}
|
||||
|
||||
.dropdown-item.logout {
|
||||
color: #f64e60;
|
||||
}
|
||||
|
||||
.dropdown-item.logout:hover {
|
||||
background-color: #ffe2e5;
|
||||
}
|
||||
|
||||
.dropdown-item i {
|
||||
font-size: 16px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Dropdown Animation */
|
||||
.dropdown-fade-enter-active {
|
||||
transition: opacity 0.3s, transform 0.3s;
|
||||
}
|
||||
|
||||
.dropdown-fade-leave-active {
|
||||
transition: opacity 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.dropdown-fade-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px) scale(0.95);
|
||||
}
|
||||
|
||||
.dropdown-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-5px) scale(0.98);
|
||||
}
|
||||
.hamburger-btn {
|
||||
display: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
color: #5e6278;
|
||||
cursor: pointer;
|
||||
margin-right: 16px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header {
|
||||
padding: 0 16px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.hamburger-btn {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: none; /* Hide search bar on mobile for simplicity, or make it an icon */
|
||||
}
|
||||
|
||||
.action-icons {
|
||||
display: none; /* Hide action icons on mobile to save space */
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
164
Zkit-admin/src/components/Modal.vue
Normal file
164
Zkit-admin/src/components/Modal.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<transition name="modal-fade">
|
||||
<div v-if="visible" class="modal-overlay" @click.self="handleClose">
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<h3>{{ title }}</h3>
|
||||
<button class="close-btn" @click="handleClose">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-cancel" @click="handleClose">取消</button>
|
||||
<button class="btn-confirm" @click="handleConfirm">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
visible: Boolean,
|
||||
title: String
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible', 'confirm'])
|
||||
|
||||
const handleClose = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
emit('confirm')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
width: auto;
|
||||
min-width: 600px;
|
||||
max-width: 90%;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: slide-up 0.3s ease-out;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.modal-container {
|
||||
min-width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
color: #1e1e2d;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
color: #a1a5b7;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
color: #5e6278;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 24px;
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 20px 24px;
|
||||
border-top: 1px solid #f0f2f5;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e4e6ef;
|
||||
background-color: #fff;
|
||||
color: #5e6278;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-cancel:hover {
|
||||
background-color: #f5f8fa;
|
||||
color: #1e1e2d;
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
background-color: #3699ff;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.btn-confirm:hover {
|
||||
background-color: #0073e9;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
.modal-fade-enter-active,
|
||||
.modal-fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-fade-enter-from,
|
||||
.modal-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes slide-up {
|
||||
from {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
187
Zkit-admin/src/components/NotificationDropdown.vue
Normal file
187
Zkit-admin/src/components/NotificationDropdown.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<div class="notification-dropdown">
|
||||
<div class="dropdown-header">
|
||||
<h3>通知</h3>
|
||||
<button class="mark-read-btn" @click="markAllAsRead" v-if="hasUnread">全部已读</button>
|
||||
</div>
|
||||
<div class="notification-list">
|
||||
<div
|
||||
v-for="item in notifications"
|
||||
:key="item.id"
|
||||
class="notification-item"
|
||||
:class="{ 'unread': !item.read }"
|
||||
@click="markAsRead(item)"
|
||||
>
|
||||
<div class="icon-wrapper" :class="item.type">
|
||||
<i class="fa-solid" :class="item.icon"></i>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p class="title">{{ item.title }}</p>
|
||||
<span class="time">{{ item.time }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown-footer">
|
||||
<button class="view-all-btn">查看全部通知</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const emit = defineEmits(['update-unread'])
|
||||
|
||||
const notifications = ref([
|
||||
{ id: 1, title: '系统更新成功', time: '5分钟前', type: 'success', icon: 'fa-check', read: false },
|
||||
{ id: 2, title: '存储空间不足警告', time: '1小时前', type: 'warning', icon: 'fa-triangle-exclamation', read: false },
|
||||
{ id: 3, title: '新用户注册: John Doe', time: '2小时前', type: 'info', icon: 'fa-user-plus', read: true },
|
||||
{ id: 4, title: '收到新的反馈消息', time: '1天前', type: 'primary', icon: 'fa-envelope', read: true }
|
||||
])
|
||||
|
||||
const hasUnread = computed(() => {
|
||||
return notifications.value.some(n => !n.read)
|
||||
})
|
||||
|
||||
const markAllAsRead = () => {
|
||||
notifications.value.forEach(n => n.read = true)
|
||||
emit('update-unread', false)
|
||||
}
|
||||
|
||||
const markAsRead = (item) => {
|
||||
if (!item.read) {
|
||||
item.read = true
|
||||
emit('update-unread', hasUnread.value)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.notification-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 12px);
|
||||
right: -80px; /* Adjust based on parent position */
|
||||
width: 320px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
border: 1px solid #e4e6ef;
|
||||
overflow: hidden;
|
||||
z-index: 1000;
|
||||
animation: dropdown-fade-in 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes dropdown-fade-in {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.dropdown-header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #f5f8fa;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dropdown-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: #181c32;
|
||||
}
|
||||
|
||||
.mark-read-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #07C160;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.mark-read-btn:hover {
|
||||
background-color: #e6f9f0;
|
||||
}
|
||||
|
||||
.notification-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.notification-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #f5f8fa;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.notification-item:hover {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.notification-item.unread {
|
||||
background-color: #fcfcfc;
|
||||
}
|
||||
|
||||
.notification-item.unread .title {
|
||||
font-weight: 600;
|
||||
color: #181c32;
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.icon-wrapper.success { background-color: #e6f9f0; color: #07C160; }
|
||||
.icon-wrapper.warning { background-color: #fff4de; color: #ffa800; }
|
||||
.icon-wrapper.info { background-color: #e1f0ff; color: #3699ff; }
|
||||
.icon-wrapper.primary { background-color: #f5f8fa; color: #5e6278; }
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 13px;
|
||||
color: #5e6278;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: #a1a5b7;
|
||||
}
|
||||
|
||||
.dropdown-footer {
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
border-top: 1px solid #f5f8fa;
|
||||
}
|
||||
|
||||
.view-all-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #07C160;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.view-all-btn:hover {
|
||||
background-color: #f5f8fa;
|
||||
}
|
||||
</style>
|
||||
230
Zkit-admin/src/components/Pagination.vue
Normal file
230
Zkit-admin/src/components/Pagination.vue
Normal file
@@ -0,0 +1,230 @@
|
||||
<template>
|
||||
<div class="pagination">
|
||||
<div class="pagination-right">
|
||||
<div class="total-count">
|
||||
共 {{ total }} 条
|
||||
</div>
|
||||
|
||||
<div class="page-size-selector">
|
||||
<select :value="pageSize" @change="changePageSize($event.target.value)">
|
||||
<option :value="7">7条/页</option>
|
||||
<option :value="10">10条/页</option>
|
||||
<option :value="20">20条/页</option>
|
||||
<option :value="50">50条/页</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="pagination-controls" v-if="totalPages > 0">
|
||||
<button
|
||||
class="page-btn"
|
||||
:disabled="currentPage === 1"
|
||||
@click="changePage(currentPage - 1)"
|
||||
>
|
||||
<i class="fa-solid fa-chevron-left"></i>
|
||||
</button>
|
||||
|
||||
<div class="page-numbers">
|
||||
<button
|
||||
v-for="page in pages"
|
||||
:key="page"
|
||||
class="page-num"
|
||||
:class="{ active: currentPage === page }"
|
||||
@click="changePage(page)"
|
||||
>
|
||||
{{ page }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="page-btn"
|
||||
:disabled="currentPage === totalPages"
|
||||
@click="changePage(currentPage + 1)"
|
||||
>
|
||||
<i class="fa-solid fa-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
total: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 7
|
||||
},
|
||||
currentPage: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:currentPage', 'update:pageSize'])
|
||||
|
||||
const totalPages = computed(() => Math.ceil(props.total / props.pageSize))
|
||||
|
||||
const pages = computed(() => {
|
||||
const p = []
|
||||
for (let i = 1; i <= totalPages.value; i++) {
|
||||
p.push(i)
|
||||
}
|
||||
return p
|
||||
})
|
||||
|
||||
const changePage = (page) => {
|
||||
if (page >= 1 && page <= totalPages.value) {
|
||||
emit('update:currentPage', page)
|
||||
}
|
||||
}
|
||||
|
||||
const changePageSize = (size) => {
|
||||
const newSize = parseInt(size)
|
||||
emit('update:pageSize', newSize)
|
||||
emit('update:currentPage', 1)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-top: 0;
|
||||
padding: 24px;
|
||||
border-top: 1px dashed #eff2f5;
|
||||
background: #ffffff;
|
||||
border-bottom-left-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
}
|
||||
|
||||
.pagination-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.total-count {
|
||||
color: #a1a5b7;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
padding: 8px 16px;
|
||||
background: #f5f8fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.page-size-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #7e8299;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.page-size-selector select {
|
||||
padding: 8px 12px;
|
||||
border: 2px solid #e4e6ef;
|
||||
border-radius: 8px;
|
||||
color: #5e6278;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
background-color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.page-size-selector select:hover {
|
||||
border-color: #07C160;
|
||||
}
|
||||
|
||||
.page-size-selector select:focus {
|
||||
border-color: #07C160;
|
||||
box-shadow: 0 0 0 3px rgba(7, 193, 96, 0.1);
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.page-btn,
|
||||
.page-num {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #7e8299;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.page-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: #f5f8fa;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.page-btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.page-btn:not(:disabled):hover {
|
||||
background: #e6f9f0;
|
||||
color: #07C160;
|
||||
border-color: #07C160;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.page-btn:not(:disabled):active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.page-numbers {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.page-num {
|
||||
min-width: 36px;
|
||||
height: 36px;
|
||||
padding: 0 8px;
|
||||
background: #f5f8fa;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.page-num:hover {
|
||||
color: #07C160;
|
||||
background: #e6f9f0;
|
||||
border-color: #07C160;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.page-num:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.page-num.active {
|
||||
background: linear-gradient(135deg, #07C160 0%, #05a050 100%);
|
||||
color: #fff;
|
||||
border-color: #07C160;
|
||||
box-shadow: 0 4px 12px rgba(7, 193, 96, 0.3);
|
||||
}
|
||||
|
||||
.page-num.active:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(7, 193, 96, 0.4);
|
||||
}
|
||||
</style>
|
||||
139
Zkit-admin/src/components/SettingsDropdown.vue
Normal file
139
Zkit-admin/src/components/SettingsDropdown.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div class="settings-dropdown">
|
||||
<div class="user-summary">
|
||||
<div class="avatar">
|
||||
<img src="https://ui-avatars.com/api/?name=Admin&background=E6F9F0&color=07C160" alt="Admin" />
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="name">管理员</div>
|
||||
<div class="email">admin@zkit.com</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-list">
|
||||
<div class="menu-item">
|
||||
<i class="fa-regular fa-user"></i>
|
||||
<span>个人资料</span>
|
||||
</div>
|
||||
<div class="menu-item">
|
||||
<i class="fa-solid fa-sliders"></i>
|
||||
<span>账号设置</span>
|
||||
</div>
|
||||
<div class="menu-item">
|
||||
<i class="fa-solid fa-palette"></i>
|
||||
<span>主题设置</span>
|
||||
</div>
|
||||
<div class="menu-divider"></div>
|
||||
<div class="menu-item">
|
||||
<i class="fa-regular fa-circle-question"></i>
|
||||
<span>帮助中心</span>
|
||||
</div>
|
||||
<div class="menu-item logout">
|
||||
<i class="fa-solid fa-right-from-bracket"></i>
|
||||
<span>退出登录</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// Logic can be added here
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.settings-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 12px);
|
||||
right: 0;
|
||||
width: 240px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
border: 1px solid #e4e6ef;
|
||||
overflow: hidden;
|
||||
z-index: 1000;
|
||||
animation: dropdown-fade-in 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes dropdown-fade-in {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.user-summary {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid #f5f8fa;
|
||||
background-color: #fcfcfc;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: 600;
|
||||
color: #181c32;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.email {
|
||||
font-size: 12px;
|
||||
color: #a1a5b7;
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 20px;
|
||||
color: #5e6278;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background-color: #f5f8fa;
|
||||
color: #07C160;
|
||||
}
|
||||
|
||||
.menu-item i {
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.menu-divider {
|
||||
height: 1px;
|
||||
background-color: #f5f8fa;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.menu-item.logout {
|
||||
color: #f64e60;
|
||||
}
|
||||
|
||||
.menu-item.logout:hover {
|
||||
background-color: #ffe2e5;
|
||||
}
|
||||
</style>
|
||||
350
Zkit-admin/src/components/Sidebar.vue
Normal file
350
Zkit-admin/src/components/Sidebar.vue
Normal file
@@ -0,0 +1,350 @@
|
||||
<template>
|
||||
<aside class="sidebar" :class="{ 'collapsed': appStore.sidebarCollapsed, 'mobile-open': appStore.mobileSidebarOpen }">
|
||||
<div class="logo">
|
||||
<div class="logo-icon">
|
||||
<i class="fa-solid fa-layer-group"></i>
|
||||
</div>
|
||||
<span class="logo-text" v-show="!appStore.sidebarCollapsed">Zkit管理系统</span>
|
||||
<button class="collapse-btn-header" @click="appStore.toggleSidebar" title="收起菜单">
|
||||
<i class="fa-solid" :class="appStore.sidebarCollapsed ? 'fa-angles-right' : 'fa-angles-left'"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Tools (Search & Actions) -->
|
||||
<div class="mobile-tools" v-show="appStore.mobileSidebarOpen">
|
||||
<div class="mobile-search">
|
||||
<i class="fa-solid fa-magnifying-glass search-icon"></i>
|
||||
<input type="text" placeholder="搜索..." />
|
||||
</div>
|
||||
<div class="mobile-actions">
|
||||
<button class="icon-btn">
|
||||
<i class="fa-regular fa-bell"></i>
|
||||
<span class="badge-dot"></span>
|
||||
</button>
|
||||
<button class="icon-btn">
|
||||
<i class="fa-solid fa-gear"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="nav-menu">
|
||||
<router-link to="/" class="nav-item" title="仪表盘">
|
||||
<span class="icon"><i class="fa-solid fa-chart-line"></i></span>
|
||||
<span class="label" v-show="!appStore.sidebarCollapsed">仪表盘</span>
|
||||
</router-link>
|
||||
<div class="nav-section" v-show="!appStore.sidebarCollapsed">管理模块</div>
|
||||
<div class="nav-section-divider" v-show="appStore.sidebarCollapsed"></div>
|
||||
<router-link to="/users" class="nav-item" title="用户管理">
|
||||
<span class="icon"><i class="fa-solid fa-users"></i></span>
|
||||
<span class="label" v-show="!appStore.sidebarCollapsed">用户管理</span>
|
||||
</router-link>
|
||||
<router-link to="/tools" class="nav-item" title="工具管理">
|
||||
<span class="icon"><i class="fa-solid fa-screwdriver-wrench"></i></span>
|
||||
<span class="label" v-show="!appStore.sidebarCollapsed">工具管理</span>
|
||||
</router-link>
|
||||
<router-link to="/announcements" class="nav-item" title="公告管理">
|
||||
<span class="icon"><i class="fa-solid fa-bullhorn"></i></span>
|
||||
<span class="label" v-show="!appStore.sidebarCollapsed">公告管理</span>
|
||||
</router-link>
|
||||
<router-link to="/faqs" class="nav-item" title="FAQ 管理">
|
||||
<span class="icon"><i class="fa-solid fa-circle-question"></i></span>
|
||||
<span class="label" v-show="!appStore.sidebarCollapsed">FAQ 管理</span>
|
||||
</router-link>
|
||||
<router-link to="/ads" class="nav-item" title="广告管理">
|
||||
<span class="icon"><i class="fa-solid fa-rectangle-ad"></i></span>
|
||||
<span class="label" v-show="!appStore.sidebarCollapsed">广告管理</span>
|
||||
</router-link>
|
||||
<router-link to="/footer" class="nav-item" title="页脚管理">
|
||||
<span class="icon"><i class="fa-solid fa-shoe-prints"></i></span>
|
||||
<span class="label" v-show="!appStore.sidebarCollapsed">页脚管理</span>
|
||||
</router-link>
|
||||
</nav>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAppStore } from '../stores/app'
|
||||
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
background: #ffffff;
|
||||
color: #5e6278;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: width 0.3s ease;
|
||||
z-index: 100;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 0 20px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.sidebar.collapsed {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 28px;
|
||||
background-color: #ffffff;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid #f5f8fa;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .logo {
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: linear-gradient(135deg, #07C160 0%, #06ad56 100%);
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
margin-right: 12px;
|
||||
box-shadow: 0 4px 12px rgba(7, 193, 96, 0.3);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .logo-icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #181c32;
|
||||
letter-spacing: 0.5px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.collapse-btn-header {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #a1a5b7;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.collapse-btn-header:hover {
|
||||
background: #f5f8fa;
|
||||
color: #07C160;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .collapse-btn-header {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
padding: 24px 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.nav-section {
|
||||
padding: 16px 28px 8px;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: #a1a5b7;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nav-section-divider {
|
||||
height: 1px;
|
||||
background-color: #f5f8fa;
|
||||
margin: 16px 20px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 28px;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border-right: 3px solid transparent;
|
||||
color: #5e6278;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
margin-right: 20px;
|
||||
border-top-right-radius: 40px;
|
||||
border-bottom-right-radius: 40px;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .nav-item {
|
||||
padding: 12px 0;
|
||||
justify-content: center;
|
||||
margin-right: 0;
|
||||
border-radius: 0;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.nav-item .icon {
|
||||
width: 24px;
|
||||
margin-right: 12px;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: color 0.3s;
|
||||
color: #a1a5b7;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .nav-item .icon {
|
||||
margin-right: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background-color: #f5f8fa;
|
||||
color: #07C160;
|
||||
}
|
||||
|
||||
.nav-item:hover .icon {
|
||||
color: #07C160;
|
||||
}
|
||||
|
||||
.nav-item.router-link-exact-active {
|
||||
background-color: #e6f9f0;
|
||||
color: #07C160;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .nav-item.router-link-exact-active {
|
||||
background-color: #e6f9f0;
|
||||
}
|
||||
|
||||
.nav-item.router-link-exact-active .icon {
|
||||
color: #07C160;
|
||||
}
|
||||
|
||||
/* Scrollbar for sidebar */
|
||||
.nav-menu::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.nav-menu::-webkit-scrollbar-thumb {
|
||||
background: #e4e6ef;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.mobile-tools {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
width: 280px !important; /* Always full width on mobile when open */
|
||||
transform: translateX(-100%);
|
||||
box-shadow: 0 0 40px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.sidebar.mobile-open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.collapse-btn-header {
|
||||
display: none; /* Hide collapse button on mobile, use backdrop to close */
|
||||
}
|
||||
|
||||
.mobile-tools {
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #f5f8fa;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.mobile-search {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mobile-search .search-icon {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
color: #a1a5b7;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.mobile-search input {
|
||||
width: 100%;
|
||||
padding: 10px 16px 10px 38px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e4e6ef;
|
||||
background-color: #f5f8fa;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
color: #5e6278;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.mobile-search input:focus {
|
||||
background-color: #fff;
|
||||
border-color: #07C160;
|
||||
}
|
||||
|
||||
.mobile-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.mobile-actions .icon-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #e4e6ef;
|
||||
background: transparent;
|
||||
color: #a1a5b7;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mobile-actions .icon-btn:hover {
|
||||
color: #07C160;
|
||||
background-color: #f5f8fa;
|
||||
border-color: #07C160;
|
||||
}
|
||||
|
||||
.badge-dot {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: #f64e60;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
77
Zkit-admin/src/layout/MainLayout.vue
Normal file
77
Zkit-admin/src/layout/MainLayout.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="main-layout">
|
||||
<Sidebar />
|
||||
<transition name="fade">
|
||||
<div v-if="appStore.mobileSidebarOpen" class="mobile-backdrop" @click="appStore.closeMobileSidebar"></div>
|
||||
</transition>
|
||||
<div class="content-wrapper">
|
||||
<Header />
|
||||
<main class="main-content">
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<transition name="fade" mode="out-in">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Sidebar from '../components/Sidebar.vue'
|
||||
import Header from '../components/Header.vue'
|
||||
import { useAppStore } from '../stores/app'
|
||||
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main-layout {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
background-color: #f5f8fa;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0; /* Prevent flex overflow */
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: 32px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Transition effects */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.mobile-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 99;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-content {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
13
Zkit-admin/src/main.js
Normal file
13
Zkit-admin/src/main.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import './assets/base.css'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
||||
57
Zkit-admin/src/router/index.js
Normal file
57
Zkit-admin/src/router/index.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import MainLayout from '../layout/MainLayout.vue'
|
||||
import Dashboard from '../views/Dashboard.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
component: MainLayout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'dashboard',
|
||||
component: Dashboard
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
name: 'users',
|
||||
component: () => import('../views/Users.vue')
|
||||
},
|
||||
{
|
||||
path: 'tools',
|
||||
name: 'tools',
|
||||
component: () => import('../views/Tools.vue')
|
||||
},
|
||||
{
|
||||
path: 'announcements',
|
||||
name: 'announcements',
|
||||
component: () => import('../views/Announcements.vue')
|
||||
},
|
||||
{
|
||||
path: 'faqs',
|
||||
name: 'faqs',
|
||||
component: () => import('../views/Faqs.vue')
|
||||
},
|
||||
{
|
||||
path: 'ads',
|
||||
name: 'ads',
|
||||
component: () => import('../views/Ads.vue')
|
||||
},
|
||||
{
|
||||
path: 'footer',
|
||||
name: 'footer',
|
||||
component: () => import('../views/Footer.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: () => import('../views/Login.vue')
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
export default router
|
||||
27
Zkit-admin/src/stores/app.js
Normal file
27
Zkit-admin/src/stores/app.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useAppStore = defineStore('app', () => {
|
||||
const sidebarCollapsed = ref(false)
|
||||
const mobileSidebarOpen = ref(false)
|
||||
|
||||
const toggleSidebar = () => {
|
||||
sidebarCollapsed.value = !sidebarCollapsed.value
|
||||
}
|
||||
|
||||
const toggleMobileSidebar = () => {
|
||||
mobileSidebarOpen.value = !mobileSidebarOpen.value
|
||||
}
|
||||
|
||||
const closeMobileSidebar = () => {
|
||||
mobileSidebarOpen.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
sidebarCollapsed,
|
||||
mobileSidebarOpen,
|
||||
toggleSidebar,
|
||||
toggleMobileSidebar,
|
||||
closeMobileSidebar
|
||||
}
|
||||
})
|
||||
79
Zkit-admin/src/style.css
Normal file
79
Zkit-admin/src/style.css
Normal file
@@ -0,0 +1,79 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
433
Zkit-admin/src/views/Ads.vue
Normal file
433
Zkit-admin/src/views/Ads.vue
Normal file
@@ -0,0 +1,433 @@
|
||||
<template>
|
||||
<div class="ads-page">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>广告管理</h3>
|
||||
<button class="btn-primary" @click="openAddModal">
|
||||
<i class="fa-solid fa-plus"></i> 新增广告
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>标题</th>
|
||||
<th>描述</th>
|
||||
<th>标签</th>
|
||||
<th>显示位置</th>
|
||||
<th>按钮文字</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(ad, index) in paginatedAds" :key="index">
|
||||
<td>{{ (currentPage - 1) * pageSize + index + 1 }}</td>
|
||||
<td>
|
||||
<div class="ad-title-cell">
|
||||
<div class="ad-icon-wrapper">
|
||||
<i :class="ad.icon"></i>
|
||||
</div>
|
||||
<span class="ad-title">{{ ad.title }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="desc-cell">{{ ad.desc }}</td>
|
||||
<td><span class="badge tag">{{ ad.tag }}</span></td>
|
||||
<td>
|
||||
<div class="location-tags">
|
||||
<span v-for="loc in ad.locations" :key="loc" class="badge location">
|
||||
{{ getLocationName(loc) }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ ad.btnText }}</td>
|
||||
<td>
|
||||
<span class="badge status" :class="ad.status === 'active' ? 'bg-light-success' : 'bg-light-danger'">
|
||||
{{ ad.status === 'active' ? '启用' : '停用' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="actions">
|
||||
<button class="action-btn edit" @click="openEditModal(ad, index)" title="编辑">
|
||||
<i class="fa-solid fa-pen-to-square"></i>
|
||||
</button>
|
||||
<button class="action-btn delete" @click="deleteAd(index)" title="删除">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<Pagination
|
||||
:total="ads.length"
|
||||
v-model:page-size="pageSize"
|
||||
v-model:current-page="currentPage"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Ad Modal -->
|
||||
<Modal
|
||||
v-model:visible="modalVisible"
|
||||
:title="isEdit ? '编辑广告' : '新增广告'"
|
||||
@confirm="handleSave"
|
||||
>
|
||||
<form class="modal-form">
|
||||
<div class="form-group">
|
||||
<label>标题</label>
|
||||
<input type="text" v-model="form.title" placeholder="请输入广告标题" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>描述</label>
|
||||
<input type="text" v-model="form.desc" placeholder="请输入广告描述" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>标签</label>
|
||||
<input type="text" v-model="form.tag" placeholder="例如: 广告, 推荐" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>图标 (FontAwesome Class)</label>
|
||||
<div class="input-with-icon">
|
||||
<input type="text" v-model="form.icon" placeholder="例如: fa-solid fa-server" />
|
||||
<div class="icon-preview" v-if="form.icon">
|
||||
<i :class="form.icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>显示位置</label>
|
||||
<div class="checkbox-list">
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" value="home" v-model="form.locations" />
|
||||
首页
|
||||
</label>
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" value="all-tools" v-model="form.locations" />
|
||||
全部工具
|
||||
</label>
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" value="faq" v-model="form.locations" />
|
||||
常见问题
|
||||
</label>
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" value="about" v-model="form.locations" />
|
||||
关于我们
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>链接</label>
|
||||
<input type="text" v-model="form.link" placeholder="请输入跳转链接" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>按钮文字</label>
|
||||
<input type="text" v-model="form.btnText" placeholder="例如: 抢购, 查看" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>状态</label>
|
||||
<select v-model="form.status">
|
||||
<option value="active">启用</option>
|
||||
<option value="inactive">停用</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
<!-- Confirm Delete Modal -->
|
||||
<ConfirmModal
|
||||
v-model:visible="confirmVisible"
|
||||
title="确认删除"
|
||||
message="确定要删除该广告吗?此操作无法撤销。"
|
||||
@confirm="confirmDelete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import Modal from '../components/Modal.vue'
|
||||
import ConfirmModal from '../components/ConfirmModal.vue'
|
||||
import Pagination from '../components/Pagination.vue'
|
||||
|
||||
const ads = ref([
|
||||
{
|
||||
title: '云服务器',
|
||||
desc: '新用户首年99元',
|
||||
tag: '广告',
|
||||
icon: 'fa-solid fa-server',
|
||||
btnText: '抢购',
|
||||
link: 'https://example.com/cloud',
|
||||
locations: ['home', 'all-tools'],
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
title: 'AI 绘画',
|
||||
desc: '一键生成艺术画作',
|
||||
tag: '推荐',
|
||||
icon: 'fa-solid fa-palette',
|
||||
btnText: '体验',
|
||||
link: 'https://example.com/ai-art',
|
||||
locations: ['all-tools', 'faq'],
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
title: 'VIP 会员',
|
||||
desc: '解锁全部高级功能',
|
||||
tag: '推广',
|
||||
icon: 'fa-solid fa-crown',
|
||||
btnText: '立即开通',
|
||||
link: '/vip',
|
||||
locations: ['home', 'all-tools', 'faq', 'about'],
|
||||
status: 'active'
|
||||
},
|
||||
])
|
||||
|
||||
const locationMap = {
|
||||
'home': '首页',
|
||||
'all-tools': '全部工具',
|
||||
'faq': '常见问题',
|
||||
'about': '关于我们'
|
||||
}
|
||||
|
||||
const getLocationName = (id) => locationMap[id] || id
|
||||
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(7)
|
||||
|
||||
const paginatedAds = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
const end = start + pageSize.value
|
||||
return ads.value.slice(start, end)
|
||||
})
|
||||
|
||||
const modalVisible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const editIndex = ref(-1)
|
||||
const confirmVisible = ref(false)
|
||||
const deleteIndex = ref(-1)
|
||||
const form = reactive({
|
||||
title: '',
|
||||
desc: '',
|
||||
tag: '',
|
||||
icon: '',
|
||||
btnText: '',
|
||||
link: '',
|
||||
locations: [],
|
||||
status: 'active'
|
||||
})
|
||||
|
||||
const openAddModal = () => {
|
||||
isEdit.value = false
|
||||
editIndex.value = -1
|
||||
Object.assign(form, { title: '', desc: '', tag: '', icon: 'fa-solid fa-ad', btnText: '', link: '', locations: [], status: 'active' })
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const openEditModal = (ad, index) => {
|
||||
isEdit.value = true
|
||||
// Calculate actual index based on pagination
|
||||
const actualIndex = (currentPage.value - 1) * pageSize.value + index
|
||||
editIndex.value = actualIndex
|
||||
Object.assign(form, JSON.parse(JSON.stringify(ad)))
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
if (isEdit.value && editIndex.value !== -1) {
|
||||
ads.value[editIndex.value] = { ...form }
|
||||
} else {
|
||||
ads.value.push({ ...form })
|
||||
// Jump to the last page to see the new ad
|
||||
const totalPages = Math.ceil(ads.value.length / pageSize.value)
|
||||
currentPage.value = totalPages
|
||||
}
|
||||
modalVisible.value = false
|
||||
}
|
||||
|
||||
const deleteAd = (index) => {
|
||||
// Calculate actual index based on pagination
|
||||
const actualIndex = (currentPage.value - 1) * pageSize.value + index
|
||||
deleteIndex.value = actualIndex
|
||||
confirmVisible.value = true
|
||||
}
|
||||
|
||||
const confirmDelete = () => {
|
||||
ads.value.splice(deleteIndex.value, 1)
|
||||
deleteIndex.value = -1
|
||||
// Adjust current page if empty
|
||||
const totalPages = Math.ceil(ads.value.length / pageSize.value)
|
||||
if (currentPage.value > totalPages && totalPages > 0) {
|
||||
currentPage.value = totalPages
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-header {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px dashed #eff2f5;
|
||||
}
|
||||
|
||||
.card-header h3 {
|
||||
font-size: 18px;
|
||||
color: #181c32;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.ad-title-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ad-icon-wrapper {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-color: #e6f9f0;
|
||||
color: #07C160;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.ad-title {
|
||||
font-weight: 600;
|
||||
color: #181c32;
|
||||
}
|
||||
|
||||
.desc-cell {
|
||||
max-width: 200px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: #a1a5b7;
|
||||
}
|
||||
|
||||
.badge.tag {
|
||||
background-color: #f5f8fa;
|
||||
color: #5e6278;
|
||||
}
|
||||
|
||||
.location-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.badge.location {
|
||||
background-color: #f5f8fa;
|
||||
color: #a1a5b7;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #b5b5c3;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background-color: #f5f8fa;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.action-btn.delete:hover {
|
||||
background-color: #ffe2e5;
|
||||
color: #f64e60;
|
||||
}
|
||||
|
||||
/* Form Styles */
|
||||
.modal-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #3f4254;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
padding: 12px;
|
||||
border: 1px solid #e4e6ef;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #5e6278;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus {
|
||||
border-color: #07C160;
|
||||
}
|
||||
|
||||
.input-with-icon {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.icon-preview {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #5e6278;
|
||||
}
|
||||
|
||||
.checkbox-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
color: #5e6278;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
462
Zkit-admin/src/views/Announcements.vue
Normal file
462
Zkit-admin/src/views/Announcements.vue
Normal file
@@ -0,0 +1,462 @@
|
||||
<template>
|
||||
<div class="announcements-page">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>公告管理</h3>
|
||||
<button class="btn-primary" @click="openAddModal">
|
||||
<i class="fa-solid fa-plus"></i> 发布公告
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>标题</th>
|
||||
<th>版本</th>
|
||||
<th>特性数量</th>
|
||||
<th>发布时间</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in paginatedAnnouncements" :key="item.id">
|
||||
<td>#{{ item.id }}</td>
|
||||
<td>
|
||||
<span class="title-text">{{ item.title }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="version-badge">{{ item.version }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="feature-count">{{ item.features?.length || 0 }} 项</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="date-cell">
|
||||
<i class="fa-regular fa-calendar"></i>
|
||||
<span>{{ item.date }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><span class="badge status" :class="getStatusClass(item.status)">{{ getStatusText(item.status) }}</span></td>
|
||||
<td>
|
||||
<div class="actions">
|
||||
<button class="action-btn edit" @click="openEditModal(item)" title="编辑">
|
||||
<i class="fa-solid fa-pen-to-square"></i>
|
||||
</button>
|
||||
<button class="action-btn delete" @click="deleteAnnouncement(item.id)" title="撤回/删除">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<Pagination
|
||||
:total="announcements.length"
|
||||
v-model:page-size="pageSize"
|
||||
v-model:current-page="currentPage"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Announcement Modal -->
|
||||
<Modal
|
||||
v-model:visible="modalVisible"
|
||||
:title="isEdit ? '编辑公告' : '发布公告'"
|
||||
@confirm="handleSave"
|
||||
>
|
||||
<form class="modal-form">
|
||||
<div class="form-group">
|
||||
<label>标题</label>
|
||||
<input type="text" v-model="form.title" placeholder="例如:全新版本 V2.0 已发布" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>版本号</label>
|
||||
<input type="text" v-model="form.version" placeholder="例如:V2.0" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>存储键名 (Storage Key)</label>
|
||||
<input type="text" v-model="form.storageKey" placeholder="例如:has_seen_announcement_v2" />
|
||||
<small class="form-hint">用于控制公告是否再次弹出,修改此值可让已关闭的用户重新看到公告</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>简介文本</label>
|
||||
<textarea v-model="form.intro" placeholder="请输入公告简介" rows="2"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>特性列表</label>
|
||||
<div class="features-editor">
|
||||
<div v-for="(feature, index) in form.features" :key="index" class="feature-item">
|
||||
<input type="text" v-model="feature.title" placeholder="特性标题" />
|
||||
<input type="text" v-model="feature.desc" placeholder="特性描述" />
|
||||
<select v-model="feature.color">
|
||||
<option value="green">绿色</option>
|
||||
<option value="blue">蓝色</option>
|
||||
<option value="purple">紫色</option>
|
||||
<option value="orange">橙色</option>
|
||||
</select>
|
||||
<button type="button" class="btn-remove" @click="removeFeature(index)">×</button>
|
||||
</div>
|
||||
<button type="button" class="btn-add-feature" @click="addFeature">+ 添加特性</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>发布时间</label>
|
||||
<input type="date" v-model="form.date" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>状态</label>
|
||||
<select v-model="form.status">
|
||||
<option value="published">已发布</option>
|
||||
<option value="draft">草稿</option>
|
||||
<option value="expired">已过期</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
<!-- Confirm Delete Modal -->
|
||||
<ConfirmModal
|
||||
v-model:visible="confirmVisible"
|
||||
title="确认删除"
|
||||
message="确定要撤回/删除该公告吗?此操作无法撤销。"
|
||||
@confirm="confirmDelete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import Modal from '../components/Modal.vue'
|
||||
import ConfirmModal from '../components/ConfirmModal.vue'
|
||||
import Pagination from '../components/Pagination.vue'
|
||||
|
||||
const announcements = ref([
|
||||
{
|
||||
id: 101,
|
||||
title: '全新版本 V2.0 已发布',
|
||||
version: 'V2.0',
|
||||
storageKey: 'has_seen_announcement_v2',
|
||||
intro: '为了提升您的工作效率,我们带来了一些令人兴奋的改进:',
|
||||
features: [
|
||||
{ title: 'PDF 工具箱', desc: '支持一键合并、拆分与压缩', color: 'green' },
|
||||
{ title: '安全升级', desc: '全新的 MD5 加密算法,速度提升 50%', color: 'blue' },
|
||||
{ title: '体验优化', desc: '修复移动端显示 bug,更加丝滑', color: 'purple' }
|
||||
],
|
||||
date: '2025-11-20',
|
||||
status: 'published'
|
||||
},
|
||||
{
|
||||
id: 102,
|
||||
title: '系统维护通知',
|
||||
version: 'V1.5',
|
||||
storageKey: 'has_seen_announcement_v1_5',
|
||||
intro: '我们将于本周六凌晨进行服务器维护,预计耗时 2 小时。',
|
||||
features: [],
|
||||
date: '2025-11-15',
|
||||
status: 'expired'
|
||||
},
|
||||
{
|
||||
id: 103,
|
||||
title: '新功能预告:AI 写作',
|
||||
version: 'V3.0',
|
||||
storageKey: 'has_seen_announcement_v3',
|
||||
intro: '即将上线 AI 写作助手,敬请期待!',
|
||||
features: [
|
||||
{ title: 'AI 写作', desc: '智能生成文章内容', color: 'purple' }
|
||||
],
|
||||
date: '2025-11-25',
|
||||
status: 'draft'
|
||||
},
|
||||
])
|
||||
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(7)
|
||||
|
||||
const paginatedAnnouncements = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
const end = start + pageSize.value
|
||||
return announcements.value.slice(start, end)
|
||||
})
|
||||
|
||||
const modalVisible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const confirmVisible = ref(false)
|
||||
const deleteId = ref(null)
|
||||
const form = reactive({
|
||||
id: null,
|
||||
title: '',
|
||||
version: '',
|
||||
storageKey: '',
|
||||
intro: '',
|
||||
features: [],
|
||||
date: '',
|
||||
status: 'draft'
|
||||
})
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const map = {
|
||||
published: '已发布',
|
||||
expired: '已过期',
|
||||
draft: '草稿'
|
||||
}
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
const getStatusClass = (status) => {
|
||||
const map = {
|
||||
published: 'bg-light-success',
|
||||
expired: 'bg-light-dark',
|
||||
draft: 'bg-light-warning'
|
||||
}
|
||||
return map[status] || 'bg-light-dark'
|
||||
}
|
||||
|
||||
const addFeature = () => {
|
||||
form.features.push({ title: '', desc: '', color: 'green' })
|
||||
}
|
||||
|
||||
const removeFeature = (index) => {
|
||||
form.features.splice(index, 1)
|
||||
}
|
||||
|
||||
const openAddModal = () => {
|
||||
isEdit.value = false
|
||||
Object.assign(form, {
|
||||
id: null,
|
||||
title: '',
|
||||
version: '',
|
||||
storageKey: '',
|
||||
intro: '',
|
||||
features: [],
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
status: 'published'
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const openEditModal = (item) => {
|
||||
isEdit.value = true
|
||||
Object.assign(form, item)
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
if (isEdit.value) {
|
||||
const index = announcements.value.findIndex(a => a.id === form.id)
|
||||
if (index !== -1) {
|
||||
announcements.value[index] = { ...form }
|
||||
}
|
||||
} else {
|
||||
const newId = Math.max(...announcements.value.map(a => a.id)) + 1
|
||||
announcements.value.unshift({ ...form, id: newId })
|
||||
// Jump to the first page to see the new announcement (since we unshift)
|
||||
currentPage.value = 1
|
||||
}
|
||||
modalVisible.value = false
|
||||
}
|
||||
|
||||
const deleteAnnouncement = (id) => {
|
||||
deleteId.value = id
|
||||
confirmVisible.value = true
|
||||
}
|
||||
|
||||
const confirmDelete = () => {
|
||||
announcements.value = announcements.value.filter(a => a.id !== deleteId.value)
|
||||
deleteId.value = null
|
||||
// Adjust current page if empty
|
||||
const totalPages = Math.ceil(announcements.value.length / pageSize.value)
|
||||
if (currentPage.value > totalPages && totalPages > 0) {
|
||||
currentPage.value = totalPages
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-header {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px dashed #eff2f5;
|
||||
}
|
||||
|
||||
.card-header h3 {
|
||||
font-size: 18px;
|
||||
color: #181c32;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-weight: 600;
|
||||
color: #181c32;
|
||||
}
|
||||
|
||||
.content-cell {
|
||||
max-width: 400px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: #a1a5b7;
|
||||
}
|
||||
|
||||
.date-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #7e8299;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #b5b5c3;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background-color: #f5f8fa;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.action-btn.delete:hover {
|
||||
background-color: #ffe2e5;
|
||||
color: #f64e60;
|
||||
}
|
||||
|
||||
/* Form Styles */
|
||||
.modal-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #3f4254;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
padding: 12px;
|
||||
border: 1px solid #e4e6ef;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #5e6278;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
background-color: #fff;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus,
|
||||
.form-group textarea:focus {
|
||||
border-color: #07C160;
|
||||
}
|
||||
|
||||
.version-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
background: linear-gradient(135deg, #07C160 0%, #05a050 100%);
|
||||
color: #fff;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.feature-count {
|
||||
color: #a1a5b7;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
font-size: 12px;
|
||||
color: #a1a5b7;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.features-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.5fr auto 32px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.feature-item input,
|
||||
.feature-item select {
|
||||
padding: 8px;
|
||||
border: 1px solid #e4e6ef;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.btn-remove {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: #ffe2e5;
|
||||
color: #f64e60;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-remove:hover {
|
||||
background: #f64e60;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-add-feature {
|
||||
padding: 10px;
|
||||
border: 2px dashed #e4e6ef;
|
||||
background: transparent;
|
||||
color: #07C160;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-add-feature:hover {
|
||||
border-color: #07C160;
|
||||
background: #e6f9f0;
|
||||
}
|
||||
</style>
|
||||
252
Zkit-admin/src/views/Dashboard.vue
Normal file
252
Zkit-admin/src/views/Dashboard.vue
Normal file
@@ -0,0 +1,252 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card" v-for="(stat, index) in stats" :key="index">
|
||||
<div class="stat-icon" :class="stat.bgClass">
|
||||
<i :class="stat.icon"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stat.value }}</div>
|
||||
<div class="stat-label">{{ stat.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-grid">
|
||||
<div class="card chart-card">
|
||||
<div class="card-header">
|
||||
<h3>营收概览</h3>
|
||||
<div class="card-actions">
|
||||
<select class="select-sm">
|
||||
<option>本周</option>
|
||||
<option>本月</option>
|
||||
<option>全年</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-placeholder">
|
||||
<!-- Placeholder for a chart -->
|
||||
<div class="bar" style="height: 60%"></div>
|
||||
<div class="bar" style="height: 80%"></div>
|
||||
<div class="bar" style="height: 40%"></div>
|
||||
<div class="bar" style="height: 90%"></div>
|
||||
<div class="bar" style="height: 70%"></div>
|
||||
<div class="bar" style="height: 50%"></div>
|
||||
<div class="bar" style="height: 75%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card activity-card">
|
||||
<div class="card-header">
|
||||
<h3>近期活动</h3>
|
||||
</div>
|
||||
<ul class="activity-list">
|
||||
<li v-for="i in 5" :key="i">
|
||||
<div class="timeline-badge"></div>
|
||||
<div class="timeline-content">
|
||||
<span class="activity-text">用户 #{{ 1000 + i }} 注册成功</span>
|
||||
<span class="time">{{ i }}小时前</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const stats = ref([
|
||||
{ label: '总用户数', value: '12,345', icon: 'fa-solid fa-users', bgClass: 'bg-light-primary text-primary' },
|
||||
{ label: '总销售额', value: '¥45,678', icon: 'fa-solid fa-wallet', bgClass: 'bg-light-success text-success' },
|
||||
{ label: '新订单', value: '890', icon: 'fa-solid fa-box-open', bgClass: 'bg-light-info text-info' },
|
||||
{ label: '待处理事项', value: '12', icon: 'fa-solid fa-clipboard-list', bgClass: 'bg-light-warning text-warning' },
|
||||
])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #fff;
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.03);
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.bg-light-primary { background-color: #e1f0ff; }
|
||||
.text-primary { color: #3699ff; }
|
||||
|
||||
.bg-light-success { background-color: #c9f7f5; }
|
||||
.text-success { color: #1bc5bd; }
|
||||
|
||||
.bg-light-info { background-color: #eee5ff; }
|
||||
.text-info { color: #8950fc; }
|
||||
|
||||
.bg-light-warning { background-color: #fff4de; }
|
||||
.text-warning { color: #ffa800; }
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #181c32;
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #b5b5c3;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
padding: 0;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.03);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 24px;
|
||||
border-bottom: 1px solid #eff2f5;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-header h3 {
|
||||
margin: 0;
|
||||
color: #181c32;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.select-sm {
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e4e6ef;
|
||||
background-color: #f5f8fa;
|
||||
color: #5e6278;
|
||||
font-size: 12px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.chart-placeholder {
|
||||
height: 240px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-around;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.bar {
|
||||
width: 40px;
|
||||
background: linear-gradient(180deg, #3699ff 0%, #0073e9 100%);
|
||||
border-radius: 6px 6px 0 0;
|
||||
opacity: 0.85;
|
||||
transition: height 1s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bar:hover {
|
||||
opacity: 1;
|
||||
transform: scaleY(1.05);
|
||||
transform-origin: bottom;
|
||||
}
|
||||
|
||||
.activity-list {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.activity-list li {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.activity-list li:not(:last-child)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 12px;
|
||||
bottom: -24px;
|
||||
width: 2px;
|
||||
background-color: #eff2f5;
|
||||
}
|
||||
|
||||
.timeline-badge {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid #fff;
|
||||
background-color: #1bc5bd;
|
||||
box-shadow: 0 0 0 1px #1bc5bd;
|
||||
flex-shrink: 0;
|
||||
margin-top: 4px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.activity-text {
|
||||
font-size: 14px;
|
||||
color: #5e6278;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.time {
|
||||
color: #b5b5c3;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.content-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
284
Zkit-admin/src/views/Faqs.vue
Normal file
284
Zkit-admin/src/views/Faqs.vue
Normal file
@@ -0,0 +1,284 @@
|
||||
<template>
|
||||
<div class="faq-page">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>FAQ 管理</h3>
|
||||
<button class="btn-primary" @click="openAddModal">
|
||||
<i class="fa-solid fa-plus"></i> 新增问题
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>问题</th>
|
||||
<th>回答</th>
|
||||
<th>排序</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(item, index) in paginatedFaqs" :key="index">
|
||||
<td>{{ (currentPage - 1) * pageSize + index + 1 }}</td>
|
||||
<td class="question-cell">
|
||||
<span class="question-text">{{ item.q }}</span>
|
||||
</td>
|
||||
<td class="answer-cell">{{ item.a }}</td>
|
||||
<td>
|
||||
<span class="sort-badge">{{ (currentPage - 1) * pageSize + index + 1 }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="actions">
|
||||
<button class="action-btn edit" @click="openEditModal(item, index)" title="编辑">
|
||||
<i class="fa-solid fa-pen-to-square"></i>
|
||||
</button>
|
||||
<button class="action-btn delete" @click="deleteFaq(index)" title="删除">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<Pagination
|
||||
:total="faqs.length"
|
||||
v-model:page-size="pageSize"
|
||||
v-model:current-page="currentPage"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- FAQ Modal -->
|
||||
<Modal
|
||||
v-model:visible="modalVisible"
|
||||
:title="isEdit ? '编辑 FAQ' : '新增 FAQ'"
|
||||
@confirm="handleSave"
|
||||
>
|
||||
<form class="modal-form">
|
||||
<div class="form-group">
|
||||
<label>问题</label>
|
||||
<input type="text" v-model="form.q" placeholder="请输入问题" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>回答</label>
|
||||
<textarea v-model="form.a" placeholder="请输入回答" rows="5"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
<!-- Confirm Delete Modal -->
|
||||
<ConfirmModal
|
||||
v-model:visible="confirmVisible"
|
||||
title="确认删除"
|
||||
message="确定要删除该 FAQ 吗?此操作无法撤销。"
|
||||
@confirm="confirmDelete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import Modal from '../components/Modal.vue'
|
||||
import ConfirmModal from '../components/ConfirmModal.vue'
|
||||
import Pagination from '../components/Pagination.vue'
|
||||
|
||||
const faqs = ref([
|
||||
{ q: 'OmniKit 需要下载安装吗?', a: '不需要。OmniKit 是完全基于浏览器的云端工具箱,您只需打开网页即可在 Windows、Mac 或手机上使用所有工具。' },
|
||||
{ q: '我的文件安全吗?会泄露吗?', a: '非常安全。对于图片压缩、MD5 计算等工具,我们采用 WebAssembly 技术在您本地浏览器处理,文件从未上传至服务器。' },
|
||||
{ q: '如何开通 VIP 会员?', a: '点击右上角的“登录”按钮,注册或登录账号后,在个人中心选择“升级 VIP”。支持微信和支付宝支付,开通后即时生效。' },
|
||||
{ q: 'VIP 有什么特权?', a: '1. 解锁 PDF 转 Word、视频处理等高算力工具;2. 移除全站所有广告;3. 优先获得极速服务器资源;4. 专属客服支持。' },
|
||||
{ q: '如果转换失败怎么办?', a: '如果遇到文件转换失败,通常是因为文件加密或损坏。您可以尝试重新上传,或者通过底部的“联系我们”发送邮件反馈。' },
|
||||
{ q: '支持哪些文件格式?', a: '我们支持多种常见的文件格式,包括 PDF, Word, Excel, JPG, PNG 等。具体支持的格式请查看每个工具的说明。' },
|
||||
{ q: '如何联系客服?', a: '您可以通过页面底部的“联系我们”链接发送邮件,或者在工作时间通过在线客服系统与我们联系。' },
|
||||
{ q: '退款政策是怎样的?', a: '如果您对我们的服务不满意,可以在购买后 7 天内申请全额退款。请联系客服处理。' },
|
||||
{ q: '可以开发票吗?', a: '支持开具电子发票。购买成功后,您可以在“我的订单”页面申请开票。' },
|
||||
{ q: '账号可以多人使用吗?', a: 'VIP 账号仅限个人使用,不支持多人共享。系统检测到异常登录可能会冻结账号。' },
|
||||
{ q: '有手机 App 吗?', a: '目前暂时没有原生 App,但我们的网站是响应式设计,完美适配手机浏览器,您可以直接添加到主屏幕使用。' },
|
||||
{ q: '如何修改密码?', a: '登录后,点击右上角头像进入“个人中心”,在“账号安全”选项卡中可以修改密码。' },
|
||||
])
|
||||
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(7)
|
||||
|
||||
const paginatedFaqs = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
const end = start + pageSize.value
|
||||
return faqs.value.slice(start, end)
|
||||
})
|
||||
|
||||
const modalVisible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const editIndex = ref(-1)
|
||||
const confirmVisible = ref(false)
|
||||
const deleteIndex = ref(-1)
|
||||
const form = reactive({
|
||||
q: '',
|
||||
a: ''
|
||||
})
|
||||
|
||||
const openAddModal = () => {
|
||||
isEdit.value = false
|
||||
editIndex.value = -1
|
||||
Object.assign(form, { q: '', a: '' })
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const openEditModal = (item, index) => {
|
||||
isEdit.value = true
|
||||
// Calculate actual index based on pagination
|
||||
const actualIndex = (currentPage.value - 1) * pageSize.value + index
|
||||
editIndex.value = actualIndex
|
||||
Object.assign(form, item)
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
if (isEdit.value && editIndex.value !== -1) {
|
||||
faqs.value[editIndex.value] = { ...form }
|
||||
} else {
|
||||
faqs.value.push({ ...form })
|
||||
// Jump to the last page to see the new faq
|
||||
const totalPages = Math.ceil(faqs.value.length / pageSize.value)
|
||||
currentPage.value = totalPages
|
||||
}
|
||||
modalVisible.value = false
|
||||
}
|
||||
|
||||
const deleteFaq = (index) => {
|
||||
// Calculate actual index based on pagination
|
||||
const actualIndex = (currentPage.value - 1) * pageSize.value + index
|
||||
deleteIndex.value = actualIndex
|
||||
confirmVisible.value = true
|
||||
}
|
||||
|
||||
const confirmDelete = () => {
|
||||
faqs.value.splice(deleteIndex.value, 1)
|
||||
deleteIndex.value = -1
|
||||
// Adjust current page if empty
|
||||
const totalPages = Math.ceil(faqs.value.length / pageSize.value)
|
||||
if (currentPage.value > totalPages && totalPages > 0) {
|
||||
currentPage.value = totalPages
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-header {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px dashed #eff2f5;
|
||||
}
|
||||
|
||||
.card-header h3 {
|
||||
font-size: 18px;
|
||||
color: #181c32;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.question-text {
|
||||
font-weight: 600;
|
||||
color: #181c32;
|
||||
}
|
||||
|
||||
.question-cell {
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.answer-cell {
|
||||
max-width: 400px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: #a1a5b7;
|
||||
}
|
||||
|
||||
.sort-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-color: #f5f8fa;
|
||||
color: #5e6278;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #b5b5c3;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background-color: #f5f8fa;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.action-btn.delete:hover {
|
||||
background-color: #ffe2e5;
|
||||
color: #f64e60;
|
||||
}
|
||||
|
||||
/* Form Styles */
|
||||
.modal-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #3f4254;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
padding: 12px;
|
||||
border: 1px solid #e4e6ef;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #5e6278;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
background-color: #fff;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus,
|
||||
.form-group textarea:focus {
|
||||
border-color: #07C160;
|
||||
}
|
||||
</style>
|
||||
279
Zkit-admin/src/views/Footer.vue
Normal file
279
Zkit-admin/src/views/Footer.vue
Normal file
@@ -0,0 +1,279 @@
|
||||
<template>
|
||||
<div class="footer-page">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>页脚管理</h3>
|
||||
<button class="btn-primary" @click="openAddModal">
|
||||
<i class="fa-solid fa-plus"></i> 新增链接
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>标题</th>
|
||||
<th>链接</th>
|
||||
<th>分类</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(item, index) in paginatedLinks" :key="index">
|
||||
<td>{{ (currentPage - 1) * pageSize + index + 1 }}</td>
|
||||
<td>
|
||||
<span class="title-text">{{ item.title }}</span>
|
||||
</td>
|
||||
<td class="link-cell">
|
||||
<a :href="item.link" target="_blank">
|
||||
<i class="fa-solid fa-arrow-up-right-from-square"></i>
|
||||
{{ item.link }}
|
||||
</a>
|
||||
</td>
|
||||
<td><span class="badge category">{{ item.category }}</span></td>
|
||||
<td>
|
||||
<div class="actions">
|
||||
<button class="action-btn edit" @click="openEditModal(item, (currentPage - 1) * pageSize + index)" title="编辑">
|
||||
<i class="fa-solid fa-pen-to-square"></i>
|
||||
</button>
|
||||
<button class="action-btn delete" @click="deleteLink((currentPage - 1) * pageSize + index)" title="删除">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<Pagination
|
||||
:total="footerLinks.length"
|
||||
v-model:page-size="pageSize"
|
||||
v-model:current-page="currentPage"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Footer Link Modal -->
|
||||
<Modal
|
||||
v-model:visible="modalVisible"
|
||||
:title="isEdit ? '编辑链接' : '新增链接'"
|
||||
@confirm="handleSave"
|
||||
>
|
||||
<form class="modal-form">
|
||||
<div class="form-group">
|
||||
<label>标题</label>
|
||||
<input type="text" v-model="form.title" placeholder="请输入链接标题" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>链接</label>
|
||||
<input type="text" v-model="form.link" placeholder="请输入跳转链接" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>分类</label>
|
||||
<select v-model="form.category">
|
||||
<option value="产品">产品</option>
|
||||
<option value="资源">资源</option>
|
||||
<option value="关于">关于</option>
|
||||
<option value="法律">法律</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
<!-- Confirm Delete Modal -->
|
||||
<ConfirmModal
|
||||
v-model:visible="confirmVisible"
|
||||
title="确认删除"
|
||||
message="确定要删除该链接吗?此操作无法撤销。"
|
||||
@confirm="confirmDelete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import Modal from '../components/Modal.vue'
|
||||
import ConfirmModal from '../components/ConfirmModal.vue'
|
||||
import Pagination from '../components/Pagination.vue'
|
||||
|
||||
const footerLinks = ref([
|
||||
{ title: '所有工具', link: '/tools', category: '产品' },
|
||||
{ title: '更新日志', link: '/changelog', category: '产品' },
|
||||
{ title: '使用文档', link: '/docs', category: '资源' },
|
||||
{ title: '关于我们', link: '/about', category: '关于' },
|
||||
{ title: '隐私政策', link: '/privacy', category: '法律' },
|
||||
])
|
||||
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(7)
|
||||
|
||||
const paginatedLinks = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
const end = start + pageSize.value
|
||||
return footerLinks.value.slice(start, end)
|
||||
})
|
||||
|
||||
const modalVisible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const editIndex = ref(-1)
|
||||
const confirmVisible = ref(false)
|
||||
const deleteIndex = ref(-1)
|
||||
const form = reactive({
|
||||
title: '',
|
||||
link: '',
|
||||
category: '产品'
|
||||
})
|
||||
|
||||
const openAddModal = () => {
|
||||
isEdit.value = false
|
||||
editIndex.value = -1
|
||||
Object.assign(form, { title: '', link: '', category: '产品' })
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const openEditModal = (item, index) => {
|
||||
isEdit.value = true
|
||||
editIndex.value = index
|
||||
Object.assign(form, item)
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
if (isEdit.value && editIndex.value !== -1) {
|
||||
footerLinks.value[editIndex.value] = { ...form }
|
||||
} else {
|
||||
footerLinks.value.push({ ...form })
|
||||
// Jump to the last page to see the new link
|
||||
const totalPages = Math.ceil(footerLinks.value.length / pageSize.value)
|
||||
currentPage.value = totalPages
|
||||
}
|
||||
modalVisible.value = false
|
||||
}
|
||||
|
||||
const deleteLink = (index) => {
|
||||
deleteIndex.value = index
|
||||
confirmVisible.value = true
|
||||
}
|
||||
|
||||
const confirmDelete = () => {
|
||||
footerLinks.value.splice(deleteIndex.value, 1)
|
||||
deleteIndex.value = -1
|
||||
// Adjust current page if empty
|
||||
const totalPages = Math.ceil(footerLinks.value.length / pageSize.value)
|
||||
if (currentPage.value > totalPages && totalPages > 0) {
|
||||
currentPage.value = totalPages
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-header {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #eff2f5;
|
||||
}
|
||||
|
||||
.card-header h3 {
|
||||
font-size: 18px;
|
||||
color: #181c32;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-weight: 600;
|
||||
color: #181c32;
|
||||
}
|
||||
|
||||
.link-cell a {
|
||||
color: #3699ff;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.link-cell a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.badge.category {
|
||||
background-color: #f5f8fa;
|
||||
color: #5e6278;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: #f5f8fa;
|
||||
color: #a1a5b7;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background-color: #3699ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.action-btn.delete:hover {
|
||||
background-color: #f64e60;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Form Styles */
|
||||
.modal-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #3f4254;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
padding: 12px;
|
||||
border: 1px solid #e4e6ef;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #5e6278;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
background-color: #fff;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus,
|
||||
.form-group textarea:focus {
|
||||
border-color: #3699ff;
|
||||
}
|
||||
</style>
|
||||
435
Zkit-admin/src/views/Login.vue
Normal file
435
Zkit-admin/src/views/Login.vue
Normal file
@@ -0,0 +1,435 @@
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<div class="login-container">
|
||||
<div class="login-left">
|
||||
<div class="brand animate-fade-up">
|
||||
<div class="logo-icon">
|
||||
<i class="fa-solid fa-layer-group"></i>
|
||||
</div>
|
||||
<h1>Zkit Admin</h1>
|
||||
</div>
|
||||
<div class="welcome-text animate-fade-up delay-1">
|
||||
<h2>欢迎回来</h2>
|
||||
<p>请输入您的账号密码登录管理后台</p>
|
||||
</div>
|
||||
<form @submit.prevent="handleLogin" class="login-form animate-fade-up delay-2">
|
||||
<div class="form-group">
|
||||
<label>用户名</label>
|
||||
<div class="input-wrapper">
|
||||
<i class="fa-solid fa-user input-icon"></i>
|
||||
<input type="text" v-model="username" placeholder="请输入用户名" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<div class="input-wrapper">
|
||||
<i class="fa-solid fa-lock input-icon"></i>
|
||||
<input type="password" v-model="password" placeholder="请输入密码" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-options">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" />
|
||||
<span>记住我</span>
|
||||
</label>
|
||||
<a href="#" class="forgot-password">忘记密码?</a>
|
||||
</div>
|
||||
<button type="submit" class="btn-login">
|
||||
登录 <i class="fa-solid fa-arrow-right"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="login-right">
|
||||
<div class="decoration">
|
||||
<div class="circle circle-1"></div>
|
||||
<div class="circle circle-2"></div>
|
||||
<div class="glass-card animate-float">
|
||||
<i class="fa-solid fa-chart-pie icon-lg"></i>
|
||||
<h3>数据可视化</h3>
|
||||
<p>高效、直观的数据分析体验</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const router = useRouter()
|
||||
|
||||
const handleLogin = () => {
|
||||
// Mock login
|
||||
router.push('/')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f0fdf4;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-image: radial-gradient(#07C160 0.5px, transparent 0.5px), radial-gradient(#07C160 0.5px, #f0fdf4 0.5px);
|
||||
background-size: 20px 20px;
|
||||
background-position: 0 0, 10px 10px;
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
display: flex;
|
||||
width: 1000px;
|
||||
height: 600px;
|
||||
background: #ffffff;
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 20px 60px rgba(7, 193, 96, 0.15);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.login-left {
|
||||
flex: 1;
|
||||
padding: 60px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: linear-gradient(135deg, #07C160 0%, #05a050 100%);
|
||||
color: #fff;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
box-shadow: 0 8px 16px rgba(7, 193, 96, 0.2);
|
||||
}
|
||||
|
||||
.brand h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
color: #181c32;
|
||||
margin: 0;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.welcome-text h2 {
|
||||
font-size: 32px;
|
||||
font-weight: 800;
|
||||
color: #181c32;
|
||||
margin: 0 0 12px 0;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
.welcome-text p {
|
||||
color: #a1a5b7;
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #3f4254;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #a1a5b7;
|
||||
font-size: 18px;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 16px 16px 16px 48px;
|
||||
border: 2px solid #f1f1f4;
|
||||
border-radius: 16px;
|
||||
font-size: 15px;
|
||||
color: #5e6278;
|
||||
outline: none;
|
||||
transition: all 0.3s ease;
|
||||
background-color: #f9f9f9;
|
||||
font-weight: 500;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
input:hover {
|
||||
border-color: #e4e6ef;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
border-color: #07C160;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 0 4px rgba(7, 193, 96, 0.1);
|
||||
}
|
||||
|
||||
input:focus + .input-icon {
|
||||
color: #07C160;
|
||||
}
|
||||
|
||||
.form-options {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #7e8299;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.checkbox-label input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 6px;
|
||||
border: 2px solid #d8d8e5;
|
||||
appearance: none;
|
||||
background-color: #fff;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.checkbox-label input[type="checkbox"]:checked {
|
||||
background-color: #07C160;
|
||||
border-color: #07C160;
|
||||
}
|
||||
|
||||
.checkbox-label input[type="checkbox"]:checked::after {
|
||||
content: '✔';
|
||||
position: absolute;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
font-size: 14px;
|
||||
color: #07C160;
|
||||
font-weight: 600;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.forgot-password:hover {
|
||||
color: #05a050;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
width: 100%;
|
||||
padding: 18px;
|
||||
background: linear-gradient(135deg, #07C160 0%, #05a050 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
box-shadow: 0 10px 20px rgba(7, 193, 96, 0.2);
|
||||
}
|
||||
|
||||
.btn-login:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 15px 30px rgba(7, 193, 96, 0.3);
|
||||
}
|
||||
|
||||
.btn-login:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Right Side Decoration */
|
||||
.login-right {
|
||||
flex: 1;
|
||||
background: linear-gradient(135deg, #07C160 0%, #047857 100%);
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.decoration {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.circle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
|
||||
backdrop-filter: blur(5px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.circle-1 {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
top: -100px;
|
||||
right: -100px;
|
||||
animation: pulse 10s infinite alternate;
|
||||
}
|
||||
|
||||
.circle-2 {
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
bottom: -150px;
|
||||
left: -150px;
|
||||
animation: pulse 15s infinite alternate-reverse;
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(20px);
|
||||
padding: 48px;
|
||||
border-radius: 32px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
max-width: 340px;
|
||||
z-index: 10;
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.icon-lg {
|
||||
font-size: 56px;
|
||||
margin-bottom: 24px;
|
||||
display: block;
|
||||
text-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.glass-card h3 {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
margin: 0 0 12px 0;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.glass-card p {
|
||||
font-size: 16px;
|
||||
opacity: 0.9;
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
.animate-fade-up {
|
||||
animation: fadeInUp 0.8s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
.animate-float {
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.delay-1 {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
|
||||
.delay-2 {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 0.5;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.login-container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.login-right {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login-left {
|
||||
padding: 40px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
394
Zkit-admin/src/views/Tools.vue
Normal file
394
Zkit-admin/src/views/Tools.vue
Normal file
@@ -0,0 +1,394 @@
|
||||
<template>
|
||||
<div class="tools-page">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>工具管理</h3>
|
||||
<button class="btn-primary" @click="openAddModal">
|
||||
<i class="fa-solid fa-plus"></i> 新增工具
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>图标</th>
|
||||
<th>标题</th>
|
||||
<th>描述</th>
|
||||
<th>分类</th>
|
||||
<th>VIP</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(tool, index) in paginatedTools" :key="index">
|
||||
<td>
|
||||
<div class="icon-wrapper" :class="`icon-bg-${tool.color}`">
|
||||
<i :class="tool.icon"></i>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="tool-title">{{ tool.title }}</span>
|
||||
</td>
|
||||
<td class="desc-cell">{{ tool.desc }}</td>
|
||||
<td>
|
||||
<span class="badge category">{{ getCategoryName(tool.category) }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="tool.vip" class="badge vip">
|
||||
<i class="fa-solid fa-crown"></i> VIP
|
||||
</span>
|
||||
<span v-else class="badge free">免费</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="actions">
|
||||
<button class="action-btn edit" @click="openEditModal(tool, index)" title="编辑">
|
||||
<i class="fa-solid fa-pen-to-square"></i>
|
||||
</button>
|
||||
<button class="action-btn delete" @click="deleteTool(index)" title="删除">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<Pagination
|
||||
:total="tools.length"
|
||||
v-model:page-size="pageSize"
|
||||
v-model:current-page="currentPage"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Tool Modal -->
|
||||
<Modal
|
||||
v-model:visible="modalVisible"
|
||||
:title="isEdit ? '编辑工具' : '新增工具'"
|
||||
@confirm="handleSave"
|
||||
>
|
||||
<form class="modal-form">
|
||||
<div class="form-group">
|
||||
<label>标题</label>
|
||||
<input type="text" v-model="form.title" placeholder="请输入工具标题" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>描述</label>
|
||||
<textarea v-model="form.desc" placeholder="请输入工具描述" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>图标 (FontAwesome Class)</label>
|
||||
<div class="input-with-icon">
|
||||
<input type="text" v-model="form.icon" placeholder="例如: fa-solid fa-image" />
|
||||
<div class="icon-preview" v-if="form.icon">
|
||||
<i :class="form.icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>颜色</label>
|
||||
<select v-model="form.color">
|
||||
<option value="blue">蓝色</option>
|
||||
<option value="rose">玫瑰红</option>
|
||||
<option value="fuchsia">紫红色</option>
|
||||
<option value="orange">橙色</option>
|
||||
<option value="cyan">青色</option>
|
||||
<option value="emerald">翠绿色</option>
|
||||
<option value="violet">紫罗兰</option>
|
||||
<option value="indigo">靛青色</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>分类</label>
|
||||
<select v-model="form.category">
|
||||
<option value="media">媒体</option>
|
||||
<option value="dev">开发</option>
|
||||
<option value="crypto">加密</option>
|
||||
<option value="life">生活</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group checkbox-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="form.vip" />
|
||||
<span class="checkbox-text">设为 VIP 工具</span>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
<!-- Confirm Delete Modal -->
|
||||
<ConfirmModal
|
||||
v-model:visible="confirmVisible"
|
||||
title="确认删除"
|
||||
message="确定要删除该工具吗?此操作无法撤销。"
|
||||
@confirm="confirmDelete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import Modal from '../components/Modal.vue'
|
||||
import ConfirmModal from '../components/ConfirmModal.vue'
|
||||
import Pagination from '../components/Pagination.vue'
|
||||
|
||||
const tools = ref([
|
||||
{ title: 'PDF 转 Word', desc: '精准保持排版,一键转换', icon: 'fa-solid fa-file-word', color: 'blue', vip: true, category: 'media' },
|
||||
{ title: '图片压缩', desc: '智能压缩,保持清晰度', icon: 'fa-solid fa-image', color: 'rose', vip: false, category: 'media' },
|
||||
{ title: '图片去底', desc: 'AI 自动识别主体抠图', icon: 'fa-solid fa-wand-magic-sparkles', color: 'fuchsia', vip: false, category: 'media' },
|
||||
{ title: '视频转 GIF', desc: '截取片段生成表情包', icon: 'fa-solid fa-film', color: 'orange', vip: true, category: 'media' },
|
||||
{ title: '二维码生成', desc: '自定义颜色、Logo嵌入', icon: 'fa-solid fa-qrcode', color: 'gray', vip: false, category: 'media' },
|
||||
{ title: 'SQL 转 ER', desc: '一键生成数据库关系图', icon: 'fa-solid fa-database', color: 'cyan', vip: true, category: 'dev' },
|
||||
{ title: 'JSON 格式化', desc: '校验、高亮、折叠代码', icon: 'fa-solid fa-code', color: 'emerald', vip: false, category: 'dev' },
|
||||
{ title: 'MD5 加密', desc: '不可逆加密,支持多种盐值', icon: 'fa-solid fa-lock', color: 'indigo', vip: false, category: 'crypto' },
|
||||
])
|
||||
|
||||
const categoryMap = {
|
||||
'all': '全部',
|
||||
'dev': '开发',
|
||||
'media': '媒体',
|
||||
'crypto': '加密',
|
||||
'life': '生活'
|
||||
}
|
||||
|
||||
const getCategoryName = (id) => categoryMap[id] || id
|
||||
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(7)
|
||||
|
||||
const paginatedTools = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
const end = start + pageSize.value
|
||||
return tools.value.slice(start, end)
|
||||
})
|
||||
|
||||
const modalVisible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const editIndex = ref(-1)
|
||||
const confirmVisible = ref(false)
|
||||
const deleteIndex = ref(-1)
|
||||
const form = reactive({
|
||||
title: '',
|
||||
desc: '',
|
||||
icon: '',
|
||||
color: 'blue',
|
||||
category: 'dev',
|
||||
vip: false
|
||||
})
|
||||
|
||||
const openAddModal = () => {
|
||||
isEdit.value = false
|
||||
editIndex.value = -1
|
||||
Object.assign(form, { title: '', desc: '', icon: 'fa-solid fa-tools', color: 'blue', category: 'dev', vip: false })
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const openEditModal = (tool, index) => {
|
||||
isEdit.value = true
|
||||
// Calculate actual index based on pagination
|
||||
const actualIndex = (currentPage.value - 1) * pageSize.value + index
|
||||
editIndex.value = actualIndex
|
||||
Object.assign(form, tool)
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
if (isEdit.value && editIndex.value !== -1) {
|
||||
tools.value[editIndex.value] = { ...form }
|
||||
} else {
|
||||
tools.value.push({ ...form })
|
||||
// Jump to the last page to see the new tool
|
||||
const totalPages = Math.ceil(tools.value.length / pageSize.value)
|
||||
currentPage.value = totalPages
|
||||
}
|
||||
modalVisible.value = false
|
||||
}
|
||||
|
||||
const deleteTool = (index) => {
|
||||
// Calculate actual index based on pagination
|
||||
const actualIndex = (currentPage.value - 1) * pageSize.value + index
|
||||
deleteIndex.value = actualIndex
|
||||
confirmVisible.value = true
|
||||
}
|
||||
|
||||
const confirmDelete = () => {
|
||||
tools.value.splice(deleteIndex.value, 1)
|
||||
deleteIndex.value = -1
|
||||
// Adjust current page if empty
|
||||
const totalPages = Math.ceil(tools.value.length / pageSize.value)
|
||||
if (currentPage.value > totalPages && totalPages > 0) {
|
||||
currentPage.value = totalPages
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-header {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px dashed #eff2f5;
|
||||
}
|
||||
|
||||
.card-header h3 {
|
||||
font-size: 18px;
|
||||
color: #181c32;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.icon-bg-blue { background: #e1f0ff; color: #3699ff; }
|
||||
.icon-bg-rose { background: #ffe2e5; color: #f64e60; }
|
||||
.icon-bg-fuchsia { background: #f8f5ff; color: #7239ea; }
|
||||
.icon-bg-orange { background: #fff4de; color: #ffa800; }
|
||||
.icon-bg-cyan { background: #c9f7f5; color: #1bc5bd; }
|
||||
.icon-bg-emerald { background: #e6f9f0; color: #07C160; }
|
||||
.icon-bg-violet { background: #eee5ff; color: #8950fc; }
|
||||
.icon-bg-indigo { background: #e0e5ff; color: #50cd89; }
|
||||
.icon-bg-gray { background: #f5f8fa; color: #a1a5b7; }
|
||||
|
||||
.tool-title {
|
||||
font-weight: 600;
|
||||
color: #181c32;
|
||||
}
|
||||
|
||||
.desc-cell {
|
||||
max-width: 300px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: #a1a5b7;
|
||||
}
|
||||
|
||||
.badge.category {
|
||||
background-color: #f5f8fa;
|
||||
color: #5e6278;
|
||||
}
|
||||
|
||||
.badge.vip {
|
||||
background-color: #fff4de;
|
||||
color: #ffa800;
|
||||
}
|
||||
|
||||
.badge.free {
|
||||
background-color: #e6f9f0;
|
||||
color: #07C160;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #b5b5c3;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background-color: #f5f8fa;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.action-btn.delete:hover {
|
||||
background-color: #ffe2e5;
|
||||
color: #f64e60;
|
||||
}
|
||||
|
||||
/* Form Styles */
|
||||
.modal-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #3f4254;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
padding: 12px;
|
||||
border: 1px solid #e4e6ef;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #5e6278;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
background-color: #fff;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus,
|
||||
.form-group textarea:focus {
|
||||
border-color: #07C160;
|
||||
}
|
||||
|
||||
.input-with-icon {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.icon-preview {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #5e6278;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-text {
|
||||
font-size: 14px;
|
||||
color: #3f4254;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
295
Zkit-admin/src/views/Users.vue
Normal file
295
Zkit-admin/src/views/Users.vue
Normal file
@@ -0,0 +1,295 @@
|
||||
<template>
|
||||
<div class="users">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>所有用户</h3>
|
||||
<button class="btn-primary" @click="openAddModal">
|
||||
<i class="fa-solid fa-plus"></i> 新增用户
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>姓名</th>
|
||||
<th>邮箱</th>
|
||||
<th>角色</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="user in paginatedUsers" :key="user.id">
|
||||
<td>#{{ user.id }}</td>
|
||||
<td>
|
||||
<span class="username-text">{{ user.name }}</span>
|
||||
</td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td><span class="badge role">{{ user.role }}</span></td>
|
||||
<td><span class="badge status" :class="getStatusClass(user.status)">{{ getStatusText(user.status) }}</span></td>
|
||||
<td>
|
||||
<div class="actions">
|
||||
<button class="action-btn edit" @click="openEditModal(user)" title="编辑">
|
||||
<i class="fa-solid fa-pen-to-square"></i>
|
||||
</button>
|
||||
<button class="action-btn delete" @click="deleteUser(user.id)" title="删除">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<Pagination
|
||||
:total="users.length"
|
||||
v-model:page-size="pageSize"
|
||||
v-model:current-page="currentPage"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- User Modal -->
|
||||
<Modal
|
||||
v-model:visible="modalVisible"
|
||||
:title="isEdit ? '编辑用户' : '新增用户'"
|
||||
@confirm="handleSave"
|
||||
>
|
||||
<form class="modal-form">
|
||||
<div class="form-group">
|
||||
<label>姓名</label>
|
||||
<input type="text" v-model="form.name" placeholder="请输入姓名" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>邮箱</label>
|
||||
<input type="email" v-model="form.email" placeholder="请输入邮箱" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>角色</label>
|
||||
<select v-model="form.role">
|
||||
<option value="管理员">管理员</option>
|
||||
<option value="编辑">编辑</option>
|
||||
<option value="访客">访客</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>状态</label>
|
||||
<select v-model="form.status">
|
||||
<option value="active">正常</option>
|
||||
<option value="inactive">停用</option>
|
||||
<option value="pending">待审核</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
<!-- Confirm Delete Modal -->
|
||||
<ConfirmModal
|
||||
v-model:visible="confirmVisible"
|
||||
title="确认删除"
|
||||
message="确定要删除该用户吗?此操作无法撤销。"
|
||||
@confirm="confirmDelete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import Modal from '../components/Modal.vue'
|
||||
import ConfirmModal from '../components/ConfirmModal.vue'
|
||||
import Pagination from '../components/Pagination.vue'
|
||||
|
||||
const users = ref([
|
||||
{ id: 1, name: 'John Doe', email: 'john@example.com', role: '管理员', status: 'active' },
|
||||
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', role: '编辑', status: 'active' },
|
||||
{ id: 3, name: 'Bob Johnson', email: 'bob@example.com', role: '访客', status: 'inactive' },
|
||||
{ id: 4, name: 'Alice Williams', email: 'alice@example.com', role: '编辑', status: 'active' },
|
||||
{ id: 5, name: 'Charlie Brown', email: 'charlie@example.com', role: '访客', status: 'pending' },
|
||||
{ id: 6, name: 'David Lee', email: 'david@example.com', role: '访客', status: 'active' },
|
||||
{ id: 7, name: 'Eva Green', email: 'eva@example.com', role: '编辑', status: 'inactive' },
|
||||
{ id: 8, name: 'Frank White', email: 'frank@example.com', role: '管理员', status: 'active' },
|
||||
{ id: 9, name: 'Grace Black', email: 'grace@example.com', role: '访客', status: 'pending' },
|
||||
{ id: 10, name: 'Henry Ford', email: 'henry@example.com', role: '编辑', status: 'active' },
|
||||
{ id: 11, name: 'Ivy Wilson', email: 'ivy@example.com', role: '访客', status: 'inactive' },
|
||||
{ id: 12, name: 'Jack Brown', email: 'jack@example.com', role: '管理员', status: 'active' },
|
||||
])
|
||||
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(7)
|
||||
|
||||
const paginatedUsers = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
const end = start + pageSize.value
|
||||
return users.value.slice(start, end)
|
||||
})
|
||||
|
||||
const modalVisible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const confirmVisible = ref(false)
|
||||
const deleteId = ref(null)
|
||||
const form = reactive({
|
||||
id: null,
|
||||
name: '',
|
||||
email: '',
|
||||
role: '访客',
|
||||
status: 'active'
|
||||
})
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const map = {
|
||||
active: '正常',
|
||||
inactive: '停用',
|
||||
pending: '待审核'
|
||||
}
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
const getStatusClass = (status) => {
|
||||
const map = {
|
||||
active: 'bg-light-success',
|
||||
inactive: 'bg-light-danger',
|
||||
pending: 'bg-light-warning'
|
||||
}
|
||||
return map[status] || 'bg-light-dark'
|
||||
}
|
||||
|
||||
const openAddModal = () => {
|
||||
isEdit.value = false
|
||||
Object.assign(form, { id: null, name: '', email: '', role: '访客', status: 'active' })
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const openEditModal = (user) => {
|
||||
isEdit.value = true
|
||||
Object.assign(form, user)
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
if (isEdit.value) {
|
||||
const index = users.value.findIndex(u => u.id === form.id)
|
||||
if (index !== -1) {
|
||||
users.value[index] = { ...form }
|
||||
}
|
||||
} else {
|
||||
const newId = Math.max(...users.value.map(u => u.id)) + 1
|
||||
users.value.push({ ...form, id: newId })
|
||||
const totalPages = Math.ceil(users.value.length / pageSize.value)
|
||||
currentPage.value = totalPages
|
||||
}
|
||||
modalVisible.value = false
|
||||
}
|
||||
|
||||
const deleteUser = (id) => {
|
||||
deleteId.value = id
|
||||
confirmVisible.value = true
|
||||
}
|
||||
|
||||
const confirmDelete = () => {
|
||||
users.value = users.value.filter(u => u.id !== deleteId.value)
|
||||
deleteId.value = null
|
||||
const totalPages = Math.ceil(users.value.length / pageSize.value)
|
||||
if (currentPage.value > totalPages && totalPages > 0) {
|
||||
currentPage.value = totalPages
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-header {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px dashed #eff2f5;
|
||||
}
|
||||
|
||||
.card-header h3 {
|
||||
font-size: 18px;
|
||||
color: #181c32;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.username-text {
|
||||
font-weight: 600;
|
||||
color: #181c32;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.badge.role {
|
||||
background-color: #f5f8fa;
|
||||
color: #5e6278;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #b5b5c3;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background-color: #f5f8fa;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.action-btn.delete:hover {
|
||||
background-color: #ffe2e5;
|
||||
color: #f64e60;
|
||||
}
|
||||
|
||||
/* Form Styles */
|
||||
.modal-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #3f4254;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
padding: 12px;
|
||||
border: 1px solid #e4e6ef;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #5e6278;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus {
|
||||
border-color: #07C160;
|
||||
}
|
||||
</style>
|
||||
7
Zkit-admin/vite.config.js
Normal file
7
Zkit-admin/vite.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
})
|
||||
Reference in New Issue
Block a user