diff --git a/api/admin/index.html b/api/admin/index.html
new file mode 100644
index 0000000..f368c55
--- /dev/null
+++ b/api/admin/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ 笑话大全 - 后台管理
+
+
+
+
+
+
diff --git a/api/admin/package.json b/api/admin/package.json
new file mode 100644
index 0000000..332766d
--- /dev/null
+++ b/api/admin/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "joke-admin",
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "vue": "^3.4.0",
+ "vue-router": "^4.2.5",
+ "pinia": "^2.1.7",
+ "element-plus": "^2.5.0",
+ "axios": "^1.6.0"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^5.0.0",
+ "vite": "^5.0.0"
+ }
+}
diff --git a/api/admin/src/App.vue b/api/admin/src/App.vue
new file mode 100644
index 0000000..4dee7af
--- /dev/null
+++ b/api/admin/src/App.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
diff --git a/api/admin/src/api/auth.js b/api/admin/src/api/auth.js
new file mode 100644
index 0000000..25be045
--- /dev/null
+++ b/api/admin/src/api/auth.js
@@ -0,0 +1,3 @@
+import request from './request'
+
+export const login = data => request.post('/auth/login', data)
diff --git a/api/admin/src/api/category.js b/api/admin/src/api/category.js
new file mode 100644
index 0000000..0f1a50a
--- /dev/null
+++ b/api/admin/src/api/category.js
@@ -0,0 +1,10 @@
+import request from './request'
+
+export const getTypes = () => request.get('/types')
+export const getCrowds = () => request.get('/crowds')
+export const createType = data => request.post('/admin/types', data)
+export const updateType = (id, data) => request.put(`/admin/types/${id}`, data)
+export const deleteType = id => request.delete(`/admin/types/${id}`)
+export const createCrowd = data => request.post('/admin/crowds', data)
+export const updateCrowd = (id, data) => request.put(`/admin/crowds/${id}`, data)
+export const deleteCrowd = id => request.delete(`/admin/crowds/${id}`)
diff --git a/api/admin/src/api/joke.js b/api/admin/src/api/joke.js
new file mode 100644
index 0000000..a8ea938
--- /dev/null
+++ b/api/admin/src/api/joke.js
@@ -0,0 +1,9 @@
+import request from './request'
+
+export const getJokes = params => request.get('/admin/jokes', { params })
+export const getJoke = id => request.get(`/admin/jokes/${id}`)
+export const createJoke = data => request.post('/admin/jokes', data)
+export const updateJoke = (id, data) => request.put(`/admin/jokes/${id}`, data)
+export const deleteJoke = id => request.delete(`/admin/jokes/${id}`)
+export const batchApprove = ids => request.put('/admin/jokes/batch-approve', ids)
+export const getStats = () => request.get('/admin/stats')
diff --git a/api/admin/src/api/request.js b/api/admin/src/api/request.js
new file mode 100644
index 0000000..9b68832
--- /dev/null
+++ b/api/admin/src/api/request.js
@@ -0,0 +1,27 @@
+import axios from 'axios'
+
+const request = axios.create({
+ baseURL: '/api',
+ timeout: 10000
+})
+
+request.interceptors.request.use(config => {
+ const token = localStorage.getItem('token')
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`
+ }
+ return config
+})
+
+request.interceptors.response.use({
+ successHandler: res => res.data,
+ errorHandler: err => {
+ if (err.response?.status === 401) {
+ localStorage.removeItem('token')
+ window.location.href = '/login'
+ }
+ return Promise.reject(err)
+ }
+})
+
+export default request
diff --git a/api/admin/src/layout/index.vue b/api/admin/src/layout/index.vue
new file mode 100644
index 0000000..c9808e6
--- /dev/null
+++ b/api/admin/src/layout/index.vue
@@ -0,0 +1,63 @@
+
+
+
+ 笑话管理后台
+
+
+ 仪表盘
+
+
+ 笑话管理
+
+
+ 类型管理
+
+
+ 人群管理
+
+
+
+
+
+ 退出登录
+
+
+
+
+
+
+
+
+
+
+
diff --git a/api/admin/src/main.js b/api/admin/src/main.js
new file mode 100644
index 0000000..6760c11
--- /dev/null
+++ b/api/admin/src/main.js
@@ -0,0 +1,12 @@
+import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+import ElementPlus from 'element-plus'
+import 'element-plus/dist/index.css'
+import App from './App.vue'
+import router from './router'
+
+const app = createApp(App)
+app.use(createPinia())
+app.use(router)
+app.use(ElementPlus)
+app.mount('#app')
diff --git a/api/admin/src/router/index.js b/api/admin/src/router/index.js
new file mode 100644
index 0000000..3b7c63f
--- /dev/null
+++ b/api/admin/src/router/index.js
@@ -0,0 +1,37 @@
+import { createRouter, createWebHistory } from 'vue-router'
+
+const routes = [
+ {
+ path: '/login',
+ name: 'Login',
+ component: () => import('@/views/login/index.vue')
+ },
+ {
+ path: '/',
+ component: () => import('@/layout/index.vue'),
+ children: [
+ { path: '', redirect: '/dashboard' },
+ { path: 'dashboard', name: 'Dashboard', component: () => import('@/views/dashboard/index.vue') },
+ { path: 'jokes', name: 'JokeList', component: () => import('@/views/joke/List.vue') },
+ { path: 'jokes/edit/:id?', name: 'JokeEdit', component: () => import('@/views/joke/Edit.vue') },
+ { path: 'category/type', name: 'CategoryType', component: () => import('@/views/category/Type.vue') },
+ { path: 'category/crowd', name: 'CategoryCrowd', component: () => import('@/views/category/Crowd.vue') },
+ ]
+ }
+]
+
+const router = createRouter({
+ history: createWebHistory(),
+ routes
+})
+
+router.beforeEach((to, from, next) => {
+ const token = localStorage.getItem('token')
+ if (to.path !== '/login' && !token) {
+ next('/login')
+ } else {
+ next()
+ }
+})
+
+export default router
diff --git a/api/admin/src/stores/auth.js b/api/admin/src/stores/auth.js
new file mode 100644
index 0000000..837e147
--- /dev/null
+++ b/api/admin/src/stores/auth.js
@@ -0,0 +1,20 @@
+import { defineStore } from 'pinia'
+import { login } from '@/api/auth'
+
+export const useAuthStore = defineStore('auth', {
+ state: () => ({
+ token: localStorage.getItem('token') || ''
+ }),
+ actions: {
+ async login(username, password) {
+ const res = await login({ username, password })
+ this.token = res.access_token
+ localStorage.setItem('token', res.access_token)
+ return res
+ },
+ logout() {
+ this.token = ''
+ localStorage.removeItem('token')
+ }
+ }
+})
diff --git a/api/admin/src/stores/joke.js b/api/admin/src/stores/joke.js
new file mode 100644
index 0000000..98f86fd
--- /dev/null
+++ b/api/admin/src/stores/joke.js
@@ -0,0 +1,34 @@
+import { defineStore } from 'pinia'
+import { getJokes, createJoke, updateJoke, deleteJoke, batchApprove } from '@/api/joke'
+
+export const useJokeStore = defineStore('joke', {
+ state: () => ({
+ jokes: [],
+ total: 0,
+ loading: false
+ }),
+ actions: {
+ async fetchJokes(params) {
+ this.loading = true
+ try {
+ const res = await getJokes(params)
+ this.jokes = res.items
+ this.total = res.total
+ } finally {
+ this.loading = false
+ }
+ },
+ async create(data) {
+ return await createJoke(data)
+ },
+ async update(id, data) {
+ return await updateJoke(id, data)
+ },
+ async remove(id) {
+ return await deleteJoke(id)
+ },
+ async approve(ids) {
+ return await batchApprove(ids)
+ }
+ }
+})
diff --git a/api/admin/src/views/category/Crowd.vue b/api/admin/src/views/category/Crowd.vue
new file mode 100644
index 0000000..4fa6bc9
--- /dev/null
+++ b/api/admin/src/views/category/Crowd.vue
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 保存
+
+
+
+
+
+
+
+
diff --git a/api/admin/src/views/category/Type.vue b/api/admin/src/views/category/Type.vue
new file mode 100644
index 0000000..90060ae
--- /dev/null
+++ b/api/admin/src/views/category/Type.vue
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 保存
+
+
+
+
+
+
+
+
diff --git a/api/admin/src/views/dashboard/index.vue b/api/admin/src/views/dashboard/index.vue
new file mode 100644
index 0000000..96833f3
--- /dev/null
+++ b/api/admin/src/views/dashboard/index.vue
@@ -0,0 +1,67 @@
+
+
+
数据概览
+
+
+
+
+
{{ stats.total }}
+
笑话总数
+
+
+
+
+
+
+
{{ stats.pending }}
+
待审核
+
+
+
+
+
+
+
{{ stats.approved }}
+
已发布
+
+
+
+
+
+
+
{{ stats.views }}
+
总浏览
+
+
+
+
+
+
+
+
+
+
diff --git a/api/admin/src/views/joke/Edit.vue b/api/admin/src/views/joke/Edit.vue
new file mode 100644
index 0000000..39e951f
--- /dev/null
+++ b/api/admin/src/views/joke/Edit.vue
@@ -0,0 +1,78 @@
+
+
+
{{ isEdit ? '编辑笑话' : '添加笑话' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存
+ 取消
+
+
+
+
+
+
diff --git a/api/admin/src/views/joke/List.vue b/api/admin/src/views/joke/List.vue
new file mode 100644
index 0000000..22bda57
--- /dev/null
+++ b/api/admin/src/views/joke/List.vue
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.status }}
+
+
+
+
+
+
+
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
+
+
diff --git a/api/admin/src/views/login/index.vue b/api/admin/src/views/login/index.vue
new file mode 100644
index 0000000..bcc76d0
--- /dev/null
+++ b/api/admin/src/views/login/index.vue
@@ -0,0 +1,84 @@
+
+
+
+
+ 笑话大全 - 后台管理
+
+
+
+
+
+
+
+
+
+
+ 登录
+
+
+
+ 默认账号: admin / admin123
+
+
+
+
+
+
+
diff --git a/api/admin/vite.config.js b/api/admin/vite.config.js
new file mode 100644
index 0000000..0ff83d7
--- /dev/null
+++ b/api/admin/vite.config.js
@@ -0,0 +1,21 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import path from 'path'
+
+export default defineConfig({
+ plugins: [vue()],
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src')
+ }
+ },
+ server: {
+ port: 3001,
+ proxy: {
+ '/api': {
+ target: 'http://localhost:8000',
+ changeOrigin: true
+ }
+ }
+ }
+})