实现基础前端设计和后台管理框架

This commit is contained in:
2025-10-14 18:23:48 +08:00
commit ad4581ce2b
33 changed files with 10644 additions and 0 deletions

30
.gitignore vendored Normal file
View File

@@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

3
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

29
README.md Normal file
View File

@@ -0,0 +1,29 @@
# fjeei
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```

18
index.html Normal file
View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
<style>
body {
margin: 0;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

8
jsconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

3595
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

26
package.json Normal file
View File

@@ -0,0 +1,26 @@
{
"name": "fjeei",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@vueup/vue-quill": "^1.2.0",
"element-plus": "^2.11.4",
"pinia": "^2.2.6",
"prismjs": "^1.30.0",
"quill": "^2.0.3",
"vue": "^3.5.13",
"vue-router": "^4.4.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"vite": "^6.0.1",
"vite-plugin-vue-devtools": "^7.6.5"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

10
src/App.vue Normal file
View File

@@ -0,0 +1,10 @@
<script setup>
import { RouterView } from 'vue-router'
</script>
<template>
<RouterView />
</template>
<style scoped>
</style>

86
src/assets/base.css Normal file
View File

@@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

1
src/assets/logo.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

0
src/assets/main.css Normal file
View File

22
src/main.js Normal file
View File

@@ -0,0 +1,22 @@
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
// 注册所有图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(createPinia())
app.use(router)
app.use(ElementPlus)
app.mount('#app')

141
src/router/index.js Normal file
View File

@@ -0,0 +1,141 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import { useAuthStore } from '@/stores/auth'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
redirect: '/main'
},
{
path: '/admin',
redirect: '/admin/dashboard'
},
{
path: '/admin/login',
name: 'admin-login',
component: () => import('../views/Admin/LoginView.vue'),
meta: { requiresAuth: false }
},
{
path: '/admin/dashboard',
name: 'admin-dashboard',
component: () => import('../views/Admin/DashboardView.vue'),
meta: { requiresAuth: true }
},
{
path: '/admin/content',
name: 'admin-content',
component: () => import('../views/Admin/ContentManagementView.vue'),
meta: { requiresAuth: true },
children: [
{
path: '',
redirect: '/admin/content/articles'
},
{
path: 'articles',
name: 'admin-content-articles',
component: () => import('../views/Admin/Content/ArticlesView.vue'),
meta: { requiresAuth: true }
},
{
path: 'articles/:id',
name: 'admin-content-article-edit',
component: () => import('../views/Admin/Content/ArticleEditView.vue'),
meta: { requiresAuth: true }
},
{
path: 'videos',
name: 'admin-content-videos',
component: () => import('../views/Admin/Content/VideosView.vue'),
meta: { requiresAuth: true }
},
{
path: 'videos/:id',
name: 'admin-content-video-edit',
component: () => import('../views/Admin/Content/VideoEditView.vue'),
meta: { requiresAuth: true }
},
{
path: 'downloads/:id',
name: 'admin-content-download-edit',
component: () => import('../views/Admin/Content/DownloadEditView.vue'),
meta: { requiresAuth: true }
},
{
path: 'downloads',
name: 'admin-content-downloads',
component: () => import('../views/Admin/Content/DownloadsView.vue'),
meta: { requiresAuth: true }
},
{
path: 'services',
name: 'admin-content-services',
component: () => import('../views/Admin/Content/ServicesView.vue'),
meta: { requiresAuth: true }
},
{
path: 'wechat',
name: 'admin-content-wechat',
component: () => import('../views/Admin/Content/WechatView.vue'),
meta: { requiresAuth: true }
},
{
path: 'links',
name: 'admin-content-links',
component: () => import('../views/Admin/Content/LinksView.vue'),
meta: { requiresAuth: true }
}
]
},
{
path: '/home',
name: 'home',
component: HomeView,
children: [
{
path: '/main',
name: 'main',
component: () => import('../views/MainView.vue')
},
{
path: '/catalog',
name: 'catalog',
component: () => import('../views/CatalogView.vue')
},
{
path: '/content',
name: 'content',
component: () => import('../views/ContentView.vue')
}
]
}
],
})
// 路由守卫
router.beforeEach((to, from, next) => {
const authStore = useAuthStore()
// 初始化认证状态
authStore.initAuth()
// 检查是否需要认证
if (to.meta.requiresAuth) {
if (authStore.isLoggedIn && authStore.checkToken()) {
next()
} else {
next('/admin/login')
}
} else if (to.path === '/admin/login' && authStore.isLoggedIn) {
// 如果已登录,重定向到仪表板
next('/admin/dashboard')
} else {
next()
}
})
export default router

68
src/stores/auth.js Normal file
View File

@@ -0,0 +1,68 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useAuthStore = defineStore('auth', () => {
// 状态
const user = ref(null)
const token = ref(localStorage.getItem('admin_token') || '')
const isLoggedIn = computed(() => !!token.value && !!user.value)
// 登录
const login = (loginData) => {
user.value = {
username: loginData.username,
loginTime: new Date().toISOString()
}
token.value = loginData.token
// 保存到本地存储
localStorage.setItem('admin_token', loginData.token)
localStorage.setItem('admin_user', JSON.stringify(user.value))
// 如果选择记住我,设置更长的过期时间
if (loginData.rememberMe) {
localStorage.setItem('admin_remember', 'true')
}
}
// 登出
const logout = () => {
user.value = null
token.value = ''
// 清除本地存储
localStorage.removeItem('admin_token')
localStorage.removeItem('admin_user')
localStorage.removeItem('admin_remember')
}
// 初始化用户信息(页面刷新时)
const initAuth = () => {
const savedToken = localStorage.getItem('admin_token')
const savedUser = localStorage.getItem('admin_user')
if (savedToken && savedUser) {
token.value = savedToken
user.value = JSON.parse(savedUser)
}
}
// 检查token是否有效模拟
const checkToken = () => {
if (!token.value) return false
// 这里可以添加token过期检查逻辑
// 目前简单返回true
return true
}
return {
user,
token,
isLoggedIn,
login,
logout,
initAuth,
checkToken
}
})

12
src/stores/counter.js Normal file
View File

@@ -0,0 +1,12 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})

View File

@@ -0,0 +1,612 @@
<template>
<div class="article-edit-container">
<!-- 页面头部 -->
<div class="page-header">
<div class="header-left">
<el-button @click="goBack" icon="ArrowLeft">返回</el-button>
<h2>{{ isEdit ? '编辑文章' : '新增文章' }}</h2>
</div>
<div class="header-right">
<el-button @click="handleSaveDraft">保存草稿</el-button>
<el-button type="primary" @click="handlePublish">发布文章</el-button>
</div>
</div>
<!-- 文章标题区域 -->
<el-card class="title-card">
<div class="title-section">
<el-input
v-model="articleForm.title"
placeholder="请输入文章标题"
size="large"
class="title-input"
maxlength="100"
show-word-limit
/>
<el-button
type="primary"
icon="Setting"
class="settings-btn"
@click="showSettingsDialog = true"
>
文章设置
</el-button>
</div>
</el-card>
<!-- 文档编辑器区域 -->
<el-card class="editor-card">
<div class="editor-header">
<div class="editor-tabs">
<el-tabs v-model="activeTab" type="card">
<el-tab-pane label="富文本编辑" name="rich">
<div class="quill-editor-container">
<QuillEditor
ref="quillEditor"
v-model:content="articleForm.content"
:options="quillOptions"
contentType="html"
@update:content="handleQuillContentChange"
class="quill-editor"
/>
</div>
</el-tab-pane>
<el-tab-pane label="预览" name="preview">
<div class="preview-content" v-html="previewContent"></div>
</el-tab-pane>
</el-tabs>
</div>
</div>
</el-card>
<!-- 文章设置弹窗 -->
<el-dialog
v-model="showSettingsDialog"
title="文章设置"
width="800px"
:before-close="handleCloseSettings"
class="settings-dialog"
>
<el-form :model="articleSettings" label-width="100px" class="settings-form">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="标题颜色">
<el-color-picker v-model="articleSettings.titleColor" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="文章来源">
<el-input v-model="articleSettings.source" placeholder="请输入文章来源" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="文章作者">
<el-input v-model="articleSettings.author" placeholder="请输入文章作者" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="URL链接">
<el-input v-model="articleSettings.url" placeholder="请输入URL链接" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="发布时间">
<el-date-picker
v-model="articleSettings.publishTime"
type="datetime"
placeholder="选择发布时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="排序数字">
<el-input-number v-model="articleSettings.sortOrder" :min="0" :max="9999" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="资讯摘要">
<el-input
v-model="articleSettings.summary"
type="textarea"
:rows="3"
placeholder="请输入文章摘要"
maxlength="200"
show-word-limit
/>
</el-form-item>
<el-form-item label="封面图">
<el-upload
class="cover-uploader"
:show-file-list="false"
:on-success="handleCoverSuccess"
:before-upload="beforeCoverUpload"
action="#"
>
<img v-if="articleSettings.coverImage" :src="articleSettings.coverImage" class="cover-image" />
<el-icon v-else class="cover-uploader-icon"><Plus /></el-icon>
</el-upload>
</el-form-item>
<el-form-item label="浏览次数">
<el-input-number v-model="articleSettings.viewCount" :min="0" />
</el-form-item>
<el-divider content-position="left">发布设置</el-divider>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="是否发布">
<el-switch v-model="articleSettings.isPublished" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="是否置顶">
<el-switch v-model="articleSettings.isTop" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="是否推荐">
<el-switch v-model="articleSettings.isRecommended" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="是否热点">
<el-switch v-model="articleSettings.isHot" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="是否幻灯">
<el-switch v-model="articleSettings.isSlide" />
</el-form-item>
<el-divider content-position="left">SEO设置</el-divider>
<el-form-item label="SEO标题">
<el-input v-model="articleSettings.seoTitle" placeholder="请输入SEO标题" maxlength="60" show-word-limit />
</el-form-item>
<el-form-item label="SEO关键词">
<el-input v-model="articleSettings.seoKeywords" placeholder="请输入SEO关键词用逗号分隔" />
</el-form-item>
<el-form-item label="SEO描述">
<el-input
v-model="articleSettings.seoDescription"
type="textarea"
:rows="3"
placeholder="请输入SEO描述"
maxlength="160"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleCloseSettings">取消</el-button>
<el-button type="primary" @click="handleSaveSettings">保存设置</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css'
const router = useRouter()
const route = useRoute()
// 页面状态
const isEdit = computed(() => route.params.id !== 'new')
const activeTab = ref('rich')
const showSettingsDialog = ref(false)
const quillEditor = ref()
// Quill编辑器配置
const quillOptions = {
theme: 'snow',
modules: {
toolbar: [
[{ 'header': [1, 2, 3, 4, 5, 6, false] }],
['bold', 'italic', 'underline', 'strike'],
[{ 'color': [] }, { 'background': [] }],
[{ 'align': [] }],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
[{ 'indent': '-1'}, { 'indent': '+1' }],
['blockquote', 'code-block'],
['link', 'image', 'video'],
['clean']
]
},
placeholder: '请输入文章内容...'
}
// 文章表单数据
const articleForm = reactive({
title: '',
content: ''
})
// 文章设置数据
const articleSettings = reactive({
titleColor: '#333333',
source: '',
author: '',
url: '',
publishTime: '',
summary: '',
coverImage: '',
sortOrder: 0,
viewCount: 0,
isPublished: false,
isTop: false,
isRecommended: false,
isHot: false,
isSlide: false,
seoTitle: '',
seoKeywords: '',
seoDescription: ''
})
// 预览内容
const previewContent = computed(() => {
return articleForm.content
})
// 生命周期
onMounted(() => {
if (isEdit.value) {
loadArticleData()
} else {
initNewArticle()
}
})
// 加载文章数据
const loadArticleData = () => {
// 模拟加载文章数据
const sampleContent = `
<h2>福建省教育装备与基建中心2024年工作要点</h2>
<p>为深入贯彻落实党的二十大精神全面推进教育现代化福建省教育装备与基建中心制定了2024年工作要点。</p>
<h3>一、总体要求</h3>
<p>坚持以习近平新时代中国特色社会主义思想为指导,全面贯彻党的教育方针,落实立德树人根本任务。</p>
<h3>二、主要任务</h3>
<ul>
<li>加强教育装备标准化建设</li>
<li>推进智慧校园建设</li>
<li>完善基础设施配套</li>
<li>提升服务质量水平</li>
</ul>
<p>教育装备是教育现代化的重要支撑,必须坚持高标准、严要求。</p>
`
Object.assign(articleForm, {
title: '福建省教育装备与基建中心2024年工作要点',
content: sampleContent
})
Object.assign(articleSettings, {
titleColor: '#333333',
source: '福建省教育装备与基建中心',
author: '张三',
url: '/article/2024-work-plan',
publishTime: '2024-01-15 10:30:00',
summary: '本文介绍了福建省教育装备与基建中心2024年的工作要点...',
coverImage: '',
sortOrder: 1,
viewCount: 1250,
isPublished: true,
isTop: false,
isRecommended: true,
isHot: false,
isSlide: false,
seoTitle: '福建省教育装备与基建中心2024年工作要点',
seoKeywords: '教育装备,基建中心,工作要点',
seoDescription: '福建省教育装备与基建中心2024年工作要点详细介绍'
})
}
// 初始化新文章
const initNewArticle = () => {
articleForm.title = ''
articleForm.content = ''
Object.assign(articleSettings, {
titleColor: '#333333',
source: '',
author: '',
url: '',
publishTime: new Date().toISOString().slice(0, 19).replace('T', ' '),
summary: '',
coverImage: '',
sortOrder: 0,
viewCount: 0,
isPublished: false,
isTop: false,
isRecommended: false,
isHot: false,
isSlide: false,
seoTitle: '',
seoKeywords: '',
seoDescription: ''
})
}
// 返回上一页
const goBack = () => {
router.go(-1)
}
// 保存草稿
const handleSaveDraft = () => {
ElMessage.success('草稿保存成功')
}
// 发布文章
const handlePublish = () => {
ElMessage.success('文章发布成功')
router.push('/admin/content/articles')
}
// Quill内容变化处理
const handleQuillContentChange = (content) => {
articleForm.content = content
}
// 获取编辑器HTML内容
const getEditorHTML = () => {
return quillEditor.value?.getHTML() || ''
}
// 设置编辑器内容
const setEditorContent = (html) => {
if (quillEditor.value) {
quillEditor.value.setHTML(html)
}
}
// 封面图上传成功
const handleCoverSuccess = (response, file) => {
articleSettings.coverImage = URL.createObjectURL(file.raw)
}
// 封面图上传前验证
const beforeCoverUpload = (file) => {
const isImage = file.type.startsWith('image/')
const isLt2M = file.size / 1024 / 1024 < 2
if (!isImage) {
ElMessage.error('只能上传图片文件!')
return false
}
if (!isLt2M) {
ElMessage.error('图片大小不能超过 2MB!')
return false
}
return true
}
// 关闭设置弹窗
const handleCloseSettings = () => {
showSettingsDialog.value = false
}
// 保存设置
const handleSaveSettings = () => {
ElMessage.success('设置保存成功')
showSettingsDialog.value = false
}
</script>
<style scoped>
.article-edit-container {
background-color: #f5f5f5;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 16px 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
}
.header-left h2 {
margin: 0;
font-size: 20px;
color: #333;
}
.title-card {
margin-bottom: 20px;
}
.title-section {
display: flex;
align-items: center;
gap: 16px;
}
.title-input {
flex: 1;
}
.title-input :deep(.el-input__inner) {
font-size: 18px;
font-weight: 600;
border: none;
box-shadow: none;
padding: 0;
}
.settings-btn {
flex-shrink: 0;
}
.editor-card {
margin-bottom: 20px;
}
.editor-header {
margin-bottom: 16px;
}
.quill-editor-container {
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
}
.quill-editor {
min-height: 700px;
}
.quill-editor :deep(.ql-toolbar) {
border-bottom: 1px solid #dcdfe6;
background: #f8f9fa;
}
.quill-editor :deep(.ql-container) {
font-size: 14px;
line-height: 1.6;
}
.quill-editor :deep(.ql-editor) {
min-height: 400px;
padding: 16px;
}
.quill-editor :deep(.ql-editor.ql-blank::before) {
color: #c0c4cc;
font-style: normal;
}
.preview-content {
min-height: 400px;
padding: 16px;
border: 1px solid #dcdfe6;
border-radius: 4px;
background: white;
line-height: 1.6;
}
/* 设置弹窗样式 */
.settings-dialog :deep(.el-dialog) {
border-radius: 8px;
}
.settings-dialog :deep(.el-dialog__header) {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 8px 8px 0 0;
padding: 20px;
}
.settings-dialog :deep(.el-dialog__title) {
color: white;
font-size: 18px;
font-weight: 600;
}
.settings-dialog :deep(.el-dialog__headerbtn .el-dialog__close) {
color: white;
font-size: 20px;
}
.settings-form {
padding: 20px;
}
.cover-uploader {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
width: 200px;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.cover-uploader:hover {
border-color: #409eff;
}
.cover-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.cover-uploader-icon {
font-size: 28px;
color: #8c939d;
}
.dialog-footer {
text-align: right;
padding: 20px;
border-top: 1px solid #ebeef5;
}
/* 响应式设计 */
@media (max-width: 768px) {
.article-edit-container {
padding: 10px;
}
.page-header {
flex-direction: column;
gap: 16px;
align-items: stretch;
}
.header-left {
justify-content: center;
}
.header-right {
display: flex;
justify-content: center;
gap: 8px;
}
.title-section {
flex-direction: column;
align-items: stretch;
}
.settings-dialog :deep(.el-dialog) {
width: 95%;
margin: 0 auto;
}
}
</style>

View File

@@ -0,0 +1,517 @@
<template>
<div class="articles-content">
<!-- 搜索和筛选区域 -->
<el-card class="search-card">
<el-form :model="searchForm" :inline="true" class="search-form">
<el-form-item label="文章标题">
<el-input
v-model="searchForm.title"
placeholder="请输入文章标题"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item label="分类">
<el-select
v-model="searchForm.category"
placeholder="请选择分类"
clearable
style="width: 150px"
>
<el-option label="全部" value="" />
<el-option label="新闻动态" value="news" />
<el-option label="政策法规" value="policy" />
<el-option label="技术文档" value="tech" />
<el-option label="通知公告" value="notice" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select
v-model="searchForm.status"
placeholder="请选择状态"
clearable
style="width: 120px"
>
<el-option label="全部" value="" />
<el-option label="草稿" value="draft" />
<el-option label="已发布" value="published" />
<el-option label="已下架" value="offline" />
</el-select>
</el-form-item>
<el-form-item label="发布时间">
<el-date-picker
v-model="searchForm.dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 240px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>
重置
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 操作按钮区域 -->
<el-card class="action-card">
<div class="action-bar">
<div class="action-left">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增文章
</el-button>
<el-button type="success" :disabled="!hasSelection" @click="handleBatchPublish">
<el-icon><Upload /></el-icon>
批量上架
</el-button>
<el-button type="warning" :disabled="!hasSelection" @click="handleBatchOffline">
<el-icon><Download /></el-icon>
批量下架
</el-button>
<el-button type="info" :disabled="!hasSelection" @click="handleBatchMove">
<el-icon><Sort /></el-icon>
批量迁移
</el-button>
<el-button type="primary" plain @click="handleMoveAll">
<el-icon><Operation /></el-icon>
全部迁移
</el-button>
</div>
<div class="action-right">
<el-button @click="handleImport">
<el-icon><Upload /></el-icon>
导入
</el-button>
<el-button @click="handleExport">
<el-icon><Download /></el-icon>
导出
</el-button>
</div>
</div>
</el-card>
<!-- 文章列表 -->
<el-card class="table-card">
<el-table
v-loading="loading"
:data="tableData"
@selection-change="handleSelectionChange"
stripe
border
style="width: 100%"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="title" label="文章标题" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
<el-link type="primary" @click="handleView(row)">
{{ row.title }}
</el-link>
</template>
</el-table-column>
<el-table-column prop="category" label="分类" width="120">
<template #default="{ row }">
<el-tag :type="getCategoryType(row.category)">
{{ getCategoryName(row.category) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="author" label="作者" width="100" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ getStatusName(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="views" label="阅读量" width="100" />
<el-table-column prop="publishTime" label="发布时间" width="160" />
<el-table-column prop="updateTime" label="更新时间" width="160" />
<el-table-column label="操作" width="250" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="handleEdit(row)">
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button
v-if="row.status === 'draft' || row.status === 'offline'"
type="success"
size="small"
@click="handlePublish(row)"
>
<el-icon><Upload /></el-icon>
上架
</el-button>
<el-button
v-if="row.status === 'published'"
type="warning"
size="small"
@click="handleOffline(row)"
>
<el-icon><Download /></el-icon>
下架
</el-button>
<el-button type="danger" size="small" @click="handleDelete(row)">
<el-icon><Delete /></el-icon>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
const router = useRouter()
// 搜索表单
const searchForm = reactive({
title: '',
category: '',
status: '',
dateRange: []
})
// 表格数据
const tableData = ref([])
const loading = ref(false)
const selectedRows = ref([])
// 分页
const pagination = reactive({
currentPage: 1,
pageSize: 20,
total: 0
})
// 计算属性
const hasSelection = computed(() => selectedRows.value.length > 0)
// 模拟数据
const mockData = [
{
id: 1,
title: '福建省教育装备与基建中心2024年工作要点',
category: 'news',
author: '张三',
status: 'published',
views: 1250,
publishTime: '2024-01-15 10:30:00',
updateTime: '2024-01-15 10:30:00'
},
{
id: 2,
title: '关于进一步加强教育装备管理的通知',
category: 'policy',
author: '李四',
status: 'published',
views: 890,
publishTime: '2024-01-14 14:20:00',
updateTime: '2024-01-14 14:20:00'
},
{
id: 3,
title: '教育装备采购技术规范指南',
category: 'tech',
author: '王五',
status: 'draft',
views: 0,
publishTime: '',
updateTime: '2024-01-13 16:45:00'
},
{
id: 4,
title: '2024年第一季度工作汇报',
category: 'notice',
author: '赵六',
status: 'offline',
views: 456,
publishTime: '2024-01-12 09:15:00',
updateTime: '2024-01-12 09:15:00'
},
{
id: 5,
title: '教育装备安全使用培训材料',
category: 'tech',
author: '钱七',
status: 'published',
views: 2100,
publishTime: '2024-01-11 11:30:00',
updateTime: '2024-01-11 11:30:00'
}
]
// 生命周期
onMounted(() => {
loadData()
})
// 加载数据
const loadData = () => {
loading.value = true
setTimeout(() => {
tableData.value = mockData
pagination.total = mockData.length
loading.value = false
}, 500)
}
// 搜索
const handleSearch = () => {
ElMessage.info('搜索功能开发中...')
loadData()
}
// 重置
const handleReset = () => {
Object.assign(searchForm, {
title: '',
category: '',
status: '',
dateRange: []
})
loadData()
}
// 新增
const handleAdd = () => {
router.push('/admin/content/articles/new')
}
// 编辑
const handleEdit = (row) => {
router.push(`/admin/content/articles/${row.id}`)
}
// 查看
const handleView = (row) => {
ElMessage.info(`查看文章: ${row.title}`)
}
// 删除
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm(
`确定要删除文章"${row.title}"吗?`,
'确认删除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
ElMessage.success('删除成功')
loadData()
} catch {
// 用户取消
}
}
// 上架
const handlePublish = (row) => {
ElMessage.success(`文章"${row.title}"已上架`)
row.status = 'published'
}
// 下架
const handleOffline = (row) => {
ElMessage.warning(`文章"${row.title}"已下架`)
row.status = 'offline'
}
// 批量上架
const handleBatchPublish = () => {
ElMessage.success(`已批量上架 ${selectedRows.value.length} 篇文章`)
selectedRows.value.forEach(row => {
row.status = 'published'
})
}
// 批量下架
const handleBatchOffline = () => {
ElMessage.warning(`已批量下架 ${selectedRows.value.length} 篇文章`)
selectedRows.value.forEach(row => {
row.status = 'offline'
})
}
// 批量迁移
const handleBatchMove = () => {
ElMessage.info(`批量迁移 ${selectedRows.value.length} 篇文章功能开发中...`)
}
// 全部迁移
const handleMoveAll = () => {
ElMessage.info('全部迁移功能开发中...')
}
// 导入
const handleImport = () => {
ElMessage.info('导入功能开发中...')
}
// 导出
const handleExport = () => {
ElMessage.info('导出功能开发中...')
}
// 选择变化
const handleSelectionChange = (selection) => {
selectedRows.value = selection
}
// 分页大小变化
const handleSizeChange = (val) => {
pagination.pageSize = val
loadData()
}
// 当前页变化
const handleCurrentChange = (val) => {
pagination.currentPage = val
loadData()
}
// 获取分类名称
const getCategoryName = (category) => {
const categoryMap = {
news: '新闻动态',
policy: '政策法规',
tech: '技术文档',
notice: '通知公告'
}
return categoryMap[category] || category
}
// 获取分类类型
const getCategoryType = (category) => {
const typeMap = {
news: 'primary',
policy: 'success',
tech: 'warning',
notice: 'info'
}
return typeMap[category] || ''
}
// 获取状态名称
const getStatusName = (status) => {
const statusMap = {
draft: '草稿',
published: '已发布',
offline: '已下架'
}
return statusMap[status] || status
}
// 获取状态类型
const getStatusType = (status) => {
const typeMap = {
draft: 'info',
published: 'success',
offline: 'warning'
}
return typeMap[status] || ''
}
</script>
<style scoped>
.articles-content {
padding: 0;
}
.search-card {
margin-bottom: 20px;
}
.search-form {
margin: 0;
}
.action-card {
margin-bottom: 20px;
}
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
}
.action-left {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.action-right {
display: flex;
gap: 10px;
}
.table-card {
margin-bottom: 20px;
}
.pagination-container {
display: flex;
justify-content: center;
margin-top: 20px;
}
.el-form-item {
margin-bottom: 0;
}
/* 响应式设计 */
@media (max-width: 768px) {
.action-bar {
flex-direction: column;
gap: 10px;
}
.action-left,
.action-right {
width: 100%;
justify-content: center;
}
.search-form {
flex-direction: column;
}
.search-form .el-form-item {
margin-right: 0;
margin-bottom: 10px;
}
}
</style>

View File

@@ -0,0 +1,795 @@
<template>
<div class="download-edit-container">
<!-- 页面头部 -->
<div class="page-header">
<div class="header-left">
<el-button @click="goBack" icon="ArrowLeft">返回</el-button>
<h2>{{ isEdit ? '编辑下载文件' : '新增下载文件' }}</h2>
</div>
<div class="header-right">
<el-button @click="handleSaveDraft">保存草稿</el-button>
<el-button type="primary" @click="handlePublish">发布文件</el-button>
</div>
</div>
<!-- 文件标题区域 -->
<el-card class="title-card">
<div class="title-section">
<el-input
v-model="downloadForm.title"
placeholder="请输入文件标题"
size="large"
class="title-input"
maxlength="100"
show-word-limit
/>
<el-button
type="primary"
icon="Setting"
class="settings-btn"
@click="showSettingsDialog = true"
>
文件设置
</el-button>
</div>
</el-card>
<!-- 文件上传区域 -->
<el-card class="upload-card">
<template #header>
<div class="card-header">
<span>文件上传</span>
</div>
</template>
<div class="upload-section">
<el-upload
class="file-uploader"
:show-file-list="false"
:on-success="handleFileSuccess"
:before-upload="beforeFileUpload"
:on-progress="handleUploadProgress"
action="#"
:accept="acceptedFileTypes"
>
<div v-if="!downloadForm.fileUrl" class="upload-area">
<el-icon class="upload-icon"><Upload /></el-icon>
<div class="upload-text">
<p>点击或拖拽文件到此区域上传</p>
<p class="upload-tip">支持 PDFWordExcelPPT图片压缩包等格式文件大小不超过 100MB</p>
</div>
</div>
<div v-else class="file-preview">
<div class="file-info">
<el-icon :size="48" :color="getFileTypeColor(downloadForm.fileType)">
<component :is="getFileTypeIcon(downloadForm.fileType)" />
</el-icon>
<div class="file-details">
<h4>{{ downloadForm.fileName }}</h4>
<p><strong>文件大小</strong>{{ downloadForm.fileSize }}</p>
<p><strong>文件类型</strong>{{ getFileTypeName(downloadForm.fileType) }}</p>
<p><strong>上传时间</strong>{{ downloadForm.uploadTime }}</p>
</div>
</div>
<div class="file-actions">
<el-button type="primary" @click="reuploadFile">重新上传</el-button>
<el-button @click="removeFile">删除文件</el-button>
</div>
</div>
</el-upload>
<!-- 上传进度 -->
<div v-if="uploading" class="upload-progress">
<el-progress :percentage="uploadProgress" :status="uploadStatus" />
<p class="progress-text">{{ uploadProgressText }}</p>
</div>
</div>
</el-card>
<!-- 文件描述区域 -->
<el-card class="description-card">
<template #header>
<span>文件描述</span>
</template>
<el-input
v-model="downloadForm.description"
type="textarea"
:rows="6"
placeholder="请输入文件详细描述..."
maxlength="1000"
show-word-limit
/>
</el-card>
<!-- 文件设置弹窗 -->
<el-dialog
v-model="showSettingsDialog"
title="文件设置"
:before-close="handleCloseSettings"
class="settings-dialog"
>
<el-form :model="fileSettings" label-width="100px" class="settings-form">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="标题颜色">
<el-color-picker v-model="fileSettings.titleColor" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="URL链接">
<el-input v-model="fileSettings.url" placeholder="请输入URL链接" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="发布时间">
<el-date-picker
v-model="fileSettings.publishTime"
type="datetime"
placeholder="选择发布时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="排序数字">
<el-input-number v-model="fileSettings.sortOrder" :min="0" :max="9999" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="详细描述">
<el-input
v-model="fileSettings.description"
type="textarea"
:rows="3"
placeholder="请输入文件详细描述"
maxlength="500"
show-word-limit
/>
</el-form-item>
<el-form-item label="显示图片">
<el-upload
class="image-uploader"
:show-file-list="false"
:on-success="handleImageSuccess"
:before-upload="beforeImageUpload"
action="#"
accept="image/*"
>
<img v-if="fileSettings.displayImage" :src="fileSettings.displayImage" class="display-image" />
<div v-else class="image-placeholder">
<el-icon><Picture /></el-icon>
<p>点击上传显示图片</p>
</div>
</el-upload>
</el-form-item>
<el-form-item label="浏览次数">
<el-input-number v-model="fileSettings.viewCount" :min="0" />
</el-form-item>
<el-divider content-position="left">发布设置</el-divider>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="是否发布">
<el-switch v-model="fileSettings.isPublished" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="是否置顶">
<el-switch v-model="fileSettings.isTop" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="是否推荐">
<el-switch v-model="fileSettings.isRecommended" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="是否热点">
<el-switch v-model="fileSettings.isHot" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="是否幻灯">
<el-switch v-model="fileSettings.isSlide" />
</el-form-item>
<el-divider content-position="left">SEO设置</el-divider>
<el-form-item label="SEO标题">
<el-input v-model="fileSettings.seoTitle" placeholder="请输入SEO标题" maxlength="60" show-word-limit />
</el-form-item>
<el-form-item label="SEO关键词">
<el-input v-model="fileSettings.seoKeywords" placeholder="请输入SEO关键词用逗号分隔" />
</el-form-item>
<el-form-item label="SEO描述">
<el-input
v-model="fileSettings.seoDescription"
type="textarea"
:rows="3"
placeholder="请输入SEO描述"
maxlength="160"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleCloseSettings">取消</el-button>
<el-button type="primary" @click="handleSaveSettings">保存设置</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
const router = useRouter()
const route = useRoute()
// 表单数据
const downloadForm = reactive({
title: '',
fileUrl: '',
fileName: '',
fileType: '',
fileSize: '',
uploadTime: '',
description: ''
})
// 文件设置
const fileSettings = reactive({
titleColor: '#333333',
url: '',
publishTime: '',
description: '',
displayImage: '',
viewCount: 0,
sortOrder: 0,
isPublished: false,
isTop: false,
isRecommended: false,
isHot: false,
isSlide: false,
seoTitle: '',
seoKeywords: '',
seoDescription: ''
})
// 上传相关
const uploading = ref(false)
const uploadProgress = ref(0)
const uploadStatus = ref('')
const uploadProgressText = ref('')
// 弹窗控制
const showSettingsDialog = ref(false)
// 支持的文件类型
const acceptedFileTypes = '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png,.gif,.zip,.rar,.7z'
// 计算属性
const isEdit = computed(() => route.params.id && route.params.id !== 'new')
// 生命周期
onMounted(() => {
if (isEdit.value) {
loadFileData()
} else {
initNewFile()
}
})
// 加载文件数据
const loadFileData = () => {
// 模拟加载数据
Object.assign(downloadForm, {
title: '福建省教育装备与基建中心2024年工作要点',
fileUrl: '/files/2024年工作要点.pdf',
fileName: '2024年工作要点.pdf',
fileType: 'pdf',
fileSize: '2.5MB',
uploadTime: '2024-01-15 10:30:00',
description: '本文件详细介绍了福建省教育装备与基建中心2024年的工作重点和计划安排包括教育装备采购、基础设施建设、技术标准制定等方面的内容。'
})
Object.assign(fileSettings, {
titleColor: '#1890ff',
url: 'https://example.com/download/2024-work-plan',
publishTime: '2024-01-15 10:30:00',
description: '本文件详细介绍了福建省教育装备与基建中心2024年的工作重点和计划安排包括教育装备采购、基础设施建设、技术标准制定等方面的内容。',
displayImage: '/images/file-preview-pdf.jpg',
viewCount: 1250,
sortOrder: 1,
isPublished: true,
isTop: false,
isRecommended: true,
isHot: false,
isSlide: false,
seoTitle: '福建省教育装备与基建中心2024年工作要点',
seoKeywords: '教育装备,基建中心,工作要点,2024',
seoDescription: '福建省教育装备与基建中心2024年工作要点包含教育装备采购、基础设施建设等详细内容。'
})
}
// 初始化新文件
const initNewFile = () => {
Object.assign(downloadForm, {
title: '',
fileUrl: '',
fileName: '',
fileType: '',
fileSize: '',
uploadTime: '',
description: ''
})
Object.assign(fileSettings, {
titleColor: '#333333',
url: '',
publishTime: '',
description: '',
displayImage: '',
viewCount: 0,
sortOrder: 0,
isPublished: false,
isTop: false,
isRecommended: false,
isHot: false,
isSlide: false,
seoTitle: '',
seoKeywords: '',
seoDescription: ''
})
}
// 返回
const goBack = () => {
router.push('/admin/content/downloads')
}
// 保存草稿
const handleSaveDraft = () => {
ElMessage.success('草稿保存成功')
}
// 发布文件
const handlePublish = () => {
if (!downloadForm.title) {
ElMessage.warning('请输入文件标题')
return
}
if (!downloadForm.fileUrl) {
ElMessage.warning('请上传文件')
return
}
ElMessage.success('文件发布成功')
}
// 文件上传成功
const handleFileSuccess = (response, file) => {
uploading.value = false
uploadProgress.value = 100
uploadStatus.value = 'success'
uploadProgressText.value = '上传完成'
// 模拟文件信息
downloadForm.fileUrl = URL.createObjectURL(file.raw)
downloadForm.fileName = file.name
downloadForm.fileType = getFileTypeFromName(file.name)
downloadForm.fileSize = formatFileSize(file.size)
downloadForm.uploadTime = new Date().toLocaleString()
setTimeout(() => {
uploadProgress.value = 0
uploadStatus.value = ''
uploadProgressText.value = ''
}, 2000)
ElMessage.success('文件上传成功')
}
// 文件上传前验证
const beforeFileUpload = (file) => {
const isValidType = checkFileType(file.name)
const isValidSize = file.size / 1024 / 1024 < 100 // 100MB
if (!isValidType) {
ElMessage.error('不支持的文件类型!')
return false
}
if (!isValidSize) {
ElMessage.error('文件大小不能超过 100MB')
return false
}
uploading.value = true
uploadProgress.value = 0
uploadStatus.value = ''
uploadProgressText.value = '准备上传...'
return true
}
// 上传进度
const handleUploadProgress = (event) => {
uploadProgress.value = Math.round((event.loaded * 100) / event.total)
uploadStatus.value = 'active'
uploadProgressText.value = `上传中... ${uploadProgress.value}%`
}
// 重新上传
const reuploadFile = () => {
downloadForm.fileUrl = ''
downloadForm.fileName = ''
downloadForm.fileType = ''
downloadForm.fileSize = ''
downloadForm.uploadTime = ''
}
// 删除文件
const removeFile = () => {
reuploadFile()
ElMessage.info('文件已删除')
}
// 图片上传成功
const handleImageSuccess = (response, file) => {
fileSettings.displayImage = URL.createObjectURL(file.raw)
ElMessage.success('图片上传成功')
}
// 图片上传前验证
const beforeImageUpload = (file) => {
const isImage = file.type.startsWith('image/')
const isLt2M = file.size / 1024 / 1024 < 2
if (!isImage) {
ElMessage.error('只能上传图片文件!')
return false
}
if (!isLt2M) {
ElMessage.error('图片大小不能超过 2MB!')
return false
}
return true
}
// 关闭设置弹窗
const handleCloseSettings = () => {
showSettingsDialog.value = false
}
// 保存设置
const handleSaveSettings = () => {
// 同步设置到表单
downloadForm.description = fileSettings.description
showSettingsDialog.value = false
ElMessage.success('设置保存成功')
}
// 检查文件类型
const checkFileType = (fileName) => {
const ext = fileName.split('.').pop().toLowerCase()
const allowedTypes = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'jpg', 'jpeg', 'png', 'gif', 'zip', 'rar', '7z']
return allowedTypes.includes(ext)
}
// 从文件名获取文件类型
const getFileTypeFromName = (fileName) => {
const ext = fileName.split('.').pop().toLowerCase()
const typeMap = {
pdf: 'pdf',
doc: 'doc',
docx: 'doc',
xls: 'xls',
xlsx: 'xls',
ppt: 'ppt',
pptx: 'ppt',
jpg: 'image',
jpeg: 'image',
png: 'image',
gif: 'image',
zip: 'zip',
rar: 'zip',
'7z': 'zip'
}
return typeMap[ext] || 'other'
}
// 格式化文件大小
const formatFileSize = (bytes) => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
// 获取文件类型名称
const getFileTypeName = (fileType) => {
const typeMap = {
pdf: 'PDF文档',
doc: 'Word文档',
xls: 'Excel表格',
ppt: 'PPT演示',
image: '图片文件',
zip: '压缩包',
other: '其他'
}
return typeMap[fileType] || fileType
}
// 获取文件类型图标
const getFileTypeIcon = (fileType) => {
const iconMap = {
pdf: 'Document',
doc: 'Document',
xls: 'Document',
ppt: 'Document',
image: 'Picture',
zip: 'Folder',
other: 'Document'
}
return iconMap[fileType] || 'Document'
}
// 获取文件类型颜色
const getFileTypeColor = (fileType) => {
const colorMap = {
pdf: '#f56c6c',
doc: '#409eff',
xls: '#67c23a',
ppt: '#e6a23c',
image: '#909399',
zip: '#606266',
other: '#909399'
}
return colorMap[fileType] || '#909399'
}
</script>
<style scoped>
.download-edit-container {
padding: 0;
background-color: #f5f5f5;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background: white;
border-bottom: 1px solid #e4e7ed;
margin-bottom: 20px;
}
.header-left {
display: flex;
align-items: center;
gap: 15px;
}
.header-left h2 {
margin: 0;
font-size: 20px;
color: #333;
}
.header-right {
display: flex;
gap: 10px;
}
.title-card {
margin-bottom: 20px;
}
.title-section {
display: flex;
align-items: center;
gap: 15px;
}
.title-input {
flex: 1;
}
.settings-btn {
flex-shrink: 0;
}
.upload-card,
.description-card {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
font-weight: 600;
color: #333;
}
.upload-section {
padding: 20px 0;
}
.file-uploader {
width: 100%;
}
.upload-area {
border: 2px dashed #d9d9d9;
border-radius: 6px;
width: 100%;
height: 200px;
text-align: center;
cursor: pointer;
position: relative;
overflow: hidden;
transition: border-color 0.3s;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.upload-area:hover {
border-color: #409eff;
}
.upload-icon {
font-size: 48px;
color: #c0c4cc;
margin-bottom: 16px;
}
.upload-text p {
margin: 0 0 8px 0;
font-size: 16px;
color: #606266;
}
.upload-tip {
font-size: 14px;
color: #909399;
}
.file-preview {
border: 1px solid #e4e7ed;
border-radius: 6px;
padding: 20px;
background: #fafafa;
}
.file-info {
display: flex;
align-items: center;
gap: 20px;
margin-bottom: 20px;
}
.file-details h4 {
margin: 0 0 8px 0;
font-size: 16px;
color: #333;
}
.file-details p {
margin: 0 0 4px 0;
font-size: 14px;
color: #666;
}
.file-actions {
display: flex;
gap: 10px;
justify-content: center;
}
.upload-progress {
margin-top: 20px;
text-align: center;
}
.progress-text {
margin-top: 10px;
font-size: 14px;
color: #666;
}
.image-uploader {
width: 200px;
}
.display-image {
width: 200px;
height: 120px;
object-fit: cover;
border-radius: 4px;
}
.image-placeholder {
width: 200px;
height: 120px;
border: 1px dashed #d9d9d9;
border-radius: 4px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
color: #c0c4cc;
}
.image-placeholder:hover {
border-color: #409eff;
}
.image-placeholder .el-icon {
font-size: 32px;
margin-bottom: 8px;
}
.image-placeholder p {
margin: 0;
font-size: 14px;
}
.dialog-footer {
text-align: right;
}
.el-dialog {
margin:var(--el-dialog-margin-top) auto 50px
}
/* 响应式设计 */
@media (max-width: 768px) {
.page-header {
flex-direction: column;
gap: 15px;
align-items: flex-start;
}
.header-right {
width: 100%;
justify-content: flex-end;
}
.title-section {
flex-direction: column;
align-items: stretch;
}
.file-info {
flex-direction: column;
text-align: center;
}
.file-actions {
flex-direction: column;
}
}
</style>

View File

@@ -0,0 +1,558 @@
<template>
<div class="downloads-container">
<!-- 搜索和筛选区域 -->
<el-card class="search-card">
<el-form :model="searchForm" :inline="true" class="search-form">
<el-form-item label="文件标题">
<el-input
v-model="searchForm.title"
placeholder="请输入文件标题"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item label="文件类型">
<el-select
v-model="searchForm.fileType"
placeholder="请选择文件类型"
clearable
style="width: 150px"
>
<el-option label="全部" value="" />
<el-option label="PDF文档" value="pdf" />
<el-option label="Word文档" value="doc" />
<el-option label="Excel表格" value="xls" />
<el-option label="PPT演示" value="ppt" />
<el-option label="图片文件" value="image" />
<el-option label="压缩包" value="zip" />
<el-option label="其他" value="other" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select
v-model="searchForm.status"
placeholder="请选择状态"
clearable
style="width: 120px"
>
<el-option label="全部" value="" />
<el-option label="草稿" value="draft" />
<el-option label="已发布" value="published" />
<el-option label="已下架" value="offline" />
</el-select>
</el-form-item>
<el-form-item label="上传时间">
<el-date-picker
v-model="searchForm.dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 240px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>
重置
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 操作按钮区域 -->
<el-card class="action-card">
<div class="action-bar">
<div class="action-left">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增下载
</el-button>
<el-button type="success" :disabled="!hasSelection" @click="handleBatchPublish">
<el-icon><Upload /></el-icon>
批量上架
</el-button>
<el-button type="warning" :disabled="!hasSelection" @click="handleBatchOffline">
<el-icon><Download /></el-icon>
批量下架
</el-button>
</div>
<div class="action-right">
<el-button @click="handleRefresh">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
</div>
</el-card>
<!-- 下载文件列表 -->
<el-card class="table-card">
<el-table
v-loading="loading"
:data="tableData"
@selection-change="handleSelectionChange"
stripe
border
style="width: 100%"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="title" label="文件标题" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
<el-link type="primary" @click="handleView(row)">
{{ row.title }}
</el-link>
</template>
</el-table-column>
<el-table-column prop="fileIcon" label="文件图标" width="80">
<template #default="{ row }">
<el-icon :size="24" :color="getFileTypeColor(row.fileType)">
<component :is="getFileTypeIcon(row.fileType)" />
</el-icon>
</template>
</el-table-column>
<el-table-column prop="fileName" label="文件名" width="200" show-overflow-tooltip />
<el-table-column prop="fileType" label="文件类型" width="100">
<template #default="{ row }">
<el-tag :type="getFileTypeTagType(row.fileType)">
{{ getFileTypeName(row.fileType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="fileSize" label="文件大小" width="100" />
<el-table-column prop="downloadCount" label="下载次数" width="100" />
<el-table-column prop="author" label="上传者" width="100" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ getStatusName(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="uploadTime" label="上传时间" width="160" />
<el-table-column prop="updateTime" label="更新时间" width="160" />
<el-table-column label="操作" width="250" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="handleEdit(row)">
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button
v-if="row.status === 'draft' || row.status === 'offline'"
type="success"
size="small"
@click="handlePublish(row)"
>
<el-icon><Upload /></el-icon>
上架
</el-button>
<el-button
v-if="row.status === 'published'"
type="warning"
size="small"
@click="handleOffline(row)"
>
<el-icon><Download /></el-icon>
下架
</el-button>
<el-button type="danger" size="small" @click="handleDelete(row)">
<el-icon><Delete /></el-icon>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
const router = useRouter()
// 搜索表单
const searchForm = reactive({
title: '',
fileType: '',
status: '',
dateRange: []
})
// 表格数据
const tableData = ref([])
const loading = ref(false)
const selectedRows = ref([])
// 分页
const pagination = reactive({
currentPage: 1,
pageSize: 20,
total: 0
})
// 计算属性
const hasSelection = computed(() => selectedRows.value.length > 0)
// 模拟数据
const mockData = [
{
id: 1,
title: '福建省教育装备与基建中心2024年工作要点',
fileName: '2024年工作要点.pdf',
fileType: 'pdf',
fileSize: '2.5MB',
downloadCount: 1250,
author: '张三',
status: 'published',
uploadTime: '2024-01-15 10:30:00',
updateTime: '2024-01-15 10:30:00'
},
{
id: 2,
title: '教育装备采购技术规范指南',
fileName: '采购技术规范.docx',
fileType: 'doc',
fileSize: '1.8MB',
downloadCount: 890,
author: '李四',
status: 'published',
uploadTime: '2024-01-14 14:20:00',
updateTime: '2024-01-14 14:20:00'
},
{
id: 3,
title: '智慧校园建设方案',
fileName: '智慧校园方案.pptx',
fileType: 'ppt',
fileSize: '5.2MB',
downloadCount: 0,
author: '王五',
status: 'draft',
uploadTime: '',
updateTime: '2024-01-13 16:45:00'
},
{
id: 4,
title: '教育装备使用手册',
fileName: '使用手册.zip',
fileType: 'zip',
fileSize: '15.6MB',
downloadCount: 456,
author: '赵六',
status: 'offline',
uploadTime: '2024-01-12 09:15:00',
updateTime: '2024-01-12 09:15:00'
},
{
id: 5,
title: '教育装备产品图片集',
fileName: '产品图片集.zip',
fileType: 'image',
fileSize: '28.3MB',
downloadCount: 2100,
author: '钱七',
status: 'published',
uploadTime: '2024-01-11 11:30:00',
updateTime: '2024-01-11 11:30:00'
}
]
// 生命周期
onMounted(() => {
loadData()
})
// 加载数据
const loadData = () => {
loading.value = true
setTimeout(() => {
tableData.value = mockData
pagination.total = mockData.length
loading.value = false
}, 500)
}
// 搜索
const handleSearch = () => {
ElMessage.info('搜索功能开发中...')
loadData()
}
// 重置
const handleReset = () => {
Object.assign(searchForm, {
title: '',
fileType: '',
status: '',
dateRange: []
})
loadData()
}
// 新增
const handleAdd = () => {
router.push('/admin/content/downloads/new')
}
// 编辑
const handleEdit = (row) => {
router.push(`/admin/content/downloads/${row.id}`)
}
// 查看
const handleView = (row) => {
ElMessage.info(`查看文件: ${row.title}`)
}
// 删除
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm(
`确定要删除文件"${row.title}"吗?`,
'确认删除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
ElMessage.success('删除成功')
loadData()
} catch {
// 用户取消
}
}
// 上架
const handlePublish = (row) => {
ElMessage.success(`文件"${row.title}"已上架`)
row.status = 'published'
}
// 下架
const handleOffline = (row) => {
ElMessage.warning(`文件"${row.title}"已下架`)
row.status = 'offline'
}
// 刷新
const handleRefresh = () => {
loadData()
}
// 批量上架
const handleBatchPublish = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要上架的文件')
return
}
ElMessage.success(`已批量上架 ${selectedRows.value.length} 个文件`)
selectedRows.value.forEach(row => {
row.status = 'published'
})
selectedRows.value = []
}
// 批量下架
const handleBatchOffline = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要下架的文件')
return
}
ElMessage.warning(`已批量下架 ${selectedRows.value.length} 个文件`)
selectedRows.value.forEach(row => {
row.status = 'offline'
})
selectedRows.value = []
}
// 选择变化
const handleSelectionChange = (selection) => {
selectedRows.value = selection
}
// 分页大小变化
const handleSizeChange = (val) => {
pagination.pageSize = val
loadData()
}
// 当前页变化
const handleCurrentChange = (val) => {
pagination.currentPage = val
loadData()
}
// 获取文件类型名称
const getFileTypeName = (fileType) => {
const typeMap = {
pdf: 'PDF文档',
doc: 'Word文档',
xls: 'Excel表格',
ppt: 'PPT演示',
image: '图片文件',
zip: '压缩包',
other: '其他'
}
return typeMap[fileType] || fileType
}
// 获取文件类型标签类型
const getFileTypeTagType = (fileType) => {
const typeMap = {
pdf: 'danger',
doc: 'primary',
xls: 'success',
ppt: 'warning',
image: 'info',
zip: '',
other: 'info'
}
return typeMap[fileType] || ''
}
// 获取文件类型图标
const getFileTypeIcon = (fileType) => {
const iconMap = {
pdf: 'Document',
doc: 'Document',
xls: 'Document',
ppt: 'Document',
image: 'Picture',
zip: 'Folder',
other: 'Document'
}
return iconMap[fileType] || 'Document'
}
// 获取文件类型颜色
const getFileTypeColor = (fileType) => {
const colorMap = {
pdf: '#f56c6c',
doc: '#409eff',
xls: '#67c23a',
ppt: '#e6a23c',
image: '#909399',
zip: '#606266',
other: '#909399'
}
return colorMap[fileType] || '#909399'
}
// 获取状态名称
const getStatusName = (status) => {
const statusMap = {
draft: '草稿',
published: '已发布',
offline: '已下架'
}
return statusMap[status] || status
}
// 获取状态类型
const getStatusType = (status) => {
const typeMap = {
draft: 'info',
published: 'success',
offline: 'warning'
}
return typeMap[status] || ''
}
</script>
<style scoped>
.downloads-container {
padding: 0;
}
.search-card {
margin-bottom: 20px;
}
.search-form {
margin: 0;
}
.action-card {
margin-bottom: 20px;
}
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
}
.action-left {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.action-right {
display: flex;
gap: 10px;
}
.table-card {
margin-bottom: 20px;
}
.pagination-container {
display: flex;
justify-content: center;
margin-top: 20px;
}
.el-form-item {
margin-bottom: 0;
}
/* 响应式设计 */
@media (max-width: 768px) {
.action-bar {
flex-direction: column;
gap: 10px;
}
.action-left,
.action-right {
width: 100%;
justify-content: center;
}
.search-form {
flex-direction: column;
}
.search-form .el-form-item {
margin-right: 0;
margin-bottom: 10px;
}
}
</style>

View File

@@ -0,0 +1,77 @@
<template>
<div class="links-content">
<el-card>
<template #header>
<div class="card-header">
<span>友情链接</span>
<el-button type="primary" size="small">
<el-icon><Plus /></el-icon>
添加链接
</el-button>
</div>
</template>
<div class="placeholder-content">
<el-icon class="placeholder-icon"><Link /></el-icon>
<h3>友情链接功能开发中...</h3>
<p>这里将实现友情链接的添加分类排序状态管理等功能</p>
<div class="feature-list">
<el-tag type="success" size="small">链接管理</el-tag>
<el-tag type="success" size="small">分类设置</el-tag>
<el-tag type="success" size="small">排序功能</el-tag>
<el-tag type="success" size="small">状态控制</el-tag>
<el-tag type="success" size="small">批量操作</el-tag>
</div>
</div>
</el-card>
</div>
</template>
<script setup>
// 友情链接页面逻辑
</script>
<style scoped>
.links-content {
padding: 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
font-weight: 600;
color: #333;
}
.placeholder-content {
text-align: center;
padding: 60px 20px;
color: #666;
}
.placeholder-icon {
font-size: 64px;
color: #ddd;
margin-bottom: 20px;
}
.placeholder-content h3 {
margin: 0 0 12px 0;
font-size: 20px;
color: #333;
}
.placeholder-content p {
margin: 0 0 20px 0;
font-size: 14px;
line-height: 1.6;
}
.feature-list {
display: flex;
justify-content: center;
gap: 8px;
flex-wrap: wrap;
}
</style>

View File

@@ -0,0 +1,77 @@
<template>
<div class="services-content">
<el-card>
<template #header>
<div class="card-header">
<span>精准服务</span>
<el-button type="primary" size="small">
<el-icon><Plus /></el-icon>
添加服务
</el-button>
</div>
</template>
<div class="placeholder-content">
<el-icon class="placeholder-icon"><Service /></el-icon>
<h3>精准服务功能开发中...</h3>
<p>这里将实现服务项目的管理分类预约评价等功能</p>
<div class="feature-list">
<el-tag type="danger" size="small">服务管理</el-tag>
<el-tag type="danger" size="small">分类设置</el-tag>
<el-tag type="danger" size="small">预约系统</el-tag>
<el-tag type="danger" size="small">评价管理</el-tag>
<el-tag type="danger" size="small">统计分析</el-tag>
</div>
</div>
</el-card>
</div>
</template>
<script setup>
// 精准服务页面逻辑
</script>
<style scoped>
.services-content {
padding: 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
font-weight: 600;
color: #333;
}
.placeholder-content {
text-align: center;
padding: 60px 20px;
color: #666;
}
.placeholder-icon {
font-size: 64px;
color: #ddd;
margin-bottom: 20px;
}
.placeholder-content h3 {
margin: 0 0 12px 0;
font-size: 20px;
color: #333;
}
.placeholder-content p {
margin: 0 0 20px 0;
font-size: 14px;
line-height: 1.6;
}
.feature-list {
display: flex;
justify-content: center;
gap: 8px;
flex-wrap: wrap;
}
</style>

View File

@@ -0,0 +1,820 @@
<template>
<div class="video-edit-container">
<!-- 页面头部 -->
<div class="page-header">
<div class="header-left">
<el-button @click="goBack" icon="ArrowLeft">返回</el-button>
<h2>{{ isEdit ? '编辑视频' : '新增视频' }}</h2>
</div>
<div class="header-right">
<el-button @click="handleSaveDraft">保存草稿</el-button>
<el-button type="primary" @click="handlePublish">发布视频</el-button>
</div>
</div>
<!-- 视频基本信息区域 -->
<el-card class="info-card">
<div class="info-section">
<el-form :model="videoForm" label-width="100px" class="info-form">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="视频标题" required>
<el-input
v-model="videoForm.title"
placeholder="请输入视频标题"
maxlength="100"
show-word-limit
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="视频分类" required>
<el-select v-model="videoForm.category" placeholder="请选择分类" style="width: 100%">
<el-option label="教学视频" value="teaching" />
<el-option label="宣传视频" value="promotion" />
<el-option label="培训视频" value="training" />
<el-option label="会议视频" value="meeting" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="视频作者">
<el-input v-model="videoForm.author" placeholder="请输入视频作者" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="视频标签">
<el-input v-model="videoForm.tags" placeholder="请输入标签,用逗号分隔" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="视频描述">
<el-input
v-model="videoForm.description"
type="textarea"
:rows="3"
placeholder="请输入视频描述"
maxlength="500"
show-word-limit
/>
</el-form-item>
</el-form>
</div>
</el-card>
<!-- 视频文件与缩略图设置区域 -->
<el-card class="media-card">
<template #header>
<div class="card-header">
<span>视频上传</span>
<el-button
type="primary"
icon="Setting"
@click="showSettingsDialog = true"
>
视频设置
</el-button>
</div>
</template>
<div class="media-section">
<el-row :gutter="20">
<!-- 左侧视频文件 -->
<el-col :span="12">
<div class="video-section">
<h4 class="section-title">视频文件</h4>
<el-upload
class="video-uploader"
:show-file-list="false"
:on-success="handleVideoSuccess"
:before-upload="beforeVideoUpload"
:on-progress="handleUploadProgress"
action="#"
accept="video/*"
>
<div v-if="!videoForm.videoUrl" class="upload-area">
<el-icon class="upload-icon"><VideoPlay /></el-icon>
<div class="upload-text">
<p>点击或拖拽视频文件到此区域上传</p>
<p class="upload-tip">支持 MP4AVIMOV 等格式文件大小不超过 500MB</p>
</div>
</div>
<div v-else class="video-preview">
<video
:src="videoForm.videoUrl"
controls
class="preview-video"
@loadedmetadata="handleVideoLoaded"
></video>
<div class="video-info">
<p><strong>文件名</strong>{{ videoForm.fileName }}</p>
<p><strong>文件大小</strong>{{ videoForm.fileSize }}</p>
<p><strong>视频时长</strong>{{ videoForm.duration }}</p>
<p><strong>分辨率</strong>{{ videoForm.resolution }}</p>
</div>
<div class="video-actions">
<el-button type="primary" @click="reuploadVideo">重新上传</el-button>
<el-button @click="removeVideo">删除视频</el-button>
</div>
</div>
</el-upload>
<!-- 上传进度 -->
<div v-if="uploading" class="upload-progress">
<el-progress :percentage="uploadProgress" :status="uploadStatus" />
<p class="progress-text">{{ uploadProgressText }}</p>
</div>
</div>
</el-col>
<!-- 右侧缩略图设置 -->
<el-col :span="12">
<div class="thumbnail-section">
<h4 class="section-title">缩略图设置</h4>
<div class="thumbnail-options">
<el-radio-group v-model="thumbnailType">
<el-radio label="auto">自动生成</el-radio>
<el-radio label="custom">自定义上传</el-radio>
</el-radio-group>
</div>
<div v-if="thumbnailType === 'auto'" class="auto-thumbnail">
<p>系统将自动从视频中提取关键帧作为缩略图</p>
</div>
<div v-else class="custom-thumbnail">
<el-upload
class="thumbnail-uploader"
:show-file-list="false"
:on-success="handleThumbnailSuccess"
:before-upload="beforeThumbnailUpload"
action="#"
accept="image/*"
>
<img v-if="videoForm.thumbnail" :src="videoForm.thumbnail" class="thumbnail-image" />
<div v-else class="thumbnail-placeholder" style="height: 150px;">
<el-icon><Picture /></el-icon>
<p>点击上传缩略图</p>
</div>
</el-upload>
</div>
</div>
</el-col>
</el-row>
</div>
</el-card>
<!-- 视频设置弹窗 -->
<el-dialog
v-model="showSettingsDialog"
title="视频设置"
width="800px"
:before-close="handleCloseSettings"
class="settings-dialog"
>
<el-form :model="videoSettings" label-width="100px" class="settings-form">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="视频来源">
<el-input v-model="videoSettings.source" placeholder="请输入视频来源" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="视频链接">
<el-input v-model="videoSettings.url" placeholder="请输入视频链接" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="发布时间">
<el-date-picker
v-model="videoSettings.publishTime"
type="datetime"
placeholder="选择发布时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="排序数字">
<el-input-number v-model="videoSettings.sortOrder" :min="0" :max="9999" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="播放次数">
<el-input-number v-model="videoSettings.viewCount" :min="0" />
</el-form-item>
<el-divider content-position="left">发布设置</el-divider>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="是否发布">
<el-switch v-model="videoSettings.isPublished" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="是否置顶">
<el-switch v-model="videoSettings.isTop" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="是否推荐">
<el-switch v-model="videoSettings.isRecommended" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="是否热点">
<el-switch v-model="videoSettings.isHot" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="是否幻灯">
<el-switch v-model="videoSettings.isSlide" />
</el-form-item>
<el-divider content-position="left">SEO设置</el-divider>
<el-form-item label="SEO标题">
<el-input v-model="videoSettings.seoTitle" placeholder="请输入SEO标题" maxlength="60" show-word-limit />
</el-form-item>
<el-form-item label="SEO关键词">
<el-input v-model="videoSettings.seoKeywords" placeholder="请输入SEO关键词用逗号分隔" />
</el-form-item>
<el-form-item label="SEO描述">
<el-input
v-model="videoSettings.seoDescription"
type="textarea"
:rows="3"
placeholder="请输入SEO描述"
maxlength="160"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleCloseSettings">取消</el-button>
<el-button type="primary" @click="handleSaveSettings">保存设置</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
const router = useRouter()
const route = useRoute()
// 页面状态
const isEdit = computed(() => route.params.id !== 'new')
const showSettingsDialog = ref(false)
const thumbnailType = ref('auto')
const uploading = ref(false)
const uploadProgress = ref(0)
const uploadStatus = ref('')
const uploadProgressText = ref('')
// 视频表单数据
const videoForm = reactive({
title: '',
category: '',
author: '',
tags: '',
description: '',
videoUrl: '',
fileName: '',
fileSize: '',
duration: '',
resolution: '',
thumbnail: ''
})
// 视频设置数据
const videoSettings = reactive({
source: '',
url: '',
publishTime: '',
summary: '',
viewCount: 0,
sortOrder: 0,
isPublished: false,
isTop: false,
isRecommended: false,
isHot: false,
isSlide: false,
seoTitle: '',
seoKeywords: '',
seoDescription: ''
})
// 生命周期
onMounted(() => {
if (isEdit.value) {
loadVideoData()
} else {
initNewVideo()
}
})
// 加载视频数据
const loadVideoData = () => {
Object.assign(videoForm, {
title: '福建省教育装备与基建中心宣传片',
category: 'promotion',
author: '张三',
tags: '宣传片,教育装备,基建中心',
description: '本视频介绍了福建省教育装备与基建中心的发展历程和主要成就...',
videoUrl: 'https://sample-videos.com/zip/10/mp4/SampleVideo_1280x720_1mb.mp4',
fileName: '宣传片.mp4',
fileSize: '125MB',
duration: '05:30',
resolution: '1920x1080',
thumbnail: 'https://via.placeholder.com/320x180/409EFF/FFFFFF?text=宣传片'
})
Object.assign(videoSettings, {
source: '福建省教育装备与基建中心',
url: '/video/promotion-video',
publishTime: '2024-01-15 10:30:00',
summary: '本视频介绍了福建省教育装备与基建中心的发展历程和主要成就...',
viewCount: 2580,
sortOrder: 1,
isPublished: true,
isTop: false,
isRecommended: true,
isHot: false,
isSlide: false,
seoTitle: '福建省教育装备与基建中心宣传片',
seoKeywords: '教育装备,基建中心,宣传片',
seoDescription: '福建省教育装备与基建中心宣传片详细介绍'
})
}
// 初始化新视频
const initNewVideo = () => {
Object.assign(videoForm, {
title: '',
category: '',
author: '',
tags: '',
description: '',
videoUrl: '',
fileName: '',
fileSize: '',
duration: '',
resolution: '',
thumbnail: ''
})
Object.assign(videoSettings, {
source: '',
url: '',
publishTime: new Date().toISOString().slice(0, 19).replace('T', ' '),
summary: '',
viewCount: 0,
sortOrder: 0,
isPublished: false,
isTop: false,
isRecommended: false,
isHot: false,
isSlide: false,
seoTitle: '',
seoKeywords: '',
seoDescription: ''
})
}
// 返回上一页
const goBack = () => {
router.go(-1)
}
// 保存草稿
const handleSaveDraft = () => {
ElMessage.success('草稿保存成功')
}
// 发布视频
const handlePublish = () => {
ElMessage.success('视频发布成功')
router.push('/admin/content/videos')
}
// 视频上传成功
const handleVideoSuccess = (response, file) => {
uploading.value = false
uploadProgress.value = 100
uploadStatus.value = 'success'
uploadProgressText.value = '上传完成'
// 模拟视频信息
videoForm.videoUrl = URL.createObjectURL(file.raw)
videoForm.fileName = file.name
videoForm.fileSize = formatFileSize(file.size)
ElMessage.success('视频上传成功')
}
// 视频上传前验证
const beforeVideoUpload = (file) => {
const isVideo = file.type.startsWith('video/')
const isLt500M = file.size / 1024 / 1024 < 500
if (!isVideo) {
ElMessage.error('只能上传视频文件!')
return false
}
if (!isLt500M) {
ElMessage.error('视频大小不能超过 500MB!')
return false
}
uploading.value = true
uploadProgress.value = 0
uploadStatus.value = ''
uploadProgressText.value = '准备上传...'
return true
}
// 上传进度
const handleUploadProgress = (event) => {
uploadProgress.value = Math.round(event.percent)
uploadProgressText.value = `上传中... ${uploadProgress.value}%`
}
// 视频加载完成
const handleVideoLoaded = (event) => {
const video = event.target
const duration = Math.round(video.duration)
const minutes = Math.floor(duration / 60)
const seconds = duration % 60
videoForm.duration = `${minutes}:${seconds.toString().padStart(2, '0')}`
videoForm.resolution = `${video.videoWidth}x${video.videoHeight}`
}
// 重新上传视频
const reuploadVideo = () => {
videoForm.videoUrl = ''
videoForm.fileName = ''
videoForm.fileSize = ''
videoForm.duration = ''
videoForm.resolution = ''
videoForm.thumbnail = ''
}
// 删除视频
const removeVideo = () => {
reuploadVideo()
ElMessage.success('视频已删除')
}
// 缩略图上传成功
const handleThumbnailSuccess = (response, file) => {
videoForm.thumbnail = URL.createObjectURL(file.raw)
ElMessage.success('缩略图上传成功')
}
// 缩略图上传前验证
const beforeThumbnailUpload = (file) => {
const isImage = file.type.startsWith('image/')
const isLt2M = file.size / 1024 / 1024 < 2
if (!isImage) {
ElMessage.error('只能上传图片文件!')
return false
}
if (!isLt2M) {
ElMessage.error('图片大小不能超过 2MB!')
return false
}
return true
}
// 关闭设置弹窗
const handleCloseSettings = () => {
showSettingsDialog.value = false
}
// 保存设置
const handleSaveSettings = () => {
ElMessage.success('设置保存成功')
showSettingsDialog.value = false
}
// 格式化文件大小
const formatFileSize = (bytes) => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
</script>
<style scoped>
.video-edit-container {
background-color: #f5f5f5;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 16px 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
}
.header-left h2 {
margin: 0;
font-size: 20px;
color: #333;
}
.info-card,
.media-card {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
font-weight: 600;
color: #333;
}
.info-form {
margin: 0;
}
.media-section {
padding: 20px 0;
}
.section-title {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
color: #333;
border-bottom: 1px solid #eee;
padding-bottom: 8px;
}
.video-section {
height: 100%;
}
.thumbnail-section {
height: 100%;
display: flex;
flex-direction: column;
}
.video-uploader {
width: 100%;
}
.upload-area {
border: 2px dashed #d9d9d9;
border-radius: 6px;
width: 100%;
height: 200px;
text-align: center;
cursor: pointer;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
transition: border-color 0.3s;
}
.upload-area:hover {
border-color: #409eff;
}
.upload-icon {
font-size: 48px;
color: #c0c4cc;
margin-bottom: 16px;
}
.upload-text p {
margin: 8px 0;
color: #666;
}
.upload-tip {
font-size: 12px;
color: #999;
}
.video-preview {
display: flex;
gap: 20px;
align-items: flex-start;
}
.preview-video {
width: 300px;
height: 200px;
border-radius: 6px;
background: #000;
}
.video-info {
flex: 1;
padding: 10px 0;
}
.video-info p {
margin: 8px 0;
color: #666;
font-size: 14px;
}
.video-actions {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 20px;
}
.upload-progress {
margin-top: 20px;
padding: 20px;
background: #f8f9fa;
border-radius: 6px;
}
.progress-text {
text-align: center;
margin-top: 10px;
color: #666;
}
.thumbnail-options {
margin-bottom: 20px;
}
.auto-thumbnail {
padding: 20px;
background: #f8f9fa;
border-radius: 6px;
text-align: center;
color: #666;
}
.custom-thumbnail {
display: flex;
justify-content: center;
flex: 1;
}
.thumbnail-uploader {
width: 100%;
max-width: 200px;
height: 120px;
}
.thumbnail-image {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 6px;
}
.thumbnail-placeholder {
width: 100%;
height: 100%;
border: 2px dashed #d9d9d9;
border-radius: 6px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
cursor: pointer;
color: #c0c4cc;
}
.thumbnail-placeholder .el-icon {
font-size: 32px;
margin-bottom: 8px;
}
.thumbnail-placeholder p {
margin: 0;
font-size: 12px;
}
/* 设置弹窗样式 */
.settings-dialog :deep(.el-dialog) {
border-radius: 8px;
}
.settings-dialog :deep(.el-dialog__header) {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 8px 8px 0 0;
padding: 20px;
}
.settings-dialog :deep(.el-dialog__title) {
color: white;
font-size: 18px;
font-weight: 600;
}
.settings-dialog :deep(.el-dialog__headerbtn .el-dialog__close) {
color: white;
font-size: 20px;
}
.settings-form {
padding: 20px;
}
.dialog-footer {
text-align: right;
padding: 20px;
border-top: 1px solid #ebeef5;
}
/* 响应式设计 */
@media (max-width: 768px) {
.video-edit-container {
padding: 10px;
}
.page-header {
flex-direction: column;
gap: 16px;
align-items: stretch;
}
.header-left {
justify-content: center;
}
.header-right {
display: flex;
justify-content: center;
gap: 8px;
}
.media-section .el-row {
flex-direction: column;
}
.media-section .el-col {
width: 100% !important;
margin-bottom: 20px;
}
.video-preview {
flex-direction: column;
}
.preview-video {
width: 100%;
max-width: 300px;
}
.thumbnail-uploader {
max-width: 100%;
}
.settings-dialog :deep(.el-dialog) {
width: 95%;
margin: 0 auto;
}
}
</style>

View File

@@ -0,0 +1,529 @@
<template>
<div class="videos-container">
<!-- 搜索和筛选区域 -->
<el-card class="search-card">
<el-form :model="searchForm" :inline="true" class="search-form">
<el-form-item label="视频标题">
<el-input
v-model="searchForm.title"
placeholder="请输入视频标题"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item label="分类">
<el-select
v-model="searchForm.category"
placeholder="请选择分类"
clearable
style="width: 150px"
>
<el-option label="全部" value="" />
<el-option label="教学视频" value="teaching" />
<el-option label="宣传视频" value="promotion" />
<el-option label="培训视频" value="training" />
<el-option label="会议视频" value="meeting" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select
v-model="searchForm.status"
placeholder="请选择状态"
clearable
style="width: 120px"
>
<el-option label="全部" value="" />
<el-option label="草稿" value="draft" />
<el-option label="已发布" value="published" />
<el-option label="已下架" value="offline" />
</el-select>
</el-form-item>
<el-form-item label="上传时间">
<el-date-picker
v-model="searchForm.dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 240px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>
重置
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 操作按钮区域 -->
<el-card class="action-card">
<div class="action-bar">
<div class="action-left">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增视频
</el-button>
<el-button type="success" :disabled="!hasSelection" @click="handleBatchPublish">
<el-icon><Upload /></el-icon>
批量上架
</el-button>
<el-button type="warning" :disabled="!hasSelection" @click="handleBatchOffline">
<el-icon><Download /></el-icon>
批量下架
</el-button>
</div>
<div class="action-right">
<el-button @click="handleRefresh">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
</div>
</el-card>
<!-- 视频列表 -->
<el-card class="table-card">
<el-table
v-loading="loading"
:data="tableData"
@selection-change="handleSelectionChange"
stripe
border
style="width: 100%"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="title" label="视频标题" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
<el-link type="primary" @click="handleView(row)">
{{ row.title }}
</el-link>
</template>
</el-table-column>
<el-table-column prop="thumbnail" label="缩略图" width="120">
<template #default="{ row }">
<el-image
:src="row.thumbnail"
:preview-src-list="[row.thumbnail]"
fit="cover"
style="width: 80px; height: 60px; border-radius: 4px;"
/>
</template>
</el-table-column>
<el-table-column prop="category" label="分类" width="120">
<template #default="{ row }">
<el-tag :type="getCategoryType(row.category)">
{{ getCategoryName(row.category) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="duration" label="时长" width="100" />
<el-table-column prop="size" label="文件大小" width="100" />
<el-table-column prop="author" label="上传者" width="100" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ getStatusName(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="views" label="播放量" width="100" />
<el-table-column prop="uploadTime" label="上传时间" width="160" />
<el-table-column prop="updateTime" label="更新时间" width="160" />
<el-table-column label="操作" width="250" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="handleEdit(row)">
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button
v-if="row.status === 'draft' || row.status === 'offline'"
type="success"
size="small"
@click="handlePublish(row)"
>
<el-icon><Upload /></el-icon>
上架
</el-button>
<el-button
v-if="row.status === 'published'"
type="warning"
size="small"
@click="handleOffline(row)"
>
<el-icon><Download /></el-icon>
下架
</el-button>
<el-button type="danger" size="small" @click="handleDelete(row)">
<el-icon><Delete /></el-icon>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
const router = useRouter()
// 搜索表单
const searchForm = reactive({
title: '',
category: '',
status: '',
dateRange: []
})
// 表格数据
const tableData = ref([])
const loading = ref(false)
const selectedRows = ref([])
// 分页
const pagination = reactive({
currentPage: 1,
pageSize: 20,
total: 0
})
// 计算属性
const hasSelection = computed(() => selectedRows.value.length > 0)
// 模拟数据
const mockData = [
{
id: 1,
title: '福建省教育装备与基建中心宣传片',
thumbnail: 'https://via.placeholder.com/320x180/409EFF/FFFFFF?text=宣传片',
category: 'promotion',
duration: '05:30',
size: '125MB',
author: '张三',
status: 'published',
views: 2580,
uploadTime: '2024-01-15 10:30:00',
updateTime: '2024-01-15 10:30:00'
},
{
id: 2,
title: '教育装备使用培训视频',
thumbnail: 'https://via.placeholder.com/320x180/67C23A/FFFFFF?text=培训视频',
category: 'training',
duration: '12:45',
size: '280MB',
author: '李四',
status: 'published',
views: 1890,
uploadTime: '2024-01-14 14:20:00',
updateTime: '2024-01-14 14:20:00'
},
{
id: 3,
title: '智慧校园建设方案介绍',
thumbnail: 'https://via.placeholder.com/320x180/E6A23C/FFFFFF?text=方案介绍',
category: 'teaching',
author: '王五',
status: 'draft',
duration: '08:20',
size: '195MB',
views: 0,
uploadTime: '',
updateTime: '2024-01-13 16:45:00'
},
{
id: 4,
title: '2024年工作会议记录',
thumbnail: 'https://via.placeholder.com/320x180/F56C6C/FFFFFF?text=会议记录',
category: 'meeting',
duration: '45:30',
size: '520MB',
author: '赵六',
status: 'offline',
views: 456,
uploadTime: '2024-01-12 09:15:00',
updateTime: '2024-01-12 09:15:00'
},
{
id: 5,
title: '教育装备安全操作指南',
thumbnail: 'https://via.placeholder.com/320x180/909399/FFFFFF?text=操作指南',
category: 'training',
duration: '15:10',
size: '320MB',
author: '钱七',
status: 'published',
views: 3200,
uploadTime: '2024-01-11 11:30:00',
updateTime: '2024-01-11 11:30:00'
}
]
// 生命周期
onMounted(() => {
loadData()
})
// 加载数据
const loadData = () => {
loading.value = true
setTimeout(() => {
tableData.value = mockData
pagination.total = mockData.length
loading.value = false
}, 500)
}
// 搜索
const handleSearch = () => {
ElMessage.info('搜索功能开发中...')
loadData()
}
// 重置
const handleReset = () => {
Object.assign(searchForm, {
title: '',
category: '',
status: '',
dateRange: []
})
loadData()
}
// 新增
const handleAdd = () => {
router.push('/admin/content/videos/new')
}
// 编辑
const handleEdit = (row) => {
router.push(`/admin/content/videos/${row.id}`)
}
// 查看
const handleView = (row) => {
ElMessage.info(`查看视频: ${row.title}`)
}
// 删除
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm(
`确定要删除视频"${row.title}"吗?`,
'确认删除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
ElMessage.success('删除成功')
loadData()
} catch {
// 用户取消
}
}
// 上架
const handlePublish = (row) => {
ElMessage.success(`视频"${row.title}"已上架`)
row.status = 'published'
}
// 下架
const handleOffline = (row) => {
ElMessage.warning(`视频"${row.title}"已下架`)
row.status = 'offline'
}
// 刷新
const handleRefresh = () => {
loadData()
}
// 批量上架
const handleBatchPublish = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要上架的视频')
return
}
ElMessage.success(`已批量上架 ${selectedRows.value.length} 个视频`)
selectedRows.value.forEach(row => {
row.status = 'published'
})
selectedRows.value = []
}
// 批量下架
const handleBatchOffline = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择要下架的视频')
return
}
ElMessage.warning(`已批量下架 ${selectedRows.value.length} 个视频`)
selectedRows.value.forEach(row => {
row.status = 'offline'
})
selectedRows.value = []
}
// 选择变化
const handleSelectionChange = (selection) => {
selectedRows.value = selection
}
// 分页大小变化
const handleSizeChange = (val) => {
pagination.pageSize = val
loadData()
}
// 当前页变化
const handleCurrentChange = (val) => {
pagination.currentPage = val
loadData()
}
// 获取分类名称
const getCategoryName = (category) => {
const categoryMap = {
teaching: '教学视频',
promotion: '宣传视频',
training: '培训视频',
meeting: '会议视频'
}
return categoryMap[category] || category
}
// 获取分类类型
const getCategoryType = (category) => {
const typeMap = {
teaching: 'primary',
promotion: 'success',
training: 'warning',
meeting: 'info'
}
return typeMap[category] || ''
}
// 获取状态名称
const getStatusName = (status) => {
const statusMap = {
draft: '草稿',
published: '已发布',
offline: '已下架'
}
return statusMap[status] || status
}
// 获取状态类型
const getStatusType = (status) => {
const typeMap = {
draft: 'info',
published: 'success',
offline: 'warning'
}
return typeMap[status] || ''
}
</script>
<style scoped>
.videos-container {
padding: 0;
}
.search-card {
margin-bottom: 20px;
}
.search-form {
margin: 0;
}
.action-card {
margin-bottom: 20px;
}
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
}
.action-left {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.action-right {
display: flex;
gap: 10px;
}
.table-card {
margin-bottom: 20px;
}
.pagination-container {
display: flex;
justify-content: center;
margin-top: 20px;
}
.el-form-item {
margin-bottom: 0;
}
/* 响应式设计 */
@media (max-width: 768px) {
.action-bar {
flex-direction: column;
gap: 10px;
}
.action-left,
.action-right {
width: 100%;
justify-content: center;
}
.search-form {
flex-direction: column;
}
.search-form .el-form-item {
margin-right: 0;
margin-bottom: 10px;
}
}
</style>

View File

@@ -0,0 +1,77 @@
<template>
<div class="wechat-content">
<el-card>
<template #header>
<div class="card-header">
<span>微信矩阵</span>
<el-button type="primary" size="small">
<el-icon><Plus /></el-icon>
添加公众号
</el-button>
</div>
</template>
<div class="placeholder-content">
<el-icon class="placeholder-icon"><ChatDotRound /></el-icon>
<h3>微信矩阵功能开发中...</h3>
<p>这里将实现微信公众号的管理内容同步二维码生成等功能</p>
<div class="feature-list">
<el-tag type="primary" size="small">公众号管理</el-tag>
<el-tag type="primary" size="small">内容同步</el-tag>
<el-tag type="primary" size="small">二维码生成</el-tag>
<el-tag type="primary" size="small">菜单配置</el-tag>
<el-tag type="primary" size="small">消息管理</el-tag>
</div>
</div>
</el-card>
</div>
</template>
<script setup>
// 微信矩阵页面逻辑
</script>
<style scoped>
.wechat-content {
padding: 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
font-weight: 600;
color: #333;
}
.placeholder-content {
text-align: center;
padding: 60px 20px;
color: #666;
}
.placeholder-icon {
font-size: 64px;
color: #ddd;
margin-bottom: 20px;
}
.placeholder-content h3 {
margin: 0 0 12px 0;
font-size: 20px;
color: #333;
}
.placeholder-content p {
margin: 0 0 20px 0;
font-size: 14px;
line-height: 1.6;
}
.feature-list {
display: flex;
justify-content: center;
gap: 8px;
flex-wrap: wrap;
}
</style>

View File

@@ -0,0 +1,348 @@
<template>
<div class="content-management-container">
<el-container>
<!-- 侧边栏 -->
<el-aside :width="sidebarWidth" class="sidebar">
<div class="sidebar-header">
<h3 :style="{ opacity: isCollapsed ? 0 : 1, width: isCollapsed ? '0' : 'auto', whiteSpace: isCollapsed ? 'nowrap' : 'auto' }">内容管理</h3>
<el-button
type="text"
class="collapse-btn"
@click="toggleCollapse"
>
<el-icon>
<component :is="isCollapsed ? 'Expand' : 'Fold'" />
</el-icon>
</el-button>
</div>
<el-menu
:default-active="activeMenu"
class="sidebar-menu"
:collapse="isCollapsed"
@select="handleMenuSelect"
>
<el-menu-item index="articles">
<el-icon><Document /></el-icon>
<template #title>
<span>文章管理</span>
</template>
</el-menu-item>
<el-menu-item index="videos">
<el-icon><VideoPlay /></el-icon>
<template #title>
<span>视频管理</span>
</template>
</el-menu-item>
<el-menu-item index="downloads">
<el-icon><Download /></el-icon>
<template #title>
<span>下载专区</span>
</template>
</el-menu-item>
<el-menu-item index="services">
<el-icon><Service /></el-icon>
<template #title>
<span>精准服务</span>
</template>
</el-menu-item>
<el-menu-item index="wechat">
<el-icon><ChatDotRound /></el-icon>
<template #title>
<span>微信矩阵</span>
</template>
</el-menu-item>
<el-menu-item index="links">
<el-icon><Link /></el-icon>
<template #title>
<span>友情链接</span>
</template>
</el-menu-item>
</el-menu>
</el-aside>
<!-- 主内容区域 -->
<el-main class="main-content" :style="{ marginLeft: mainContentMarginLeft }">
<div class="content-header">
<el-breadcrumb separator="/">
<el-breadcrumb-item>后台管理</el-breadcrumb-item>
<el-breadcrumb-item>内容管理</el-breadcrumb-item>
<el-breadcrumb-item>{{ currentModuleName }}</el-breadcrumb-item>
</el-breadcrumb>
<el-button type="primary" icon="HomeFilled" @click="router.push('/admin/dashboard')">返回首页</el-button>
</div>
<div class="content-body">
<router-view />
</div>
</el-main>
</el-container>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
const isCollapsed = ref(false)
// 侧边栏宽度
const sidebarWidth = computed(() => isCollapsed.value ? '83px' : '250px')
// 主内容区域左边距
const mainContentMarginLeft = computed(() => isCollapsed.value ? '83px' : '250px')
const moduleNames = {
articles: '文章管理',
videos: '视频管理',
downloads: '下载专区',
services: '精准服务',
wechat: '微信矩阵',
links: '友情链接'
}
// 当前激活的菜单项
const activeMenu = computed(() => {
const path = route.path
if (path.includes('/articles')) return 'articles'
if (path.includes('/videos')) return 'videos'
if (path.includes('/downloads')) return 'downloads'
if (path.includes('/services')) return 'services'
if (path.includes('/wechat')) return 'wechat'
if (path.includes('/links')) return 'links'
return 'articles'
})
const currentModuleName = computed(() => {
return moduleNames[activeMenu.value] || '内容管理'
})
// 侧边栏缩起展开
const toggleCollapse = () => {
isCollapsed.value = !isCollapsed.value
}
// 菜单选择处理
const handleMenuSelect = (index) => {
router.push(`/admin/content/${index}`)
}
// 监听路由变化,确保侧边栏状态正确
watch(() => route.path, (newPath) => {
// 路由变化时的处理逻辑
}, { immediate: true })
</script>
<style scoped>
.content-management-container {
min-height: 100vh;
background-color: #f5f5f5;
}
.sidebar {
background: white;
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.1);
height: 100vh;
overflow-y: auto;
overflow-x: hidden;
transition: width 0.3s ease;
position: fixed;
top: 0;
left: 0;
z-index: 1000;
display: flex;
flex-direction: column;
/* 优化滚动条样式 */
scrollbar-width: thin;
scrollbar-color: #c1c1c1 transparent;
}
.sidebar::-webkit-scrollbar {
width: 6px;
}
.sidebar::-webkit-scrollbar-track {
background: transparent;
}
.sidebar::-webkit-scrollbar-thumb {
background-color: #c1c1c1;
border-radius: 3px;
}
.sidebar::-webkit-scrollbar-thumb:hover {
background-color: #a8a8a8;
}
.sidebar-header {
padding: 20px;
border-bottom: 1px solid #eee;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
display: flex;
justify-content: space-between;
align-items: center;
min-height: 60px;
flex-shrink: 0;
}
.sidebar-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
transition: opacity 0.3s ease;
}
.el-menu--collapse {
width: calc(var(--el-menu-icon-width) + var(--el-menu-base-level-padding)* 2 + 18px);
}
.collapse-btn {
color: white !important;
font-size: 18px;
padding: 0;
min-height: auto;
}
.collapse-btn:hover {
background-color: rgba(255, 255, 255, 0.1) !important;
}
.sidebar-menu {
border: none;
padding: 10px 0;
flex: 1;
overflow-y: auto;
overflow-x: hidden;
}
.sidebar-menu .el-menu-item {
height: 50px;
line-height: 50px;
margin: 2px 10px;
border-radius: 6px;
transition: all 0.3s;
}
.sidebar-menu .el-menu-item:hover {
background-color: #f0f2ff;
color: #667eea;
}
.sidebar-menu .el-menu-item.is-active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.sidebar-menu .el-menu-item .el-icon {
margin-right: 8px;
font-size: 16px;
}
.main-content {
padding: 0;
background-color: #f5f5f5;
transition: margin-left 0.3s ease;
}
.content-header {
background: white;
padding: 16px 20px;
border-bottom: 1px solid #eee;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
display: flex;
justify-content: space-between;
align-items: center;
}
.content-body {
padding: 20px;
}
.module-content {
min-height: 600px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
font-weight: 600;
color: #333;
}
.placeholder-content {
text-align: center;
padding: 60px 20px;
color: #666;
}
.placeholder-icon {
font-size: 64px;
color: #ddd;
margin-bottom: 20px;
}
.placeholder-content h3 {
margin: 0 0 12px 0;
font-size: 20px;
color: #333;
}
.placeholder-content p {
margin: 0 0 20px 0;
font-size: 14px;
line-height: 1.6;
}
.feature-list {
display: flex;
justify-content: center;
gap: 8px;
flex-wrap: wrap;
}
/* 子路由内容样式 */
.content-body {
padding: 20px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.sidebar {
width: 300px !important;
}
.content-body {
padding: 15px;
}
.placeholder-content {
padding: 40px 15px;
}
.placeholder-icon {
font-size: 48px;
}
}
/* 侧边栏缩起时的样式 */
.sidebar.collapsed .sidebar-header h3 {
opacity: 0;
}
.sidebar.collapsed .sidebar-menu .el-menu-item span {
display: none;
}
.sidebar.collapsed .sidebar-menu .el-menu-item {
text-align: center;
padding: 0;
}
.sidebar.collapsed .sidebar-menu .el-menu-item .el-icon {
margin-right: 0;
}
</style>

View File

@@ -0,0 +1,316 @@
<template>
<div class="dashboard-container">
<el-container>
<!-- 头部 -->
<el-header class="dashboard-header">
<div class="header-left">
<h1>福建省教育装备与基建中心</h1>
<span>后台管理系统</span>
</div>
<div class="header-right">
<el-dropdown @command="handleCommand">
<span class="user-info">
<el-icon><User /></el-icon>
{{ authStore.user?.username }}
<el-icon><ArrowDown /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile">个人资料</el-dropdown-item>
<el-dropdown-item command="settings">系统设置</el-dropdown-item>
<el-dropdown-item divided command="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-header>
<!-- 主体内容 -->
<el-main class="dashboard-main">
<div class="welcome-card">
<el-card>
<template #header>
<div class="card-header">
<span>欢迎使用后台管理系统</span>
</div>
</template>
<div class="welcome-content">
<el-icon class="welcome-icon"><SuccessFilled /></el-icon>
<h2>登录成功</h2>
<p>欢迎回来{{ authStore.user?.username }}</p>
<p>当前时间{{ currentTime }}</p>
</div>
</el-card>
</div>
<div class="stats-grid">
<el-row :gutter="20">
<el-col :span="6">
<el-card class="stat-card" @click="navigateTo('/admin/content')">
<div class="stat-content">
<div class="stat-icon content-icon">
<el-icon><Document /></el-icon>
</div>
<div class="stat-info">
<h3>内容管理</h3>
<p>管理网站内容文章新闻等</p>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card" @click="navigateTo('/admin/site-config')">
<div class="stat-content">
<div class="stat-icon config-icon">
<el-icon><House /></el-icon>
</div>
<div class="stat-info">
<h3>站点配置</h3>
<p>配置网站参数主题导航等</p>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card" @click="navigateTo('/admin/system')">
<div class="stat-content">
<div class="stat-icon system-icon">
<el-icon><Setting /></el-icon>
</div>
<div class="stat-info">
<h3>系统管理</h3>
<p>用户管理权限设置系统维护</p>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card" @click="navigateTo('/admin/business')">
<div class="stat-content">
<div class="stat-icon business-icon">
<el-icon><OfficeBuilding /></el-icon>
</div>
<div class="stat-info">
<h3>业务后台</h3>
<p>教育装备管理基建项目管理</p>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
</el-main>
</el-container>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useAuthStore } from '@/stores/auth'
const router = useRouter()
const authStore = useAuthStore()
const currentTime = ref('')
let timeInterval = null
onMounted(() => {
updateTime()
timeInterval = setInterval(updateTime, 1000)
})
onUnmounted(() => {
if (timeInterval) {
clearInterval(timeInterval)
}
})
const updateTime = () => {
const now = new Date()
currentTime.value = now.toLocaleString('zh-CN')
}
const handleCommand = (command) => {
switch (command) {
case 'profile':
ElMessage.info('个人资料功能开发中...')
break
case 'settings':
ElMessage.info('系统设置功能开发中...')
break
case 'logout':
handleLogout()
break
}
}
const handleLogout = async () => {
try {
await ElMessageBox.confirm(
'确定要退出登录吗?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
authStore.logout()
ElMessage.success('已退出登录')
router.push('/admin/login')
} catch {
// 用户取消
}
}
const navigateTo = (path) => {
router.push(path)
}
</script>
<style scoped>
.dashboard-container {
min-height: 100vh;
background-color: #f5f5f5;
}
.dashboard-header {
background: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
}
.header-left h1 {
margin: 0;
font-size: 20px;
color: #333;
font-weight: 600;
}
.header-left span {
color: #666;
font-size: 14px;
margin-left: 10px;
}
.user-info {
display: flex;
align-items: center;
cursor: pointer;
padding: 8px 12px;
border-radius: 4px;
transition: background-color 0.3s;
}
.user-info:hover {
background-color: #f5f5f5;
}
.user-info .el-icon {
margin: 0 4px;
}
.dashboard-main {
padding: 20px;
}
.welcome-card {
margin-bottom: 20px;
}
.card-header {
font-size: 18px;
font-weight: 600;
color: #333;
}
.welcome-content {
text-align: center;
padding: 20px;
}
.welcome-icon {
font-size: 48px;
color: #67c23a;
margin-bottom: 16px;
}
.welcome-content h2 {
margin: 0 0 8px 0;
color: #333;
}
.welcome-content p {
margin: 4px 0;
color: #666;
}
.stats-grid {
margin-top: 20px;
}
.stat-card {
transition: transform 0.3s, box-shadow 0.3s;
cursor: pointer;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.stat-content {
display: flex;
align-items: center;
padding: 10px;
}
.stat-icon {
width: 50px;
height: 50px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
}
.stat-icon .el-icon {
font-size: 24px;
color: white;
}
.content-icon {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.config-icon {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
}
.system-icon {
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
}
.business-icon {
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
}
.stat-info h3 {
margin: 0 0 4px 0;
font-size: 16px;
color: #333;
}
.stat-info p {
margin: 0;
font-size: 14px;
color: #666;
}
</style>

View File

@@ -0,0 +1,225 @@
<template>
<div class="login-container">
<div class="login-box">
<div class="login-header">
<h2>福建省教育装备与基建中心</h2>
<p>后台管理系统</p>
</div>
<el-form
ref="loginFormRef"
:model="loginForm"
:rules="loginRules"
class="login-form"
@submit.prevent="handleLogin"
>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="请输入用户名"
size="large"
prefix-icon="User"
clearable
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="请输入密码"
size="large"
prefix-icon="Lock"
show-password
clearable
@keyup.enter="handleLogin"
/>
</el-form-item>
<el-form-item>
<el-checkbox v-model="loginForm.rememberMe">
记住我
</el-checkbox>
</el-form-item>
<el-form-item>
<el-button
type="primary"
size="large"
class="login-button"
:loading="loading"
@click="handleLogin"
>
{{ loading ? '登录中...' : '登录' }}
</el-button>
</el-form-item>
</el-form>
<div class="login-footer">
<p>测试账号admin / 123456</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useAuthStore } from '@/stores/auth'
const router = useRouter()
const authStore = useAuthStore()
const loginFormRef = ref()
const loading = ref(false)
const loginForm = reactive({
username: '',
password: '',
rememberMe: false
})
const loginRules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '用户名长度在 3 到 20 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
]
}
const handleLogin = async () => {
if (!loginFormRef.value) return
try {
const valid = await loginFormRef.value.validate()
if (!valid) return
loading.value = true
// 模拟登录请求
await new Promise(resolve => setTimeout(resolve, 1000))
// 模拟登录验证
if (loginForm.username === 'admin' && loginForm.password === '123456') {
// 保存登录状态
authStore.login({
username: loginForm.username,
token: 'mock-token-' + Date.now(),
rememberMe: loginForm.rememberMe
})
ElMessage.success('登录成功!')
// 跳转到后台首页
router.push('/admin/dashboard')
} else {
ElMessage.error('用户名或密码错误!')
}
} catch (error) {
ElMessage.error('登录失败,请重试!')
} finally {
loading.value = false
}
}
</script>
<style scoped>
.login-container {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.login-box {
background: white;
border-radius: 12px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
padding: 40px;
width: 100%;
max-width: 400px;
position: relative;
overflow: hidden;
}
.login-box::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #667eea, #764ba2);
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.login-header h2 {
color: #333;
font-size: 24px;
font-weight: 600;
margin: 0 0 8px 0;
}
.login-header p {
color: #666;
font-size: 14px;
margin: 0;
}
.login-form {
margin-top: 20px;
}
.login-form .el-form-item {
margin-bottom: 20px;
}
.login-button {
width: 100%;
height: 45px;
font-size: 16px;
font-weight: 500;
border-radius: 6px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
}
.login-button:hover {
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
}
.login-footer {
text-align: center;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #eee;
}
.login-footer p {
color: #999;
font-size: 12px;
margin: 0;
}
/* 响应式设计 */
@media (max-width: 480px) {
.login-box {
padding: 30px 20px;
margin: 10px;
}
.login-header h2 {
font-size: 20px;
}
}
</style>

344
src/views/CatalogView.vue Normal file
View File

@@ -0,0 +1,344 @@
<template>
<!-- 导航页 -->
<div class="catalog">
<!-- 当前位置 -->
<div class="catalog-location">
<!-- 家图标 -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="rgba(102,102,102,1)">
<path
d="M20 20C20 20.5523 19.5523 21 19 21H5C4.44772 21 4 20.5523 4 20V11L1 11L11.3273 1.6115C11.7087 1.26475 12.2913 1.26475 12.6727 1.6115L23 11L20 11V20Z">
</path>
</svg>
<div class="catalog-location-text">
<span>当前位置 </span>
<span>首页</span>
<span> > </span>
<span>{{ pageName }}</span>
</div>
</div>
<!-- 主页面 -->
<div class="catalog-main">
<!-- 左侧竖直导航列 -->
<div class="catalog-main-nav">
<div class="catalog-main-nav-item-header" @click="toggleNav">
{{ pageName }}
<svg :class="{ 'rotated': isNavExpanded }" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16"
height="16" fill="#ffffff" class="toggle-icon">
<path d="M8 9l4 4 4-4"></path>
</svg>
</div>
<transition name="roll">
<ul v-if="isNavExpanded" class="nav-items">
<li v-for="item in navItem" :key="item.id" class="catalog-main-nav-item" @click="handleNavItemClick(item)">
<span class="nav-item-text">{{ item.name }}</span>
</li>
</ul>
</transition>
</div>
<!-- 右侧列表 -->
<div :class="['catalog-main-list', { 'full-width': !isNavExpanded }]">
<!-- 标题 -->
<div class="catalog-main-list-title">{{ pageName }}</div>
<!-- 分割条 -->
<div class="catalog-main-list-split"></div>
<!-- 内容 -->
<div class="catalog-main-list-content">
<ul>
<li v-for="item in contentItem" :key="item.id" class="content-list-item">
<div class="catalog-main-list-content-item">
<span class="content-item-title">{{ item.title }}</span>
<span class="catalog-main-list-content-item-date">{{ item.date }}</span>
</div>
</li>
</ul>
</div>
<!-- 分割条 -->
<div class="catalog-main-list-split"></div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
// 页面名称
const pageName = ref('概况信息')
// 导航项
const navItem = ref([
{ id: 1, name: '发展概况' },
{ id: 2, name: '中心领导' },
{ id: 3, name: '科室职责' }
])
// 内容项
const contentItem = ref([
{ id: 1, title: '科室职责', date: '2024-04-28' },
{ id: 2, title: '中心领导', date: '2024-04-28' },
{ id: 3, title: '发展概况', date: '2020-09-24' }
])
// 导航展开状态
const isNavExpanded = ref(true)
// 切换导航展开与收起
const toggleNav = () => {
isNavExpanded.value = !isNavExpanded.value
}
// 处理导航项点击(示例功能,可以根据需求扩展)
const handleNavItemClick = (item) => {
console.log('导航项点击:', item)
// 这里可以添加路由跳转或其他逻辑
}
// 设置导航的初始展开状态基于窗口宽度
const setInitialNavState = () => {
if (window.innerWidth <= 768) {
isNavExpanded.value = false
} else {
isNavExpanded.value = true
}
}
// 监听窗口大小变化以调整导航栏展开状态
const handleResize = () => {
if (window.innerWidth <= 768) {
isNavExpanded.value = false
} else {
isNavExpanded.value = true
}
}
onMounted(() => {
setInitialNavState()
window.addEventListener('resize', handleResize)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
})
</script>
<style scoped>
.catalog {
padding: 15px;
box-sizing: border-box;
}
/* 当前位置 */
.catalog-location {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.catalog-location-text {
margin-left: 10px;
font-size: 14px;
color: #666666;
}
/* 主页面布局 */
.catalog-main {
display: flex;
padding: 20px;
min-height: 500px;
box-sizing: border-box;
}
/* 左侧导航 */
.catalog-main-nav {
width: 250px;
/* 固定宽度 */
border-radius: 5px;
transition: all 0.3s ease;
box-sizing: border-box;
position: relative;
overflow: hidden;
}
/* 导航头部 */
.catalog-main-nav-item-header {
padding: 15px;
font-size: 17px;
font-weight: bold;
color: white;
background-color: red;
border-top: 4px solid #ffb400;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
}
/* Toggle 图标旋转 */
.toggle-icon.rotated {
transform: rotate(180deg);
}
/* 导航列表 */
.nav-items {
padding: 0;
margin: 0;
list-style: none;
}
/* 导航项 */
.catalog-main-nav-item {
background-color: #f5f5f5;
padding: 17px;
font-size: 15px;
color: #666666;
border-bottom: 1px solid #e2e2e2;
list-style: none;
position: relative;
display: flex;
align-items: center;
transition: background-color 0.3s ease;
}
/* 导航项文本 */
.nav-item-text {
display: inline-block;
transition: all 0.3s ease;
white-space: nowrap;
}
/* 左侧导航项悬停效果 */
.catalog-main-nav-item:hover {
background-color: #ffe5e5;
/* 淡红色背景 */
}
.catalog-main-nav-item:hover .nav-item-text {
color: red;
transform: translateX(5px);
}
/* 右侧列表 */
.catalog-main-list {
padding: 20px;
width: calc(100% - 250px);
transition: width 0.3s ease;
box-sizing: border-box;
}
.catalog-main-list.full-width {
width: 100%;
}
.catalog-main-list-title {
font-size: 17px;
padding: 0 14px;
font-weight: bold;
color: black;
border-left: red solid 5px;
}
.catalog-main-list-split {
margin-top: 20px;
height: 1px;
background-color: #e2e2e2;
}
.catalog-main-list-content {
margin-top: 20px;
font-size: 15px;
color: #666666;
}
.catalog-main-list-content ul {
width: 100%;
display: flex;
flex-direction: column;
list-style: none;
padding-left: 0;
margin-top: 16px;
margin-bottom: 0;
}
.content-list-item {
display: flex;
align-items: center;
margin-bottom: 5px;
transition: background-color 0.3s ease;
position: relative;
}
/* 保留 ::before 伪元素 */
.content-list-item::before {
content: "";
display: inline-block;
width: 5px;
height: 5px;
background-color: #cccccc;
margin-right: 10px;
flex-shrink: 0;
flex-grow: 0;
}
/* 内容项布局 */
.catalog-main-list-content-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
width: 100%;
}
.content-item-title {
display: inline-block;
transition: all 0.3s ease;
}
/* 右侧内容项悬停效果 */
.content-list-item:hover {
background-color: #f0f0f0;
/* 淡灰色背景 */
}
.content-list-item:hover .content-item-title {
color: red;
transform: translateX(5px);
}
/* 响应式设计 */
@media (max-width: 768px) {
.catalog-main {
flex-direction: column;
}
.catalog-main-nav {
width: 100%;
margin-bottom: 20px;
}
.catalog-main-list {
width: 100%;
}
}
/* 过渡动画 for nav items (卷帘门效果) */
.roll-enter-active,
.roll-leave-active {
transition: max-height 0.5s ease, opacity 0.5s ease;
overflow: hidden;
}
.roll-enter-from,
.roll-leave-to {
max-height: 0;
opacity: 0;
}
.roll-enter-to,
.roll-leave-from {
max-height: 500px;
/* 根据内容调整 */
opacity: 1;
}
</style>

109
src/views/ContentView.vue Normal file
View File

@@ -0,0 +1,109 @@
<template>
<!-- 内容详情页 -->
<div class="content">
<!-- 当前位置 -->
<div class="content-location">
<!-- 家图标 -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="rgba(102,102,102,1)">
<path
d="M20 20C20 20.5523 19.5523 21 19 21H5C4.44772 21 4 20.5523 4 20V11L1 11L11.3273 1.6115C11.7087 1.26475 12.2913 1.26475 12.6727 1.6115L23 11L20 11V20Z">
</path>
</svg>
<div class="content-location-text">
<span>当前位置 </span>
<span>首页</span>
<span> > </span>
<span>{{ pageName }}</span>
</div>
</div>
<div class="content-info">
<!-- 大标题 -->
<div class="content-title">{{ content.title }}</div>
<!-- 分割行 -->
<div class="content-line"></div>
<!-- 发布时间 -->
<div class="content-time">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="rgba(102,102,102,1)">
<path
d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM13 12H17V14H11V7H13V12Z">
</path>
</svg>
<p>时间{{ content.time }}</p>
</div>
<!-- 内容 如何展示有待商榷 -->
<div class="content-text">{{ content.content }}</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const pageName = ref('内容详情');
const content = ref({
title: '内容详情',
time: '2023-01-01',
content: '内容详情'
})
</script>
<style scoped>
.content {
padding: 15px;
box-sizing: border-box;
}
/* 当前位置 */
.content-location {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.content-location-text {
margin-left: 10px;
font-size: 14px;
color: #666666;
}
.content-info {
padding: 20px;
background-color: #ffffff;
border-radius: 5px;
}
/* 大标题 */
.content-title {
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
width: 100%;
text-align: center;
}
/* 分割行 */
.content-line {
width: 100%;
height: 1px;
background-color: #eeeeee;
margin-bottom: 20px;
}
/* 发布时间 */
.content-time {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.content-time p {
margin-left: 10px;
font-size: 14px;
color: #666666;
}
/* 内容 */
.content-text {
font-size: 16px;
color: #333333;
}
</style>

418
src/views/HomeView.vue Normal file
View File

@@ -0,0 +1,418 @@
<template>
<!-- 首页 -->
<div class="home">
<!-- 顶栏标题 -->
<div class="header">
<div class="header-content">
<h3>中文域名福建省教育装备与基建中心.公益</h3>
</div>
</div>
<!-- 导航栏 -->
<div class="navigation">
<div class="nav-links">
<a v-for="nav in navList" :key="nav.id" href="#"><RouterLink :to="nav.path">{{ nav.name }}</RouterLink></a>
</div>
<div class="search-container">
<input type="text" id="search" placeholder="请输入关键字">
<button id="search-btn">搜索</button>
</div>
</div>
<!-- 路由视图 -->
<RouterView />
<!-- 分割线 -->
<div class="red-line"></div>
<!-- 底栏 -->
<div class="footer">
<!-- 友情链接下拉框 -->
<div class="friend-links">
<select class="friend-link" v-for="item in friendlyLinkList" :key="item.select">
<option v-for="link in item.options" :key="link.name" :value="link.link">{{ link.name }}</option>
</select>
</div>
<!-- 底栏二维码和文字 -->
<div class="footer-bottom-container">
<div class="footer-qr">
<div class="qr-code-container">
<img src="http://www.fjeei.com/MenHu/JYZBWeb/Reception/images/blue.png" alt="蓝色图片" />
<span>省机关单位</span>
</div>
<div class="qr-code-container">
<img
src="http://www.fjeei.com/MenHu/JYZBWeb/images/%E7%A6%8F%E5%BB%BA%E6%95%99%E8%82%B2%E8%A3%85%E5%A4%87%E7%BD%91%E5%85%AC%E4%BC%97%E5%8F%B7.png?T=20210918"
alt="福建省教育装备公众号" />
<span>福建省教育装备公众号</span>
</div>
<div class="qr-code-container">
<img src="http://www.fjeei.com/MenHu/JYZBWeb/images/qrcode.png" alt="福建教育微信" />
<span>福建教育微信</span>
</div>
</div>
<div class="footer-text">
<p>福建省教育装备与基建中心</p>
<p>地址福建省福州市鼓楼区杨桥东路126号 邮编350001</p>
<div class="footer-info">
<img src="http://www.fjeei.com/MenHu/JYZBWeb/Reception/images/dh.png" alt="导航图标" class="footer-icon" />
<span>闽公网安备35011943142-00001 闽ICP19027281号 网站访问 73715 </span>
</div>
<p>中文域名福建省教育装备与基建中心.公益 闽ICP19027281号-3</p>
<p>技术支持福州金网际软件开发有限公司</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { RouterView, RouterLink } from 'vue-router'
// 导航栏数据
const navList = ref([
{ name: '网站首页', path: '/' },
{ name: '概况信息', path: '/catalog' },
{ name: '教育装备', path: '#' },
{ name: '学校基建', path: '#' },
{ name: '国资管理', path: '#' },
{ name: '校园风险', path: '#' },
{ name: '政府采购', path: '#' },
{ name: '下载专区', path: '#' },
])
// 友情链接列表
const friendlyLinkList = ref([
{
select: "教育部", options: [
{ name: "教育部", link: "#" },
{ name: "中华人民共和国教育部", link: "#" },
]
},
{
select: "各省教育装备部门", options: [
{ name: "各省教育装备部门", link: "#" },
{ name: "浙江省教育技术中心", link: "#" },
{ name: "河南教育装备网", link: "#" },
{ name: "山东省教育技术装备中心", link: "#" }
]
},
{
select: "教育局", options: [
{ name: "教育局", link: "#" },
{ name: "福州市教育局", link: "#" },
{ name: "厦门市教育局", link: "#" },
{ name: "泉州市教育局", link: "#" },
{ name: "莆田市教育局", link: "#" },
{ name: "南平市教育局", link: "#" },
{ name: "三明市教育局", link: "#" },
{ name: "宁德市教育局", link: "#" },
{ name: "漳州市教育局", link: "#" },
{ name: "龙岩市教育局", link: "#" }
]
},
{
select: "政府采购网", options: [
{ name: "政府采购网", link: "#" },
{ name: "中国政府采购网", link: "#" },
{ name: "福建省政府采购网", link: "#" },
{ name: "福建省公共资源交易中心网", link: "#" },
{ name: "福建省产权交易网", link: "#" }
]
},
{
select: "行业协会", options: [
{ name: "中国教育装备行业协会", link: "#" },
{ name: "福建省教育装备行业协会", link: "#" }
]
},
])
</script>
<style scoped>
/* 基本布局 */
.home {
margin: 20px;
display: flex;
flex-direction: column;
max-width: 1200px;
/* 设置最大宽度,防止过宽 */
margin: 0 auto;
/* 居中对齐 */
box-sizing: border-box;
}
.header {
width: 100%;
height: 180px;
background-image: url('http://www.fjeei.com/MenHu/JYZBWeb/Reception/images/banner.png');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
position: relative;
}
.header-content {
position: absolute;
bottom: -5%;
left: 10.5%;
color: red;
font-style: italic;
font-size: 1.0em;
}
.navigation {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
background-color: red;
padding: 5px 0;
}
.nav-links {
display: flex;
flex-wrap: wrap;
}
.nav-links a {
text-decoration: none;
color: #fff;
font-size: 1em;
padding: 0.5em 0.7em;
white-space: nowrap;
border-radius: 5px;
transition: background-color 0.3s;
}
.nav-links a:hover {
background-color: brown;
}
.search-container {
display: flex;
align-items: center;
gap: 0.5em;
margin-top: 0.5em;
}
#search {
width: 80%;
max-width: 300px;
height: 2em;
border: 1px solid #ccc;
border-radius: 1em;
padding: 0 0.5em;
box-sizing: border-box;
font-size: 1em;
}
#search-btn {
margin-right: 10px;
width: 5em;
height: 2.2em;
background-color: #FF9F09;
border: none;
border-radius: 1em;
color: white;
cursor: pointer;
font-size: 1em;
transition: background-color 0.3s;
}
#search-btn:hover {
background-color: #ff8409;
}
.red-line {
border-bottom: 4px solid red;
margin: 1em 0;
}
/* 底栏样式 */
.footer {
display: flex;
flex-direction: column;
align-items: center;
background-color: #f9f9f9;
padding: 2% 0;
}
.friend-links {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 1%;
margin-bottom: 2%;
width: 100%;
}
.friend-link {
width: 100%;
max-width: 210px;
border: 1px solid #dcdcdc;
height: 2.2em;
line-height: 2.2em;
border-radius: 0.3em;
padding: 0 0.5em;
box-sizing: border-box;
font-size: 1em;
}
.footer-bottom-container {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
gap: 1em;
}
.footer-text {
text-align: center;
color: gray;
font-size: 0.9em;
line-height: 1.5em;
}
.footer-text p,
.footer-text span {
margin: 0.3em 0;
}
.footer-info {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5em;
margin: 0.5em 0;
}
.footer-icon {
width: 1.2em;
height: 1.2em;
}
.footer-qr {
display: flex;
flex-direction: row;
justify-content: center;
gap: 1em;
width: 100%;
margin-bottom: 1em;
}
.footer-qr .qr-code-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5em;
width: auto;
}
.footer-qr img {
width: 80px;
height: auto;
}
.footer-qr span {
font-size: 0.9em;
text-align: center;
}
/* 响应式设计 */
@media (min-width: 768px) {
.navigation {
flex-wrap: nowrap;
}
.search-container {
margin-top: 0;
}
.footer-bottom-container {
flex-direction: column;
align-items: center;
gap: 1em;
}
.footer-text {
max-width: 100%;
text-align: center;
}
.footer-qr {
flex-direction: row;
justify-content: center;
gap: 1em;
}
.footer-qr .qr-code-container img {
width: 80px;
max-width: 80px;
}
}
@media (max-width: 767px) {
.home {
max-width: 100%;
}
.header-content {
font-size: 1em;
position: absolute;
bottom: 5%;
left: 5%;
}
.navigation {
flex-direction: column;
align-items: flex-start;
flex-wrap: wrap;
}
.nav-links {
flex-direction: row;
gap: 0.5em;
flex-wrap: wrap;
}
.nav-links a {
padding: 0.5em;
white-space: nowrap;
}
.search-container {
width: 100%;
margin-top: 0.5em;
}
.footer-bottom-container {
flex-direction: column;
align-items: center;
gap: 1em;
}
.friend-links {
flex-direction: column;
align-items: center;
}
.friend-link {
max-width: 100%;
}
.footer-text {
max-width: 100%;
text-align: center;
}
.footer-qr {
flex-direction: row;
justify-content: center;
gap: 0.5em;
}
.footer-qr .qr-code-container img {
width: 60px;
max-width: 80px;
}
}
</style>

755
src/views/MainView.vue Normal file
View File

@@ -0,0 +1,755 @@
<template>
<!-- 主页面 -->
<div class="main">
<!-- 宣传图 -->
<div class="study-image">
<img
src="http://www.fjeei.com/MenHu/JYZBWeb/images/zt_%E5%AD%A6%E4%B9%A0%E8%B4%AF%E5%BD%BB%E5%85%A8%E5%9B%BD%E6%95%99%E8%82%B2%E5%A4%A7%E4%BC%9A%E7%B2%BE%E7%A5%9E.png?v=1.2.2"
alt="study" loading="lazy" />
<img
src="http://www.fjeei.com/MenHu/JYZBWeb/images/zt_%E5%AD%A6%E4%B9%A0%E8%B4%AF%E5%BD%BB%E5%85%9A%E7%9A%84%E4%BA%8C%E5%8D%81%E5%B1%8A%E4%B8%89%E4%B8%AD%E5%85%A8%E4%BC%9A%E7%B2%BE%E7%A5%9E.png?v=1.2.2"
alt="study" loading="lazy" />
</div>
<!-- 最新新闻 -->
<div id="latest-news">
<h4 id="latest-news-title">{{ latest_news.title }}</h4>
<p id="latest-news-content">{{ truncatedText }}</p>
</div>
<!-- 轮播图和工作动态 -->
<div class="carousel-and-news">
<!-- 轮播图部分 -->
<div class="carousel">
<div class="carousel-slides" :style="{ transform: `translateX(-${currentSlide * 100}%)` }">
<div class="carousel-slide" v-for="(slide, index) in slides" :key="index">
<img :src="slide.image" :alt="`Slide ${index + 1}`" loading="lazy" />
<div class="carousel-caption">
<span class="carousel-caption-text">{{ slide.text }}</span>
</div>
</div>
</div>
<div class="carousel-indicators">
<span v-for="(slide, index) in slides" :key="index" class="indicator"
:class="{ active: index === currentSlide }" @click="goToSlide(index)"></span>
</div>
</div>
<!-- 工作动态部分 -->
<div class="news">
<div class="title">
<h4 class="title-name">工作动态</h4>
<span class="more">更多>></span>
</div>
<div class="line"></div>
<!-- 突出新闻列表中第一条新闻 -->
<div class="highlight-news">
<h4 class="highlight-news-title">{{ newsList[0].title }}</h4>
<div class="highlight-news-content">
<span class="highlight-news-text">{{ newsList[0].content }}</span>
<span class="highlight-news-detail">[详情]</span>
</div>
</div>
<div class="line"></div>
<!-- 其他新闻列表 -->
<div class="news-list">
<ul>
<li v-for="(news, index) in newsList" :key="index">
<div class="news-item">
<span class="news-title">{{ news.title }}</span>
<span class="news-date">{{ news.date }}</span>
</div>
</li>
</ul>
</div>
</div>
</div>
<!-- 专题专栏 -->
<div class="special-column">
<!-- 专题专栏文字 -->
<div class="special-column-text">
<span class="special-column-text-title">专题专栏</span>
</div>
<!-- 滚动展示图片 -->
<div class="special-column-images">
<div class="special-column-image" :style="{ transform: `translateX(${state.offsetX}px)` }"
v-for="(image, index) in state.images" :key="index">
<img :src="image" alt="Special Column Image" loading="lazy" />
</div>
</div>
</div>
<!-- 更多展示 政策文件 党建专栏 政务信息 -->
<div class="more-display">
<div class="more-display-item" v-for="(moreDisplay, index) in moreDisplayList" :key="index">
<div class="news">
<div class="title">
<h4 class="title-name">{{ moreDisplay.title_name }}</h4>
<span class="more">更多>></span>
</div>
<div class="line"></div>
<div class="news-list">
<ul>
<li v-for="(news, index) in moreDisplay.list" :key="index">
<div class="news-item">
<span class="news-title">{{ news.title }}</span>
<span class="news-date">{{ news.date }}</span>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { reactive, ref, computed, onMounted, onBeforeUnmount } from 'vue'
// 最新新闻
const latest_news = ref({
title: "习近平在福建考察时强调 扭住目标不放松 一张蓝图绘到底 在中国式现代化建设中奋勇争先",
content: "中共中央总书记、国家主席、中央军委主席习近平近日在福建考察时强调,福建要深入贯彻党的二十大和二十届三中全会精神,全面贯彻新发展理念,"
})
// 获取新闻内容的截断显示
const truncatedText = computed(() => {
const text = latest_news.value.content;
const maxLength = 60; // 调整截断长度
return text.length > maxLength ? text.substring(0, maxLength) + '...[详情]' : text;
});
// 轮播图
const slides = ref([
{ text: '福建省教育装备与基建中心工会举办“管理情绪 健康生活”心理健康专题讲座', image: 'http://www.fjeei.com/fjupload/Files/638391175715226093_DSC_6784.JPG' },
{ text: '2023年全省教育装备管理人员培训班圆满结束', image: 'http://www.fjeei.com/fjupload/Files/638375443823620292_%E5%9B%BE%E7%89%874.png' },
{ text: '2023年全省中小学图书馆创新应用案例评选活动圆满结束', image: 'http://www.fjeei.com/fjupload/Files/638372983341265839_488c51827667ef0a91a4cb470d3f98c.jpg' },
{ text: '省教育装备与基建中心赴厦门开展 “两大责任险”工作情况调研', image: 'http://www.fjeei.com/fjupload/Files/638354977326635940_%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20231112085700.jpg' },
{ text: '坚定文化自信,增强担当本领!省教育装备与基建中心第二党支部开展主题党日活动', image: 'http://www.fjeei.com/fjupload/Files/638314252419302903_1.jpg' },
{ text: '福建省教育装备与基建中心党总支与中国教育装备行业协会党支部开展共建联学(现场教育)活动', image: 'http://www.fjeei.com/fjupload/Files/638290768488449388_%E5%9B%BE%E7%89%874.png' },
]);
// 当前幻灯片索引
const currentSlide = ref(0);
let timer;
// 点击跳转
const goToSlide = (index) => {
currentSlide.value = index;
};
// 自动轮播
const startAutoPlay = () => {
timer = setInterval(() => {
currentSlide.value = (currentSlide.value + 1) % slides.value.length;
}, 3000); // 每3秒切换一次
};
// 新闻列表
const newsList = ref([
{ title: '我省开展“两大责任险”工作调研', content: '为做好新一轮校方责任保险及附加校方无过...', date: '2024-12-17' },
{ title: '2024年中小学图书馆创新应用案例遴选活动圆满结束', date: '2024-11-18' },
{ title: '关于2024年福建省中小学图书馆创新应用 案例遴选结果的公示', date: '2024-11-15' },
{ title: '关于召开“一带一路”国际教育装备创新发展大会暨教育装备博览会的通知', date: '2024-11-14' },
{ title: '关于2024年福建省中小学实验教学 说课活动获奖名单的公示', date: '2024-07-31' },
{ title: '福建省教育装备与基建中心在2024年书香校园阅读大会上作主旨报告', date: '2024-04-22' },
]);
// 专题专栏图片滚动
const originalImages = [
"http://www.fjeei.com/MenHu/JYZBWeb/images/zt_%E4%B9%A0%E8%BF%91%E5%B9%B3%E6%96%B0%E6%97%B6%E4%BB%A3%E4%B8%BB%E9%A2%98%E6%95%99%E8%82%B2.png?v=1.0.2",
"http://www.fjeei.com/MenHu/JYZBWeb/images/zt_%E8%AF%B4%E8%AF%BE%E5%B0%81%E9%9D%A2.jpg?v=1.0.2",
"http://www.fjeei.com/MenHu/JYZBWeb/images/zt_%E8%87%AA%E5%88%B6%E6%95%99%E5%85%B7%E5%B0%81%E9%9D%A2.jpg?v=1.0.2",
"http://www.fjeei.com/MenHu/JYZBWeb/images/zt_%E7%A6%8F%E5%BB%BA%E6%95%99%E8%82%B2%E5%9F%BA%E5%BB%BA%E7%AE%A1%E7%90%86%E5%B0%81%E9%9D%A2.jpg?v=1.0.2",
"http://www.fjeei.com/MenHu/JYZBWeb/images/zt_%E5%88%9B%E6%96%B0%E5%88%9B%E6%84%8F%E6%B4%BB%E5%8A%A8%E5%B0%81%E9%9D%A2.jpg?v=1.0.2"
];
// 复制图片列表以实现无缝滚动
const state = reactive({
images: [...originalImages, ...originalImages], // 复制一份图片
offsetX: 0,
});
// 图片滚动逻辑
const imageWidth = 250; // 每张图片的宽度包括padding
const totalImages = originalImages.length;
const totalWidth = imageWidth * totalImages;
// 当前的动画间隔
let interval = null;
// 开始滚动
const startScrolling = () => {
interval = setInterval(() => {
state.offsetX -= 1; // 向左滚动每次移动1px
if (Math.abs(state.offsetX) >= totalWidth) {
state.offsetX = 0; // 重置到起始位置
}
}, 15); // 每15毫秒移动一次
};
// 停止滚动
const stopScrolling = () => {
clearInterval(interval);
};
// 政策文件列表
const policyList = ref([
{ title: '教育部等五部门关于完善安全事故处理机制维护学校教育教学秩序的意见', date: '2024-07-10' },
{ title: '国务院办公厅关于加强中小学幼儿园 安全风险防控体系建设的意见国办发201735号', date: '2024-04-12' },
{ title: '福建省教育装备与基建中心关于转发《中小学实验教学基本目录》的通知', date: '2024-01-04' },
{ title: '教育部教育技术与资源发展中心(中央电化教育馆)关于发布《中小学实验教学基本目录》的通知', date: '2023-12-13' },
{ title: '《中小学实验教学基本目录2023年版》解读文章见', date: '2023-12-13' },
{ title: '教育部等八部门关于印发《新时代 基础教育强师计划》的通知', date: '2022-04-19' },
]);
// 党建专栏列表
const partyBuildingList = ref([
{ title: '福建省教育装备与基建中心召开党纪学习教育动员部署会', date: '2024-04-16' },
{ title: '我们的节日·拗九节 | 省教育装备与基建中心组织党员干部参与社区志愿服务', date: '2024-03-11' },
{ title: '福建省教育装备与基建中心召开2023年政治生态分析研判会暨党风廉政建设工作会', date: '2024-01-19' },
{ title: '走进船政 | 福建省教育装备与基建中心第二党支部开展“廉洁驻我心 清风再起航”主题党日活动', date: '2023-12-25' },
{ title: '看历史沧桑,品清廉之美,中心青年干部和党员走进冶山春秋园', date: '2023-11-28' },
{ title: '坚定文化自信,增强担当本领!省教育装备与基建中心第二党支部开展主题党日活动', date: '2023-09-27' },
]);
// 政务信息列表
const governmentInfoList = ref([
{ title: '关于召开“一带一路”国际教育装备创新发展大会暨教育装备博览会的通知', date: '2024-11-14' },
{ title: '关于开展2025年福建教育装备新技术新产品 遴选推广活动的公告知', date: '2024-10-18' },
{ title: '2023 年度福建省教育装备与基建中心决算公开', date: '2024-09-10' },
{ title: '2024年度福建省教育装备与基建中心预算公开', date: '2024-03-11' },
{ title: '关于2023年福建省中小学图书馆创新应用 案例评选结果的公示', date: '2023-12-05' },
{ title: '2022 年度福建省教育装备与基建中心部门决算', date: '2023-09-15' },
]);
// 更多展示数据
const moreDisplayList = ref([
{ class: 'policy', title_name: '政策文件', list: policyList },
{ class: 'party-building', title_name: '党建专栏', list: partyBuildingList },
{ class: 'government-info', title_name: '政务信息', list: governmentInfoList }
]);
// 生命周期钩子
onMounted(() => {
// 启动自动轮播
startAutoPlay();
// 启动图片滚动
startScrolling();
});
onBeforeUnmount(() => {
// 在组件卸载前清除定时器
clearInterval(timer);
stopScrolling();
});
</script>
<style scoped>
.main {
margin-top: 20px;
display: flex;
flex-direction: column;
gap: 20px;
}
/* 宣传图 */
.study-image {
display: flex;
justify-content: space-between;
gap: 20px;
flex-wrap: wrap;
/* 允许换行以适应不同屏幕尺寸 */
overflow: hidden;
}
.study-image img {
flex: 1 1 48%;
/* 使每张图在大屏幕上占据约一半宽度 */
max-width: 48%;
height: auto;
/* 保持图像比例 */
border-radius: 8px;
/* 添加圆角 */
box-sizing: border-box;
}
@media (max-width: 768px) {
.study-image img {
flex: 1 1 100%;
max-width: 100%;
/* 在小屏幕上占满全宽 */
}
}
/* 最新新闻 */
#latest-news {
text-align: center;
background-color: #66666620;
padding: 20px;
border-radius: 8px;
}
#latest-news-title {
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 10px;
color: #1658A0;
}
#latest-news-content {
font-size: 1rem;
line-height: 1.5;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 轮播图和工作动态 */
.carousel-and-news {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
/* 轮播图 */
.carousel {
position: relative;
flex: 1 1 48%;
min-width: 300px;
height: 30%;
overflow: hidden;
border-radius: 8px;
}
.carousel-slides {
display: flex;
transition: transform 0.5s ease;
}
.carousel-slide {
min-width: 100%;
position: relative;
}
.carousel-caption {
position: absolute;
height: 40px;
bottom: 0px;
left: 0;
width: 97%;
padding: 10px;
background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent);
/* 渐变背景 */
z-index: 10;
}
.carousel-caption-text {
color: #fff;
font-size: 1rem;
text-align: center;
white-space: normal;
/* 允许多行显示 */
overflow: hidden;
text-overflow: ellipsis;
padding: 0 10px;
display: -webkit-box;
/* 兼容多行省略 */
-webkit-line-clamp: 1;
/* 显示最多2行 */
-webkit-box-orient: vertical;
}
.carousel-slide img {
width: 100%;
height: 100%;
object-fit: cover;
}
.carousel-indicators {
position: absolute;
bottom: 12px;
left: 50%;
transform: translateX(-50%);
/* 居中指示器 */
display: flex;
gap: 5px;
}
.indicator {
width: 10px;
height: 10px;
background-color: #9c9c9c;
border-radius: 50%;
cursor: pointer;
}
.indicator.active {
background-color: white;
}
/* 工作动态 */
.news {
flex: 1 1 48%;
min-width: 300px;
}
.news .title {
display: flex;
justify-content: space-between;
align-items: center;
}
.title-name {
margin: 0;
padding-left: 10px;
border-left: 5px solid #ff0000;
font-size: 1.1rem;
}
.more {
font-size: 0.9rem;
color: grey;
cursor: pointer;
}
.line {
border-bottom: 1px solid #e0e0e0;
margin: 10px 0;
}
.highlight-news-title {
color: red;
margin: 15px 0;
font-size: 1.5rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.highlight-news-content {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.9rem;
margin-bottom: 15px;
}
.highlight-news-text {
flex: 1;
margin-right: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.highlight-news-detail {
color: red;
cursor: pointer;
white-space: nowrap;
}
/* 新闻列表 */
.news-list ul {
width: 100%;
display: flex;
flex-direction: column;
list-style: none;
padding-left: 0;
margin-top: 16px;
margin-bottom: 0;
}
.news-list ul li {
display: flex;
align-items: center;
margin-bottom: 5px;
}
.news-list ul li::before {
content: "";
display: inline-block;
width: 5px;
height: 5px;
background-color: #cccccc;
margin-right: 10px;
flex-shrink: 0;
flex-grow: 0;
}
.news-item {
display: flex;
/* 使用Flex布局 */
justify-content: space-between;
align-items: center;
padding: 10px 0;
width: 97%;
}
.news-title {
font-size: 1.1rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
/* 占据尽可能多的空间 */
margin-right: 10px;
}
.news-title:hover {
color: red;
cursor: pointer;
}
/* 新闻日期 */
.news-date {
font-size: 0.9rem;
color: gray;
flex-shrink: 0;
white-space: nowrap;
text-align: right;
/* 日期靠右 */
}
/* 更多展示 */
.more-display {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.more-display-item {
flex: 1 1 30%;
min-width: 250px;
}
/* 专题专栏 */
.special-column {
display: flex;
align-items: center;
background-color: #f5f5f5;
padding: 10px;
border-radius: 8px;
flex-wrap: wrap;
}
.special-column-text {
flex: 0 0 100px;
background-color: red;
color: white;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
margin-right: 20px;
height: 100px;
text-align: center;
}
.special-column-text-title {
font-size: 1.2rem;
line-height: 1.2;
}
.special-column-images {
flex: 1;
display: flex;
overflow: hidden;
position: relative;
width: 100%;
height: 80px;
/* 固定高度以匹配图片高度 */
}
.special-column-image {
flex: 0 0 240px;
/* 固定宽度 */
margin-right: 10px;
}
.special-column-images img {
width: 100%;
height: 100%;
object-fit: fill;
/* 保持比例并填满容器 */
border-radius: 4px;
}
/* 响应式布局 */
@media (max-width: 1200px) {
/* 宣传图在小屏幕时显示两张图 */
.study-image {
flex-direction: row;
flex-wrap: wrap;
}
.carousel-and-news {
flex-direction: column;
align-items: stretch;
/* 确保子元素拉伸至全宽 */
}
.carousel,
.news {
flex: 1 1 100%;
width: 100%;
/* 确保宽度为100% */
margin: 0;
/* 移除可能的外边距 */
}
.special-column {
flex-direction: column;
align-items: center;
}
.special-column-text {
margin-right: 0;
margin-bottom: 10px;
width: 100%;
height: auto;
}
.special-column-text-title {
font-size: 1.1rem;
}
.special-column-images img {
max-width: 200px;
height: 70px;
}
.more-display {
flex-direction: column;
align-items: stretch;
/* 确保子元素拉伸至全宽 */
}
.more-display-item {
flex: 1 1 100%;
width: 100%;
/* 确保宽度为100% */
margin: 0;
/* 移除可能的外边距 */
}
}
@media (max-width: 768px) {
.study-image {
width: 100%;
flex-direction: column;
align-items: center;
flex-wrap: wrap;
/* 允许换行 */
}
.study-image img {
width: 100%;
flex: 1 1 100%;
margin-bottom: 10px;
}
#latest-news-title {
font-size: 1.3rem;
}
#latest-news-content {
font-size: 0.9rem;
white-space: nowrap;
text-align: center;
}
.carousel {
height: 200px;
}
.carousel-caption-text {
font-size: 1rem;
}
.news .title-name,
.more-display-item .news .title-name {
font-size: 1rem;
}
.highlight-news-title {
font-size: 1rem;
}
.highlight-news-content {
flex-direction: row;
align-items: center;
}
.highlight-news-text,
.highlight-news-detail {
white-space: nowrap;
}
.news-title,
.more-display-item .news-title {
font-size: 0.8rem;
}
.news-date,
.more-display-item .news-date {
font-size: 0.8rem;
}
}
@media (max-width: 480px) {
.carousel {
height: 150px;
}
.carousel-caption-text {
font-size: 0.8rem;
}
.highlight-news-title {
font-size: 0.9rem;
}
.highlight-news-content {
font-size: 0.8rem;
}
.news-title,
.more-display-item .news-title {
font-size: 0.7rem;
}
.news-date,
.more-display-item .news-date {
font-size: 0.7rem;
}
.special-column-text {
font-size: 1rem;
height: auto;
padding: 10px 0;
}
.special-column-images img {
max-width: 180px;
height: 60px;
}
.special-column {
padding: 5px;
}
.more-display-item {
padding: 10px 0;
flex: 1 1 100%;
max-width: 100%;
width: 100%;
/* 确保宽度为100% */
margin: 0;
/* 移除可能的外边距 */
}
}
</style>

18
vite.config.js Normal file
View File

@@ -0,0 +1,18 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})