Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 
 
 

202 wiersze
7.7 KiB

  1. import { fileURLToPath } from 'node:url'
  2. const isProduction = process.env.NODE_ENV === 'production'
  3. const defaultApiBase = isProduction
  4. ? 'https://api.aionline.cc'
  5. : 'http://192.168.31.168:16888'
  6. function resolveApiBase() {
  7. const value = String(process.env.NUXT_PUBLIC_API_BASE || defaultApiBase).trim().replace(/\/+$/, '')
  8. let url
  9. try {
  10. url = new URL(value)
  11. } catch {
  12. throw new Error(`[pc_nuxt] NUXT_PUBLIC_API_BASE 不是有效 URL:${value || '(empty)'}`)
  13. }
  14. if (!['http:', 'https:'].includes(url.protocol)) {
  15. throw new Error(`[pc_nuxt] NUXT_PUBLIC_API_BASE 仅支持 http/https:${value}`)
  16. }
  17. const hostname = url.hostname.toLowerCase()
  18. const isPrivateHost = hostname === 'localhost'
  19. || hostname === '0.0.0.0'
  20. || /^127\./.test(hostname)
  21. || hostname === '[::1]'
  22. || hostname.endsWith('.local')
  23. || /^10\./.test(hostname)
  24. || /^192\.168\./.test(hostname)
  25. || /^169\.254\./.test(hostname)
  26. || /^172\.(1[6-9]|2\d|3[01])\./.test(hostname)
  27. if (isProduction && isPrivateHost) {
  28. throw new Error(`[pc_nuxt] 生产环境禁止使用本地/局域网 API:${value},请设置 NUXT_PUBLIC_API_BASE`)
  29. }
  30. return value
  31. }
  32. function resolveServerApiBase() {
  33. const defaultServerApiBase = isProduction ? 'http://127.0.0.1:16888' : apiBase
  34. const value = String(process.env.NUXT_API_SERVER_BASE || defaultServerApiBase).trim().replace(/\/+$/, '')
  35. let url
  36. try {
  37. url = new URL(value)
  38. } catch {
  39. throw new Error(`[pc_nuxt] NUXT_API_SERVER_BASE 不是有效 URL:${value || '(empty)'}`)
  40. }
  41. if (!['http:', 'https:'].includes(url.protocol)) {
  42. throw new Error(`[pc_nuxt] NUXT_API_SERVER_BASE 仅支持 http/https:${value}`)
  43. }
  44. return value
  45. }
  46. if (process.env.API_BASE && !process.env.NUXT_PUBLIC_API_BASE) {
  47. console.warn('[pc_nuxt] API_BASE 已废弃,请改用 NUXT_PUBLIC_API_BASE')
  48. }
  49. const apiBase = resolveApiBase()
  50. const apiServerBase = resolveServerApiBase()
  51. // https://nuxt.com/docs/api/configuration/nuxt-config
  52. export default defineNuxtConfig({
  53. compatibilityDate: '2025-07-15',
  54. devtools: { enabled: true },
  55. devServer: { port: 6888, host: '0.0.0.0' },
  56. modules: ['@nuxt/ui'],
  57. // Avoid the project's /api/** proxy route. Nuxt Icon runtime should be served by Nuxt itself.
  58. icon: {
  59. localApiEndpoint: '/_nuxt_icon',
  60. },
  61. css: ['~/assets/css/main.css'],
  62. /**
  63. * 暗色模式:@nuxtjs/color-mode 由 @nuxt/ui 自带(classSuffix 已被置空,即 <html class="dark">)。
  64. * 默认保持亮色,不跟随系统;用户在页头手动切换后写入 cookie 持久化。
  65. */
  66. colorMode: {
  67. preference: 'light',
  68. fallback: 'light',
  69. classSuffix: '',
  70. storage: 'cookie',
  71. storageKey: 'ai-color-mode',
  72. },
  73. // 禁用 @nuxt/fonts 远程字体提供商(内网环境无法访问 Google Fonts)
  74. ui: {
  75. fonts: false,
  76. },
  77. // 运行时公开配置(客户端可访问)
  78. runtimeConfig: {
  79. // 服务端 SSR / Nitro 代理专用后端地址。可设为 http://127.0.0.1:16888,避免服务端回打公网域名。
  80. apiServerBase,
  81. public: {
  82. // ThinkJS ai_api 地址,按环境覆盖
  83. apiBase,
  84. // 邀请链接正式域名;未配置时自动使用当前访问域名。
  85. siteUrl: process.env.NUXT_PUBLIC_SITE_URL || '',
  86. },
  87. },
  88. // SSR / SEO
  89. ssr: true,
  90. /**
  91. * 兼容原 PC 站的自定义路由(SEO 不断链)
  92. * 原站分页形式:/info2、/info/new2、/info/course2、/info/what2。
  93. * 已迁移工具继续以旧站已收录路径作为主入口。
  94. */
  95. hooks: {
  96. 'pages:extend'(pages) {
  97. /*
  98. Windows 下长时间运行的 dev 进程偶尔会漏掉新增页面文件的 add 事件,
  99. 表现为:SSR 已命中页面并吐出内容,客户端水合后 vue-router 匹配不到而进入 404。
  100. 这里对所有新增页面显式兜底;已被自动扫描到的路径会跳过,不会重复注册。
  101. 新增 app/pages 下的页面时,记得同步加到这个清单里。
  102. */
  103. const fallbackPages = [
  104. { name: 'invite', path: '/invite', file: './app/pages/invite/index.vue' },
  105. { name: 'invite-landing', path: '/invite/landing', file: './app/pages/invite/landing.vue' },
  106. { name: 'invite-records', path: '/invite/records', file: './app/pages/invite/records.vue' },
  107. { name: 'terms', path: '/terms', file: './app/pages/terms.vue' },
  108. { name: 'privacy', path: '/privacy', file: './app/pages/privacy.vue' },
  109. { name: 'earn-points', path: '/earn-points', file: './app/pages/earn-points.vue' },
  110. { name: 'portrait-photo', path: '/portrait-photo', file: './app/pages/portrait-photo.vue' },
  111. { name: 'old-photo', path: '/old-photo', file: './app/pages/old-photo.vue' },
  112. { name: 'id-photo', path: '/id-photo', file: './app/pages/id-photo.vue' },
  113. { name: 'recharge', path: '/recharge', file: './app/pages/recharge.vue' },
  114. { name: 'bill', path: '/bill', file: './app/pages/bill.vue' },
  115. { name: 'works', path: '/works', file: './app/pages/works.vue' },
  116. ]
  117. for (const page of fallbackPages) {
  118. if (pages.some(item => item.path === page.path)) continue
  119. pages.push({
  120. ...page,
  121. file: fileURLToPath(new URL(page.file, import.meta.url)),
  122. })
  123. }
  124. // 复用已有页面组件,追加原站的分页路径(/info2 = 第 2 页)
  125. const legacy = [
  126. { base: '/info', name: 'legacy-info-page' },
  127. { base: '/info/new', name: 'legacy-info-new-page' },
  128. { base: '/info/course', name: 'legacy-info-course-page' },
  129. { base: '/info/what', name: 'legacy-info-what-page' },
  130. ]
  131. for (const item of legacy) {
  132. const origin = pages.find(p => p.path === item.base)
  133. if (!origin) continue
  134. pages.push({
  135. name: item.name,
  136. path: `${item.base}:page(\\d+)`,
  137. file: origin.file,
  138. children: [],
  139. })
  140. pages.push({
  141. name: `${item.name}-html`,
  142. path: `${item.base}:page(\\d+).html`,
  143. file: origin.file,
  144. children: [],
  145. })
  146. }
  147. },
  148. },
  149. // 旧 ThinkJS 自动 controller 地址及重构早期地址统一 301 到已收录主路径。
  150. routeRules: {
  151. '/ai-name': { redirect: { to: '/writing/naming/', statusCode: 301 } },
  152. '/anime-avatar': { redirect: { to: '/draw/anime/', statusCode: 301 } },
  153. '/writing/naming.html': { redirect: { to: '/writing/naming/', statusCode: 301 } },
  154. '/draw/anime.html': { redirect: { to: '/draw/anime/', statusCode: 301 } },
  155. '/index.html': { redirect: { to: '/', statusCode: 301 } },
  156. '/anime/index.html': { redirect: { to: '/draw/anime/', statusCode: 301 } },
  157. '/pic/index.html': { redirect: { to: '/pic/', statusCode: 301 } },
  158. '/news/index.html': { redirect: { to: '/info', statusCode: 301 } },
  159. '/news/list.html': { redirect: { to: '/info/new', statusCode: 301 } },
  160. '/news/course.html': { redirect: { to: '/info/course', statusCode: 301 } },
  161. '/news/ask.html': { redirect: { to: '/info/what', statusCode: 301 } },
  162. '/my/invite.html': { redirect: { to: '/invite', statusCode: 301 } },
  163. },
  164. app: {
  165. head: {
  166. titleTemplate: '%s',
  167. title: 'AI在线网站-你的AI伙伴让创作更简单',
  168. meta: [
  169. { charset: 'utf-8' },
  170. { name: 'viewport', content: 'width=device-width, initial-scale=1' },
  171. { name: 'description', content: 'AI在线是一个 AI 工具的集合地,可以找到各种各样的 AI 工具,满足你的不同需求。无论你是想翻译一段文字,还是想生成一篇文章,或者是想制作一张图片,AI在线都能为你提供帮助, AI 工具大全宝库!' },
  172. { name: 'keywords', content: 'AI,AI在线,AI伙伴,免费AI网站' },
  173. ],
  174. link: [{ rel: 'icon', type: 'image/png', href: '/logo.png' }],
  175. },
  176. },
  177. })