├── demo ├── info.txt ├── dynamic_image_admin.png └── dynamic_image_home.png ├── src ├── index.css ├── main.js ├── App.vue ├── router.js ├── stores │ └── linkStore.js ├── views │ ├── Home.vue │ └── Admin.vue └── components │ └── DateRangePicker.vue ├── postcss.config.js ├── jsconfig.json ├── tailwind.config.js ├── index.html ├── vite.config.js ├── README.md ├── package.json ├── LICENSE └── functions └── api └── [[route]].js /demo/info.txt: -------------------------------------------------------------------------------- 1 | 项目的图片示例 2 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /demo/dynamic_image_admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltxlong/dynamic-image/HEAD/demo/dynamic_image_admin.png -------------------------------------------------------------------------------- /demo/dynamic_image_home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltxlong/dynamic-image/HEAD/demo/dynamic_image_home.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["./src/*"] 6 | } 7 | }, 8 | "exclude": ["node_modules", "dist"] 9 | } -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{vue,js,ts,jsx,tsx}", 6 | ], 7 | darkMode: 'class', 8 | theme: { 9 | extend: {}, 10 | }, 11 | plugins: [], 12 | } 13 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import { createPinia } from 'pinia' 3 | import App from './App.vue' 4 | import VueDatePicker from '@vuepic/vue-datepicker' 5 | import '@vuepic/vue-datepicker/dist/main.css' 6 | 7 | const app = createApp(App) 8 | app.use(createPinia()) 9 | app.component('VueDatePicker', VueDatePicker) 10 | app.mount('#app') 11 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 26 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 链接管理系统 7 | 8 | 9 |
10 | 22 | 23 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import { fileURLToPath, URL } from 'node:url' 4 | 5 | export default defineConfig({ 6 | plugins: [vue()], 7 | resolve: { 8 | alias: { 9 | '@': fileURLToPath(new URL('./src', import.meta.url)) 10 | } 11 | }, 12 | build: { 13 | outDir: 'dist' 14 | }, 15 | server: { 16 | proxy: { 17 | '/api': { 18 | target: 'http://localhost:8787', 19 | changeOrigin: true 20 | } 21 | }, 22 | historyApiFallback: { 23 | rewrites: [ 24 | { from: /^\/api\/.*$/, to: '/api' }, 25 | { from: /./, to: '/index.html' } 26 | ] 27 | } 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dynamic-image 2 | 3 | 💕fork 本项目,然后直接部署到cloudflare pages💕期待你的star 4 | 5 | 构建命令:npm run build 6 | 7 | 构建输出目录:dist 8 | 9 | 环境变量-管理后台密码变量名:ADMIN_PASSWORD 10 | 11 | 绑定-KV数据库变量名:DYNAMIC_IMAGE 12 | 13 | 图片获取接口:/api 14 | 15 | 获取所有图片配置:/api/links 16 | 17 | ![](https://github.com/ltxlong/dynamic-image/raw/main/demo/dynamic_image_home.png) 18 | 19 | ![](https://github.com/ltxlong/dynamic-image/raw/main/demo/dynamic_image_admin.png) 20 | 21 | # 功能 22 | 23 | - 图片链接的增删改查、激活/暂停( /api接口返回的是激活状态的链接 ) 24 | 25 | - 限制访问:只有设置了的域名才可以访问 /api 接口来获取图片 26 | 27 | - 显示模式: 28 | 29 | 随机模式-所有链接随机返回一条 30 | 31 | 标签模式-基于优先级: 32 | 33 | 日标签 > 周标签 > 月标签 34 | 35 | 日标签是日期范围 36 | 37 | 周标签可以多选 38 | 39 | 月标签可以多选 40 | 41 | 如果有多个url有相同的标签,那么这些链接随机返回一条 42 | 43 | 如果没有对应的标签,那么切换为随机模式查找 44 | 45 | [![Star History Chart](https://api.star-history.com/svg?repos=ltxlong/dynamic-image&type=Date)](https://star-history.com/#ltxlong/dynamic-image&Date) 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudflare-link-manager", 3 | "private": true, 4 | "version": "1.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "pages:dev": "wrangler pages dev --proxy 5173 -- npm run dev", 11 | "pages:deploy": "npm run build && wrangler pages deploy dist" 12 | }, 13 | "dependencies": { 14 | "@popperjs/core": "^2.11.8", 15 | "@vuepic/vue-datepicker": "^10.0.0", 16 | "@vueuse/core": "^10.0.0", 17 | "date-fns": "^2.30.0", 18 | "pinia": "^2.1.0", 19 | "tsparticles": "^2.12.0", 20 | "vue": "^3.3.0", 21 | "vue-router": "^4.2.0" 22 | }, 23 | "devDependencies": { 24 | "@vitejs/plugin-vue": "^4.2.0", 25 | "autoprefixer": "^10.4.0", 26 | "postcss": "^8.4.0", 27 | "tailwindcss": "^3.3.0", 28 | "vite": "^4.3.0", 29 | "wrangler": "^3.0.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 ltxlong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import Home from './views/Home.vue' 3 | import Admin from './views/Admin.vue' 4 | 5 | // 定义不需要登录就能访问的白名单路径,精确匹配 6 | const whiteList = ['/', '/admin', '/api'] 7 | 8 | const routes = [ 9 | { 10 | path: '/', 11 | component: Home 12 | }, 13 | { 14 | path: '/admin', 15 | component: Admin 16 | }, 17 | { 18 | path: '/:pathMatch(.*)*', 19 | name: 'NotFound', 20 | component: Home 21 | } 22 | ] 23 | 24 | export const router = createRouter({ 25 | history: createWebHistory(), 26 | routes 27 | }) 28 | 29 | // 全局导航守卫 30 | router.beforeEach((to, from, next) => { 31 | 32 | if (to.path === '/' || to.path === '/admin') { 33 | next() 34 | return 35 | } 36 | 37 | // 检查是否登录 38 | if (!sessionStorage.getItem('token')) { 39 | 40 | // 精确匹配白名单路径 41 | const isWhiteListed = whiteList.some(path => { 42 | return to.path === path || to.path === path + '/'; 43 | }); 44 | 45 | if (isWhiteListed) { 46 | next() 47 | return 48 | } 49 | 50 | next('/') 51 | return 52 | } 53 | 54 | next() 55 | }) 56 | 57 | export default router 58 | -------------------------------------------------------------------------------- /src/stores/linkStore.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | export const useLinkStore = defineStore('links', { 4 | state: () => ({ 5 | links: [], 6 | displayMode: 1, // 1: 随机, 2: 标签 7 | }), 8 | 9 | actions: { 10 | getAuthHeaders() { 11 | const token = sessionStorage.getItem('token') 12 | return { 13 | 'Content-Type': 'application/json', 14 | 'Authorization': token ? `Bearer ${token}` : '' 15 | } 16 | }, 17 | 18 | async fetchLinks() { 19 | const response = await fetch('/api/links') 20 | if (response.ok) { 21 | this.links = await response.json() 22 | } 23 | }, 24 | 25 | async addLink(link) { 26 | const response = await fetch('/api/links', { 27 | method: 'POST', 28 | headers: this.getAuthHeaders(), 29 | body: JSON.stringify(link) 30 | }) 31 | if (response.ok) { 32 | const newLink = await response.json() 33 | this.links.push(newLink) 34 | } else { 35 | throw new Error('添加链接失败') 36 | } 37 | }, 38 | 39 | async deleteLink(id) { 40 | const response = await fetch(`/api/links/${id}`, { 41 | method: 'DELETE', 42 | headers: this.getAuthHeaders() 43 | }) 44 | if (response.ok) { 45 | this.links = this.links.filter(link => link.id !== id) 46 | } else { 47 | throw new Error('删除链接失败') 48 | } 49 | }, 50 | 51 | async fetchSettings() { 52 | const response = await fetch('/api/settings', { 53 | headers: this.getAuthHeaders() 54 | }) 55 | if (response.ok) { 56 | const settings = await response.json() 57 | this.displayMode = settings.displayMode 58 | } 59 | }, 60 | 61 | async updateDisplayMode(mode) { 62 | const response = await fetch('/api/settings', { 63 | method: 'POST', 64 | headers: this.getAuthHeaders(), 65 | body: JSON.stringify({ displayMode: mode }) 66 | }) 67 | if (response.ok) { 68 | this.displayMode = mode 69 | } else { 70 | throw new Error('更新显示模式失败') 71 | } 72 | }, 73 | 74 | async updateLink(link) { 75 | const response = await fetch(`/api/links/${link.id}`, { 76 | method: 'PUT', 77 | headers: this.getAuthHeaders(), 78 | body: JSON.stringify(link) 79 | }) 80 | if (response.ok) { 81 | const index = this.links.findIndex(l => l.id === link.id) 82 | if (index !== -1) { 83 | this.links[index] = link 84 | } 85 | } else { 86 | throw new Error('更新链接失败') 87 | } 88 | }, 89 | 90 | async toggleStatus(id) { 91 | const link = this.links.find(l => l.id === id) 92 | if (!link) throw new Error('链接不存在') 93 | 94 | const updatedLink = { 95 | ...link, 96 | active: !link.active 97 | } 98 | 99 | const response = await fetch(`/api/links/${id}`, { 100 | method: 'PUT', 101 | headers: this.getAuthHeaders(), 102 | body: JSON.stringify(updatedLink) 103 | }) 104 | 105 | if (response.ok) { 106 | const index = this.links.findIndex(l => l.id === id) 107 | if (index !== -1) { 108 | this.links[index] = updatedLink 109 | } 110 | } else { 111 | throw new Error('更新状态失败') 112 | } 113 | } 114 | } 115 | }) 116 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 109 | 110 | 118 | -------------------------------------------------------------------------------- /src/components/DateRangePicker.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 106 | 107 | 315 | -------------------------------------------------------------------------------- /functions/api/[[route]].js: -------------------------------------------------------------------------------- 1 | // 添加 CORS 头部的辅助函数 2 | function addCorsHeaders(response) { 3 | const headers = new Headers(response.headers) 4 | headers.set('Access-Control-Allow-Origin', '*') 5 | headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS') 6 | headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization') 7 | 8 | return new Response(response.body, { 9 | status: response.status, 10 | statusText: response.statusText, 11 | headers 12 | }) 13 | } 14 | 15 | // 检查访问限制 16 | async function checkAccessRestriction(request, db) { 17 | // 获取设置 18 | const settings = await db.get('settings', { type: 'json' }) || {} 19 | const accessUrls = settings.accessUrls || [] 20 | 21 | // 如果没有设置访问限制,允许访问 22 | if (!accessUrls.length) { 23 | return null 24 | } 25 | 26 | // 获取请求的 referer 27 | const referer = request.headers.get('referer') 28 | if (!referer) { 29 | return new Response(JSON.stringify({ 30 | success: false, 31 | error: '访问被拒绝' 32 | }), { 33 | status: 403, 34 | headers: { 'Content-Type': 'application/json' } 35 | }) 36 | } 37 | 38 | // 检查 referer 是否在允许列表中 39 | const refererUrl = new URL(referer) 40 | const isAllowed = accessUrls.some(allowedUrl => 41 | refererUrl.hostname.endsWith(allowedUrl.toLowerCase()) 42 | ) 43 | 44 | if (!isAllowed) { 45 | return new Response(JSON.stringify({ 46 | success: false, 47 | error: '访问被拒绝' 48 | }), { 49 | status: 403, 50 | headers: { 'Content-Type': 'application/json' } 51 | }) 52 | } 53 | 54 | return null 55 | } 56 | 57 | export async function onRequest(context) { 58 | const { env, request } = context 59 | const url = new URL(request.url) 60 | 61 | // 处理 OPTIONS 请求 62 | if (request.method === 'OPTIONS') { 63 | return addCorsHeaders(new Response(null, { status: 204 })) 64 | } 65 | 66 | // 检查是否使用 KV 数据库 67 | const useKV = !!env.DYNAMIC_IMAGE 68 | const db = useKV ? env.DYNAMIC_IMAGE : await connectToDatabase(env.DATABASE_URL) 69 | 70 | // API 路由处理 71 | if (url.pathname === '/api') { 72 | // 检查访问限制 73 | const accessResponse = await checkAccessRestriction(request, db) 74 | if (accessResponse) return addCorsHeaders(accessResponse) 75 | 76 | return addCorsHeaders(await handleGetLink(db, useKV)) 77 | } 78 | 79 | if (url.pathname === '/api/auth') { 80 | return addCorsHeaders(await handleAuth(request, env.ADMIN_PASSWORD, db)) 81 | } 82 | 83 | if (url.pathname === '/api/settings') { 84 | const authResponse = await checkAuth(request, db) 85 | if (authResponse) return addCorsHeaders(authResponse) 86 | 87 | if (request.method === 'POST') { 88 | return addCorsHeaders(await handleUpdateSettings(request, db, useKV)) 89 | } 90 | if (request.method === 'GET') { 91 | return addCorsHeaders(await handleGetSettings(db, useKV)) 92 | } 93 | } 94 | 95 | if (url.pathname === '/api/links') { 96 | if (request.method === 'GET') { 97 | return addCorsHeaders(await handleGetLinks(db, useKV)) 98 | } 99 | if (request.method === 'POST') { 100 | const authResponse = await checkAuth(request, db) 101 | if (authResponse) return addCorsHeaders(authResponse) 102 | return addCorsHeaders(await handleAddLink(request, db, useKV)) 103 | } 104 | } 105 | 106 | if (url.pathname.startsWith('/api/links/')) { 107 | const authResponse = await checkAuth(request, db) 108 | if (authResponse) return addCorsHeaders(authResponse) 109 | 110 | const id = url.pathname.split('/').pop() 111 | if (request.method === 'DELETE') { 112 | return addCorsHeaders(await handleDeleteLink(id, db, useKV)) 113 | } 114 | if (request.method === 'PUT') { 115 | return addCorsHeaders(await handleUpdateLink(id, request, db, useKV)) 116 | } 117 | } 118 | 119 | return addCorsHeaders(new Response('Not Found', { status: 404 })) 120 | } 121 | 122 | async function handleAuth(request, adminPassword, db) { 123 | try { 124 | const { password } = await request.json() 125 | 126 | if (password === adminPassword) { 127 | // 生成新的token (使用 UUID v4) 128 | const token = crypto.randomUUID() 129 | 130 | // 保存token 131 | await db.put('token', token) 132 | 133 | return new Response(JSON.stringify({ success: true, token: token }), { 134 | headers: { 'Content-Type': 'application/json' } 135 | }) 136 | } 137 | 138 | return new Response(JSON.stringify({ 139 | success: false, 140 | error: '密码错误' 141 | }), { 142 | status: 401, 143 | headers: { 'Content-Type': 'application/json' } 144 | }) 145 | } catch (error) { 146 | return new Response(JSON.stringify({ 147 | success: false, 148 | error: '请求格式错误' 149 | }), { 150 | status: 400, 151 | headers: { 'Content-Type': 'application/json' } 152 | }) 153 | } 154 | } 155 | 156 | async function handleAddLink(request, db, useKV) { 157 | try { 158 | const link = await request.json() 159 | 160 | if (useKV) { 161 | link.id = Date.now().toString() 162 | // 将单个链接存储为独立的 KV 163 | await db.put(`link:${link.id}`, JSON.stringify(link)) 164 | 165 | // 维护一个链接 ID 列表 166 | let linkIds = await db.get('linkIds', { type: 'json' }) || [] 167 | linkIds.push(link.id) 168 | await db.put('linkIds', JSON.stringify(linkIds)) 169 | 170 | return new Response(JSON.stringify(link), { 171 | headers: { 'Content-Type': 'application/json' } 172 | }) 173 | } else { 174 | // 外部数据库实现 175 | // ... 176 | } 177 | } catch (error) { 178 | return new Response(JSON.stringify({ 179 | success: false, 180 | error: '添加链接失败' 181 | }), { 182 | status: 500, 183 | headers: { 'Content-Type': 'application/json' } 184 | }) 185 | } 186 | } 187 | 188 | async function handleDeleteLink(id, db, useKV) { 189 | try { 190 | if (useKV) { 191 | // 删除单个链接 192 | await db.delete(`link:${id}`) 193 | 194 | // 更新链接 ID 列表 195 | let linkIds = await db.get('linkIds', { type: 'json' }) || [] 196 | linkIds = linkIds.filter(linkId => linkId !== id) 197 | await db.put('linkIds', JSON.stringify(linkIds)) 198 | 199 | return new Response(JSON.stringify({ success: true }), { 200 | headers: { 'Content-Type': 'application/json' } 201 | }) 202 | } else { 203 | // 外部数据库实现 204 | // ... 205 | } 206 | } catch (error) { 207 | return new Response(JSON.stringify({ 208 | success: false, 209 | error: '删除链接失败' 210 | }), { 211 | status: 500, 212 | headers: { 'Content-Type': 'application/json' } 213 | }) 214 | } 215 | } 216 | 217 | async function checkAuth(request, db) { 218 | const authHeader = request.headers.get('Authorization') 219 | if (!authHeader || !authHeader.startsWith('Bearer ')) { 220 | return new Response(JSON.stringify({ 221 | success: false, 222 | error: '未登录' 223 | }), { 224 | status: 401, 225 | headers: { 'Content-Type': 'application/json' } 226 | }) 227 | } 228 | 229 | const token = authHeader.split(' ')[1] 230 | const savedToken = await db.get('token') 231 | 232 | if (token !== savedToken) { 233 | return new Response(JSON.stringify({ 234 | success: false, 235 | error: 'token无效' 236 | }), { 237 | status: 401, 238 | headers: { 'Content-Type': 'application/json' } 239 | }) 240 | } 241 | } 242 | 243 | // 获取图片内容 244 | async function fetchImage(url) { 245 | try { 246 | const response = await fetch(url) 247 | if (!response.ok) { 248 | console.error('获取图片失败:', response.status, response.statusText) 249 | return null 250 | } 251 | 252 | // 保持原始响应,只修改缓存头部 253 | const headers = new Headers(response.headers) 254 | headers.set('cache-control', 'public, max-age=86400') // 24小时缓存 255 | 256 | return new Response(response.body, { 257 | status: response.status, 258 | statusText: response.statusText, 259 | headers 260 | }) 261 | } catch (error) { 262 | console.error('获取图片失败:', error) 263 | return null 264 | } 265 | } 266 | 267 | async function handleGetLink(db, useKV) { 268 | try { 269 | if (useKV) { 270 | // 获取所有链接 ID 271 | const linkIds = await db.get('linkIds', { type: 'json' }) || [] 272 | 273 | // 获取所有链接详情 274 | const links = await Promise.all( 275 | linkIds.map(async id => { 276 | const linkData = await db.get(`link:${id}`, { type: 'json' }) 277 | return linkData 278 | }) 279 | ) 280 | 281 | // 获取设置 282 | const settings = await db.get('settings', { type: 'json' }) || { 283 | displayMode: 1, 284 | accessUrls: [] 285 | } 286 | 287 | // 选择链接 288 | const selectedUrl = selectLink(links.filter(Boolean), settings) 289 | if (!selectedUrl) { 290 | return new Response('', { status: 404 }) 291 | } 292 | 293 | // 返回 302 重定向 294 | return new Response(null, { 295 | status: 302, 296 | headers: { 297 | 'Location': selectedUrl, 298 | 'Cache-Control': 'no-cache' 299 | } 300 | }) 301 | } else { 302 | // 外部数据库实现... 303 | const links = await db.query('SELECT * FROM links') 304 | const settings = await db.query('SELECT * FROM settings LIMIT 1') 305 | const selectedUrl = selectLink(links, settings[0]) 306 | if (!selectedUrl) { 307 | return new Response('', { status: 404 }) 308 | } 309 | 310 | // 返回 302 重定向 311 | return new Response(null, { 312 | status: 302, 313 | headers: { 314 | 'Location': selectedUrl, 315 | 'Cache-Control': 'no-cache' 316 | } 317 | }) 318 | } 319 | } catch (error) { 320 | return new Response('获取链接失败', { status: 500 }) 321 | } 322 | } 323 | 324 | function selectLink(links, settings) { 325 | if (!links || !links.length) return '' 326 | 327 | // 只选择激活状态的链接 328 | let filteredLinks = links.filter(link => link.active) 329 | if (!filteredLinks.length) return '' 330 | 331 | // 如果是随机模式或没有设置模式,直接随机返回 332 | if (!settings.displayMode || settings.displayMode === 1) { 333 | return getRandomLink(filteredLinks) 334 | } 335 | 336 | // 标签模式 337 | if (settings.displayMode === 2) { 338 | const now = new Date() 339 | let selectedLinks = [] 340 | 341 | // 1. 先查找日标签 342 | selectedLinks = getDayLinks(filteredLinks, now) 343 | 344 | if (selectedLinks.length) { 345 | return getRandomLink(selectedLinks) 346 | } 347 | 348 | // 2. 查找周标签 349 | selectedLinks = getWeekLinks(filteredLinks, now) 350 | 351 | if (selectedLinks.length) { 352 | return getRandomLink(selectedLinks) 353 | } 354 | 355 | // 3. 查找月标签 356 | selectedLinks = getMonthLinks(filteredLinks, now) 357 | 358 | if (selectedLinks.length) { 359 | return getRandomLink(selectedLinks) 360 | } 361 | 362 | // 4. 如果都没有匹配的标签,返回随机链接 363 | return getRandomLink(filteredLinks) 364 | } 365 | } 366 | 367 | // 获取日标签匹配的链接 368 | function getDayLinks(links, now) { 369 | const currentDate = new Date(now.getFullYear(), now.getMonth(), now.getDate()) 370 | const timestamp = currentDate.getTime() 371 | 372 | return links.filter(link => { 373 | if (!link.holidayTag) return false 374 | // 如果holidayTag是数组(包含年份),直接比较时间戳 375 | if (Array.isArray(link.holidayTag)) { 376 | if (link.holidayTag.length !== 2) return false 377 | const startDate = new Date(link.holidayTag[0]) 378 | const endDate = new Date(link.holidayTag[1]) 379 | return timestamp >= startDate.getTime() && timestamp <= endDate.getTime() 380 | } 381 | // 向后兼容:如果是单个日期,保持原有逻辑 382 | const tagDate = new Date(link.holidayTag) 383 | const linkDate = new Date(now.getFullYear(), tagDate.getMonth(), tagDate.getDate()) 384 | return linkDate.getTime() === timestamp 385 | }) 386 | } 387 | 388 | // 获取周标签匹配的链接 389 | function getWeekLinks(links, now) { 390 | const weekDay = now.getDay() 391 | 392 | return links.filter(link => 393 | Array.isArray(link.weekTags) && link.weekTags.includes(weekDay) 394 | ) 395 | } 396 | 397 | // 获取月标签匹配的链接 398 | function getMonthLinks(links, now) { 399 | const month = now.getMonth() + 1 400 | 401 | return links.filter(link => 402 | Array.isArray(link.monthTags) && link.monthTags.includes(month) 403 | ) 404 | } 405 | 406 | // 从链接数组中随机选择一个链接 407 | function getRandomLink(links) { 408 | if (!links.length) return '' 409 | return links[Math.floor(Math.random() * links.length)].url 410 | } 411 | 412 | async function handleGetLinks(db, useKV) { 413 | try { 414 | if (useKV) { 415 | // 获取所有链接 ID 416 | const linkIds = await db.get('linkIds', { type: 'json' }) || [] 417 | 418 | // 获取所有链接详情 419 | const links = await Promise.all( 420 | linkIds.map(async id => { 421 | const linkData = await db.get(`link:${id}`, { type: 'json' }) 422 | return linkData 423 | }) 424 | ) 425 | 426 | return new Response(JSON.stringify(links.filter(Boolean)), { 427 | headers: { 'Content-Type': 'application/json' } 428 | }) 429 | } else { 430 | // 外部数据库实现 431 | const links = await db.query('SELECT * FROM links') 432 | return new Response(JSON.stringify(links), { 433 | headers: { 'Content-Type': 'application/json' } 434 | }) 435 | } 436 | } catch (error) { 437 | return new Response(JSON.stringify({ 438 | success: false, 439 | error: '获取链接列表失败' 440 | }), { 441 | status: 500, 442 | headers: { 'Content-Type': 'application/json' } 443 | }) 444 | } 445 | } 446 | 447 | async function handleUpdateLink(id, request, db, useKV) { 448 | try { 449 | const link = await request.json() 450 | 451 | if (useKV) { 452 | await db.put(`link:${id}`, JSON.stringify(link)) 453 | return new Response(JSON.stringify(link), { 454 | headers: { 'Content-Type': 'application/json' } 455 | }) 456 | } else { 457 | // 外部数据库实现... 458 | } 459 | } catch (error) { 460 | return new Response(JSON.stringify({ 461 | success: false, 462 | error: '更新链接失败' 463 | }), { 464 | status: 500, 465 | headers: { 'Content-Type': 'application/json' } 466 | }) 467 | } 468 | } 469 | 470 | async function handleUpdateSettings(request, db, useKV) { 471 | try { 472 | const data = await request.json() 473 | let settings = await db.get('settings', { type: 'json' }) || {} 474 | 475 | // 更新显示模式 476 | if (data.displayMode !== undefined) { 477 | settings.displayMode = data.displayMode 478 | } 479 | 480 | // 更新访问限制URL 481 | if (data.accessUrls !== undefined) { 482 | settings.accessUrls = data.accessUrls 483 | } 484 | 485 | await db.put('settings', JSON.stringify(settings)) 486 | 487 | return new Response(JSON.stringify({ 488 | success: true, 489 | settings 490 | }), { 491 | headers: { 'Content-Type': 'application/json' } 492 | }) 493 | } catch (error) { 494 | return new Response(JSON.stringify({ 495 | success: false, 496 | error: '更新设置失败' 497 | }), { 498 | status: 500, 499 | headers: { 'Content-Type': 'application/json' } 500 | }) 501 | } 502 | } 503 | 504 | async function handleGetSettings(db, useKV) { 505 | try { 506 | const settings = await db.get('settings', { type: 'json' }) || { 507 | displayMode: 1, 508 | accessUrls: [] 509 | } 510 | 511 | return new Response(JSON.stringify(settings), { 512 | headers: { 'Content-Type': 'application/json' } 513 | }) 514 | } catch (error) { 515 | return new Response(JSON.stringify({ 516 | success: false, 517 | error: '获取设置失败' 518 | }), { 519 | status: 500, 520 | headers: { 'Content-Type': 'application/json' } 521 | }) 522 | } 523 | } 524 | -------------------------------------------------------------------------------- /src/views/Admin.vue: -------------------------------------------------------------------------------- 1 | 1341 | 1342 | 1769 | 1770 | 1937 | --------------------------------------------------------------------------------