From 03ffa9151728b3ead51964f4bc8251a2909cd1e3 Mon Sep 17 00:00:00 2001
From: Dragon-Fish <xiaoyujun@aojiaogame.com>
Date: Mon, 22 Apr 2024 10:57:19 +0800
Subject: [PATCH 1/2] perf: make it modern

---
 api/utils.ts  | 43 +++++++++++++++++++++++++++++++++----------
 tsconfig.json |  1 +
 2 files changed, 34 insertions(+), 10 deletions(-)

diff --git a/api/utils.ts b/api/utils.ts
index 5ac7b926..b26b3ea1 100644
--- a/api/utils.ts
+++ b/api/utils.ts
@@ -97,24 +97,47 @@ ajax.interceptors.response.use((ctx) => {
 export function replacePximgUrlsInString(str: string): string {
   if (!str.includes('pximg.net')) return str
   return str
-    .replace(/https:\/\/i\.pximg\.net\//g, PXIMG_BASEURL_I)
-    .replace(/https:\/\/s\.pximg\.net\//g, PXIMG_BASEURL_S)
+    .replaceAll('https://i.pximg.net/', PXIMG_BASEURL_I)
+    .replaceAll('https://s.pximg.net/', PXIMG_BASEURL_S)
 }
 
 export function replacePximgUrlsInObject(
   obj: Record<string, any> | string
 ): Record<string, any> | string {
   if (typeof obj === 'string') return replacePximgUrlsInString(obj)
-  if (['arraybuffer', 'blob'].includes(obj.constructor.name.toLowerCase())) {
-    return obj
-  }
 
-  return JSON.parse(safelyStringify(obj), (key: string, val: any) => {
-    if (typeof val === 'string' && val.includes('pximg.net')) {
-      return replacePximgUrlsInString(val)
+  return deepReplaceString(obj, replacePximgUrlsInString)
+}
+
+function isObject(value: any): value is Record<string, any> {
+  return typeof value === 'object' && value !== null
+}
+
+export function deepReplaceString<T>(
+  obj: T,
+  replacer: (value: string) => string
+): T {
+  if (Array.isArray(obj)) {
+    return obj.map((value) =>
+      deepReplaceString(value, replacer)
+    ) as unknown as T
+  } else if (isObject(obj)) {
+    if (
+      ['arraybuffer', 'blob', 'formdata'].includes(
+        obj.constructor.name.toLowerCase()
+      )
+    ) {
+      return obj
     }
-    return val
-  })
+    const result: Record<string, any> = {}
+    for (const [key, value] of Object.entries(obj)) {
+      result[key] = deepReplaceString(value, replacer)
+    }
+    return result as T
+  } else if (typeof obj === 'string') {
+    return replacer(obj) as unknown as T
+  }
+  return obj
 }
 
 export function safelyStringify(value: any, space?: number) {
diff --git a/tsconfig.json b/tsconfig.json
index 0ccd2dc0..8e350031 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -20,6 +20,7 @@
     "types": ["unplugin-icons/types/vue"]
   },
   "include": [
+    "api/**/*.ts",
     "src/**/*.ts",
     "src/**/*.d.ts",
     "src/**/*.tsx",

From c797d0ab6773006281f2cc2f63e89cd20b53dd1d Mon Sep 17 00:00:00 2001
From: Dragon-Fish <xiaoyujun@aojiaogame.com>
Date: Fri, 26 Apr 2024 12:07:51 +0800
Subject: [PATCH 2/2] feat: + site notice

---
 components.d.ts                       |  8 +++--
 src/App.vue                           |  6 ++--
 src/components/SiteHeader.vue         |  2 +-
 src/components/SiteNoticeBanner.vue   | 52 +++++++++++++++++++++++++++
 src/plugins/router.ts                 |  5 +++
 src/view/notifications/2024-04-26.vue | 46 ++++++++++++++++++++++++
 6 files changed, 114 insertions(+), 5 deletions(-)
 create mode 100644 src/components/SiteNoticeBanner.vue
 create mode 100644 src/view/notifications/2024-04-26.vue

diff --git a/components.d.ts b/components.d.ts
index 134580e5..b2548a5c 100644
--- a/components.d.ts
+++ b/components.d.ts
@@ -25,18 +25,21 @@ declare module 'vue' {
     LazyLoad: typeof import('./src/components/LazyLoad.vue')['default']
     ListLink: typeof import('./src/components/SideNav/ListLink.vue')['default']
     NaiveuiProvider: typeof import('./src/components/NaiveuiProvider.vue')['default']
+    NAlert: typeof import('naive-ui')['NAlert']
     NButton: typeof import('naive-ui')['NButton']
     NCard: typeof import('naive-ui')['NCard']
+    NCountdown: typeof import('naive-ui')['NCountdown']
     NEmpty: typeof import('naive-ui')['NEmpty']
     NFlex: typeof import('naive-ui')['NFlex']
+    NLi: typeof import('naive-ui')['NLi']
     NPagination: typeof import('naive-ui')['NPagination']
-    NPaginator: typeof import('naive-ui')['NPaginator']
     NProgress: typeof import('./src/components/NProgress.vue')['default']
     NSpace: typeof import('naive-ui')['NSpace']
+    NStatistic: typeof import('naive-ui')['NStatistic']
     NTabPane: typeof import('naive-ui')['NTabPane']
     NTabs: typeof import('naive-ui')['NTabs']
-    NTabsPane: typeof import('naive-ui')['NTabsPane']
     NTag: typeof import('naive-ui')['NTag']
+    NUl: typeof import('naive-ui')['NUl']
     Placeholder: typeof import('./src/components/Placeholder.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
@@ -45,6 +48,7 @@ declare module 'vue' {
     SideNav: typeof import('./src/components/SideNav/SideNav.vue')['default']
     SiteFooter: typeof import('./src/components/SiteFooter.vue')['default']
     SiteHeader: typeof import('./src/components/SiteHeader.vue')['default']
+    SiteNoticeBanner: typeof import('./src/components/SiteNoticeBanner.vue')['default']
     UgoiraViewer: typeof import('./src/components/UgoiraViewer.vue')['default']
   }
 }
diff --git a/src/App.vue b/src/App.vue
index 693889dc..07821691 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,11 +1,13 @@
 <template lang="pug">
 NaiveuiProvider#app-full-container
+  SiteNoticeBanner
+  SiteHeader
+
   main
     article
       RouterView
 
   SideNav
-  SiteHeader
   SiteFooter
   NProgress
 </template>
@@ -51,7 +53,7 @@ onMounted(async () => {
   flex-direction: column
 
 main
-  padding-top: 50px
+  // padding-top: 50px
   position: relative
   flex: 1
   article
diff --git a/src/components/SiteHeader.vue b/src/components/SiteHeader.vue
index e920f2bb..2ef5a362 100644
--- a/src/components/SiteHeader.vue
+++ b/src/components/SiteHeader.vue
@@ -142,7 +142,7 @@ onMounted(() => {
   color: var(--theme-background-color)
   display: flex
   align-items: center
-  position: fixed
+  position: sticky
   height: 50px
   width: 100%
   box-sizing: border-box
diff --git a/src/components/SiteNoticeBanner.vue b/src/components/SiteNoticeBanner.vue
new file mode 100644
index 00000000..aa3c56cd
--- /dev/null
+++ b/src/components/SiteNoticeBanner.vue
@@ -0,0 +1,52 @@
+<template lang="pug">
+Transition(name='fade')
+  #sitenotice-banner(v-if='isShow')
+    NAlert(
+      @close='handleClose'
+      closable
+      style='font-size: 1.5rem'
+      title='全站公告'
+      type='warning'
+    )
+      NUl
+        NLi: RouterLink(to='/notifications/2024-04-26') 关于 PixivNow 将可能停止服务的通知(2024年4月26日)
+</template>
+
+<script setup lang="ts">
+import {} from 'vue'
+
+const alreadyShown = ref(false)
+const forceShow = computed(
+  () =>
+    route.name === 'about-us' || Date.now() > new Date('2024-09-01').getTime()
+)
+const isShow = computed(() => {
+  if (route.path === '/notifications/2024-04-26') {
+    return false
+  }
+  if (forceShow.value) return true
+  return !alreadyShown.value
+})
+const key = `pixivnow:sitenotice/2024-04-26`
+const route = useRoute()
+
+onMounted(() => {
+  alreadyShown.value = !!localStorage.getItem(key)
+})
+
+function handleClose() {
+  localStorage.setItem(key, '1')
+  alreadyShown.value = 1
+}
+</script>
+
+<style scoped lang="sass">
+.fade-enter-active,
+.fade-leave-active
+  transition: all 0.5s ease-in-out
+
+.fade-enter-from,
+.fade-leave-to
+  opacity: 0
+  height: 0
+</style>
diff --git a/src/plugins/router.ts b/src/plugins/router.ts
index 154413ae..2b20b726 100644
--- a/src/plugins/router.ts
+++ b/src/plugins/router.ts
@@ -61,6 +61,11 @@ const routes: RouteRecordRaw[] = [
     name: 'about-us',
     component: () => import('@/view/about.vue'),
   },
+  {
+    path: '/notifications/2024-04-26',
+    name: 'notification-2024-04-26',
+    component: () => import('@/view/notifications/2024-04-26.vue'),
+  },
   {
     path: '/:pathMatch(.*)*',
     name: 'not-found',
diff --git a/src/view/notifications/2024-04-26.vue b/src/view/notifications/2024-04-26.vue
new file mode 100644
index 00000000..74d3b5f7
--- /dev/null
+++ b/src/view/notifications/2024-04-26.vue
@@ -0,0 +1,46 @@
+<template lang="pug">
+#notification-view.body-inner
+  h1.align-center 关于 PixivNow 将可能停止服务的通知(2024年4月26日)
+  Card
+    p 各位,早上好中午好晚上好:
+    p 我们希望通知您,由于 Vercel 在 2024年4月4日 对其<a href="https://vercel.com/blog/improved-infrastructure-pricing" target="_blank">定价策略</a>进行了修改,细化了收费指标,这对我们的项目产生了重大影响。特别是,我们的项目在部分指标上的用量<strong>已经远超过了</strong>免费计划的限额。由于 PixivNow 是一个开源项目,并且到目前为止我们<strong>没有任何盈利</strong>,我们很难为了这个兴趣使然的项目自掏腰包。
+    p 幸运的是,Vercel 为现有的免费计划用户提供了 6 个月的缓冲期,这意味着我们的服务在接下来的六个月内不会受到影响。但是在此之后(大约是 2024年9月),我们的服务有极大概率将被迫中断。
+    p 在接下来的几个月里,我们将探索所有可能的解决方案以继续提供服务(前提是尽量不要花钱)。
+    p 我们非常感谢您一直以来对 PixivNow 的支持和理解。如果您希望继续支持我们,我们正在考虑接受赞助来帮助维持项目的运行。您可以通过访问我们的赞助页面了解更多信息,并考虑成为我们的赞助者。
+
+    .flex(style='justify-content: center')
+      NStatistic(
+        label='死亡倒计时'
+        style='border: 1px solid #efefef; padding: 0.5rem; border-radius: 0.25rem'
+      )
+        NCountdown(:duration='duration')
+
+    div(style='text-align: right')
+      strong Dragon Fish
+      br
+      time 2024年4月26日
+
+  Card(title='赞助我们')
+    .align-center
+      iframe(
+        frameborder='0'
+        height='200'
+        scrolling='no'
+        src='https://afdian.net/leaflet?slug=dragon-fish'
+        width='640'
+      )
+
+  Card(title='联系我们')
+    ul
+      li QQ群:1026023666
+</template>
+
+<script setup lang="ts">
+import {} from 'vue'
+
+const fromTime = new Date()
+const toTime = new Date('2024-09-30T23:59:59Z')
+const duration = toTime.getTime() - fromTime.getTime()
+</script>
+
+<style scoped lang="sass"></style>