feat: update backend
This commit is contained in:
4
frontend/admin/.env
Normal file
4
frontend/admin/.env
Normal file
@@ -0,0 +1,4 @@
|
||||
NODE_ENV=development
|
||||
VUE_APP_API_URL=/api
|
||||
VUE_APP_TITLE=趣云CMS(开发模式)
|
||||
|
||||
@@ -1,5 +1,32 @@
|
||||
# Vue 3 + Vite
|
||||
# 趣云管理后台
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
这是趣云管理后台的前端应用,使用 Vue 3 开发。
|
||||
|
||||
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
|
||||
## 项目设置
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 启动开发服务器
|
||||
npm run dev
|
||||
|
||||
# 构建生产版本
|
||||
npm run build
|
||||
|
||||
# 预览生产构建
|
||||
npm run preview
|
||||
```
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 文章管理
|
||||
- 媒体库管理
|
||||
- 响应式设计
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Vue 3
|
||||
- Vue Router
|
||||
- Tailwind CSS
|
||||
- Vite 构建工具
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
"": {
|
||||
"name": "admin",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.0.15",
|
||||
"@tailwindcss/vite": "^4.0.17",
|
||||
"axios": "^1.8.4",
|
||||
"daisyui": "^5.0.9",
|
||||
"tailwindcss": "^4.0.15",
|
||||
"tailwindcss": "^4.0.17",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "4",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
@@ -116,33 +118,33 @@
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.37.0", "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.37.0.tgz", { "os": "win32", "cpu": "x64" }, "sha512-LWbXUBwn/bcLx2sSsqy7pK5o+Nr+VCoRoAohfJ5C/aBio9nfJmGQqHAhU6pwxV/RmyTk5AqdySma7uwWGlmeuA=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.0.15", "https://registry.npmmirror.com/@tailwindcss/node/-/node-4.0.15.tgz", { "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "tailwindcss": "4.0.15" } }, "sha512-IODaJjNmiasfZX3IoS+4Em3iu0fD2HS0/tgrnkYfW4hyUor01Smnr5eY3jc4rRgaTDrJlDmBTHbFO0ETTDaxWA=="],
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.0.17", "https://registry.npmmirror.com/@tailwindcss/node/-/node-4.0.17.tgz", { "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "tailwindcss": "4.0.17" } }, "sha512-LIdNwcqyY7578VpofXyqjH6f+3fP4nrz7FBLki5HpzqjYfXdF2m/eW18ZfoKePtDGg90Bvvfpov9d2gy5XVCbg=="],
|
||||
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.0.15", "https://registry.npmmirror.com/@tailwindcss/oxide/-/oxide-4.0.15.tgz", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.0.15", "@tailwindcss/oxide-darwin-arm64": "4.0.15", "@tailwindcss/oxide-darwin-x64": "4.0.15", "@tailwindcss/oxide-freebsd-x64": "4.0.15", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.15", "@tailwindcss/oxide-linux-arm64-gnu": "4.0.15", "@tailwindcss/oxide-linux-arm64-musl": "4.0.15", "@tailwindcss/oxide-linux-x64-gnu": "4.0.15", "@tailwindcss/oxide-linux-x64-musl": "4.0.15", "@tailwindcss/oxide-win32-arm64-msvc": "4.0.15", "@tailwindcss/oxide-win32-x64-msvc": "4.0.15" } }, "sha512-e0uHrKfPu7JJGMfjwVNyt5M0u+OP8kUmhACwIRlM+JNBuReDVQ63yAD1NWe5DwJtdaHjugNBil76j+ks3zlk6g=="],
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.0.17", "https://registry.npmmirror.com/@tailwindcss/oxide/-/oxide-4.0.17.tgz", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.0.17", "@tailwindcss/oxide-darwin-arm64": "4.0.17", "@tailwindcss/oxide-darwin-x64": "4.0.17", "@tailwindcss/oxide-freebsd-x64": "4.0.17", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.17", "@tailwindcss/oxide-linux-arm64-gnu": "4.0.17", "@tailwindcss/oxide-linux-arm64-musl": "4.0.17", "@tailwindcss/oxide-linux-x64-gnu": "4.0.17", "@tailwindcss/oxide-linux-x64-musl": "4.0.17", "@tailwindcss/oxide-win32-arm64-msvc": "4.0.17", "@tailwindcss/oxide-win32-x64-msvc": "4.0.17" } }, "sha512-B4OaUIRD2uVrULpAD1Yksx2+wNarQr2rQh65nXqaqbLY1jCd8fO+3KLh/+TH4Hzh2NTHQvgxVbPdUDOtLk7vAw=="],
|
||||
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.0.15", "https://registry.npmmirror.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.15.tgz", { "os": "android", "cpu": "arm64" }, "sha512-EBuyfSKkom7N+CB3A+7c0m4+qzKuiN0WCvzPvj5ZoRu4NlQadg/mthc1tl5k9b5ffRGsbDvP4k21azU4VwVk3Q=="],
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.0.17", "https://registry.npmmirror.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.17.tgz", { "os": "android", "cpu": "arm64" }, "sha512-3RfO0ZK64WAhop+EbHeyxGThyDr/fYhxPzDbEQjD2+v7ZhKTb2svTWy+KK+J1PHATus2/CQGAGp7pHY/8M8ugg=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.0.15", "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.15.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-ObVAnEpLepMhV9VoO0JSit66jiN5C4YCqW3TflsE9boo2Z7FIjV80RFbgeL2opBhtxbaNEDa6D0/hq/EP03kgQ=="],
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.0.17", "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.17.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-e1uayxFQCCDuzTk9s8q7MC5jFN42IY7nzcr5n0Mw/AcUHwD6JaBkXnATkD924ZsHyPDvddnusIEvkgLd2CiREg=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.0.15", "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.15.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-IElwoFhUinOr9MyKmGTPNi1Rwdh68JReFgYWibPWTGuevkHkLWKEflZc2jtI5lWZ5U9JjUnUfnY43I4fEXrc4g=="],
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.0.17", "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.17.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-d6z7HSdOKfXQ0HPlVx1jduUf/YtBuCCtEDIEFeBCzgRRtDsUuRtofPqxIVaSCUTOk5+OfRLonje6n9dF6AH8wQ=="],
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.0.15", "https://registry.npmmirror.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.15.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-6BLLqyx7SIYRBOnTZ8wgfXANLJV5TQd3PevRJZp0vn42eO58A2LykRKdvL1qyPfdpmEVtF+uVOEZ4QTMqDRAWA=="],
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.0.17", "https://registry.npmmirror.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.17.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-EjrVa6lx3wzXz3l5MsdOGtYIsRjgs5Mru6lDv4RuiXpguWeOb3UzGJ7vw7PEzcFadKNvNslEQqoAABeMezprxQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.0.15", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.15.tgz", { "os": "linux", "cpu": "arm" }, "sha512-Zy63EVqO9241Pfg6G0IlRIWyY5vNcWrL5dd2WAKVJZRQVeolXEf1KfjkyeAAlErDj72cnyXObEZjMoPEKHpdNw=="],
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.0.17", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.17.tgz", { "os": "linux", "cpu": "arm" }, "sha512-65zXfCOdi8wuaY0Ye6qMR5LAXokHYtrGvo9t/NmxvSZtCCitXV/gzJ/WP5ksXPhff1SV5rov0S+ZIZU+/4eyCQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.0.15", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.15.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-2NemGQeaTbtIp1Z2wyerbVEJZTkAWhMDOhhR5z/zJ75yMNf8yLnE+sAlyf6yGDNr+1RqvWrRhhCFt7i0CIxe4Q=="],
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.0.17", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.17.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-+aaq6hJ8ioTdbJV5IA1WjWgLmun4T7eYLTvJIToiXLHy5JzUERRbIZjAcjgK9qXMwnvuu7rqpxzej+hGoEcG5g=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.0.15", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.15.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-342GVnhH/6PkVgKtEzvNVuQ4D+Q7B7qplvuH20Cfz9qEtydG6IQczTZ5IT4JPlh931MG1NUCVxg+CIorr1WJyw=="],
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.0.17", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.17.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-/FhWgZCdUGAeYHYnZKekiOC0aXFiBIoNCA0bwzkICiMYS5Rtx2KxFfMUXQVnl4uZRblG5ypt5vpPhVaXgGk80w=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.0.15", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.15.tgz", { "os": "linux", "cpu": "x64" }, "sha512-g76GxlKH124RuGqacCEFc2nbzRl7bBrlC8qDQMiUABkiifDRHOIUjgKbLNG4RuR9hQAD/MKsqZ7A8L08zsoBrw=="],
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.0.17", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.17.tgz", { "os": "linux", "cpu": "x64" }, "sha512-gELJzOHK6GDoIpm/539Golvk+QWZjxQcbkKq9eB2kzNkOvrP0xc5UPgO9bIMNt1M48mO8ZeNenCMGt6tfkvVBg=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.0.15", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.15.tgz", { "os": "linux", "cpu": "x64" }, "sha512-Gg/Y1XrKEvKpq6WeNt2h8rMIKOBj/W3mNa5NMvkQgMC7iO0+UNLrYmt6zgZufht66HozNpn+tJMbbkZ5a3LczA=="],
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.0.17", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.17.tgz", { "os": "linux", "cpu": "x64" }, "sha512-68NwxcJrZn94IOW4TysMIbYv5AlM6So1luTlbYUDIGnKma1yTFGBRNEJ+SacJ3PZE2rgcTBNRHX1TB4EQ/XEHw=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.0.15", "https://registry.npmmirror.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.15.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-7QtSSJwYZ7ZK1phVgcNZpuf7c7gaCj8Wb0xjliligT5qCGCp79OV2n3SJummVZdw4fbTNKUOYMO7m1GinppZyA=="],
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.0.17", "https://registry.npmmirror.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.17.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-AkBO8efP2/7wkEXkNlXzRD4f/7WerqKHlc6PWb5v0jGbbm22DFBLbIM19IJQ3b+tNewQZa+WnPOaGm0SmwMNjw=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.0.15", "https://registry.npmmirror.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.15.tgz", { "os": "win32", "cpu": "x64" }, "sha512-JQ5H+5MLhOjpgNp6KomouE0ZuKmk3hO5h7/ClMNAQ8gZI2zkli3IH8ZqLbd2DVfXDbdxN2xvooIEeIlkIoSCqw=="],
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.0.17", "https://registry.npmmirror.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.17.tgz", { "os": "win32", "cpu": "x64" }, "sha512-7/DTEvXcoWlqX0dAlcN0zlmcEu9xSermuo7VNGX9tJ3nYMdo735SHvbrHDln1+LYfF6NhJ3hjbpbjkMOAGmkDg=="],
|
||||
|
||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.0.15", "https://registry.npmmirror.com/@tailwindcss/vite/-/vite-4.0.15.tgz", { "dependencies": { "@tailwindcss/node": "4.0.15", "@tailwindcss/oxide": "4.0.15", "lightningcss": "1.29.2", "tailwindcss": "4.0.15" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-JRexava80NijI8cTcLXNM3nQL5A0ptTHI8oJLLe8z1MpNB6p5J4WCdJJP8RoyHu8/eB1JzEdbpH86eGfbuaezQ=="],
|
||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.0.17", "https://registry.npmmirror.com/@tailwindcss/vite/-/vite-4.0.17.tgz", { "dependencies": { "@tailwindcss/node": "4.0.17", "@tailwindcss/oxide": "4.0.17", "lightningcss": "1.29.2", "tailwindcss": "4.0.17" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-HJbBYDlDVg5cvYZzECb6xwc1IDCEM3uJi3hEZp3BjZGCNGJcTsnCpan+z+VMW0zo6gR0U6O6ElqU1OoZ74Dhww=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.6", "https://registry.npmmirror.com/@types/estree/-/estree-1.0.6.tgz", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
|
||||
|
||||
@@ -156,6 +158,8 @@
|
||||
|
||||
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.13", "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", { "dependencies": { "@vue/compiler-dom": "3.5.13", "@vue/shared": "3.5.13" } }, "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA=="],
|
||||
|
||||
"@vue/devtools-api": ["@vue/devtools-api@6.6.4", "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="],
|
||||
|
||||
"@vue/reactivity": ["@vue/reactivity@3.5.13", "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.13.tgz", { "dependencies": { "@vue/shared": "3.5.13" } }, "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg=="],
|
||||
|
||||
"@vue/runtime-core": ["@vue/runtime-core@3.5.13", "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.13.tgz", { "dependencies": { "@vue/reactivity": "3.5.13", "@vue/shared": "3.5.13" } }, "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw=="],
|
||||
@@ -166,24 +170,62 @@
|
||||
|
||||
"@vue/shared": ["@vue/shared@3.5.13", "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.13.tgz", {}, "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ=="],
|
||||
|
||||
"asynckit": ["asynckit@0.4.0", "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
|
||||
"axios": ["axios@1.8.4", "https://registry.npmmirror.com/axios/-/axios-1.8.4.tgz", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"combined-stream": ["combined-stream@1.0.8", "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"daisyui": ["daisyui@5.0.9", "https://registry.npmmirror.com/daisyui/-/daisyui-5.0.9.tgz", {}, "sha512-RsaehHh45f+0shWgZZaOY09/8eOae2voRsqJCD71j9yrnYgcke0Nj5ys0ZxrW4SPcc4+q96kWyJu0Z8P1zZdoA=="],
|
||||
|
||||
"delayed-stream": ["delayed-stream@1.0.0", "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.0.3", "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.0.3.tgz", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.18.1", "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="],
|
||||
|
||||
"entities": ["entities@4.5.0", "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||
|
||||
"esbuild": ["esbuild@0.25.1", "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.1.tgz", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.1", "@esbuild/android-arm": "0.25.1", "@esbuild/android-arm64": "0.25.1", "@esbuild/android-x64": "0.25.1", "@esbuild/darwin-arm64": "0.25.1", "@esbuild/darwin-x64": "0.25.1", "@esbuild/freebsd-arm64": "0.25.1", "@esbuild/freebsd-x64": "0.25.1", "@esbuild/linux-arm": "0.25.1", "@esbuild/linux-arm64": "0.25.1", "@esbuild/linux-ia32": "0.25.1", "@esbuild/linux-loong64": "0.25.1", "@esbuild/linux-mips64el": "0.25.1", "@esbuild/linux-ppc64": "0.25.1", "@esbuild/linux-riscv64": "0.25.1", "@esbuild/linux-s390x": "0.25.1", "@esbuild/linux-x64": "0.25.1", "@esbuild/netbsd-arm64": "0.25.1", "@esbuild/netbsd-x64": "0.25.1", "@esbuild/openbsd-arm64": "0.25.1", "@esbuild/openbsd-x64": "0.25.1", "@esbuild/sunos-x64": "0.25.1", "@esbuild/win32-arm64": "0.25.1", "@esbuild/win32-ia32": "0.25.1", "@esbuild/win32-x64": "0.25.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ=="],
|
||||
|
||||
"estree-walker": ["estree-walker@2.0.2", "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"follow-redirects": ["follow-redirects@1.15.9", "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
|
||||
|
||||
"form-data": ["form-data@4.0.2", "https://registry.npmmirror.com/form-data/-/form-data-4.0.2.tgz", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" } }, "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"has-tostringtag": ["has-tostringtag@1.0.2", "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"jiti": ["jiti@2.4.2", "https://registry.npmmirror.com/jiti/-/jiti-2.4.2.tgz", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.29.2", "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.29.2.tgz", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.29.2", "lightningcss-darwin-x64": "1.29.2", "lightningcss-freebsd-x64": "1.29.2", "lightningcss-linux-arm-gnueabihf": "1.29.2", "lightningcss-linux-arm64-gnu": "1.29.2", "lightningcss-linux-arm64-musl": "1.29.2", "lightningcss-linux-x64-gnu": "1.29.2", "lightningcss-linux-x64-musl": "1.29.2", "lightningcss-win32-arm64-msvc": "1.29.2", "lightningcss-win32-x64-msvc": "1.29.2" } }, "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA=="],
|
||||
@@ -210,22 +252,32 @@
|
||||
|
||||
"magic-string": ["magic-string@0.30.17", "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.17.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"mime-db": ["mime-db@1.52.0", "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"mime-types": ["mime-types@2.1.35", "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"postcss": ["postcss@8.5.3", "https://registry.npmmirror.com/postcss/-/postcss-8.5.3.tgz", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
|
||||
|
||||
"proxy-from-env": ["proxy-from-env@1.1.0", "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||
|
||||
"rollup": ["rollup@4.37.0", "https://registry.npmmirror.com/rollup/-/rollup-4.37.0.tgz", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.37.0", "@rollup/rollup-android-arm64": "4.37.0", "@rollup/rollup-darwin-arm64": "4.37.0", "@rollup/rollup-darwin-x64": "4.37.0", "@rollup/rollup-freebsd-arm64": "4.37.0", "@rollup/rollup-freebsd-x64": "4.37.0", "@rollup/rollup-linux-arm-gnueabihf": "4.37.0", "@rollup/rollup-linux-arm-musleabihf": "4.37.0", "@rollup/rollup-linux-arm64-gnu": "4.37.0", "@rollup/rollup-linux-arm64-musl": "4.37.0", "@rollup/rollup-linux-loongarch64-gnu": "4.37.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.37.0", "@rollup/rollup-linux-riscv64-gnu": "4.37.0", "@rollup/rollup-linux-riscv64-musl": "4.37.0", "@rollup/rollup-linux-s390x-gnu": "4.37.0", "@rollup/rollup-linux-x64-gnu": "4.37.0", "@rollup/rollup-linux-x64-musl": "4.37.0", "@rollup/rollup-win32-arm64-msvc": "4.37.0", "@rollup/rollup-win32-ia32-msvc": "4.37.0", "@rollup/rollup-win32-x64-msvc": "4.37.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-iAtQy/L4QFU+rTJ1YUjXqJOJzuwEghqWzCEYD2FEghT7Gsy1VdABntrO4CLopA5IkflTyqNiLNwPcOJ3S7UKLg=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.0.15", "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.0.15.tgz", {}, "sha512-6ZMg+hHdMJpjpeCCFasX7K+U615U9D+7k5/cDK/iRwl6GptF24+I/AbKgOnXhVKePzrEyIXutLv36n4cRsq3Sg=="],
|
||||
"tailwindcss": ["tailwindcss@4.0.17", "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.0.17.tgz", {}, "sha512-OErSiGzRa6rLiOvaipsDZvLMSpsBZ4ysB4f0VKGXUrjw2jfkJRd6kjRKV2+ZmTCNvwtvgdDam5D7w6WXsdLJZw=="],
|
||||
|
||||
"tapable": ["tapable@2.2.1", "https://registry.npmmirror.com/tapable/-/tapable-2.2.1.tgz", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="],
|
||||
|
||||
"vite": ["vite@6.2.2", "https://registry.npmmirror.com/vite/-/vite-6.2.2.tgz", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ=="],
|
||||
|
||||
"vue": ["vue@3.5.13", "https://registry.npmmirror.com/vue/-/vue-3.5.13.tgz", { "dependencies": { "@vue/compiler-dom": "3.5.13", "@vue/compiler-sfc": "3.5.13", "@vue/runtime-dom": "3.5.13", "@vue/server-renderer": "3.5.13", "@vue/shared": "3.5.13" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ=="],
|
||||
|
||||
"vue-router": ["vue-router@4.5.0", "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.0.tgz", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.2.0" } }, "sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Admin</title>
|
||||
<link rel="stylesheet" href="/src/style.css" />
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>趣云管理后台</title>
|
||||
<link rel="icon" href="/favicon.ico" type="image/x-icon">
|
||||
<meta name="description" content="趣云内容管理系统后台">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
414
frontend/admin/medias.html
Normal file
414
frontend/admin/medias.html
Normal file
@@ -0,0 +1,414 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Media Management</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<link href="//at.alicdn.com/t/font_2184398_zfvo7yhbkl9.css" rel="stylesheet">
|
||||
<style>
|
||||
.upload-area {
|
||||
border: 2px dashed #cbd5e0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.upload-area.active {
|
||||
border-color: #4299e1;
|
||||
background-color: rgba(66, 153, 225, 0.1);
|
||||
}
|
||||
.table-row {
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
.table-row:hover {
|
||||
background-color: rgba(237, 242, 247, 0.7);
|
||||
}
|
||||
.pagination-item.active {
|
||||
background-color: #4299e1;
|
||||
color: white;
|
||||
}
|
||||
.media-table th {
|
||||
position: relative;
|
||||
}
|
||||
.media-table th:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 25%;
|
||||
height: 50%;
|
||||
width: 1px;
|
||||
background-color: #e2e8f0;
|
||||
}
|
||||
.media-table th:last-child:after {
|
||||
display: none;
|
||||
}
|
||||
.preview-thumbnail {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 min-h-screen">
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-semibold text-gray-800">Media Library</h1>
|
||||
<div class="flex space-x-2">
|
||||
<div class="relative">
|
||||
<input type="text" placeholder="Search media..." class="pl-10 pr-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<i class="iconfont icon-search absolute left-3 top-3 text-gray-400"></i>
|
||||
</div>
|
||||
<select class="border rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option>All media</option>
|
||||
<option>Images</option>
|
||||
<option>Videos</option>
|
||||
<option>Documents</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload Area -->
|
||||
<div id="upload-area" class="upload-area rounded-lg p-10 mb-8 flex flex-col items-center justify-center cursor-pointer">
|
||||
<i class="iconfont icon-upload text-5xl text-blue-500 mb-4"></i>
|
||||
<p class="text-gray-600 text-center mb-2">Drag and drop files here or click to browse</p>
|
||||
<p class="text-gray-400 text-sm text-center">Supports: JPG, PNG, GIF, MP4, PDF</p>
|
||||
<input type="file" id="file-input" multiple class="hidden">
|
||||
</div>
|
||||
|
||||
<!-- Media Table Section -->
|
||||
<div class="mb-12 bg-white rounded-lg shadow overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full media-table">
|
||||
<thead class="bg-gray-50 border-b border-gray-200">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<div class="flex items-center">
|
||||
File Name
|
||||
<button class="ml-1 text-gray-400 hover:text-gray-600">
|
||||
<i class="iconfont icon-sort"></i>
|
||||
</button>
|
||||
</div>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<div class="flex items-center">
|
||||
Upload Time
|
||||
<button class="ml-1 text-gray-400 hover:text-gray-600">
|
||||
<i class="iconfont icon-sort"></i>
|
||||
</button>
|
||||
</div>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<div class="flex items-center">
|
||||
File Size
|
||||
<button class="ml-1 text-gray-400 hover:text-gray-600">
|
||||
<i class="iconfont icon-sort"></i>
|
||||
</button>
|
||||
</div>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<div class="flex items-center">
|
||||
File Type
|
||||
<button class="ml-1 text-gray-400 hover:text-gray-600">
|
||||
<i class="iconfont icon-sort"></i>
|
||||
</button>
|
||||
</div>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<!-- Image file row -->
|
||||
<tr class="table-row">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 h-10 w-10 mr-3">
|
||||
<img class="preview-thumbnail" src="https://via.placeholder.com/300x225" alt="Image preview">
|
||||
</div>
|
||||
<div class="text-sm font-medium text-gray-900">sunset-beach.jpg</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
Today, 10:30 AM
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
2.4 MB
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800">
|
||||
Image
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center text-sm font-medium">
|
||||
<div class="flex justify-center space-x-2">
|
||||
<button class="p-1 text-blue-600 hover:text-blue-900 focus:outline-none" title="Preview">
|
||||
<i class="iconfont icon-eye"></i>
|
||||
</button>
|
||||
<button class="p-1 text-gray-600 hover:text-gray-900 focus:outline-none" title="Download">
|
||||
<i class="iconfont icon-download"></i>
|
||||
</button>
|
||||
<button class="p-1 text-red-600 hover:text-red-900 focus:outline-none" title="Delete">
|
||||
<i class="iconfont icon-delete"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- PDF file row -->
|
||||
<tr class="table-row">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 h-10 w-10 mr-3 bg-gray-100 rounded flex items-center justify-center">
|
||||
<i class="iconfont icon-file-pdf text-2xl text-red-500"></i>
|
||||
</div>
|
||||
<div class="text-sm font-medium text-gray-900">presentation.pdf</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
Yesterday, 3:45 PM
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
4.8 MB
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">
|
||||
PDF
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center text-sm font-medium">
|
||||
<div class="flex justify-center space-x-2">
|
||||
<button class="p-1 text-blue-600 hover:text-blue-900 focus:outline-none" title="Preview">
|
||||
<i class="iconfont icon-eye"></i>
|
||||
</button>
|
||||
<button class="p-1 text-gray-600 hover:text-gray-900 focus:outline-none" title="Download">
|
||||
<i class="iconfont icon-download"></i>
|
||||
</button>
|
||||
<button class="p-1 text-red-600 hover:text-red-900 focus:outline-none" title="Delete">
|
||||
<i class="iconfont icon-delete"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Video file row -->
|
||||
<tr class="table-row">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 h-10 w-10 mr-3 bg-gray-100 rounded flex items-center justify-center">
|
||||
<i class="iconfont icon-file-video text-2xl text-blue-500"></i>
|
||||
</div>
|
||||
<div class="text-sm font-medium text-gray-900">promo_video.mp4</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
Aug 28, 2023
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
24.8 MB
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-purple-100 text-purple-800">
|
||||
Video
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center text-sm font-medium">
|
||||
<div class="flex justify-center space-x-2">
|
||||
<button class="p-1 text-blue-600 hover:text-blue-900 focus:outline-none" title="Preview">
|
||||
<i class="iconfont icon-play"></i>
|
||||
</button>
|
||||
<button class="p-1 text-gray-600 hover:text-gray-900 focus:outline-none" title="Download">
|
||||
<i class="iconfont icon-download"></i>
|
||||
</button>
|
||||
<button class="p-1 text-red-600 hover:text-red-900 focus:outline-none" title="Delete">
|
||||
<i class="iconfont icon-delete"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Document file row -->
|
||||
<tr class="table-row">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 h-10 w-10 mr-3 bg-gray-100 rounded flex items-center justify-center">
|
||||
<i class="iconfont icon-file-word text-2xl text-blue-700"></i>
|
||||
</div>
|
||||
<div class="text-sm font-medium text-gray-900">report_q3.docx</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
Aug 25, 2023
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
1.2 MB
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-indigo-100 text-indigo-800">
|
||||
Document
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center text-sm font-medium">
|
||||
<div class="flex justify-center space-x-2">
|
||||
<button class="p-1 text-blue-600 hover:text-blue-900 focus:outline-none" title="Preview">
|
||||
<i class="iconfont icon-eye"></i>
|
||||
</button>
|
||||
<button class="p-1 text-gray-600 hover:text-gray-900 focus:outline-none" title="Download">
|
||||
<i class="iconfont icon-download"></i>
|
||||
</button>
|
||||
<button class="p-1 text-red-600 hover:text-red-900 focus:outline-none" title="Delete">
|
||||
<i class="iconfont icon-delete"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Audio file row -->
|
||||
<tr class="table-row">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 h-10 w-10 mr-3 bg-gray-100 rounded flex items-center justify-center">
|
||||
<i class="iconfont icon-file-audio text-2xl text-green-600"></i>
|
||||
</div>
|
||||
<div class="text-sm font-medium text-gray-900">podcast_interview.mp3</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
Aug 20, 2023
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
18.5 MB
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
|
||||
Audio
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center text-sm font-medium">
|
||||
<div class="flex justify-center space-x-2">
|
||||
<button class="p-1 text-blue-600 hover:text-blue-900 focus:outline-none" title="Preview">
|
||||
<i class="iconfont icon-play"></i>
|
||||
</button>
|
||||
<button class="p-1 text-gray-600 hover:text-gray-900 focus:outline-none" title="Download">
|
||||
<i class="iconfont icon-download"></i>
|
||||
</button>
|
||||
<button class="p-1 text-red-600 hover:text-red-900 focus:outline-none" title="Delete">
|
||||
<i class="iconfont icon-delete"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Table footer with pagination -->
|
||||
<div class="bg-gray-50 px-6 py-3 border-t border-gray-200 flex items-center justify-between">
|
||||
<div class="flex-1 flex justify-between sm:hidden">
|
||||
<button class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
||||
Previous
|
||||
</button>
|
||||
<button class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-700">
|
||||
Showing <span class="font-medium">1</span> to <span class="font-medium">5</span> of <span class="font-medium">24</span> results
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
|
||||
<button class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
|
||||
<span class="sr-only">Previous</span>
|
||||
<i class="iconfont icon-arrow-left"></i>
|
||||
</button>
|
||||
<button class="relative inline-flex items-center px-4 py-2 border border-blue-500 bg-blue-500 text-sm font-medium text-white">
|
||||
1
|
||||
</button>
|
||||
<button class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">
|
||||
2
|
||||
</button>
|
||||
<button class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">
|
||||
3
|
||||
</button>
|
||||
<span class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700">
|
||||
...
|
||||
</span>
|
||||
<button class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">
|
||||
5
|
||||
</button>
|
||||
<button class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
|
||||
<span class="sr-only">Next</span>
|
||||
<i class="iconfont icon-arrow-right"></i>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const uploadArea = document.getElementById('upload-area');
|
||||
const fileInput = document.getElementById('file-input');
|
||||
|
||||
// Handle click on upload area
|
||||
uploadArea.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
});
|
||||
|
||||
// Handle file selection
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFiles(e.target.files);
|
||||
});
|
||||
|
||||
// Handle drag and drop
|
||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||
uploadArea.addEventListener(eventName, preventDefaults, false);
|
||||
});
|
||||
|
||||
function preventDefaults(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
['dragenter', 'dragover'].forEach(eventName => {
|
||||
uploadArea.addEventListener(eventName, () => {
|
||||
uploadArea.classList.add('active');
|
||||
}, false);
|
||||
});
|
||||
|
||||
['dragleave', 'drop'].forEach(eventName => {
|
||||
uploadArea.addEventListener(eventName, () => {
|
||||
uploadArea.classList.remove('active');
|
||||
}, false);
|
||||
});
|
||||
|
||||
uploadArea.addEventListener('drop', (e) => {
|
||||
const dt = e.dataTransfer;
|
||||
const files = dt.files;
|
||||
handleFiles(files);
|
||||
}, false);
|
||||
|
||||
function handleFiles(files) {
|
||||
// In a real application, you would upload these files to a server
|
||||
// For this demo, we'll just show an alert
|
||||
const fileNames = Array.from(files).map(file => file.name).join(', ');
|
||||
alert(`Files selected: ${fileNames}`);
|
||||
|
||||
// Here you would typically send these files to your server
|
||||
// const formData = new FormData();
|
||||
// Array.from(files).forEach(file => {
|
||||
// formData.append('files', file);
|
||||
// });
|
||||
// fetch('/upload', {
|
||||
// method: 'POST',
|
||||
// body: formData
|
||||
// });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -10,9 +10,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.0.17",
|
||||
"axios": "^1.8.4",
|
||||
"daisyui": "^5.0.9",
|
||||
"tailwindcss": "^4.0.17",
|
||||
"vue": "^3.5.13"
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
|
||||
231
frontend/admin/posts.html
Normal file
231
frontend/admin/posts.html
Normal file
@@ -0,0 +1,231 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>文章列表 - 管理后台</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800">文章列表</h1>
|
||||
<div class="flex space-x-2">
|
||||
<button class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md transition">
|
||||
创建文章
|
||||
</button>
|
||||
<div class="relative">
|
||||
<input type="text" placeholder="搜索文章..." class="border rounded-md px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<button class="absolute right-2 top-2.5 text-gray-400 hover:text-gray-600">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow rounded-lg overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
标题
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
价格
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
发布时间
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
发布状态
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
媒体类型
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
观看次数
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
操作
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<!-- 示例数据行 -->
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 h-10 w-10">
|
||||
<img class="h-10 w-10 rounded-md object-cover" src="https://via.placeholder.com/150" alt="文章缩略图">
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<div class="text-sm font-medium text-gray-900">如何高效学习编程</div>
|
||||
<div class="text-sm text-gray-500">作者: 张三</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900">¥29.99</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900">2023-06-15 14:30</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
|
||||
已发布
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900">文章, 视频</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
1,254
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div class="flex space-x-2">
|
||||
<button class="text-indigo-600 hover:text-indigo-900">编辑</button>
|
||||
<button class="text-red-600 hover:text-red-900">删除</button>
|
||||
<button class="text-gray-600 hover:text-gray-900">查看</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 h-10 w-10">
|
||||
<img class="h-10 w-10 rounded-md object-cover" src="https://via.placeholder.com/150" alt="文章缩略图">
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<div class="text-sm font-medium text-gray-900">前端开发最佳实践</div>
|
||||
<div class="text-sm text-gray-500">作者: 李四</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900">¥49.99</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900">2023-06-10 09:15</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800">
|
||||
草稿
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900">文章</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
789
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div class="flex space-x-2">
|
||||
<button class="text-indigo-600 hover:text-indigo-900">编辑</button>
|
||||
<button class="text-red-600 hover:text-red-900">删除</button>
|
||||
<button class="text-gray-600 hover:text-gray-900">查看</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 h-10 w-10">
|
||||
<img class="h-10 w-10 rounded-md object-cover" src="https://via.placeholder.com/150" alt="文章缩略图">
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<div class="text-sm font-medium text-gray-900">数据分析入门指南</div>
|
||||
<div class="text-sm text-gray-500">作者: 王五</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900">¥0.00</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900">2023-06-05 16:45</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">
|
||||
已下架
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900">文章, 音频</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
2,567
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div class="flex space-x-2">
|
||||
<button class="text-indigo-600 hover:text-indigo-900">编辑</button>
|
||||
<button class="text-red-600 hover:text-red-900">删除</button>
|
||||
<button class="text-gray-600 hover:text-gray-900">查看</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="bg-white px-4 py-3 border-t border-gray-200 sm:px-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1 flex justify-between sm:hidden">
|
||||
<a href="#" class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
||||
上一页
|
||||
</a>
|
||||
<a href="#" class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
||||
下一页
|
||||
</a>
|
||||
</div>
|
||||
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-700">
|
||||
显示第 <span class="font-medium">1</span> 到 <span class="font-medium">10</span> 条,共 <span class="font-medium">50</span> 条结果
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
|
||||
<a href="#" class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
|
||||
<span class="sr-only">上一页</span>
|
||||
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">
|
||||
1
|
||||
</a>
|
||||
<a href="#" class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">
|
||||
2
|
||||
</a>
|
||||
<a href="#" class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-blue-600 hover:bg-gray-50">
|
||||
3
|
||||
</a>
|
||||
<span class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700">
|
||||
...
|
||||
</span>
|
||||
<a href="#" class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">
|
||||
8
|
||||
</a>
|
||||
<a href="#" class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">
|
||||
9
|
||||
</a>
|
||||
<a href="#" class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">
|
||||
10
|
||||
</a>
|
||||
<a href="#" class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
|
||||
<span class="sr-only">下一页</span>
|
||||
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,9 +1,42 @@
|
||||
<script setup>
|
||||
import HelloWorld from './components/HelloWorld.vue'
|
||||
// 不再需要下拉菜单的状态,可以删除相关代码
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button class="btn">Button</button>
|
||||
<button class="btn btn-primary">Button</button>
|
||||
<button class="btn w-64 rounded-full">Button</button>
|
||||
<div class="min-h-screen bg-base-200">
|
||||
<div class="navbar bg-base-100 shadow-md">
|
||||
<div class="navbar-start">
|
||||
<div class="px-2 mx-2">
|
||||
<h1 class="text-xl font-bold">Hello</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-center">
|
||||
<div class="tabs tabs-boxed bg-base-100">
|
||||
<router-link to="/posts" class="tab" :class="{ 'tab-active': $route.path === '/posts' }">
|
||||
文章管理
|
||||
</router-link>
|
||||
<router-link to="/medias" class="tab" :class="{ 'tab-active': $route.path === '/medias' }">
|
||||
媒体库
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<button class="btn btn-error btn-sm">退出登录</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="container mx-auto py-6 px-4">
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'App'
|
||||
}
|
||||
</script>
|
||||
48
frontend/admin/src/api/httpClient.js
Normal file
48
frontend/admin/src/api/httpClient.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import axios from 'axios';
|
||||
|
||||
// Create axios instance with default config
|
||||
const httpClient = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
// Request interceptor
|
||||
httpClient.interceptors.request.use(
|
||||
config => {
|
||||
// You can add auth token here if needed
|
||||
// const token = localStorage.getItem('token');
|
||||
// if (token) {
|
||||
// config.headers.Authorization = `Bearer ${token}`;
|
||||
// }
|
||||
return config;
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Response interceptor
|
||||
httpClient.interceptors.response.use(
|
||||
response => {
|
||||
return response.data;
|
||||
},
|
||||
error => {
|
||||
// Handle HTTP errors here
|
||||
if (error.response) {
|
||||
// Server responded with error status
|
||||
console.error('API Error:', error.response.status, error.response.data);
|
||||
} else if (error.request) {
|
||||
// Request made but no response received
|
||||
console.error('API Error: No response received', error.request);
|
||||
} else {
|
||||
// Something else happened
|
||||
console.error('API Error:', error.message);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default httpClient;
|
||||
31
frontend/admin/src/api/mockService.js
Normal file
31
frontend/admin/src/api/mockService.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Mock service that provides fake data for development
|
||||
*/
|
||||
export const mockService = {
|
||||
/**
|
||||
* Get mock count of posts
|
||||
* @returns {Promise<{count: number}>}
|
||||
*/
|
||||
getPostsCount() {
|
||||
console.log('Using MOCK service for posts count');
|
||||
return Promise.resolve({ count: 42 });
|
||||
},
|
||||
|
||||
/**
|
||||
* Get mock count of media items
|
||||
* @returns {Promise<{count: number}>}
|
||||
*/
|
||||
getMediasCount() {
|
||||
console.log('Using MOCK service for medias count');
|
||||
return Promise.resolve({ count: 18 });
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all mock statistics in a single call
|
||||
* @returns {Promise<{posts: number, medias: number}>}
|
||||
*/
|
||||
getAllStatistics() {
|
||||
console.log('Using MOCK service for all statistics');
|
||||
return Promise.resolve({ posts: 42, medias: 18 });
|
||||
}
|
||||
};
|
||||
67
frontend/admin/src/api/statisticsService.js
Normal file
67
frontend/admin/src/api/statisticsService.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import httpClient from './httpClient';
|
||||
import { mockService } from './mockService';
|
||||
|
||||
// Simplify environment detection and ensure the console log works
|
||||
let isDevelopment = true; // Default to development mode
|
||||
|
||||
// Try different ways to detect environment
|
||||
try {
|
||||
if (typeof process !== 'undefined' && process.env) {
|
||||
console.log('Detected process.env, NODE_ENV:', process.env.NODE_ENV);
|
||||
isDevelopment = process.env.NODE_ENV === 'development';
|
||||
} else if (typeof import.meta !== 'undefined' && import.meta.env) {
|
||||
console.log('Detected import.meta.env, MODE:', import.meta.env.MODE);
|
||||
isDevelopment = import.meta.env.MODE === 'development';
|
||||
} else {
|
||||
console.log('No environment variables detected, defaulting to development mode');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error detecting environment:', error);
|
||||
}
|
||||
|
||||
// Force console log with timeout to ensure it runs after other initialization
|
||||
setTimeout(() => {
|
||||
console.log('%cCurrent environment: ' + (isDevelopment ? 'DEVELOPMENT' : 'PRODUCTION'),
|
||||
'background: #222; color: #bada55; font-size: 16px; padding: 4px;');
|
||||
}, 0);
|
||||
|
||||
// Use the appropriate service based on environment
|
||||
const apiService = isDevelopment ? mockService : {
|
||||
getPostsCount() {
|
||||
return httpClient.get('/posts/count');
|
||||
},
|
||||
|
||||
getMediasCount() {
|
||||
return httpClient.get('/medias/count');
|
||||
},
|
||||
|
||||
getAllStatistics() {
|
||||
return httpClient.get('/statistics');
|
||||
}
|
||||
};
|
||||
|
||||
export const statisticsService = {
|
||||
/**
|
||||
* Get count of posts
|
||||
* @returns {Promise<{count: number}>}
|
||||
*/
|
||||
getPostsCount() {
|
||||
return apiService.getPostsCount();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get count of media items
|
||||
* @returns {Promise<{count: number}>}
|
||||
*/
|
||||
getMediasCount() {
|
||||
return apiService.getMediasCount();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all statistics in a single call
|
||||
* @returns {Promise<{posts: number, medias: number}>}
|
||||
*/
|
||||
getAllStatistics() {
|
||||
return apiService.getAllStatistics();
|
||||
}
|
||||
};
|
||||
@@ -1,5 +1,16 @@
|
||||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import { router } from './router';
|
||||
import './style.css';
|
||||
|
||||
createApp(App).mount('#app')
|
||||
// Log environment information during app initialization
|
||||
console.log('=== Environment Information ===');
|
||||
console.log('NODE_ENV:', process.env.NODE_ENV);
|
||||
if (typeof import.meta !== 'undefined') {
|
||||
console.log('Vite MODE:', import.meta.env.MODE);
|
||||
console.log('Vite DEV:', import.meta.env.DEV);
|
||||
console.log('Vite PROD:', import.meta.env.PROD);
|
||||
}
|
||||
console.log('=============================');
|
||||
|
||||
createApp(App).use(router).mount('#app');
|
||||
|
||||
135
frontend/admin/src/pages/HomePage.vue
Normal file
135
frontend/admin/src/pages/HomePage.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div class="home-page">
|
||||
<h1>仪表盘</h1>
|
||||
|
||||
<div class="statistics-container">
|
||||
<div class="stat-card" @click="navigateTo('/posts')">
|
||||
<div class="stat-icon">
|
||||
<i class="fa fa-file-text"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h2>文章数量</h2>
|
||||
<div class="stat-value">{{ postCount }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card" @click="navigateTo('/medias')">
|
||||
<div class="stat-icon">
|
||||
<i class="fa fa-image"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h2>媒体数量</h2>
|
||||
<div class="stat-value">{{ mediaCount }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error-message">
|
||||
{{ error }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { statisticsService } from '../api/statisticsService';
|
||||
|
||||
const router = useRouter();
|
||||
const postCount = ref(0);
|
||||
const mediaCount = ref(0);
|
||||
const error = ref('');
|
||||
const loading = ref(false);
|
||||
|
||||
const fetchStatistics = async () => {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
|
||||
try {
|
||||
// Option 1: Make parallel requests using our service
|
||||
const [postsData, mediasData] = await Promise.all([
|
||||
statisticsService.getPostsCount(),
|
||||
statisticsService.getMediasCount()
|
||||
]);
|
||||
|
||||
postCount.value = postsData.count;
|
||||
mediaCount.value = mediasData.count;
|
||||
|
||||
// Option 2: If you have a combined endpoint
|
||||
// const data = await statisticsService.getAllStatistics();
|
||||
// postCount.value = data.posts;
|
||||
// mediaCount.value = data.medias;
|
||||
} catch (err) {
|
||||
console.error('Error fetching statistics:', err);
|
||||
error.value = '获取统计数据时出错';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const navigateTo = (path) => {
|
||||
router.push(path);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchStatistics();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.statistics-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
min-width: 250px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 2.5rem;
|
||||
margin-right: 20px;
|
||||
color: #3498db;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat-content h2 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 1.2rem;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
background-color: #ffecec;
|
||||
color: #f44336;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid #f44336;
|
||||
}
|
||||
</style>
|
||||
481
frontend/admin/src/pages/MediasPage.vue
Normal file
481
frontend/admin/src/pages/MediasPage.vue
Normal file
@@ -0,0 +1,481 @@
|
||||
<template>
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-semibold text-gray-800">Media Library</h1>
|
||||
<div class="flex space-x-2">
|
||||
<div class="relative">
|
||||
<input type="text" placeholder="Search media..."
|
||||
class="pl-10 pr-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
v-model="searchQuery" @input="filterMedia">
|
||||
<i class="iconfont icon-search absolute left-3 top-3 text-gray-400"></i>
|
||||
</div>
|
||||
<select class="border rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
v-model="selectedType" @change="filterMedia">
|
||||
<option value="all">All media</option>
|
||||
<option value="image">Images</option>
|
||||
<option value="video">Videos</option>
|
||||
<option value="audio">Audio</option>
|
||||
<option value="document">Documents</option>
|
||||
<option value="pdf">PDFs</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload Area -->
|
||||
<div id="upload-area" :class="{ 'active': isDragging }"
|
||||
class="upload-area rounded-lg p-10 mb-8 flex flex-col items-center justify-center cursor-pointer"
|
||||
@click="triggerFileInput" @dragenter.prevent="onDragEnter" @dragover.prevent="onDragOver"
|
||||
@dragleave.prevent="onDragLeave" @drop.prevent="onDrop">
|
||||
<i class="iconfont icon-upload text-5xl text-blue-500 mb-4"></i>
|
||||
<p class="text-gray-600 text-center mb-2">Drag and drop files here or click to browse</p>
|
||||
<p class="text-gray-400 text-sm text-center">Supports: JPG, PNG, GIF, MP4, PDF</p>
|
||||
<input type="file" id="file-input" multiple class="hidden" ref="fileInput" @change="onFileSelected">
|
||||
</div>
|
||||
|
||||
<!-- Media Table Section -->
|
||||
<div class="mb-12 bg-white rounded-lg shadow overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full media-table">
|
||||
<thead class="bg-gray-50 border-b border-gray-200">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<div class="flex items-center">
|
||||
File Name
|
||||
<button class="ml-1 text-gray-400 hover:text-gray-600" @click="sortBy('fileName')">
|
||||
<i class="iconfont icon-sort"></i>
|
||||
</button>
|
||||
</div>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<div class="flex items-center">
|
||||
Upload Time
|
||||
<button class="ml-1 text-gray-400 hover:text-gray-600"
|
||||
@click="sortBy('uploadTime')">
|
||||
<i class="iconfont icon-sort"></i>
|
||||
</button>
|
||||
</div>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<div class="flex items-center">
|
||||
File Size
|
||||
<button class="ml-1 text-gray-400 hover:text-gray-600" @click="sortBy('fileSize')">
|
||||
<i class="iconfont icon-sort"></i>
|
||||
</button>
|
||||
</div>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<div class="flex items-center">
|
||||
File Type
|
||||
<button class="ml-1 text-gray-400 hover:text-gray-600" @click="sortBy('fileType')">
|
||||
<i class="iconfont icon-sort"></i>
|
||||
</button>
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<tr v-for="file in paginatedFiles" :key="file.id" class="table-row">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 h-10 w-10 mr-3" v-if="file.fileType === 'Image'">
|
||||
<img class="preview-thumbnail" :src="file.thumbnailUrl" :alt="file.fileName">
|
||||
</div>
|
||||
<div v-else
|
||||
class="flex-shrink-0 h-10 w-10 mr-3 bg-gray-100 rounded flex items-center justify-center">
|
||||
<i :class="getFileTypeIcon(file.fileType)" class="text-2xl"></i>
|
||||
</div>
|
||||
<div class="text-sm font-medium text-gray-900">{{ file.fileName }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ file.uploadTime }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ file.fileSize }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full"
|
||||
:class="getFileTypeBadgeClass(file.fileType)">
|
||||
{{ file.fileType }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center text-sm font-medium">
|
||||
<div class="flex justify-center space-x-2">
|
||||
<button class="p-1 text-blue-600 hover:text-blue-900 focus:outline-none"
|
||||
title="Preview" @click="previewFile(file)">
|
||||
<i
|
||||
:class="file.fileType === 'Video' || file.fileType === 'Audio' ? 'iconfont icon-play' : 'iconfont icon-eye'"></i>
|
||||
</button>
|
||||
<button class="p-1 text-gray-600 hover:text-gray-900 focus:outline-none"
|
||||
title="Download" @click="downloadFile(file)">
|
||||
<i class="iconfont icon-download"></i>
|
||||
</button>
|
||||
<button class="p-1 text-red-600 hover:text-red-900 focus:outline-none"
|
||||
title="Delete" @click="deleteFile(file)">
|
||||
<i class="iconfont icon-delete"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Table footer with pagination -->
|
||||
<div class="bg-gray-50 px-6 py-3 border-t border-gray-200 flex items-center justify-between">
|
||||
<div class="flex-1 flex justify-between sm:hidden">
|
||||
<button
|
||||
class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
|
||||
@click="prevPage" :disabled="currentPage === 1"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': currentPage === 1 }">
|
||||
Previous
|
||||
</button>
|
||||
<button
|
||||
class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
|
||||
@click="nextPage" :disabled="currentPage === totalPages"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': currentPage === totalPages }">
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-700">
|
||||
Showing <span class="font-medium">{{ startItem }}</span> to <span class="font-medium">{{
|
||||
endItem }}</span> of <span class="font-medium">{{ filteredFiles.length }}</span> results
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
|
||||
<button
|
||||
class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
|
||||
@click="prevPage" :disabled="currentPage === 1"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': currentPage === 1 }">
|
||||
<span class="sr-only">Previous</span>
|
||||
<i class="iconfont icon-arrow-left"></i>
|
||||
</button>
|
||||
<button v-for="page in paginationPages" :key="page" :class="getPageButtonClass(page)"
|
||||
@click="goToPage(page)">
|
||||
{{ page }}
|
||||
</button>
|
||||
<span v-if="showEllipsis"
|
||||
class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700">
|
||||
...
|
||||
</span>
|
||||
<button v-if="totalPages > 3" :class="getPageButtonClass(totalPages)"
|
||||
@click="goToPage(totalPages)">
|
||||
{{ totalPages }}
|
||||
</button>
|
||||
<button
|
||||
class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
|
||||
@click="nextPage" :disabled="currentPage === totalPages"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': currentPage === totalPages }">
|
||||
<span class="sr-only">Next</span>
|
||||
<i class="iconfont icon-arrow-right"></i>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
// Reactive state
|
||||
const fileInput = ref(null);
|
||||
const mediaFiles = ref([
|
||||
{
|
||||
id: 1,
|
||||
fileName: 'sunset-beach.jpg',
|
||||
uploadTime: 'Today, 10:30 AM',
|
||||
fileSize: '2.4 MB',
|
||||
fileType: 'Image',
|
||||
thumbnailUrl: 'https://via.placeholder.com/300x225'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
fileName: 'presentation.pdf',
|
||||
uploadTime: 'Yesterday, 3:45 PM',
|
||||
fileSize: '4.8 MB',
|
||||
fileType: 'PDF',
|
||||
thumbnailUrl: null
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
fileName: 'promo_video.mp4',
|
||||
uploadTime: 'Aug 28, 2023',
|
||||
fileSize: '24.8 MB',
|
||||
fileType: 'Video',
|
||||
thumbnailUrl: null
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
fileName: 'report_q3.docx',
|
||||
uploadTime: 'Aug 25, 2023',
|
||||
fileSize: '1.2 MB',
|
||||
fileType: 'Document',
|
||||
thumbnailUrl: null
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
fileName: 'podcast_interview.mp3',
|
||||
uploadTime: 'Aug 20, 2023',
|
||||
fileSize: '18.5 MB',
|
||||
fileType: 'Audio',
|
||||
thumbnailUrl: null
|
||||
}
|
||||
]);
|
||||
const searchQuery = ref('');
|
||||
const selectedType = ref('all');
|
||||
const currentPage = ref(1);
|
||||
const itemsPerPage = ref(5);
|
||||
const isDragging = ref(false);
|
||||
const sortOrder = ref({
|
||||
field: null,
|
||||
direction: 'asc'
|
||||
});
|
||||
|
||||
// Computed properties
|
||||
const filteredFiles = computed(() => {
|
||||
let result = [...mediaFiles.value];
|
||||
|
||||
// Search filter
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
result = result.filter(file =>
|
||||
file.fileName.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
|
||||
// Type filter
|
||||
if (selectedType.value !== 'all') {
|
||||
const type = selectedType.value.charAt(0).toUpperCase() + selectedType.value.slice(1);
|
||||
result = result.filter(file =>
|
||||
file.fileType.toLowerCase() === selectedType.value
|
||||
);
|
||||
}
|
||||
|
||||
// Sorting
|
||||
if (sortOrder.value.field) {
|
||||
result.sort((a, b) => {
|
||||
if (sortOrder.value.direction === 'asc') {
|
||||
return a[sortOrder.value.field] > b[sortOrder.value.field] ? 1 : -1;
|
||||
} else {
|
||||
return a[sortOrder.value.field] < b[sortOrder.value.field] ? 1 : -1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
const paginatedFiles = computed(() => {
|
||||
const start = (currentPage.value - 1) * itemsPerPage.value;
|
||||
const end = start + itemsPerPage.value;
|
||||
return filteredFiles.value.slice(start, end);
|
||||
});
|
||||
|
||||
const totalPages = computed(() => {
|
||||
return Math.ceil(filteredFiles.value.length / itemsPerPage.value);
|
||||
});
|
||||
|
||||
const startItem = computed(() => {
|
||||
if (filteredFiles.value.length === 0) return 0;
|
||||
return (currentPage.value - 1) * itemsPerPage.value + 1;
|
||||
});
|
||||
|
||||
const endItem = computed(() => {
|
||||
if (filteredFiles.value.length === 0) return 0;
|
||||
const end = currentPage.value * itemsPerPage.value;
|
||||
return end > filteredFiles.value.length ? filteredFiles.value.length : end;
|
||||
});
|
||||
|
||||
const paginationPages = computed(() => {
|
||||
// For simple pagination, just show first 3 pages
|
||||
const pages = [];
|
||||
const maxPages = Math.min(3, totalPages.value);
|
||||
for (let i = 1; i <= maxPages; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
return pages;
|
||||
});
|
||||
|
||||
const showEllipsis = computed(() => {
|
||||
return totalPages.value > 3;
|
||||
});
|
||||
|
||||
// Methods
|
||||
const getFileTypeIcon = (fileType) => {
|
||||
switch (fileType) {
|
||||
case 'PDF':
|
||||
return 'iconfont icon-file-pdf text-red-500';
|
||||
case 'Video':
|
||||
return 'iconfont icon-file-video text-blue-500';
|
||||
case 'Document':
|
||||
return 'iconfont icon-file-word text-blue-700';
|
||||
case 'Audio':
|
||||
return 'iconfont icon-file-audio text-green-600';
|
||||
default:
|
||||
return 'iconfont icon-file text-gray-500';
|
||||
}
|
||||
};
|
||||
|
||||
const getFileTypeBadgeClass = (fileType) => {
|
||||
switch (fileType) {
|
||||
case 'Image':
|
||||
return 'bg-blue-100 text-blue-800';
|
||||
case 'PDF':
|
||||
return 'bg-red-100 text-red-800';
|
||||
case 'Video':
|
||||
return 'bg-purple-100 text-purple-800';
|
||||
case 'Document':
|
||||
return 'bg-indigo-100 text-indigo-800';
|
||||
case 'Audio':
|
||||
return 'bg-green-100 text-green-800';
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
};
|
||||
|
||||
const getPageButtonClass = (page) => {
|
||||
return page === currentPage.value
|
||||
? 'relative inline-flex items-center px-4 py-2 border border-blue-500 bg-blue-500 text-sm font-medium text-white'
|
||||
: 'relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50';
|
||||
};
|
||||
|
||||
const triggerFileInput = () => {
|
||||
fileInput.value.click();
|
||||
};
|
||||
|
||||
const onDragEnter = (e) => {
|
||||
isDragging.value = true;
|
||||
};
|
||||
|
||||
const onDragOver = (e) => {
|
||||
isDragging.value = true;
|
||||
};
|
||||
|
||||
const onDragLeave = (e) => {
|
||||
isDragging.value = false;
|
||||
};
|
||||
|
||||
const onDrop = (e) => {
|
||||
isDragging.value = false;
|
||||
const files = e.dataTransfer.files;
|
||||
handleFiles(files);
|
||||
};
|
||||
|
||||
const onFileSelected = (e) => {
|
||||
const files = e.target.files;
|
||||
handleFiles(files);
|
||||
};
|
||||
|
||||
const handleFiles = (files) => {
|
||||
const fileNames = Array.from(files).map(file => file.name).join(', ');
|
||||
alert(`Files selected: ${fileNames}`);
|
||||
// In a real app, we would upload these files to the server
|
||||
};
|
||||
|
||||
const filterMedia = () => {
|
||||
currentPage.value = 1; // Reset to first page when filtering
|
||||
};
|
||||
|
||||
const sortBy = (field) => {
|
||||
if (sortOrder.value.field === field) {
|
||||
// Toggle direction
|
||||
sortOrder.value.direction = sortOrder.value.direction === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
// New field, default to ascending
|
||||
sortOrder.value.field = field;
|
||||
sortOrder.value.direction = 'asc';
|
||||
}
|
||||
};
|
||||
|
||||
const prevPage = () => {
|
||||
if (currentPage.value > 1) {
|
||||
currentPage.value--;
|
||||
}
|
||||
};
|
||||
|
||||
const nextPage = () => {
|
||||
if (currentPage.value < totalPages.value) {
|
||||
currentPage.value++;
|
||||
}
|
||||
};
|
||||
|
||||
const goToPage = (page) => {
|
||||
currentPage.value = page;
|
||||
};
|
||||
|
||||
const previewFile = (file) => {
|
||||
alert(`Preview file: ${file.fileName}`);
|
||||
};
|
||||
|
||||
const downloadFile = (file) => {
|
||||
alert(`Download file: ${file.fileName}`);
|
||||
};
|
||||
|
||||
const deleteFile = (file) => {
|
||||
if (confirm(`Are you sure you want to delete ${file.fileName}?`)) {
|
||||
// In a real app, we would make an API call to delete the file
|
||||
mediaFiles.value = mediaFiles.value.filter(f => f.id !== file.id);
|
||||
alert(`Deleted file: ${file.fileName}`);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.upload-area {
|
||||
border: 2px dashed #cbd5e0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.upload-area.active {
|
||||
border-color: #4299e1;
|
||||
background-color: rgba(66, 153, 225, 0.1);
|
||||
}
|
||||
|
||||
.table-row {
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.table-row:hover {
|
||||
background-color: rgba(237, 242, 247, 0.7);
|
||||
}
|
||||
|
||||
.pagination-item.active {
|
||||
background-color: #4299e1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.media-table th {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.media-table th:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 25%;
|
||||
height: 50%;
|
||||
width: 1px;
|
||||
background-color: #e2e8f0;
|
||||
}
|
||||
|
||||
.media-table th:last-child:after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.preview-thumbnail {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
341
frontend/admin/src/pages/PostsPage.vue
Normal file
341
frontend/admin/src/pages/PostsPage.vue
Normal file
@@ -0,0 +1,341 @@
|
||||
<template>
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800">文章列表</h1>
|
||||
<div class="flex space-x-2">
|
||||
<button class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md transition"
|
||||
@click="createPost">
|
||||
创建文章
|
||||
</button>
|
||||
<div class="relative">
|
||||
<input type="text" placeholder="搜索文章..."
|
||||
class="border rounded-md px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
v-model="searchQuery" @input="searchPosts">
|
||||
<button class="absolute right-2 top-2.5 text-gray-400 hover:text-gray-600">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow rounded-lg overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
标题
|
||||
</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
价格
|
||||
</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
发布时间
|
||||
</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
发布状态
|
||||
</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
媒体类型
|
||||
</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
观看次数
|
||||
</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
操作
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<tr v-for="post in displayedPosts" :key="post.id">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 h-10 w-10">
|
||||
<img class="h-10 w-10 rounded-md object-cover" :src="post.thumbnailUrl"
|
||||
:alt="post.title">
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<div class="text-sm font-medium text-gray-900">{{ post.title }}</div>
|
||||
<div class="text-sm text-gray-500">作者: {{ post.author }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900">¥{{ post.price }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900">{{ post.publishTime }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full"
|
||||
:class="getStatusClass(post.status)">
|
||||
{{ post.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900">{{ post.mediaTypes.join(', ') }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ post.viewCount }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div class="flex space-x-2">
|
||||
<button @click="editPost(post)"
|
||||
class="text-indigo-600 hover:text-indigo-900">编辑</button>
|
||||
<button @click="deletePost(post)"
|
||||
class="text-red-600 hover:text-red-900">删除</button>
|
||||
<button @click="viewPost(post)"
|
||||
class="text-gray-600 hover:text-gray-900">查看</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="bg-white px-4 py-3 border-t border-gray-200 sm:px-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1 flex justify-between sm:hidden">
|
||||
<a href="#"
|
||||
class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
|
||||
@click.prevent="prevPage" :class="{ 'opacity-50 cursor-not-allowed': currentPage === 1 }">
|
||||
上一页
|
||||
</a>
|
||||
<a href="#"
|
||||
class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
|
||||
@click.prevent="nextPage"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': currentPage === totalPages }">
|
||||
下一页
|
||||
</a>
|
||||
</div>
|
||||
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-700">
|
||||
显示第 <span class="font-medium">{{ startItem }}</span> 到 <span class="font-medium">{{
|
||||
endItem }}</span> 条,共 <span class="font-medium">{{ totalItems }}</span> 条结果
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px"
|
||||
aria-label="Pagination">
|
||||
<a href="#"
|
||||
class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
|
||||
@click.prevent="prevPage"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': currentPage === 1 }">
|
||||
<span class="sr-only">上一页</span>
|
||||
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
|
||||
fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
<template v-for="page in paginationItems" :key="page">
|
||||
<a v-if="page !== '...'" href="#"
|
||||
class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium hover:bg-gray-50"
|
||||
:class="page === currentPage ? 'text-blue-600' : 'text-gray-700'"
|
||||
@click.prevent="goToPage(page)">
|
||||
{{ page }}
|
||||
</a>
|
||||
<span v-else
|
||||
class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700">
|
||||
...
|
||||
</span>
|
||||
</template>
|
||||
<a href="#"
|
||||
class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
|
||||
@click.prevent="nextPage"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': currentPage === totalPages }">
|
||||
<span class="sr-only">下一页</span>
|
||||
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
|
||||
fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
// Reactive state
|
||||
const posts = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: '如何高效学习编程',
|
||||
author: '张三',
|
||||
thumbnailUrl: 'https://via.placeholder.com/150',
|
||||
price: '29.99',
|
||||
publishTime: '2023-06-15 14:30',
|
||||
status: '已发布',
|
||||
mediaTypes: ['文章', '视频'],
|
||||
viewCount: 1254
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '前端开发最佳实践',
|
||||
author: '李四',
|
||||
thumbnailUrl: 'https://via.placeholder.com/150',
|
||||
price: '49.99',
|
||||
publishTime: '2023-06-10 09:15',
|
||||
status: '草稿',
|
||||
mediaTypes: ['文章'],
|
||||
viewCount: 789
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '数据分析入门指南',
|
||||
author: '王五',
|
||||
thumbnailUrl: 'https://via.placeholder.com/150',
|
||||
price: '0.00',
|
||||
publishTime: '2023-06-05 16:45',
|
||||
status: '已下架',
|
||||
mediaTypes: ['文章', '音频'],
|
||||
viewCount: 2567
|
||||
}
|
||||
]);
|
||||
const searchQuery = ref('');
|
||||
const currentPage = ref(3);
|
||||
const itemsPerPage = ref(10);
|
||||
const totalItems = ref(50);
|
||||
|
||||
// Computed properties
|
||||
const filteredPosts = computed(() => {
|
||||
if (!searchQuery.value) {
|
||||
return posts.value;
|
||||
}
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return posts.value.filter(post =>
|
||||
post.title.toLowerCase().includes(query) ||
|
||||
post.author.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
|
||||
const displayedPosts = computed(() => {
|
||||
return filteredPosts.value;
|
||||
});
|
||||
|
||||
const totalPages = computed(() => {
|
||||
return Math.ceil(totalItems.value / itemsPerPage.value);
|
||||
});
|
||||
|
||||
const startItem = computed(() => {
|
||||
return (currentPage.value - 1) * itemsPerPage.value + 1;
|
||||
});
|
||||
|
||||
const endItem = computed(() => {
|
||||
const end = currentPage.value * itemsPerPage.value;
|
||||
return end > totalItems.value ? totalItems.value : end;
|
||||
});
|
||||
|
||||
const paginationItems = computed(() => {
|
||||
const items = [];
|
||||
const maxPagesToShow = 5;
|
||||
|
||||
if (totalPages.value <= maxPagesToShow) {
|
||||
// Show all pages
|
||||
for (let i = 1; i <= totalPages.value; i++) {
|
||||
items.push(i);
|
||||
}
|
||||
} else {
|
||||
// Show limited pages with ellipsis
|
||||
if (currentPage.value <= 3) {
|
||||
// Current page is near the start
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
items.push(i);
|
||||
}
|
||||
items.push('...');
|
||||
items.push(totalPages.value);
|
||||
} else if (currentPage.value >= totalPages.value - 2) {
|
||||
// Current page is near the end
|
||||
items.push(1);
|
||||
items.push('...');
|
||||
for (let i = totalPages.value - 2; i <= totalPages.value; i++) {
|
||||
items.push(i);
|
||||
}
|
||||
} else {
|
||||
// Current page is in the middle
|
||||
items.push(1);
|
||||
items.push('...');
|
||||
items.push(currentPage.value - 1);
|
||||
items.push(currentPage.value);
|
||||
items.push(currentPage.value + 1);
|
||||
items.push('...');
|
||||
items.push(totalPages.value);
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
});
|
||||
|
||||
// Methods
|
||||
const getStatusClass = (status) => {
|
||||
switch (status) {
|
||||
case '已发布':
|
||||
return 'bg-green-100 text-green-800';
|
||||
case '草稿':
|
||||
return 'bg-yellow-100 text-yellow-800';
|
||||
case '已下架':
|
||||
return 'bg-red-100 text-red-800';
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
};
|
||||
|
||||
const createPost = () => {
|
||||
alert('创建新文章功能待实现');
|
||||
};
|
||||
|
||||
const editPost = (post) => {
|
||||
alert(`编辑文章: ${post.title}`);
|
||||
};
|
||||
|
||||
const deletePost = (post) => {
|
||||
if (confirm(`确定要删除文章 "${post.title}" 吗?`)) {
|
||||
// In a real app, we would make an API call to delete the post
|
||||
alert(`已删除文章: ${post.title}`);
|
||||
}
|
||||
};
|
||||
|
||||
const viewPost = (post) => {
|
||||
alert(`查看文章: ${post.title}`);
|
||||
};
|
||||
|
||||
const searchPosts = () => {
|
||||
currentPage.value = 1; // Reset to first page when searching
|
||||
};
|
||||
|
||||
const prevPage = () => {
|
||||
if (currentPage.value > 1) {
|
||||
currentPage.value--;
|
||||
}
|
||||
};
|
||||
|
||||
const nextPage = () => {
|
||||
if (currentPage.value < totalPages.value) {
|
||||
currentPage.value++;
|
||||
}
|
||||
};
|
||||
|
||||
const goToPage = (page) => {
|
||||
currentPage.value = page;
|
||||
};
|
||||
</script>
|
||||
26
frontend/admin/src/router.js
Normal file
26
frontend/admin/src/router.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
|
||||
// Define your routes here
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: () => import('./pages/HomePage.vue')
|
||||
},
|
||||
{
|
||||
path: '/posts',
|
||||
name: 'Posts',
|
||||
component: () => import('./pages/PostsPage.vue')
|
||||
},
|
||||
{
|
||||
path: '/medias',
|
||||
name: 'Medias',
|
||||
component: () => import('./pages/MediasPage.vue')
|
||||
}
|
||||
];
|
||||
|
||||
// Create the router instance
|
||||
export const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
});
|
||||
@@ -1,8 +1,20 @@
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { defineConfig } from 'vite'
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import { resolve } from 'path';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue(), tailwindcss()],
|
||||
})
|
||||
plugins: [
|
||||
vue(),
|
||||
tailwindcss(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user