feat: add prime vue

This commit is contained in:
yanghao05
2025-04-01 17:51:50 +08:00
parent f696dfdfe8
commit fe9a80405d
12 changed files with 339 additions and 1023 deletions

View File

@@ -4,10 +4,14 @@
"": {
"name": "admin",
"dependencies": {
"@primeuix/themes": "^1.0.0",
"@tailwindcss/vite": "^4.0.17",
"axios": "^1.8.4",
"daisyui": "^5.0.9",
"primeicons": "^7.0.0",
"primevue": "^4.3.3",
"tailwindcss": "^4.0.17",
"tailwindcss-primeui": "^0.6.1",
"vue": "^3.5.13",
"vue-router": "4",
},
@@ -78,6 +82,18 @@
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
"@primeuix/styled": ["@primeuix/styled@0.5.0", "https://registry.npmmirror.com/@primeuix/styled/-/styled-0.5.0.tgz", { "dependencies": { "@primeuix/utils": "^0.5.0" } }, "sha512-k5CTQ+10cXIXxZTep7sktmYe8lJkjmUaFVDAc1OCsWTJR+bhBy/s6zWIatGljVtuf3RmTSxtlrHQeFLjPmdUNQ=="],
"@primeuix/styles": ["@primeuix/styles@1.0.0", "https://registry.npmmirror.com/@primeuix/styles/-/styles-1.0.0.tgz", { "dependencies": { "@primeuix/styled": "^0.5.0" } }, "sha512-j/TlbqihLNMP37zFNjxac5dTRaQEf5Ldrv0P7NwKigCCc/+MI5j4MddxDw1LnxkGhWCJ1Gjbt9uwyQteWtSv7A=="],
"@primeuix/themes": ["@primeuix/themes@1.0.0", "https://registry.npmmirror.com/@primeuix/themes/-/themes-1.0.0.tgz", { "dependencies": { "@primeuix/styled": "^0.5.0" } }, "sha512-fxUgcAP9H6FeytbE8c4QvRt8aBnoyZJqvtnnVwHT8PHr1dNSnC1nYKGrXpebcx3SpNy9Hp9oVidGsl6u61+pXQ=="],
"@primeuix/utils": ["@primeuix/utils@0.5.2", "https://registry.npmmirror.com/@primeuix/utils/-/utils-0.5.2.tgz", {}, "sha512-fHL0DGnyhL/9toBoV0cO6L+Xg/uaxmOHJW4SrDNMq6GQ7cDXR3Y0vDLvX5j/8kIsaC7LB3329sQLNHgtEETnWw=="],
"@primevue/core": ["@primevue/core@4.3.3", "https://registry.npmmirror.com/@primevue/core/-/core-4.3.3.tgz", { "dependencies": { "@primeuix/styled": "^0.5.0", "@primeuix/utils": "^0.5.1" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-kSkN5oourG7eueoFPIqiNX3oDT/f0I5IRK3uOY/ytz+VzTZp5yuaCN0Nt42ZQpVXjDxMxDvUhIdaXVrjr58NhQ=="],
"@primevue/icons": ["@primevue/icons@4.3.3", "https://registry.npmmirror.com/@primevue/icons/-/icons-4.3.3.tgz", { "dependencies": { "@primeuix/utils": "^0.5.1", "@primevue/core": "4.3.3" } }, "sha512-ouQaxHyeFB6MSfEGGbjaK5Qv9efS1xZGetZoU5jcPm090MSYLFtroP1CuK3lZZAQals06TZ6T6qcoNukSHpK5w=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.37.0", "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.37.0.tgz", { "os": "android", "cpu": "arm" }, "sha512-l7StVw6WAa8l3vA1ov80jyetOAEo1FtHvZDbzXDO/02Sq/QVvqlHkYoFwDJPIMj0GKiistsBudfx5tGFnwYWDQ=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.37.0", "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.37.0.tgz", { "os": "android", "cpu": "arm64" }, "sha512-6U3SlVyMxezt8Y+/iEBcbp945uZjJwjZimu76xoG7tO1av9VO691z8PkhzQ85ith2I8R2RddEPeSfcbyPfD4hA=="],
@@ -264,6 +280,10 @@
"postcss": ["postcss@8.5.3", "https://registry.npmmirror.com/postcss/-/postcss-8.5.3.tgz", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
"primeicons": ["primeicons@7.0.0", "https://registry.npmmirror.com/primeicons/-/primeicons-7.0.0.tgz", {}, "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw=="],
"primevue": ["primevue@4.3.3", "https://registry.npmmirror.com/primevue/-/primevue-4.3.3.tgz", { "dependencies": { "@primeuix/styled": "^0.5.0", "@primeuix/styles": "^1.0.0", "@primeuix/utils": "^0.5.1", "@primevue/core": "4.3.3", "@primevue/icons": "4.3.3" } }, "sha512-nooYVoEz5CdP3EhUkD6c3qTdRmpLHZh75fBynkUkl46K8y5rksHTjdSISiDijwTA5STQIOkyqLb+RM+HQ6nC1Q=="],
"proxy-from-env": ["proxy-from-env@1.1.0", "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
"rollup": ["rollup@4.37.0", "https://registry.npmmirror.com/rollup/-/rollup-4.37.0.tgz", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.37.0", "@rollup/rollup-android-arm64": "4.37.0", "@rollup/rollup-darwin-arm64": "4.37.0", "@rollup/rollup-darwin-x64": "4.37.0", "@rollup/rollup-freebsd-arm64": "4.37.0", "@rollup/rollup-freebsd-x64": "4.37.0", "@rollup/rollup-linux-arm-gnueabihf": "4.37.0", "@rollup/rollup-linux-arm-musleabihf": "4.37.0", "@rollup/rollup-linux-arm64-gnu": "4.37.0", "@rollup/rollup-linux-arm64-musl": "4.37.0", "@rollup/rollup-linux-loongarch64-gnu": "4.37.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.37.0", "@rollup/rollup-linux-riscv64-gnu": "4.37.0", "@rollup/rollup-linux-riscv64-musl": "4.37.0", "@rollup/rollup-linux-s390x-gnu": "4.37.0", "@rollup/rollup-linux-x64-gnu": "4.37.0", "@rollup/rollup-linux-x64-musl": "4.37.0", "@rollup/rollup-win32-arm64-msvc": "4.37.0", "@rollup/rollup-win32-ia32-msvc": "4.37.0", "@rollup/rollup-win32-x64-msvc": "4.37.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-iAtQy/L4QFU+rTJ1YUjXqJOJzuwEghqWzCEYD2FEghT7Gsy1VdABntrO4CLopA5IkflTyqNiLNwPcOJ3S7UKLg=="],
@@ -272,6 +292,8 @@
"tailwindcss": ["tailwindcss@4.0.17", "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.0.17.tgz", {}, "sha512-OErSiGzRa6rLiOvaipsDZvLMSpsBZ4ysB4f0VKGXUrjw2jfkJRd6kjRKV2+ZmTCNvwtvgdDam5D7w6WXsdLJZw=="],
"tailwindcss-primeui": ["tailwindcss-primeui@0.6.1", "https://registry.npmmirror.com/tailwindcss-primeui/-/tailwindcss-primeui-0.6.1.tgz", { "peerDependencies": { "tailwindcss": ">=3.1.0" } }, "sha512-T69Rylcrmnt8zy9ik+qZvsLuRIrS9/k6rYJSIgZ1trnbEzGDDQSCIdmfyZknevqiHwpSJHSmQ9XT2C+S/hJY4A=="],
"tapable": ["tapable@2.2.1", "https://registry.npmmirror.com/tapable/-/tapable-2.2.1.tgz", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="],
"vite": ["vite@6.2.2", "https://registry.npmmirror.com/vite/-/vite-6.2.2.tgz", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ=="],

View File

@@ -9,10 +9,14 @@
"preview": "vite preview"
},
"dependencies": {
"@primeuix/themes": "^1.0.0",
"@tailwindcss/vite": "^4.0.17",
"axios": "^1.8.4",
"daisyui": "^5.0.9",
"primeicons": "^7.0.0",
"primevue": "^4.3.3",
"tailwindcss": "^4.0.17",
"tailwindcss-primeui": "^0.6.1",
"vue": "^3.5.13",
"vue-router": "4"
},

View File

@@ -1,42 +1,53 @@
<script setup>
// 不再需要下拉菜单的状态,可以删除相关代码
import Button from 'primevue/button';
import Menubar from 'primevue/menubar';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const navItems = ref([
{
label: 'Dashboard',
icon: 'pi pi-home',
command: () => router.push('/')
},
{
label: 'Media',
icon: 'pi pi-image',
command: () => router.push('/media')
},
{
label: 'Articles',
icon: 'pi pi-file',
command: () => router.push('/articles')
},
{
label: 'Settings',
icon: 'pi pi-cog',
command: () => router.push('/settings')
}
]);
</script>
<template>
<div class="min-h-screen bg-base-200">
<div class="navbar bg-base-100 shadow-md">
<div class="navbar-start">
<div class="px-2 mx-2">
<h1 class="text-xl font-bold">Hello</h1>
</div>
</div>
<div class="navbar-center">
<div class="tabs tabs-boxed bg-base-100">
<router-link to="/posts" class="tab" :class="{ 'tab-active': $route.path === '/posts' }">
文章管理
</router-link>
<router-link to="/medias" class="tab" :class="{ 'tab-active': $route.path === '/medias' }">
媒体库
</router-link>
</div>
</div>
<div class="navbar-end">
<button class="btn btn-error btn-sm">退出登录</button>
</div>
</div>
<div class="flex flex-col min-h-screen">
<header class="sticky top-0 z-50 shadow-md">
<Menubar :model="navItems" class="!rounded-none">
<template #start>
<div class="flex items-center pr-4">
<h2 class="m-0 text-2xl text-primary font-semibold">Admin Panel</h2>
</div>
</template>
<template #end>
<Button label="Logout" icon="pi pi-power-off" severity="secondary" text />
</template>
</Menubar>
</header>
<main class="container mx-auto py-6 px-4">
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<router-view />
</div>
</div>
<main class="flex-1 p-6 bg-surface-ground">
<router-view />
</main>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>

View File

@@ -0,0 +1,45 @@
import axios from 'axios';
// Create a base axios instance
export const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
// Request interceptor
apiClient.interceptors.request.use(
(config) => {
// You can add auth tokens here if needed
const token = localStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor
apiClient.interceptors.response.use(
(response) => {
return response;
},
(error) => {
// Handle common errors (e.g., unauthorized, server errors)
if (error.response) {
// Handle specific status codes
if (error.response.status === 401) {
// Handle unauthorized
console.error('Unauthorized request');
// You might want to redirect to login page
}
}
return Promise.reject(error);
}
);

View File

@@ -0,0 +1,132 @@
import { apiClient } from './apiClient';
// Environment detection
let isDevelopment = false; // Default to development mode
// Try different ways to detect environment
try {
if (typeof process !== 'undefined' && process.env) {
console.log('Detected process.env, NODE_ENV:', process.env.NODE_ENV);
isDevelopment = process.env.NODE_ENV === 'development';
} else if (typeof import.meta !== 'undefined' && import.meta.env) {
console.log('Detected import.meta.env, MODE:', import.meta.env.MODE);
isDevelopment = import.meta.env.MODE === 'development';
}
} catch (error) {
console.error('Error detecting environment:', error);
}
setTimeout(() => {
console.log('%cCurrent environment: ' + (isDevelopment ? 'DEVELOPMENT' : 'PRODUCTION'),
'background: #222; color: #bada55; font-size: 16px; padding: 4px;');
}, 0);
// Mock service implementation
const mockService = {
/**
* Mock implementation for getting media count
* @returns {Promise<{count: number}>}
*/
getMediaCount: async () => {
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 800));
// Return mock data
return {
data: {
count: Math.floor(Math.random() * 500) + 100 // Random count between 100-600
}
};
},
/**
* Mock implementation for getting article count
* @returns {Promise<{count: number}>}
*/
getArticleCount: async () => {
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 600));
// Return mock data
return {
data: {
count: Math.floor(Math.random() * 200) + 50 // Random count between 50-250
}
};
}
};
// Real API implementation
const realApiService = {
/**
* Real implementation for getting media count
* @returns {Promise<{count: number}>}
*/
getMediaCount: async () => {
try {
return await apiClient.get('/api/media/count');
} catch (error) {
console.error('Error fetching media count:', error);
throw error;
}
},
/**
* Real implementation for getting article count
* @returns {Promise<{count: number}>}
*/
getArticleCount: async () => {
try {
return await apiClient.get('/api/articles/count');
} catch (error) {
console.error('Error fetching article count:', error);
throw error;
}
}
};
// Log which service we're using
console.log(`Using ${isDevelopment ? 'MOCK' : 'REAL'} API service for stats`);
// Use the appropriate service based on environment
const apiService = isDevelopment ? mockService : realApiService;
export const statsApi = {
/**
* Get the total count of media items
* @returns {Promise<{count: number}>} The media count
*/
getMediaCount: async () => {
const response = await apiService.getMediaCount();
return response.data;
},
/**
* Get the total count of articles
* @returns {Promise<{count: number}>} The article count
*/
getArticleCount: async () => {
const response = await apiService.getArticleCount();
return response.data;
},
/**
* Get all statistics in a single call
* @returns {Promise<{mediaCount: number, articleCount: number}>}
*/
getAllStats: async () => {
try {
const [mediaResponse, articleResponse] = await Promise.all([
apiService.getMediaCount(),
apiService.getArticleCount()
]);
return {
mediaCount: mediaResponse.data.count,
articleCount: articleResponse.data.count
};
} catch (error) {
console.error('Error fetching all stats:', error);
throw error;
}
}
};

View File

@@ -1,43 +0,0 @@
<script setup>
import { ref } from 'vue'
defineProps({
msg: String,
})
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
target="_blank"
>Vue Docs Scaling up Guide</a
>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

View File

@@ -1,6 +1,11 @@
import Aura from '@primeuix/themes/aura';
import PrimeVue from 'primevue/config';
import { createApp } from 'vue';
import App from './App.vue';
import { router } from './router';
import { router } from './router.js';
// Import only the required PrimeVue styles
import 'primeicons/primeicons.css'; // Icons
import './style.css';
// Log environment information during app initialization
@@ -13,4 +18,18 @@ if (typeof import.meta !== 'undefined') {
}
console.log('=============================');
createApp(App).use(router).mount('#app');
const app = createApp(App)
app.use(router);
app.use(PrimeVue, {
theme: {
preset: Aura,
},
ripple: true,
options: {
darkModeSelector: '.my-app-dark',
}
})
// Remove global component registrations
app.mount('#app');

View File

@@ -1,135 +1,91 @@
<template>
<div class="home-page">
<h1>仪表盘</h1>
<div class="statistics-container">
<div class="stat-card" @click="navigateTo('/posts')">
<div class="stat-icon">
<i class="fa fa-file-text"></i>
</div>
<div class="stat-content">
<h2>文章数量</h2>
<div class="stat-value">{{ postCount }}</div>
</div>
</div>
<div class="stat-card" @click="navigateTo('/medias')">
<div class="stat-icon">
<i class="fa fa-image"></i>
</div>
<div class="stat-content">
<h2>媒体数量</h2>
<div class="stat-value">{{ mediaCount }}</div>
</div>
</div>
</div>
<div v-if="error" class="error-message">
{{ error }}
</div>
</div>
</template>
<script setup>
import Button from 'primevue/button';
import Card from 'primevue/card';
import Message from 'primevue/message';
import ProgressSpinner from 'primevue/progressspinner';
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { statisticsService } from '../api/statisticsService';
import { statsApi } from '../api/statsApi';
const router = useRouter();
const postCount = ref(0);
const mediaCount = ref(0);
const error = ref('');
const loading = ref(false);
const articleCount = ref(0);
const loading = ref(true);
const error = ref(null);
const fetchStatistics = async () => {
const fetchCounts = async () => {
loading.value = true;
error.value = '';
error.value = null;
try {
// Option 1: Make parallel requests using our service
const [postsData, mediasData] = await Promise.all([
statisticsService.getPostsCount(),
statisticsService.getMediasCount()
// Use the API service instead of direct fetch calls
const [mediaData, articleData] = await Promise.all([
statsApi.getMediaCount(),
statsApi.getArticleCount()
]);
postCount.value = postsData.count;
mediaCount.value = mediasData.count;
// Option 2: If you have a combined endpoint
// const data = await statisticsService.getAllStatistics();
// postCount.value = data.posts;
// mediaCount.value = data.medias;
mediaCount.value = mediaData.count;
articleCount.value = articleData.count;
} catch (err) {
console.error('Error fetching statistics:', err);
error.value = '获取统计数据时出错';
console.error('Error fetching data:', err);
error.value = 'Failed to load data. Please try again later.';
} finally {
loading.value = false;
}
};
const navigateTo = (path) => {
router.push(path);
};
onMounted(() => {
fetchStatistics();
fetchCounts();
});
</script>
<style scoped>
.home-page {
padding: 20px;
}
<template>
<div class="w-full">
<div class="flex justify-between items-center mb-8 pb-2">
<h1 class="m-0 text-2xl text-gray-800 font-medium">Dashboard</h1>
<Button @click="fetchCounts" icon="pi pi-refresh" rounded text aria-label="Refresh" />
</div>
.statistics-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-top: 20px;
}
<div v-if="loading" class="flex flex-col items-center justify-center py-12 text-center min-h-[200px]">
<ProgressSpinner />
<p class="mt-4 text-gray-500">Loading data...</p>
</div>
.stat-card {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 20px;
display: flex;
min-width: 250px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
<div v-else-if="error" class="flex flex-col items-center justify-center py-12 text-center min-h-[200px]">
<Message severity="error" :closable="false">{{ error }}</Message>
<Button @click="fetchCounts" label="Retry" icon="pi pi-refresh" severity="secondary" class="mt-3" />
</div>
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
}
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 my-8">
<Card class="transition-all duration-200 hover:-translate-y-1 hover:shadow-lg">
<template #header>
<div class="flex items-center justify-center p-4 bg-primary bg-opacity-10">
<i class="pi pi-image text-4xl! text-white"></i>
</div>
</template>
<template #title>Media</template>
<template #content>
<div class="text-4xl font-bold mb-2 text-primary">{{ mediaCount }}</div>
<div class="text-base text-gray-500 mb-4">Total Media Items</div>
</template>
<template #footer>
<Button label="View All Media" icon="pi pi-arrow-right" link />
</template>
</Card>
.stat-icon {
font-size: 2.5rem;
margin-right: 20px;
color: #3498db;
display: flex;
align-items: center;
}
.stat-content h2 {
margin: 0 0 10px 0;
font-size: 1.2rem;
color: #555;
}
.stat-value {
font-size: 2rem;
font-weight: bold;
color: #333;
}
.error-message {
margin-top: 20px;
padding: 10px;
background-color: #ffecec;
color: #f44336;
border-radius: 4px;
border-left: 4px solid #f44336;
}
</style>
<Card class="transition-all duration-200 hover:-translate-y-1 hover:shadow-lg">
<template #header>
<div class="flex items-center justify-center p-4 bg-primary bg-opacity-10">
<i class="pi pi-file text-4xl! text-white"></i>
</div>
</template>
<template #title>Articles</template>
<template #content>
<div class="text-4xl font-bold mb-2 text-primary">{{ articleCount }}</div>
<div class="text-base text-gray-500 mb-4">Total Articles</div>
</template>
<template #footer>
<Button label="View All Articles" icon="pi pi-arrow-right" link />
</template>
</Card>
</div>
</div>
</template>

View File

@@ -1,481 +0,0 @@
<template>
<div class="container mx-auto px-4 py-8">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-semibold text-gray-800">Media Library</h1>
<div class="flex space-x-2">
<div class="relative">
<input type="text" placeholder="Search media..."
class="pl-10 pr-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
v-model="searchQuery" @input="filterMedia">
<i class="iconfont icon-search absolute left-3 top-3 text-gray-400"></i>
</div>
<select class="border rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
v-model="selectedType" @change="filterMedia">
<option value="all">All media</option>
<option value="image">Images</option>
<option value="video">Videos</option>
<option value="audio">Audio</option>
<option value="document">Documents</option>
<option value="pdf">PDFs</option>
</select>
</div>
</div>
<!-- Upload Area -->
<div id="upload-area" :class="{ 'active': isDragging }"
class="upload-area rounded-lg p-10 mb-8 flex flex-col items-center justify-center cursor-pointer"
@click="triggerFileInput" @dragenter.prevent="onDragEnter" @dragover.prevent="onDragOver"
@dragleave.prevent="onDragLeave" @drop.prevent="onDrop">
<i class="iconfont icon-upload text-5xl text-blue-500 mb-4"></i>
<p class="text-gray-600 text-center mb-2">Drag and drop files here or click to browse</p>
<p class="text-gray-400 text-sm text-center">Supports: JPG, PNG, GIF, MP4, PDF</p>
<input type="file" id="file-input" multiple class="hidden" ref="fileInput" @change="onFileSelected">
</div>
<!-- Media Table Section -->
<div class="mb-12 bg-white rounded-lg shadow overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full media-table">
<thead class="bg-gray-50 border-b border-gray-200">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<div class="flex items-center">
File Name
<button class="ml-1 text-gray-400 hover:text-gray-600" @click="sortBy('fileName')">
<i class="iconfont icon-sort"></i>
</button>
</div>
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<div class="flex items-center">
Upload Time
<button class="ml-1 text-gray-400 hover:text-gray-600"
@click="sortBy('uploadTime')">
<i class="iconfont icon-sort"></i>
</button>
</div>
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<div class="flex items-center">
File Size
<button class="ml-1 text-gray-400 hover:text-gray-600" @click="sortBy('fileSize')">
<i class="iconfont icon-sort"></i>
</button>
</div>
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<div class="flex items-center">
File Type
<button class="ml-1 text-gray-400 hover:text-gray-600" @click="sortBy('fileType')">
<i class="iconfont icon-sort"></i>
</button>
</div>
</th>
<th
class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="file in paginatedFiles" :key="file.id" class="table-row">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10 mr-3" v-if="file.fileType === 'Image'">
<img class="preview-thumbnail" :src="file.thumbnailUrl" :alt="file.fileName">
</div>
<div v-else
class="flex-shrink-0 h-10 w-10 mr-3 bg-gray-100 rounded flex items-center justify-center">
<i :class="getFileTypeIcon(file.fileType)" class="text-2xl"></i>
</div>
<div class="text-sm font-medium text-gray-900">{{ file.fileName }}</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ file.uploadTime }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ file.fileSize }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full"
:class="getFileTypeBadgeClass(file.fileType)">
{{ file.fileType }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-center text-sm font-medium">
<div class="flex justify-center space-x-2">
<button class="p-1 text-blue-600 hover:text-blue-900 focus:outline-none"
title="Preview" @click="previewFile(file)">
<i
:class="file.fileType === 'Video' || file.fileType === 'Audio' ? 'iconfont icon-play' : 'iconfont icon-eye'"></i>
</button>
<button class="p-1 text-gray-600 hover:text-gray-900 focus:outline-none"
title="Download" @click="downloadFile(file)">
<i class="iconfont icon-download"></i>
</button>
<button class="p-1 text-red-600 hover:text-red-900 focus:outline-none"
title="Delete" @click="deleteFile(file)">
<i class="iconfont icon-delete"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Table footer with pagination -->
<div class="bg-gray-50 px-6 py-3 border-t border-gray-200 flex items-center justify-between">
<div class="flex-1 flex justify-between sm:hidden">
<button
class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
@click="prevPage" :disabled="currentPage === 1"
:class="{ 'opacity-50 cursor-not-allowed': currentPage === 1 }">
Previous
</button>
<button
class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
@click="nextPage" :disabled="currentPage === totalPages"
:class="{ 'opacity-50 cursor-not-allowed': currentPage === totalPages }">
Next
</button>
</div>
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p class="text-sm text-gray-700">
Showing <span class="font-medium">{{ startItem }}</span> to <span class="font-medium">{{
endItem }}</span> of <span class="font-medium">{{ filteredFiles.length }}</span> results
</p>
</div>
<div>
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
<button
class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
@click="prevPage" :disabled="currentPage === 1"
:class="{ 'opacity-50 cursor-not-allowed': currentPage === 1 }">
<span class="sr-only">Previous</span>
<i class="iconfont icon-arrow-left"></i>
</button>
<button v-for="page in paginationPages" :key="page" :class="getPageButtonClass(page)"
@click="goToPage(page)">
{{ page }}
</button>
<span v-if="showEllipsis"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700">
...
</span>
<button v-if="totalPages > 3" :class="getPageButtonClass(totalPages)"
@click="goToPage(totalPages)">
{{ totalPages }}
</button>
<button
class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
@click="nextPage" :disabled="currentPage === totalPages"
:class="{ 'opacity-50 cursor-not-allowed': currentPage === totalPages }">
<span class="sr-only">Next</span>
<i class="iconfont icon-arrow-right"></i>
</button>
</nav>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue';
// Reactive state
const fileInput = ref(null);
const mediaFiles = ref([
{
id: 1,
fileName: 'sunset-beach.jpg',
uploadTime: 'Today, 10:30 AM',
fileSize: '2.4 MB',
fileType: 'Image',
thumbnailUrl: 'https://via.placeholder.com/300x225'
},
{
id: 2,
fileName: 'presentation.pdf',
uploadTime: 'Yesterday, 3:45 PM',
fileSize: '4.8 MB',
fileType: 'PDF',
thumbnailUrl: null
},
{
id: 3,
fileName: 'promo_video.mp4',
uploadTime: 'Aug 28, 2023',
fileSize: '24.8 MB',
fileType: 'Video',
thumbnailUrl: null
},
{
id: 4,
fileName: 'report_q3.docx',
uploadTime: 'Aug 25, 2023',
fileSize: '1.2 MB',
fileType: 'Document',
thumbnailUrl: null
},
{
id: 5,
fileName: 'podcast_interview.mp3',
uploadTime: 'Aug 20, 2023',
fileSize: '18.5 MB',
fileType: 'Audio',
thumbnailUrl: null
}
]);
const searchQuery = ref('');
const selectedType = ref('all');
const currentPage = ref(1);
const itemsPerPage = ref(5);
const isDragging = ref(false);
const sortOrder = ref({
field: null,
direction: 'asc'
});
// Computed properties
const filteredFiles = computed(() => {
let result = [...mediaFiles.value];
// Search filter
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase();
result = result.filter(file =>
file.fileName.toLowerCase().includes(query)
);
}
// Type filter
if (selectedType.value !== 'all') {
const type = selectedType.value.charAt(0).toUpperCase() + selectedType.value.slice(1);
result = result.filter(file =>
file.fileType.toLowerCase() === selectedType.value
);
}
// Sorting
if (sortOrder.value.field) {
result.sort((a, b) => {
if (sortOrder.value.direction === 'asc') {
return a[sortOrder.value.field] > b[sortOrder.value.field] ? 1 : -1;
} else {
return a[sortOrder.value.field] < b[sortOrder.value.field] ? 1 : -1;
}
});
}
return result;
});
const paginatedFiles = computed(() => {
const start = (currentPage.value - 1) * itemsPerPage.value;
const end = start + itemsPerPage.value;
return filteredFiles.value.slice(start, end);
});
const totalPages = computed(() => {
return Math.ceil(filteredFiles.value.length / itemsPerPage.value);
});
const startItem = computed(() => {
if (filteredFiles.value.length === 0) return 0;
return (currentPage.value - 1) * itemsPerPage.value + 1;
});
const endItem = computed(() => {
if (filteredFiles.value.length === 0) return 0;
const end = currentPage.value * itemsPerPage.value;
return end > filteredFiles.value.length ? filteredFiles.value.length : end;
});
const paginationPages = computed(() => {
// For simple pagination, just show first 3 pages
const pages = [];
const maxPages = Math.min(3, totalPages.value);
for (let i = 1; i <= maxPages; i++) {
pages.push(i);
}
return pages;
});
const showEllipsis = computed(() => {
return totalPages.value > 3;
});
// Methods
const getFileTypeIcon = (fileType) => {
switch (fileType) {
case 'PDF':
return 'iconfont icon-file-pdf text-red-500';
case 'Video':
return 'iconfont icon-file-video text-blue-500';
case 'Document':
return 'iconfont icon-file-word text-blue-700';
case 'Audio':
return 'iconfont icon-file-audio text-green-600';
default:
return 'iconfont icon-file text-gray-500';
}
};
const getFileTypeBadgeClass = (fileType) => {
switch (fileType) {
case 'Image':
return 'bg-blue-100 text-blue-800';
case 'PDF':
return 'bg-red-100 text-red-800';
case 'Video':
return 'bg-purple-100 text-purple-800';
case 'Document':
return 'bg-indigo-100 text-indigo-800';
case 'Audio':
return 'bg-green-100 text-green-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
const getPageButtonClass = (page) => {
return page === currentPage.value
? 'relative inline-flex items-center px-4 py-2 border border-blue-500 bg-blue-500 text-sm font-medium text-white'
: 'relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50';
};
const triggerFileInput = () => {
fileInput.value.click();
};
const onDragEnter = (e) => {
isDragging.value = true;
};
const onDragOver = (e) => {
isDragging.value = true;
};
const onDragLeave = (e) => {
isDragging.value = false;
};
const onDrop = (e) => {
isDragging.value = false;
const files = e.dataTransfer.files;
handleFiles(files);
};
const onFileSelected = (e) => {
const files = e.target.files;
handleFiles(files);
};
const handleFiles = (files) => {
const fileNames = Array.from(files).map(file => file.name).join(', ');
alert(`Files selected: ${fileNames}`);
// In a real app, we would upload these files to the server
};
const filterMedia = () => {
currentPage.value = 1; // Reset to first page when filtering
};
const sortBy = (field) => {
if (sortOrder.value.field === field) {
// Toggle direction
sortOrder.value.direction = sortOrder.value.direction === 'asc' ? 'desc' : 'asc';
} else {
// New field, default to ascending
sortOrder.value.field = field;
sortOrder.value.direction = 'asc';
}
};
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--;
}
};
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++;
}
};
const goToPage = (page) => {
currentPage.value = page;
};
const previewFile = (file) => {
alert(`Preview file: ${file.fileName}`);
};
const downloadFile = (file) => {
alert(`Download file: ${file.fileName}`);
};
const deleteFile = (file) => {
if (confirm(`Are you sure you want to delete ${file.fileName}?`)) {
// In a real app, we would make an API call to delete the file
mediaFiles.value = mediaFiles.value.filter(f => f.id !== file.id);
alert(`Deleted file: ${file.fileName}`);
}
};
</script>
<style scoped>
.upload-area {
border: 2px dashed #cbd5e0;
transition: all 0.3s ease;
}
.upload-area.active {
border-color: #4299e1;
background-color: rgba(66, 153, 225, 0.1);
}
.table-row {
transition: background-color 0.2s ease;
}
.table-row:hover {
background-color: rgba(237, 242, 247, 0.7);
}
.pagination-item.active {
background-color: #4299e1;
color: white;
}
.media-table th {
position: relative;
}
.media-table th:after {
content: '';
position: absolute;
right: 0;
top: 25%;
height: 50%;
width: 1px;
background-color: #e2e8f0;
}
.media-table th:last-child:after {
display: none;
}
.preview-thumbnail {
width: 40px;
height: 40px;
object-fit: cover;
border-radius: 4px;
}
</style>

View File

@@ -1,341 +0,0 @@
<template>
<div class="container mx-auto px-4 py-8">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">文章列表</h1>
<div class="flex space-x-2">
<button class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md transition"
@click="createPost">
创建文章
</button>
<div class="relative">
<input type="text" placeholder="搜索文章..."
class="border rounded-md px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
v-model="searchQuery" @input="searchPosts">
<button class="absolute right-2 top-2.5 text-gray-400 hover:text-gray-600">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
</div>
</div>
</div>
<div class="bg-white shadow rounded-lg overflow-hidden">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
标题
</th>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
价格
</th>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
发布时间
</th>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
发布状态
</th>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
媒体类型
</th>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
观看次数
</th>
<th scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
操作
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="post in displayedPosts" :key="post.id">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10">
<img class="h-10 w-10 rounded-md object-cover" :src="post.thumbnailUrl"
:alt="post.title">
</div>
<div class="ml-4">
<div class="text-sm font-medium text-gray-900">{{ post.title }}</div>
<div class="text-sm text-gray-500">作者: {{ post.author }}</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900">¥{{ post.price }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900">{{ post.publishTime }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full"
:class="getStatusClass(post.status)">
{{ post.status }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900">{{ post.mediaTypes.join(', ') }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ post.viewCount }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div class="flex space-x-2">
<button @click="editPost(post)"
class="text-indigo-600 hover:text-indigo-900">编辑</button>
<button @click="deletePost(post)"
class="text-red-600 hover:text-red-900">删除</button>
<button @click="viewPost(post)"
class="text-gray-600 hover:text-gray-900">查看</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="bg-white px-4 py-3 border-t border-gray-200 sm:px-6">
<div class="flex items-center justify-between">
<div class="flex-1 flex justify-between sm:hidden">
<a href="#"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
@click.prevent="prevPage" :class="{ 'opacity-50 cursor-not-allowed': currentPage === 1 }">
上一页
</a>
<a href="#"
class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
@click.prevent="nextPage"
:class="{ 'opacity-50 cursor-not-allowed': currentPage === totalPages }">
下一页
</a>
</div>
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p class="text-sm text-gray-700">
显示第 <span class="font-medium">{{ startItem }}</span> <span class="font-medium">{{
endItem }}</span> <span class="font-medium">{{ totalItems }}</span> 条结果
</p>
</div>
<div>
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px"
aria-label="Pagination">
<a href="#"
class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
@click.prevent="prevPage"
:class="{ 'opacity-50 cursor-not-allowed': currentPage === 1 }">
<span class="sr-only">上一页</span>
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd"
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
clip-rule="evenodd" />
</svg>
</a>
<template v-for="page in paginationItems" :key="page">
<a v-if="page !== '...'" href="#"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium hover:bg-gray-50"
:class="page === currentPage ? 'text-blue-600' : 'text-gray-700'"
@click.prevent="goToPage(page)">
{{ page }}
</a>
<span v-else
class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700">
...
</span>
</template>
<a href="#"
class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
@click.prevent="nextPage"
:class="{ 'opacity-50 cursor-not-allowed': currentPage === totalPages }">
<span class="sr-only">下一页</span>
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd" />
</svg>
</a>
</nav>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue';
// Reactive state
const posts = ref([
{
id: 1,
title: '如何高效学习编程',
author: '张三',
thumbnailUrl: 'https://via.placeholder.com/150',
price: '29.99',
publishTime: '2023-06-15 14:30',
status: '已发布',
mediaTypes: ['文章', '视频'],
viewCount: 1254
},
{
id: 2,
title: '前端开发最佳实践',
author: '李四',
thumbnailUrl: 'https://via.placeholder.com/150',
price: '49.99',
publishTime: '2023-06-10 09:15',
status: '草稿',
mediaTypes: ['文章'],
viewCount: 789
},
{
id: 3,
title: '数据分析入门指南',
author: '王五',
thumbnailUrl: 'https://via.placeholder.com/150',
price: '0.00',
publishTime: '2023-06-05 16:45',
status: '已下架',
mediaTypes: ['文章', '音频'],
viewCount: 2567
}
]);
const searchQuery = ref('');
const currentPage = ref(3);
const itemsPerPage = ref(10);
const totalItems = ref(50);
// Computed properties
const filteredPosts = computed(() => {
if (!searchQuery.value) {
return posts.value;
}
const query = searchQuery.value.toLowerCase();
return posts.value.filter(post =>
post.title.toLowerCase().includes(query) ||
post.author.toLowerCase().includes(query)
);
});
const displayedPosts = computed(() => {
return filteredPosts.value;
});
const totalPages = computed(() => {
return Math.ceil(totalItems.value / itemsPerPage.value);
});
const startItem = computed(() => {
return (currentPage.value - 1) * itemsPerPage.value + 1;
});
const endItem = computed(() => {
const end = currentPage.value * itemsPerPage.value;
return end > totalItems.value ? totalItems.value : end;
});
const paginationItems = computed(() => {
const items = [];
const maxPagesToShow = 5;
if (totalPages.value <= maxPagesToShow) {
// Show all pages
for (let i = 1; i <= totalPages.value; i++) {
items.push(i);
}
} else {
// Show limited pages with ellipsis
if (currentPage.value <= 3) {
// Current page is near the start
for (let i = 1; i <= 3; i++) {
items.push(i);
}
items.push('...');
items.push(totalPages.value);
} else if (currentPage.value >= totalPages.value - 2) {
// Current page is near the end
items.push(1);
items.push('...');
for (let i = totalPages.value - 2; i <= totalPages.value; i++) {
items.push(i);
}
} else {
// Current page is in the middle
items.push(1);
items.push('...');
items.push(currentPage.value - 1);
items.push(currentPage.value);
items.push(currentPage.value + 1);
items.push('...');
items.push(totalPages.value);
}
}
return items;
});
// Methods
const getStatusClass = (status) => {
switch (status) {
case '已发布':
return 'bg-green-100 text-green-800';
case '草稿':
return 'bg-yellow-100 text-yellow-800';
case '已下架':
return 'bg-red-100 text-red-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
const createPost = () => {
alert('创建新文章功能待实现');
};
const editPost = (post) => {
alert(`编辑文章: ${post.title}`);
};
const deletePost = (post) => {
if (confirm(`确定要删除文章 "${post.title}" 吗?`)) {
// In a real app, we would make an API call to delete the post
alert(`已删除文章: ${post.title}`);
}
};
const viewPost = (post) => {
alert(`查看文章: ${post.title}`);
};
const searchPosts = () => {
currentPage.value = 1; // Reset to first page when searching
};
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--;
}
};
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++;
}
};
const goToPage = (page) => {
currentPage.value = page;
};
</script>

View File

@@ -7,16 +7,6 @@ const routes = [
name: 'Home',
component: () => import('./pages/HomePage.vue')
},
{
path: '/posts',
name: 'Posts',
component: () => import('./pages/PostsPage.vue')
},
{
path: '/medias',
name: 'Medias',
component: () => import('./pages/MediasPage.vue')
}
];
// Create the router instance

View File

@@ -1,2 +1,4 @@
@import "tailwindcss";
@plugin "daisyui";
@import "tailwindcss-primeui";
@custom-variant dark (&:where(.my-app-dark, .my-app-dark *));